30天學習計劃 js忍者秘籍 第6章 原型與面向對象


2016.9.9


第6章 原型與面向對象

原型雖然是定義對象的一種很方便的方式,但它的本質依然是函數特性。

使用原型所定義的屬性和功能會自動應用到對象的實例上。一旦進行了定義,原型的屬性就會變成實例化對象的屬性,從而作為復雜對象創建的概覽。

js原型的主要用途就是使用一種類風格的面向對象和繼承技術進行編碼。

6.1 實例化和原型

所有的函數在初始化的時候都有一個prototype屬性,該屬性的初始值是一個空對象。只有函數在作為構造器的時候,prototype屬性才會發揮更大的作用。

使用new關鍵字調用一個函數,使得該函數可以作為構造器進行實例化,并產生一個新的空對象實例作為其上下文。

6.1.1 對象實例化

創建新對象最簡單的方法

var o={};

通過賦值語句可以再給它添加一些屬性

o.name = 'satio';

o.occupation = 'marksman';

o.cyberization = 20;

1)原型作為對象概覽

示例6.1 使用原型方法創建一個新實例

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Ninja(){}

Ninja.prototype.swingSword?=?function(){

return?true;

}

var?ninja1?=?Ninja();

assert(ninja1?===?undefined,'No?instance?of?Ninja?created.')

var?ninja2?=?new?Ninja();

assert(ninja2?&&?ninja2.swingSword?&&?ninja2.swingSword(),'Instance?exists?and?method?is?callable.')

首先,我們像普通函數一樣調用,并將其結果存儲在變量ninja1中,由于該函數體沒有返回任何東西,所以ninja1的值是undefined。

接著,使用new操作符將其作為構造器進行調用,此時,新創建了一個對象,并且成為函數的上下文。new操作符返回的結果是該新對象的引用。ninja2引用的是新創建對象,并且該新對象有一個swingSword()方法可以讓我們進行調用。

這說明,函數作為構造器進行調用時,函數的原型是新對象的一個概覽。

原型可以讓我們預定義屬性,包括方法,這些屬性和方法會自動應用在新對象實例上。

我們并沒有在構造器里顯式做任何事情,通過將swingSword()方法添加到構造器的prototype屬性上,swingSword()方法就可以附加到新對象上了。

2)實例屬性

使用new操作符將函數作為構造器進行調用的時候,其上下文被定義為新對象實例。這意味著,除了通過原型給函數附加屬性的形式以外,我們還可以在構造器函數內通過this參數初始化值。

示例6.2 觀察初始化活動的優先級

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Ninja(){

this.swung?=?false;

this.swingSword?=?function(){

return?!this.swung;

}

}

Ninja.prototype.swingSword?=?function(){

return?this.swung;

}

var?ninja?=?new?Ninja();

assert(ninja.swingSword(),'Called?the?instance?method,?not?the?prototype?method.')

以上示例表明,在構造器內創建的實例方法會阻擋在原型上定義的同名方法。

在構造器內的綁定操作優先級永遠都高于在原型上的綁定操作優先級。因為構造器的this上下文指向的是實例自身,所以我們可以在構造器內對核心內容執行初始化操作。

3)協調引用

示例6.3 觀察原型變化時的行為

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Ninja(){

this.swung?=?true;

}

var?ninja?=?new?Ninja();

Ninja.prototype.swingSword?=?function(){

return?this.swung;

}

assert(ninja.swingSword(),'Method?exists,?even?out?of?order.')

測試證明原型的改變會影響其構造器已經創建的對象

在引用對象的一個屬性時,首先檢查該對象本身是否擁有該屬性,如果有,則直接返回,否則,再查看對象的原型,檢查該原型上是否有所要的屬性,如果有則直接返回,如果沒有,則該值是undefined.

對象不僅與其構造器有關,還與構造器所創建對象的原型相關

js中的每個對象,都有一個名為constructor的隱式屬性,該屬性引用的是創建該對象的構造器。由于prototype是構造器的一個屬性,所以每個對象都有一種方式可以找到自己的原型。


原型是實時附加在對象上的,可以很好的與該對象的自身屬性協調在一起。引用時,可以隨時使用原型。

示例6.4 進一步觀察原型改變時的行為

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Ninja(){

this.swung?=?true;

this.swingsSword?=?function(){

return?!this.swung;

}

}

var?ninja?=?new?Ninja();

Ninja.prototype.swingsSword?=?function(){

return?this.swung;

}

assert(ninja.swingsSword(),'Called?the?instance?method,not?the?prototype?method.')

示例測試不通過表示:

查找屬性時,首先查找對象自身,如果沒找到,再查找構造器的原型

6.1.2 通過構造器判斷對象類型

對象的構造器可以通過constructor屬性獲得,任何時候我們都可以重新引用該構造器,甚至可以使用它進行類型檢查。

示例6.5 判斷一個實例的類型以及其構造器

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Ninja(){}

var?ninja?=?new?Ninja();

assert(typeof?ninja?==?'object','The?type?of?the?instance?is?object.')

assert(ninja?instanceof?Ninja,'Instanceof?identifies?the?constructor.')

assert(ninja.constructor?==?Ninja,'The?ninja?object?was?create?by?the?Ninja?function')

instanceof操作符告訴我們確定一個實例是否是由特定的函數構造器所創建。

利用constructor屬性可以驗證實例的起源。

示例6.6 使用constructor屬性實例化一個新對象

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Ninja(){}

var?ninja?=?new?Ninja();

var?ninja2?=?new?ninja.constructor();

assert(ninja2?instanceof?Ninja,'It\'s?a?Ninja.')

assert(ninja?!==?ninja2,'But?not?the?same?Ninja.')

示例定義了一個構造器,然后使用該構造器創建一個實例。接著使用該實例的constructor屬性再創建第二個實例。測試結果表明,成功創建了第二個Ninja對象,并且該變量指定的并不是同一個實例。

我們不用知道原有的構造器函數就可以再次創建一個新實例,即使是原始的構造器在作用域內已經不存在了,我們也完全可以在幕后使用該引用。


9.12


6.1.3 繼承與原型鏈

可以利用instanceof作為對象繼承的一種形式。

示例6.7 嘗試用原型實現繼承

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Person(){}

Person.prototype.dance?=?function(){};

function?Ninja(){}

Ninja.prototype?=?{dance:Person.prototype.dance};

var?ninja?=?new?Ninja();

assert(ninja?instanceof?Ninja,'ninja?receives?functionality?from?the?ninja?prototype.')

assert(ninja?instanceof?Person,'...?and?the?Person?prototype.')

assert(ninja?instanceof?Object,'...?and?the?Object?prototype.')

測試結果,第二個沒有通過

即使沒做任何事情,所有的對象也都會是object的實例。

創建原型鏈最好的方式 ,使用一個對象的實例作為另一個對象的原型:

Subclass.prototype = new Superclass();

Ninja.prototype = new Person();

這將保持原型鏈,是因為Subclass實例的原型將是Superclass的一個實例,該實例不僅擁有原型,還持有Superclass的所有屬性,并且該原型指向其自身超類的一個實例,以此類推。

示例6.8 用原型實現繼承

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Person(){}

Person.prototype.dance?=?function(){};

function?Ninja(){}

Ninja.prototype?=?new?Person();

var?ninja?=?new?Ninja();

assert(ninja?instanceof?Ninja,'ninja?receives?functionality?from?the?ninja?prototype.')

assert(ninja?instanceof?Person,'...?and?the?Person?prototype.')

assert(ninja?instanceof?Object,'...?and?the?Object?prototype.')

assert(typeof?ninja.dance?==?'function','...?and?can?dance.')

測試結果,通過

通過執行instanceof操作,可以判斷函數是否繼承了其原型鏈中任何對象的功能。

dance()方法在原型鏈上被所有實例繼承。

所有原生js對象構造器(如Object,Array,String,Number,RegExp,Function)都有可以被操作和擴展的原型屬性,因為每個對象構造器自身就是一個函數。這證明原型是這門語言的一個非常強大的特性。利用原型,我們可以擴展這門語言自身的功能,從而給這門語言引用新的特性或丟失的特性。

示例6.9 javascript 1.6 forEach()方法的一種不過時的實現

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

if(!Array.prototype.forEach){

Array.prototype.forEach?=?function(callback,context){

for(var?i=0;?i

callback.call(context?||?null,?this[i],?i,?this);

}

}

}

['a','b','c'].forEach(function(value,index,array){

assert(value,array[index]+'?is?in?position?'+index+'?out?of?'+(array.length-1))

})

在定義一個可能已經存在的方法之前,需要檢查一下,使得代碼可以向前兼容。

示例將方法添加到Array原型上,并使用傳統的for循環遍歷該數組,為每一個條目都調用下callback方法。傳遞給callback的是當前所遍歷的條目、索引以及原始數組。context||null表達式可以防止我們將undefined傳遞給call()。

所有的內置對象,比如Array,包括其原型,我們都可以按照自己的意愿來擴展它,但需要記住,在原始對象上引入新的屬性和方法,與在全局作用域內聲明一個變量一樣危險,因為原生對象的原型只有一個實例,所以有發生命名沖突的重大可能性。

6.1.4 HTML DOM原型

在現代瀏覽器中,所有DOM元素都繼承于HTMLElement構造器。通過訪問HTMLElement的原型,瀏覽器可以為我們提供擴展任意HTML節點的能力。

示例6.10 通過HTMLElement的原型,給所有HTML元素都添加一個新方法

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

I'm?going?to?be?removed.

Me?too!

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

HTMLElement.prototype.remove?=?function(){

if(this.parentNode){

this.parentNode.removeChild(this);

}

}

var?a?=?document.getElementById('a');

a.parentNode.removeChild(a);

document.getElementById('b').remove();

assert(!document.getElementById('a'),'a?is?gone.')

assert(!document.getElementById('b'),'b?is?gone?too.')

在示例中,通過增強HTMLElement構造器的原型,我們為所有的DOM元素都添加了一個新方法remove()。

盡管瀏覽器暴露了基構造器和原型,但它們選擇性地禁用了通過構造器創建元素的能力。

6.2 疑難陷阱

js有幾個和原型、實例化以及繼承相關的陷阱。

6.2.1 擴展對象

我們也許會犯的極其嚴重的錯誤就是去擴展原生Object.prototype。因為,在擴展該原型時,所有的對象都會接收這些額外的屬性。在我們遍歷對象屬性的時候,新屬性的出現可能會導致各種意想不到的行為。

示例6.11 給Object原型添加額外屬性會發生意想不到的行為

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

Object.prototype.keys?=?function(){

var?keys?=?[];

for(var?p?in?this)?keys.push(p);

return?keys;

}

var?obj?=?{a:1,b:2,c:3};

assert(obj.keys().length?==?3,'There?are?three?properties?in?this?object.')

console.log(obj.keys())

示例測試沒有通過

問題出在了給Object添加的keys()方法上,我們給所有的對象都添加了這一屬性,這會影響到所有對象,并迫使所有的代碼都必須要考慮到這個額外的屬性,這可能會破壞頁面開發人員的合理代碼。

js提供一個名為hasOwnProperty()方法,使用該方法可以確定一個屬性是在對象實例上定義的,還是從原型里導入的。

示例6.12 使用hasOwnProperty()方法辨別Object原型擴展

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

Object.prototype.keys?=?function(){

var?keys?=?[];

for(var?i?in?this)

if(this.hasOwnProperty(i))

keys.push(i);

return?keys;

}

var?obj?=?{a:1,b:2,c:3};

assert(obj.keys().length?==?3,'There?are?three?properties?in?this?object.')

console.log(obj.keys())

重新定義了方法,忽略了非實例屬性,這樣測試就通過了。

使用hasOwnProperty()忽略原型對象上的屬性,從而跳過原型上的屬性。

但是,不能僅僅因為它能解決這個問題,就濫用它,從而使其成為我們代碼的用戶的一個負擔。

6.2.2 擴展數字

除了Object,對其他原生對象的原型進行擴展相對來說比較安全。但是,另外一個有問題的原生對象是Number。

示例6.13 在Number原型上添加一個方法

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

Number.prototype.add?=?function(num){

return?this+num;

}

var?n=5;

assert(n.add(3)?==?8,'It?works?if?a?number?is?in?a?variable.')

assert((5).add(3)?==?8,'Also?works?if?a?number?is?wrapped?in?parentheses.');

assert(5.add(3)==8,'What?about?a?simple?literal?')

在瀏覽器中加載頁面時,頁面不會加載,會報錯。語法解析器不能處理字面量這種情況。


9.13


6.2.3 子類化原生對象

原生對象的子類化,一個對象是Object的子類(因為它是所有原型鏈的根)

示例:6.14 子類化Array對象

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?myArray(){}

myArray.prototype?=?new?Array();

var?mine?=?new?myArray();

mine.push(1,2,3);

assert(mine.length?==?3,'All?the?items?are?in?out?sub-classed?array.')

assert(mine?instanceof?Array,'Verify?that?we?implement?Array?functionality.')

示例在有些瀏覽器可能不支持。

更好的策略是單獨實現原生對象的各個功能,而不是擴展出子類。

示例6.15 模擬Array功能,而不是擴展出子類

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?myArray(){}

myArray.prototype.length?=?0;

(function(){

var?methods?=?['push','pop','shift','unshift','slice','splice','join'];

for(var?i=0;?i

(function(name){

myArray.prototype[name]?=?function(){

return?Array.prototype[name].apply(this,arguments);

}

})(methods[i]);

}

})()

var?mine?=?new?myArray();

mine.push(1,2,3);

assert(mine.length?==?3,'All?the?items?are?in?out?sub-classed?array.')

assert(!(mine?instanceof?Array),'Verify?that?we?implement?Array?functionality.')

我們使用了一個即時函數,使用apply()方法,將從Array中選中的方法復制到新類上。使用數組里的方法名是為了保持整潔性,且易于擴展。

6.2.4 實例化問題

函數有兩種用途:作為“普通”的函數,以及作為構造器。因為如此,用戶可能并不很清楚在代碼中用的到底是哪個。

示例6.16 函數調用時不使用new操作符時的結果

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?User(first,last){

this.name?=?first?+?'?'?+?last;

}

var?user?=?User('Ichigo','Kurosaki');

assert(user,'User?instantiated.')

assert(user.name?==?'Ichigo?Kurosaki','User?name?correctly?assigned')

測試失敗。原因是忘記在User()函數上調用new操作符了。這種情況下,new操作符的缺失,將會導致函數是作為普通函數進行調用的,而不是構造器,而且并沒有實例化一個新對象。新手用戶很容易誤入這個陷阱,不使用操作符調用該函數,將導致意想不到的結果。

注意:約定,函數作為構造器的時候,其名稱以一個大寫字符開頭,而非構造器函數則不必。此外,構造器往往是名詞,用于描述類的概念,如Ninja,samurai,tachikoma這樣的“類”,而普通函數則命名為動詞,或動詞/對象,用于描述所做的事情,如swingSword,hideBehindAplant這樣。

示例6.17 不小心將變量引入到全局命名空間中

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?User(first,last){

this.name?=?first?+?'?'?+?last;

}

var?name?=?'Rukia';

var?user?=?User('Ichigo','Kurosaki');

assert(name?==?'Rukia','Name?was?set?to?Rukia')

測試失敗,這個也犯了錯誤,忘記使用new操作符。

函數作為構造器調用時,函數調用的上下文是新分配的對象。但作為普通函數調用時,其上下文是全局作用域,也就是this.name引用指向的不是新創建對象的name屬性,而是全局作用域內的name變量。

示例6.18 判斷函數是否是作為構造器進行調用的

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?Test(){

return?this?instanceof?arguments.callee;

}

assert(!Test(),'We?didn\'t?instantiate?,?so?it?return?false.');

assert(new?Test(),'We?dis?instantiate?,?returning?true')

回想一些重要的概念:

.通過arguments.callee可以得到當前執行函數的引用。

.“普通”函數的上下文是全局作用域(除非強制修改)。

.利用instanceof 操作符測試已構建對象是否構建于指定的構造器。

基于這些,函數在作為構造器進行執行的時候,表達式 this instanceof arguments.callee 的結果是true,如果作為普通函數執行,則返回false。

示例6.19 在調用者上修復該問題

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function?User(first,last){

if(!(this?instanceof?arguments.callee)){

return?new?User(first,last);

}

this.name?=?first+'?'+last;

}

var?name?=?'Rukia';

var?user?=?User('Ichigo','Kurosaki');

assert(name?==?'Rukia','Name?was?set?to?Rukia');

assert(user?instanceof?User,'User?instantiated');

assert(user.name?==?'Ichigo?Kurosaki','User?name?correctly?assigned')

判斷函數是否按不正確的方法進行了調用,如果是,則再實例化User自身,將其作為函數的結果進行返回。這樣的結果是,無論函數調用的時候是否是作為普通函數進行調用的,最終都返回一個User實例。

callee屬性即將在新版本js中廢棄,并且不支持嚴格格式。

還要再思考下,使用這種方法是否就一定是正確的,我們只是想出一個巧妙的解決辦法,但不代表就一定要用。


9.14


6.3 編寫類風格的代碼

js可以讓我們通過原型實現繼承。

類是面向對象開發人員所期望的內容,盡管js本身不支持傳統的類繼承。

開發人員希望有如下特性:

.一套可以構建新構造器函數和原型的輕量級系統。

.一種簡單的方式來執行原型繼承

.一種可以訪問被函數原型所覆蓋的方法的途徑

示例6.20 經典繼承語法示例

test?suite

#results?.pass{color:green;}

#results?.fail{color:red;}

function?assert(value,desc){

var?li?=?document.createElement('li');

li.className?=?value???'pass'?:?'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

(function(){

var?initializing?=?false,superPattern?=?/xyz/.test(function(){xyz;})???/\b_super\b/?:?/.*/;

Object.subClass?=?function(properties){

var?_super?=?this.prototype;

initializing?=?true;

var?proto?=?new?this();

initializing?=?false;

for(var?name?in?properties){

proto[name]?=?typeof?properties[name]?==?'function'?&&?typeof?_super[name]?==?'function'?&&?superPattern.test(properties[name])??

(function(name,fn){

return?function(){

var?tmp?=?this._super;

this._super?=?_super[name];

var?ret?=?fn.apply(this,arguments);

this._super?=?tmp;

return?ret;

}

})(name,properties[name])?:?properties[name];

}

function?Class(){

if(!initializing?&&?this.init){

this.init.apply(this,arguments);

}

}

Class.prototype?=?proto;

Class.constructor?=?Class;

Class.subClass?=?arguments.callee;

return?Class;

}

})()

var?Person?=?Object.subClass({

init:function(isDancing){

this.dancing?=?isDancing

},

dance:function(){

return?this.dancing;

}

});

var?Ninja?=?Person.subClass({

init:function(){

this._super(false);

},

dance:function(){

return?this._super();

},

swingSword:function(){

return?true;

}

})

var?person?=?new?Person(true);

assert(person.dance(),'The?person?i?sdancing.');

var?ninja?=?new?Ninja();

assert(ninja.swingSword(),'The?sword?is?swinging');

assert(!ninja.dance(),'The?ninja?is?not?dancing');

assert(person?instanceof?Person,'Person?is?a?Person')

assert(ninja?instanceof?Ninja?&&?ninja?instanceof?Person,'Ninja?is?a?Ninja?and?a?Person')

關于本例,有幾點重要的注意事項:

.通過調用現有的構造器函數的subClass()方法可以創建一個新“類”,例如,通過Object創建一個Person類,以及通過Person創建一個Ninja類。

.為了讓構造器的創建更加簡單。我們建議的語法是,為每個類只提供一個init()方法,就像為Person和Ninja提供的init()方法一樣。

.我們所有的“類”最終都繼承于一個祖先:Object。因此,如果要創建一個新類,它必須是Object的一個子類,或者是一個在層級上繼承于Object的類(完全模仿當前的原型系統)

.該語法的最大挑戰是訪問被覆蓋的方法,而且有時這些方法的上下文也有可能被修改了。通過this._super()調用Person超類的原始init()和dance()方法,我們就可以了解這種用法。


9.18


6.3.1 檢測函數是否可序列化

函數序列化就是簡單接收一個函數,然后返回該函數的源碼文本。

一個函數在其上下文中序列化成字符串,會導致它的toString方法被調用。可以用這種方式測試函數是否可以序列化。

/xyz/.test(function(){xyz;}) 該表達式創建一個包含xyz的函數,將該函數傳遞給正則表達式的test()方法,該正則表達式對字符串“xyz”進行測試。如果函數能夠正常序列化(test()方法將接收一個字符串,然后將觸發函數的toString()方法),最終結果返回true。

superPattern =/xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 建立一個名為superPattern的變量,稍后用它來判斷一個函數是否包含字符串"_super"。只有函數支持序列化才能進行判斷,所以在不支持序列化的瀏覽器上,我們使用一個匹配任意字符串的模式進行代替。

6.3.2 子類的實例化

Object.subClass = function(properties){

var _super = this.prototype;

}

給Object添加一個subClass()方法,該方法接收一個參數,該參數是我們期望添加到子類的屬性集。

為了用函數原型模擬繼承:創建父類的一個實例,并將其賦值給子類的原型。

我們在代碼中定義了一個initializing變量,每當我們想使用原型實例化一個類的時候,都將該變量設置為true。

因此,在構造實例時,我們可以確保不在實例化模式下進行構建實例,并可以相應地運行或跳過init()方法。

if(!initializing && this.init){

this.init.apply(this,argument);

}

init()方法可以運行各種昂貴的啟動代碼(連接到服務器、創建DOM元素,還有其他未知內容),所以如果只是創建一個實例作為原型的話,你們要規避任何不必要的昂貴啟動代碼。

6.3.3 保留父級方法

在我們的特定實現中,我們創建一個名為_super的臨時新方法,該方法只能從子類方法內部進行訪問,并且該方法引用的是父類中的原有方法。

var Person = Object.subClass({

init:function(){

this.dancing = isDancing;

}

});

var Ninja = Person.subclass({

init:function(){

this._super(false);

}

})

在Ninja構造器內,我們調用了Person的構造器,并傳入了一個相應的值。這可以防止重新復制代碼——我們可以重用父類中已經編寫好的代碼。

為了增強子類,我們向subClass()方法傳入了一個對象哈希,只需要將父類的屬性和傳入的屬性合并在一起就可以了。

首先,使用如下代碼,創建一個超類的實例作為一個原型:

initizlizing = true;

var proto = new this();

initizlizing = false;

將傳入的屬性合并到proto對象(一個原型的原型)中。如果不在意父類函數,合并代碼將非常簡單:

for(var name in properties) proto[name] = properties[name];

檢測即將被包裝的子類函數,可以使用如下條件表達式:

typeof properties[name] == 'function' && typeof _super[name] == 'function' && superPattern.test(properties[name]);

此表達式包含三個檢測條件:子類屬性是否是一個函數?超類屬性是否是一個函數?子類函數是否包含一個_super()引用?

如果條件表達式表明我們必須包裝功能,我們通過給即時函數的結果進行賦值,將該結果作為子類的屬性:

(function(name,fn){

return function(){

var tmp = this._super;

this._super = _super[name];

var ret = fn.apply(this,arguments);

this._super = tmp;

return ret;

}

})(name,properties[name])

該即時函數創建并返回了一個新函數,該新函數包裝并執行了子類的函數,同時可以通過_super屬性訪問父類函數。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容