【javascript高階程式設計】讀書摘錄3 第六章、面向物件
第六章、面向物件的程式設計
這一章應該是Javascript中最抽象的一章,其中原型、原型鏈、建構函式等多次出現,幾乎貫穿了整個章節。而對於建立物件和繼承,也都是基於原型和建構函式而來的。因此這一部分的內容需要細細琢磨。尤其是對於原型、原型鏈,應該多畫圖,加深理解。
1、面向物件的語言有一個標誌,那就是它們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的物件。ECMAScript中沒有類的概念,因此它的物件業餘基於類的語言中的物件有所不同
2、建立物件:
(1)、最簡單的方式
建立一個Object例項,然後為它新增屬性和方法:
var person = new Object(); person.name = "Nico"; person.age = 29; person.job = "software engineer"; person.sayName = function(){ console.log( this.name ); } person.sayName();
這種模式有一個缺點,使用同一個介面建立很多物件時,會產生大量的重複程式碼。
(2)、工廠模式
這種模式抽象了穿件具體物件的過程:
function createPerson( name, age, job ){ var o = new Object(); o.name = name; o.job = job; o.age = age; o.sayName = function(){ console.log(this.name); }; return o; } var p1 = createPerson("Nico", 29,"soft eg"); var p2 = createPerson("Greg", 25,"doctor"); p1.sayName(); p2.sayName(); console.log(p1.constructor); console.log(p2.constructor);
工廠模式雖然解決了建立多個相似物件的程式碼冗餘問題,卻沒有解決物件識別的問題(無法區分物件的型別)
(3)建構函式模式
function Person( name, age, job ){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ console.log( this.name ); } } var p1 = new Person("Nico", 29,"soft eg"); var p2 = new Person("Greg", 25,"doctor"); p1.sayName(); p2.sayName(); console.log(p1.constructor); console.log(p2.constructor); console.log(p1 instanceof Person); console.log(p1 instanceof Object);
物件的constructor屬性最初是用來標識物件屬性的,但是,提到檢測物件型別,還是instanceof更加可靠一點
雖然建構函式非常好用,但是並非沒有缺點,使用建構函式的主要問題,就是每個方法都要在每個例項上重新建立一遍,上述例子中,p1和p2的sayName被建立了兩遍,這兩個函式並不相等:
console.log(p1.sayName == p2.sayName);//false
建立兩個相同的函式並沒有必要,可以把sayName函式移到建構函式外部來解決這個問題:
function Person( name, age, job ){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
console.log( this.name );
}
var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Greg", 25,"doctor");
p1.sayName();
p2.sayName();
這樣又帶來了新的問題:全域性變數中定義的函式實際上只能被某個物件呼叫,這讓全域性作用域名不副實。如果物件需要定義很多方法,那麼就需要定義很多全域性函式,於是這個自定義的引用型別就毫無封裝性可言了。可以用原型模式來解決這些問題。
(4)、原型模式
每個函式都有一個prototype屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由任何特定型別的所有例項共享的屬性和方法。使用原型物件的好處是讓所有物件例項共享它所包含的屬性和方法。因此,不必在建構函式中定義物件例項的資訊,而是可以將這些資訊直接新增到原型物件中:
function Person(){
}
Person.prototype.name = "Nico";
Person.prototype.age = 29;
Person.prototype.job = "soft eg";
Person.prototype.sayName = function(){
console.log( this.name );
}
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
console.log(p1.sayName == p2.sayName);//true
上述例子中,建立的新物件具有相同的屬性和方法,新物件的屬性和方法是由所有例項共享的。p1和p2訪問的都是同一組屬性和同一個sayName函式。
理解原型物件:
無論何時,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向函式的原型物件。在預設情況下,所有原型物件都會自動獲取一個constructor屬性,這個屬性包含一個指向prototype屬性所在函式的指標:
function Person(){
}
Person.prototype.name = "Nico";
Person.prototype.age = 29;
Person.prototype.job = "soft eg";
這個例子中,Person.prototype.constructor指向Person。Person的原型物件預設只會取得constructor屬性,至於其他方法,則都是從Object中繼承而來的。呼叫函式建立一個例項後,例項的內部將包含一個指標,指向建構函式的原型物件。在很多實現中,這個內部屬性的名字是__proto__,而且通過指令碼可以訪問到(FireFox,Safari,Chrome和Flash的ActionScript中,都可以通過指令碼訪問__proto__),例如在firefox中:
function Person(){
}
Person.prototype.name = "Nico";
Person.prototype.age = 29;
Person.prototype.job = "soft eg";
var p = new Person();
console.log(p);
打印出的結果:
這個連線存在於例項與建構函式的原型物件之間,而不是例項與建構函式之間。下圖展示了各個物件之間的關係(分別是Person的建構函式,Person的原型物件和Person的兩個例項):
在此,Person.prototype指向了原型物件,而Person.prototype.constructor又指回了Person。原型物件除了包含constructor屬性外,還包括後來新增的其他屬性。Person的每個例項,person1和person2都包含一個內部屬性,該屬性僅僅指向了Person.prototype,它們與建構函式沒有直接的關係。值得注意的是,雖然這兩個例項都不包含屬性和方法,但是卻可以呼叫person1.sayName,這是通過查詢物件屬性的過程來實現的。可以通過isPrototypeOf()確定物件之間的關係。
物件屬性的查詢過程:每當程式碼讀取某個物件的屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋先從物件例項本身開始,如果在例項中找到了具有給定名字的屬性,那麼返回該屬性的值;如果沒有找到,則繼續搜尋指標指向的原型物件,在原型物件中查詢屬性,如果在原型物件中找到了這個屬性,則返回該屬性的值。
雖然可以通過物件例項訪問儲存在原型,但卻不能通過物件例項重寫原型中的值。如果在例項中添加了一個屬性,(如果原型物件中存在該屬性)那麼該屬性就會覆蓋原型物件中的屬性:
function Person(){
}
Person.prototype.name = "Nico";
Person.prototype.age = 29;
Person.prototype.job = "soft eg";
Person.prototype.sayName = function(){
console.log( this.name );
}
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
p1.name = "new name";
console.log(p1.name);// new name 來自例項p1
console.log(p2.name);// Nico 來自原型物件
為物件新增一個屬性時,這個屬性就會遮蔽原型物件中的同名屬性,換句話說,新增這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使把這個屬性設定為null, 也不會恢復其指向原型的連線。不過,使用delete操作符則可以完全刪除例項屬性,從而可以從新訪問原型中的屬性:
var p1 = new Person();
var p2 = new Person();
p1.name = "new name";
delete p1.name;
delete p2.name;
console.log(p1.name);
console.log(p2.name);
使用hasOwnProperty()方法可以檢測一個屬性時存在於例項中,還是存在於原型中,這個方法只有在給定屬性存在於物件例項中時,才會返回true:
var p1 = new Person();
var p2 = new Person();
console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
p1.name = "new name";
console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
delete p1.name;
delete p2.name;
console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
in操作符可以檢測通過物件能否訪問特定的屬性,無論這個屬性存在於原型中還是存在於例項中。
for-in迴圈可以返回所有能夠通過物件訪問的,可列舉的屬性,其中既包含了存在例項中的屬性,也包含了存在於原型中的屬性:
function Person(){
}
Person.prototype.name = "Nico";
Person.prototype.age = 29;
Person.prototype.job = "soft eg";
Person.prototype.sayName = function(){
console.log( this.name );
}
var p1 = new Person();
p1.aprop = 'a property';
p1.aprop2 = 'another property';
for( var pro in p1 ){
console.log(pro + ":" + p1[pro]);
}
IE中存在一個bug,即遮蔽不可列舉的例項屬性不會出現在for-in屬性中。
為了從視覺上更好地封裝原型的功能,更常用的做法是用一個包含所有屬性和方法的物件字面量來重寫整個原型物件:
function Person(){
}
Person.prototype = {
name: "Nico",
age : 29,
job : "soft eg",
sayName:function(){
console.log(this.name );
}
};
這種方法建立的物件,其constructor屬性不再指向Person了,儘管通過instanceOf還能返回正確的結果,但是constructor已經無法確定物件的型別了:
function Person(){
}
Person.prototype = {
name : "Nico",
age : 29,
job : "soft eg",
sayName: function(){
console.log( this.name );
}
};
var p1 = new Person();
console.log(p1.constructor);
console.log(p1 instanceof Person);
如果constructor值特別重要,可以將它設定為適當的值:
Person.prototype = {
constructor:Person,
name : "Nico",
age : 29,
job : "soft eg",
sayName: function(){
console.log( this.name );
}
};
儘管可以隨時為原型新增屬性和方法,並且修改能夠立即在所有的物件例項中反應出來,但如果是重寫整個原型物件,那麼情況就不一樣了:呼叫建構函式會為例項新增一個指向最初原型的__proto__指標,而把原型修改為另一個物件就等於切斷了建構函式與最初的原型物件之間的聯絡:例項中的指標僅僅指向原型,而不指向建構函式:
function Person(){
}
var p1 = new Person();
Person.prototype = {
name : "Nico",
age : 29,
job : "soft eg",
sayName: function(){
console.log( this.name );
}
};
console.log(p1.__proto__);
var p2 = new Person();
console.log(p2.__proto__);
原型模式的重要性不僅體現在自定義型別方面,就連所有的引用型別,都是按照這種方式建立的,通過原生物件的原型,不僅可以取得所有預設方法的引用,而且可以定義新方法:
String.prototype.startWith =function(text){
returnthis.indexOf(text) == 0;
}
var msg = "hello world";
console.log(msg.startWith("hello"));
原型物件的問題:
原型模式的缺點在於:它省略了為建構函式傳遞初始化引數這一環節,結果所有例項在預設的情況下都取得相同的屬性值。原型模式的最大問題是由其共享的本性導致的:
function Person(){
}
Person.prototype = {
name : "Nico",
age : 29,
job : "soft eg",
friends: ["f1", "f2"],
sayName: function(){
console.log( this.name );
}
}
var p1 = new Person();
var p2 = new Person();
p1.friends.push("f3");
console.log(p1.friends);
console.log(p2.friends);
基於以上原因,很少單獨使用原型模式。比較常用的方式是組合使用建構函式模式和原型模式:
function Person( name, age, job ){
this.name = name;
this.job = job;
this.age = age;
this.friends = [ ];
}
Person.prototype = {
constructor: Person,
sayName : function(){
console.log( this.name );
}
}
var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Gego", 25,"doctor");
p1.friends.push("f1","f2");
p2.friends.push("f2","f3","f4");
console.log(p1.friends);
console.log(p2.friends);
這個例項中,所有的例項屬性都是在建構函式中定義的,而所有例項共享的屬性則是在原型中定義的。這種建構函式與原型混合而成的模式,是目前ECMAScript中使用最廣泛、認同度最高的一種建立自定義型別的方式。
動態原型模式:
可以通過判斷某個應該存在的方法是否有效,來決定是否需要初始化原型:
function Person( name, age, job ){
this.name = name;
this.job = job;
this.age = age;
if( typeof this.sayName != "function" ){
Person.prototype.sayName = function(){
console.log( this.name );
}
}
}
var p1 = new Person("Nico", 29,"soft eg");
p1.sayName();
寄生建構函式模式:
這種模式的基本思想與建構函式相似,不同的是建立物件的方式:
function Person( name, age, job ){
var o = new Object();
o.name = name;
o.job = job;
o.age = age;
o.sayName = function(){
console.log( this.name );
};
return o;
}
var p1 = new Person("Nico", 29,"soft eg");
p1.sayName();
寄生建構函式模式返回的物件與建構函式或者建構函式的原型屬性之間沒有關係,也就是說,寄生建構函式返回的物件與建構函式外部建立的物件沒什麼不同,不能依賴於instanceof來確定物件型別。
穩妥建構函式模式:
穩妥物件,指的是沒有公共屬性,而且方法也不引用this的物件,穩妥物件使用於一些安全的環境中。穩妥物件與寄生建構函式有兩點不同:一是建立物件的例項方法不引用this,二是不適用new操作符呼叫建構函式:
function Person( name, age, job ){
var o = new Object();
o.sayName = function(){
console.log( name );
};
return o;
}
var p1 = Person("Nico", 29,"soft");
var p2 = Person("cc", 22, "soft");
p1.sayName();
p2.sayName();
繼承:
許多OO語言都支援兩種繼承方式,介面繼承和實現繼承,介面繼承只繼承方法簽名,實現繼承則繼承實際的方法。由於ECMA中函式沒有簽名,因此ECMAScript中無法實現介面繼承,只支援實現繼承,這是通過原型鏈來實現的。
ECMAScript中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法,其基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。建構函式、原型和例項的關係:每個建構函式都有一個原型物件,原型物件包含一個指向建構函式的指標(constructor),而例項則包含一個指向原型物件的內部指標。因此,如果讓原型物件等於另一個型別的例項,結果是此時的原型物件將包含一個指向另一個原型的指標,相應的,另一個原型中也包含了指向另一個建構函式的指標。如此層層遞進,就構成了例項與原型的鏈條,這就是所謂原型鏈的基本概念。
實現原型鏈的基本模式:
function SuperType(){
this.name = "super";
}
SuperType.prototype.getSuperValue =function(){
console.log( this.name) ;
};
function SubType(){
this.subName = "sub";
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
console.log( this.subName );
}
var instance = new SubType();
instance.getSuperValue();
instance.getSubValue();
console.log(instance instanceof Object);
console.log(instance instanceof SuperType);
console.log(instance instanceof SubType);
實現的本質是重寫原型物件,代之以一個新型別的例項。原來存在於SuperType例項中的所有屬性和方法,現在也存在於SubType.prototype中了。這個例子中的例項以及建構函式和原型之間的關係如下:
通過實現原型鏈,本質上擴充套件了前面接掃的原型搜尋機制。當讀取一個例項屬性時,首先會在例項中搜索該屬性,如果沒有找到該屬性,就會繼續搜尋例項的原型,在通過原型鏈實現繼承的情況下,搜尋過程就會沿著原型鏈繼續向上。上面的過程:搜尋例項、搜尋SubType.prototype, 搜尋SuperType.prototype.。
事實上,上面介紹的原型鏈還缺少一環,所有的引用型別都繼承自Object,而這個繼承也是通過原型鏈實現的。因此,所有的函式的預設原型都是Object的例項,因此預設原型都包含一個內部指標,指向Object.prototype,這也是所有的自定義型別預設都會繼承toString, valueOf()等預設方法的根本原因.一句話:SubType繼承了SuperType,而SuperType繼承了Object,當呼叫instance.toString時,實際上呼叫的是儲存在Object.prototype中的那個方法。
確定原型和例項的關係:instanceof操作符和isPrototypeOf函式。
子型別有時需要重寫超型別中的某個方法,或者需要新增超型別中不存在的某個方法。不管怎樣,給原型新增方法的程式碼一定要放在替換原型的語句之後(即SubType.protoType = new SuperType()之後)。而且通過原型鏈實現繼承時,不能使用物件字面量建立原型方法,因為這樣做為重寫原型鏈。
原型鏈的兩個問題:(1)引用型別屬性的共享問題。(2)不能向超型別的建構函式中傳遞引數。由於上述兩個原因,實踐中很少單獨使用原型鏈。
借用建構函式:
這種技術的基本思想非常簡單,子子型別的建構函式內部呼叫超型別建構函式。函式只不過是在特定環境中執行程式碼的物件,因此可以通過apply和call方法在新建立的物件上執行建構函式:
function Super(){
this.colors = ["red", "green", "blue"];
}
function Sub(){
Super.call(this);
}
var p1 = new Sub();
p1.colors.push("yellow");
console.log(p1.colors);
var p2 = new Sub();
console.log(p2.colors);
組合繼承:組合繼承也叫偽經典繼承,試講原型鏈和建構函式的技術組合在一起,發揮兩者之長。基本思路是使用原型鏈實現對原型屬性和方法的繼承,而通過建構函式實現對例項屬性的繼承。這樣,即通過原型上定義方法實現了函式複用,又能保證每個例項都有自己的屬性:
function Super( name ){
this.name = name;
this.colors = ["red", "blue"];
}
Super.prototype.sayName = function(){
console.log( this.name );
}
function Sub(name , age){
Super.call(this, name);
this.age = age;
}
Sub.prototype = new Super();
Sub.prototype.sayAge = function(){
console.log( this.age );
}
var p1 = new Sub("nico", 29);
p1.colors.push("green");
p1.sayName();
p1.sayAge();
組合繼承是javascript中最常用的繼承模式。
寄生組合式繼承時引用型別最理想的繼承正規化:
function obj( o ){
function F(){}
F.prototype = o;
return new F();
}
function inherit(Sub, Super){
var proto = obj( Super.prototype );
proto.constructor = Sub;
Sub.prototype = proto;
}
function Super(){
this.name = name;
this.colors = ["red", "blue" ];
}
Super.prototype.sayName = function(){
console.log( this.name );
}
function Sub(name, age){
Super.call( this, name );
this.age = age;
}
inherit(Sub, Super);
Sub.prototype.sayAge = function(){
console.log( this.age );
}
TODO: YUI