1.原型鏈
javascript中沒有類的概念,需要利用原型鏈來模擬。
我們知道,構造函數、原型對象、實例之間有如下關系:
- 構造函數會自動獲得一個prototype屬性,指向其原型對象
- 原型對象在函數創建時自動獲得,其中有一個constructor屬性,指向其構造函數
- 實例中有一個
__proto__
指針,指向該類型的原型對象
如下圖所示:
原型鏈實現繼承,就是將一個類型的原型(子類型)指向另一個類型(父類型)的一個實例。
我們知道,在尋找標識符(也就是變量或者函數)時,會先尋找實例屬性,找到了即返回,如果沒有找到,即通過__proto__
指針去查找原型屬性。
當用父類型的實例作為子類型的原型對象后,就可以找到父類型中的全部屬性和方法,也就實現了繼承,同時也切斷了子類型構造函數與本來的原型對象的聯系。如下圖所示:
//父類型構造函數
function SuperType() {
this.supername = "super"
}
//父類型原型對象
SuperType.prototype.getSuperName = function () {
console.log(this.supername)
};
//子類型構造函數
function SubType() {
this.subname = "sub";
}
//改變子類型原型屬性的指向,圖中1標記所示。
//同時切斷了子構造函數與它在創建時自動獲得的原型對象的聯系,如圖中紅叉所示。
SubType.prototype = new SuperType();
//添加子類型的方法
SubType.prototype.getSubName = function () {
console.log(this.supername)
};
var subInstance = new SubType();
subInstance.getSuperName();//輸出super,獲得了父類型的方法
有一點需要注意,所有的實例都是Object類型的實例,這是因為函數創建時直接獲得的原型對象的__proto__
指針都是指向的Object.prototype
。
而此時,子類型的實例又可以作為另一個類型(孫子)的原型對象,孫子類型的實例又可以作為曾孫類型的原型對象,由此形成一個鏈條。
純原型鏈繼承的問題
一個類型(父)的實例作為另一個類型(子)的原型對象,那么父類型的實例屬性就變成了子類型實例的原型屬性,如果這屬性恰好是一個引用類型,那么這個屬性所對應的引用就會被所有子類實例所共享。一個子類型實例對這個屬性的任何操作都會影響其他所有的子類型實例。
2.借用構造函數實現繼承
暗中觀察一個構造函數:
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
var person1 = new Person();
可以看到,構造函數中的屬性都定義在this
上。回憶一下使用new
操作符創建一個實例時的步驟:新建一個對象;然后讓this
指向這個對象,執行構造函數;構造函數執行完后,這個對象上就有創建好的各種屬性;最后返回這個對象。
同樣的道理,我們可以在子類型構造函數中使用call()
、apply()
方法來改變構造方法的作用域,使其this
指向子類構造函數的作用域,達到在子類型中創建父類型中定義的各種屬性的目的。
//示例代碼來自《JavaScript高級程序設計》
function SuperType(name) {
this.name = name;
}
function SubType() {
SuperType.call(this,"gordenz");
this.age = "29";
}
var instance = new SubType();
console.log(instance.name); //gordenz,獲得了定義在父類中的屬性
console.log(instance.age); //29
call()
,apply()
的區別:第一個參數都是函數執行的作用域 ;apply()
的第二個參數接收一個參數數組,而call()
方法則需要將接收的參數一一列在后面(即除了第一個參數外,可能有很多個參數)
全部使用構造函數實現繼承的問題
1.無法實現同一函數的復用,每個實例都會創建一個副本;
2.子類型無法繼承到定義在父類型的原型對象中的屬性和方法。
3.組合繼承
組合繼承可以類比創建對象時的構造函數模式與原型模式。
即,使用原型鏈繼承原型屬性和方法,使用構造函數繼承實例屬性。
//示例代碼來自《JavaScript高級程序設計》
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function () {
alert(this.name)
};
function SubType(name,age) {
//繼承屬性
SuperType.call(this,name);
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();
//為作為原型對象的父類型實例添加constructor屬性
SubType.prototype.constructor = SubType;
//添加屬于子類的原型方法
SubType.prototype.sayAge = function () {
alert(this.age);
};
var instance1 = new SubType("gordenz",24);
instance1.colors.push('black');
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //gorden
instance1.sayAge(); //24
var instance2 = new SubType("huan",23);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //huan
instance2.sayAge(); //23
//父類的原型方法都得到了共享,而引用類型的屬性也不會在各實例間相互影響
這樣就完成了一個較好的繼承,兩個實例都擁有自己的屬性,同時又公用了方法。
相對之前的原型鏈繼承,此處多了一個步驟,就是將父類型的實例作為子類型的原型對象后,為其手工添加了一個constructor屬性,使其指向了我們邏輯上的構造函數。如圖:
如圖中2所示,我們手工為子類型的原型對象(它也是一個父類型的實例)指定了constructor屬性,讓他正確的指向子類型的構造函數。
如果我們沒有手動指定這個constructor屬性,那么此時他其實是指向的父類的構造函數。為什么呢?因為當前子類原型對象是一個父類型的實例,這個constructor屬性繼承自父類原型對象,所以他的指向是父類構造函數,這與我們的邏輯不符。
同時圖中也展示了原型鏈的終點:null。