一、javascript中的屬性、方法
1.首先,關於javascript中的函數/“方法”,說明兩點:
1)如果訪問的對象屬性是一個函數,有些開發者容易認為該函數屬於這個對象,因此把“屬性訪問”叫做“方法訪問”,而實際上,函數永遠不會屬於一個對象,對象擁有的,只是函數的引用。確實,有些函數體內部使用到了this引用,有時候這些this確實會指向調用位置的對象引用,但是這種用法從本質上並沒有把一個函數變成一個方法,只是發生了this綁定罷了。因此,如果屬性訪問返回的是一個函數,那它也並不是一個方法,屬性訪問返回的函數和其他函數並沒有任何區別,只是有時候會發生隱式的this綁定罷了
2)javascript中很難確定“復制”函數究竟意味着什么。事實上,在javascript中,函數無法(用標准、可靠的方法)真正地復制,能夠復制的只是函數的引用。
2.對象的原型[[prototype]]鏈與函數的原型
原型鏈:對象[[prototype]]l鏈是一種機制,是對象中的一個內部鏈接引用另一個對象。
原型:函數.prototype指向的就是一個對象,叫做函數的原型對象。
A.對象原型鏈
javascript對象有一個特殊的[[prototype]]內置屬性,其實就是對於其他對象的引用,幾乎所有的對象在創建時[[prototype]]的屬性都會被賦予一個非空的值(除了object.create(null)).所有普通的[[prototype]]鏈最終都會指向內置的object.prototype。里面包含了很多常見的功能,諸如.toString(),.valueOf(),.hasOwnProperty(),.isPrototypeOf();
原型鏈本質上是屬性查找使用的。myObject.a不僅僅是在myObject中查找名字為a的屬性。在語言規范中,myObject.a實際上是執行了[[Get]]操作。對象默認的[[Get]]操作首先在對象中查找是否有名稱相同的屬性,如果找到就返回這個屬性的值。如果沒有找到,按照[[Get]]算法的定義會遍歷對象的原型鏈(prototype鏈),直到找到匹配的屬性名或者查找完整條[[prototype]]鏈,如果是后者的話,[[Get]]操作的返回值是undefined.
在javascript中,給一個對象設置屬性如myObject.foo='bar',不僅僅是添加一個新屬性或者修改已有的屬性,,完整的流程如下:
1)如果對象中包含名為foo的普通屬性,不論上層原型鏈存不存在,這條賦值語句會直接修改myObject已有的屬性名,此時會發生屏蔽;
2)如果對象myObject中不包含名為foo的屬性,[[prototype]]鏈就會遍歷,如果原型鏈上找不到foo,foo就會被直接添加到myObject上。
3)如果myOjbect中不包含foo這個屬性,但foo存在於原型鏈的上層,賦值語句myObjec.foo的行為會有些不同,具體如下:
a.如果[[prototype]]鏈上層名為foo的普通數據訪問屬性沒有被標記為只讀(writable,false),那就會直接在myObject添加一個名為foo的新屬性,它是屏蔽屬性;
b.如果[[prototype]]鏈上層存在foo,且被標記為只讀,那么無法修改已有屬性或者在myObject上面創建屏蔽屬性,在費嚴格模式下,這條賦值語句會被忽略。
c.如果[[prototype]]鏈上層存在foo並且它是一個setter,那么一定會調用這個setter,foo不會被添加到myObject上,也不會重新定義foo這個setter。
可見只有在myObject中包含foo屬性,或者myObject與原型鏈都不包含foo屬性,或者myObject不包含foo屬性,原型鏈的foo屬性沒有被標記為只讀的情況下,才會發生屏蔽。如果在上面b,c兩種情況下也希望屏蔽foo,不能使用=操作符,而是使用object.defineProperty來向myObject添加屬性foo。
B:函數原型
對象原型鏈[[prototype]]與函數原型prototype,這兩個是截然不同的事物,雖然二者存在一定的關聯。對象的原型鏈[[prototype]]如上所述,是對象的一個隱藏屬性,用於屬性查找。在chrome等瀏覽器的實現中,可以通過_proto_訪問到。
函數作為對象,自然也有內置的隱藏屬性[[prototype]],其原型鏈的終點指向Function.prototype,而Function.prototype又指向Object.prototype.
此外,任何函數比如function Foo()默認都有一個特殊的顯式屬性prototype,(String,Number,Object,Function這些所謂的子類型說白了就是一些內置函數,因此都有prototype屬性)。函數的prototype屬性是顯示的,它指向一個對象,這個對象通常稱之為函數原型。
那么函數原型這個對象究竟是什么呢?
最直接的解釋就是:這個對象時在調用a=new Foo()時創建的,執行這句話同時令新創建的對象a,其a.[[prototype]]鏈接到這個Foo.prototype所指向的原型對象。如果多次調用new Foo(),那么他們的[[prototype]]關聯的是同一個對象,都是Foo.prototype所指向的對象。可見:1)函數作為對象,其Foo.[[prototype]]是不鏈接到Foo.prototype的,而是new 調用創建的對象連接到Foo.prototype指向的原型對象;2)new調用會在新創建的對象和函數原型之間創建關聯,這個關聯不存在於對象和構造函數之間,只是上述代碼會同時為Foo.prototype添加construnctor屬性,該屬性指向“構造函數“Foo”(再次強調,javascript沒有構造函數,只有函數的構造調用),對象通過原型鏈也能訪問construnctor屬性,不過這除了營造出類似“構造對象”的假象,貌似用處不大!!!
實際上,new Foo()這個函數調用實際上並沒有直接創建關聯,這個關聯只是一個意外的副作用。new Foo()只是間接完成了我們的目的:一個關聯到其他對象的新對象。那么有沒有更直接的方法做到這一點呢?當然,那就是Object.create(...)!
var bar=Object.create(foo)會創建一個新對象(bar),並把其[[prototype]]關聯到指定對象(foo)。這樣就可以充分發揮[[prototype]]委托的威力而避免不必要的麻煩(比如使用new 構造函數會生成.prototype和.constuctor的引用)
Object.create(null),會創建一個空[[prototype]]鏈的對象,這個對象無法進行委托。因此不會受到原型鏈的干擾,非常適合用於存儲數據。
在ES5之前的Object.create()的polyfill的代碼:
if(!Object.create){ Object.create=function(obj){ function Foo(){}; Foo.prototype=obj; return new Foo(); }; }
一、javascript中所謂“類”
類說白了只是一種設計模式(模板模式),在編程尤其是函數式編程中,類並不是必須的編程基礎,只是一種可選的代碼抽象。只不過在有些語言如java中,並不給你選擇的機會,在c/c++中,會提供過程化和面向類這兩種語法。類本身僅僅是一種抽象的表示,需要實例化才能對它進行操作。類通過復制操作被實例化為對象形式,繼承操作也類似,子類會包含父類行為的復制后的原始副本,因此子類可以重寫所有的繼承行為或者新行為而不會影響到父類。可見,在面向類的語言中,類的繼承,類的實例化本質上是復制。
javascript屬於哪一種呢?事實上,javascript擁有一些近似類的語法,但在近似類的表象之下,javascript的機制和類完全不同。在類的繼承以及實例化時,javascript的對象機制並不會自動執行復制行為,而是通過原型鏈關聯到實際的父類屬性,看起來似乎其他語言“繼承”的是方法的簽名,而javascript繼承的是實際的方法。
記住:javascript中只有對象,並不存在可以實例化的“類”,一個對象並不會被復制到其他對象中,他們只會被“關聯”起來。
同樣,在面向類的語言中,繼承、實例化着復制操作。就實例化來說,javascript不存在將類實例化為對象的說法,javascipt中本來就只有對象;就繼承來說,javascript只會在兩個對象之間創建一個關聯,這樣一個對象就可以通過委托訪問另一個對象的屬性和函數。“委托”這個術語比“繼承”更准確地描述Javascipt中對象的關聯機制!
不過,為了使得javascript表現出和其他語言類似的復制行為(不論繼承還是實例化,本質都是復制),javscript開發者想出了很多方法來模擬類的復制行為:如混入,寄生繼承和基於原型的繼承。
二、在javascript中模擬類的實現
1.混入
包括在jQuery源代碼中,模擬其他語言的類復制行為,這種方法就是“混入”(mixin)。
手動實現的mixin(在很多庫中也叫做extends)代碼如下:
function mixin(sourceObj,targetObj){ for(var key in sourceObj){ if(!(key in targetObj)){ targetObj[key]=sourceObj[key]; } } }
1)這不能夠解決javascript中的顯式偽多態問題,在javascript中調用類似父類中的同名方法沒有super這樣的用法(相對多態),只能使用絕對引用父類名.方法名.call,(顯式偽多態)。這樣的后果是,在支持多態的面向類的語言中,子類和父類的關系只需要在類定義的開頭創建即可,因此只需要在這一個地方維護兩個類的聯系。然而javascript的顯示偽多態會在所有需要使用多態的地方引入一個函數關聯,因而增加了代碼的維護成本。
2)混入也無法完全模擬類的復制行為,因為對象(和函數)只能復制引用,無法復制被引用的對象或函數本身。
2.寄生式繼承
寄生式繼承的主要推廣者是Doulgas Crockford.寄生式繼承的主要思路是創建一個用於封裝繼承過程的函數,該函數在內部以某種方式增強對象,最后返回這個對象。
function SuperType(name){ this.name=name; } SuperType.prototype.sayName=function(){ return this.name; }; //寄生式繼承 function SubType(name,age){ var instance=new SuperType(name);//實際上寄生式繼承中,這里不一定非得是new,凡是返回一個對象的操作都可以 var sayName=instance.sayName; instance.age=age; //方法重寫 instance.sayName=function(){ var name=sayName.call(this); console.log('welcome '+name); }; instance.sayAge=function(){ console.log(this.age); } return instance; } //測試 //事實上,使用new時候會創建一個對象,但是由於我們返回了Instance這個對象,new創建的這個對象會被忽略,因此下面的代碼加不加new實質上是一樣的 var subInstance=new SubType('bobo',28); subInstance.sayName();//輸出welcome bobo subInstance.sayAge();//輸出28
寄生式繼承的主要問題之一是不能復用函數,在上面的例子中,如果調用兩次SubType()[或者調用new SubType()達到的效果是一致的],那么返回的兩個SubType“實例”各自擁有自己的一套函數。此外,引用父類的方法,需要首先將方法的引用賦值給對應變量,如上面的代碼所示。
3.基於原型的繼承
基於原型的繼承,其思路是使用原型鏈實現對原型屬性和方法的繼承,借用構造函數實現對實例屬性的繼承,這也是被普遍使用的一種方法。下面是一個案例:
//基於原型的繼承 function SuperType(name){ this.name=name; } SuperType.prototype.getName=function(){ return this.name; }; function SubType(name,age){ SuperType.call(this,name); this.age=age; } SubType.prototype=Object.create(SuperType.prototype); SubType.prototype.sayName=function(){ //調用this的方法 var name=this.getName(); console.log('welcome '+name); } SubType.prototype.sayAge=function(){ console.log(this.age); } //測試 var subInstance=new SubType('bobo',28); subInstance.sayName();//輸出welcome bobo subInstance.sayAge();//輸出28
上面代碼最核心的部分就是:
SubType.prototype=Object.create(SuperType.prototype).這句話調用會創建一個新對象SubType.prototype,並將其內部的[[prototype]]關聯到指定對象,本例中是SuperType.prototype.
注意,有兩種常見的錯誤,實際上他們都有一些問題:
//達不到想要的效果 SubType.prototype=Foo.prototype; //基本達到需求,但存在一些副作用 SubType.prototype=new SuperType();
第一種,SubType.prototype=SuperType.prototype並不會創建一個關聯到Foo.prototype的新對象,而只是讓SubType.prototype直接引用SuperType.prototype,因此執行類似SubType.prototype.xxx的賦值語句的時候,將直接修改Foo.prototype對象本身。實際上這樣不是你想要的效果,因為此時根本不需要SubType對象,直接使用SuperType就可以了,代碼還可以更簡單。
第二種:SubType.prototype=new SuperType()的確會創建關聯到SuperType.protoType的新對象。但使用了SuperType的構造函數調用,如果函數SuperType有一些副作用(比如寫日志,注冊到其他對象等等),就會影響到SubType()的后代,造成不可預知的后果。其次是調用了兩次SuperType這個函數。第一次是在子類構造函數的內部,通過調用父類的SuperType為子類的對象添加name屬性;第二次是在設置子類原型鏈prototype的地方,對SuperType實行了構造調用,這會導致子類的原型鏈中也存在一個name屬性(並且值為undefined),只不過由於屬性屏蔽,子類實例對象中的name屬性屏蔽了其原型鏈中的name屬性b罷了。
三、javascipt中的“類”關系(稱為內省或者反射)
假設有對象a,如何尋找a委托的對象呢?
1)站在“類”的角度來判斷:
a instanceof Foo;
instanceof回答的問題是,在a的整條[[prototype]]鏈中是否有指向Foo.prototype的對象?其左操作符是一個對象,右操作符是一個函數。不能兩個對象(比如a和b)是否通過[[prototype]]相互關聯。
2)更簡潔的判斷[[prototype]]反射的方法
Foo.prototype.isPrototypeof(a)
isPrototypeof同樣能夠回答上述問題:在a的整條[[prototype]]鏈中,是否出現過Foo.prototype?
同樣的問題,同樣的答案,但在第二種方法中並不需要訪問函數Foo,只需要兩個對象就可以判斷他們之間的關系,
如:b.isPrototypeOf(c)
四、面向委托的設計
記住!javascript中只有對象,並不存在可以實例化的“類”,上述任何模擬類的方法都顯得不倫不類。對象直接定義自己的行為即可!!!
我們不需要類來創建兩個對象的關系,只需要通過委托來關聯對象就足夠了。從現在開始,盡量拋棄所有的function ,new (),.prototype的寫法!!
出於各種原因,以“父類”,“子類”,“繼承”,“多態”結束的屬於(包括原型繼承)和其他面向對象的術語都無法幫助理解javascript的真實機制!相比之下,“委托”是一個更合適的描述,委托行為意味着某些對象在找不到屬性或者方法引用時會把這個請求委托給另一個對象,javascript對象之間的關系是委托而不是復制!
下面以具體案例為例,對比基於類的寫法和基於委托的寫法二者的不同:
1.基於類的寫法
//基於類的寫法 function SuperType(name){ this.name=name; } SuperType.prototype.getName=function(){ return this.name; } function SubType(name,age){ SuperType.call(this,name); this.age=age; } SubType.prototype=Object.create(SuperType.prototype); SubType.prototype.getAge=function(){ return this.age; }; SubType.prototype.sayHello=function(){ var name=this.getName(); return 'hello '+name; }; //測試 var instance=new SubType('bobo',28); console.log(instance.sayHello());//輸出hello bobo console.log(instance.getAge());//輸出28
2.基於委托的寫法
//采用基於委托的寫法 var superObj={ init:function(name){ this.name=name; }, getName:function(){ return this.name; } }; var subObj=Object.create(superObj); //不能像下面這么寫了 //對象字面量的寫法會創建一個新對象賦值給subObj,原來的關聯就不存在了 // subObj={ // setup:function(name,age){ // this.init(name); // this.age=age; // }, // sayHello:function(){ // var name=this.getName(); // return 'hello '+name; // }, // getAge:function(){ // return this.age; // } // }; //只能這么寫 subObj.setup=function(name,age){ this.init(name); this.age=age; }; subObj.sayHello=function(){ return 'hello '+this.name; }; subObj.getAge=function(){ return this.age; }; var b1=Object.create(subObj); b1.setup('bobo',28); console.log(b1.sayHello()); //hello bobo console.log(b1.getAge()); //28 var b2=Object.create(subObj); b2.setup('leishao',27); console.log(b2.sayHello()); //hello leishao console.log(b2.getAge()); //27
對比兩種寫法:
1)基於委托的寫法中不會出現function構造函數,new,prototype,有的只是對象和對象之間的關聯;
2)基於類的寫法,子類通常和父類取相同的方法名來實現重寫的效果,而在基於委托的寫法中,則需要盡量避免這種方法,避免重名在原型鏈查找中引起不可預測的后果。
3)基於類的寫法中,屬性一般在構造函數中聲明,創建對象和對象初始化是在一次構造函數的調用中一次性完成的(var instance=new SuperType('bobo',28));然而在基於委托的寫法中,創建對象和對象初始化分開,分成了兩次(var b1=Object.crate(subObj);b1.setup('bobo',28)),屬性一般也在初始化函數中定義。這樣固然多謝了代碼,但同時也增加了靈活性,可以根據需要讓他們出現在不同的地方。