JS中的原型可以類比於Java中的父類。
在Java中實現繼承有介面繼承與實現繼承兩種方式,介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法。
由於JS中的函式沒有簽名,在ECMAScript
無法實現介面繼承,ECMAScript
只支援實現繼承,而其實現繼承的主要依靠原型與原型鏈來實現的。
原型模式
我們建立的每個函式都有一個prototype(原型)
屬性,這個屬性是一個指標,指向一個物件(原型物件),而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。如下面例子所示:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
複製程式碼
在上面程式碼中,我們將sayName
方法直接新增到Person
的prototype
屬性中,建構函式為空,此時通過建構函式生成的person1
與person2
兩個例項物件,均可共享Person.prototype
的屬性與方法。
理解原型物件
無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype
屬性,這個屬性指向函式的原型物件。在預設情況下,所有原型物件都會自動獲得一個constructor(構造)
屬性,這個屬性是一個指向prototype
屬性所在函式的指標。拿上面的程式碼示例來說,Person.prototype.constructor
指向Person
,通過Person.prototype
可繼續為原型物件新增方法與屬性。
建立了自定義的建構函式後,其原型物件預設只會取得constructor
屬性,至於其他方法,都是從Object
繼承而來的。當呼叫建構函式建立一個新例項後,該例項的內部將包含一個指標(內部屬性[[Prototype]]
),指向建構函式的原型物件。雖然在指令碼中沒有標準方式訪問[[Prototype]]
,但每個物件上支援一個屬性__proto__
,可通過該屬性訪問原型物件。
需要注意的是,
__proto__
這個連線存在於例項物件與原型物件之間,而不是存在於例項於建構函式之間。
以前面使用Person
建構函式建立例項物件的程式碼為例,下圖展示了Person
與Person1
、Person2
及原型物件之間的關係。
原型關係判斷
對於判斷物件之間是否存在原型關係,有以下三種方式實現。
__proto__
alert(person1.__proto__ == Person.prototype) //true
alert(person2.__proto__ == Person.prototype) //true
複製程式碼
由於
person1.__proto__
與person2.__proto__
都指向原型物件,而Person.prototype
也指向原型物件,所以返回值都為true
isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
複製程式碼
A.isPrototypeOf(B)
,判斷A
是否是B
的原型物件。對於person1.__proto__.isPrototypeOf(person1)
的返回值,也是為true,因為person1.__proto__
等於Person.prototype
。
getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
複製程式碼
Object.getPrototypeOf(person1)
返回person1
的原型物件。
屬性操作
屬性讀取
每當程式碼讀取某個物件的屬性時,都會執行一次搜尋,目標是給定名字的屬性。搜尋首先從例項物件本身開始,如果物件本身存在該屬性,則直接返回該屬性的值,如果沒有找到,則繼續搜尋該物件的原型物件,若有就返回屬性值,如果都沒找到,則會返回undefined
前面提到過,原型物件最初只包含
constructor
屬性,而該屬性也是共享的,因此可以通過例項物件訪問
alert(person2.constructor) //function Person(){}
複製程式碼
呼叫person2.constructor
時返回function Person(){}
,證明了原型物件的constructor
屬性確實指向建構函式。
屬性修改
雖然可以通過例項訪問儲存在原型中的值,但卻不能通過例項物件重寫原型中的值。如果我們在例項中新增了一個屬性,而該屬性與例項原型中的一個屬性同名,那我們就在例項中建立該屬性,該屬性會遮蔽原型中的那個屬性,如下所示。
person1.name = "Greg";
alert(person1.name); //"Greg" ——來自例項
alert(person2.name); //"Nicholas" ——來自原型物件
複製程式碼
當為物件新增一個屬性時,這個屬性就會遮蔽原型物件中儲存的同名屬性,雖然原型物件中的同名屬性依舊存在;想要取消遮蔽,可以使用delete
操作符完全刪除例項物件中的屬性,然後才能訪問原型中的屬性,如下所示。
person1.name = "Greg";
alert(person1.name); //"Greg" ——來自例項
alert(person2.name); //"Nicholas" ——來自原型物件
delete person1.name;
alert(person1.name); //"Nicholas" ——來自原型物件
複製程式碼
判斷屬性是否存在hasOwnProperty
此外,使用hasOwnProperty()
方法可以檢測一個屬性是存在於例項中(該方法也是從Object
中繼承而來),只有在給定屬性存在於例項物件中時,才會返回true
。
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //"Greg"——來自例項
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"——來自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //"Nicholas"——來自原型
alert(person1.hasOwnProperty("name")); //false
複製程式碼
判斷屬性是否存在in
操作符
此外,使用in
操作符可以檢測一個屬性是存在於例項或其原型物件中,存在返回true
。
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
複製程式碼
對於屬性name
,它存在於person1
的原型物件中,但不存在於person1
例項中,所以呼叫hasOwnProperty
方法返回false
,呼叫in
操作符返回true
.
組合使用
hasOwnProperty
與in
操作符可正確判斷屬性是存在於例項中還是原型物件中。
原型物件的問題
- 省略了為建構函式傳遞初始化引數這一環節,導致所有例項預設情況下都將取得相同的屬性值。
- 原型中的屬性被很多例項共享,這對於函式來說非常合適,對於那些包含基本值得屬性來說也可以,但對於引用型別來說,問題就比較突出了。
function Person(){
}
Person.prototype = {
constructor: Person,
friends : ["Shelby", "Court"],
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
複製程式碼
person1
對屬性friends
的任何操作,都會立馬反應到person2
上。