說在前面:
為了使程式碼更為簡潔方便理解, 本文中的程式碼均將“非核心實現”部分的程式碼移出。
一、原型鏈方式
關於原型鏈,可點選《深入淺出,JS原型鏈的工作原理》,本文不再重複敘述。
思路:讓子建構函式的原型等於父建構函式的例項
function A() {
}
A.prototype.fn = function (){
console.log("in A");
}
function B() {
}
B.prototype = new A(); // 讓子建構函式的原型等於父建構函式的例項
var b = new B();
b.fn(); // in A
console.log(b instanceof B); // true
console.log(b instanceof A); // true
console.log(b instanceof Object); // true
缺陷:如果父建構函式中的屬性為引用型別,則子建構函式的例項會出現相互影響的情況;
function A() {
this.prop = [`1`,"2"];
}
A.prototype.fn = function (){
console.log(this.prop);
}
function B() {
}
B.prototype = new A();
var b1 = new B();
var b2 = new B();
b1.fn(); // ["1", "2"]
b2.fn(); // ["1", "2"]
b1.prop.push(`3`); // 子建構函式例項b1修改繼承過來的屬性
b2.prop.push(`4`); // 子建構函式例項b2修改繼承過來的屬性
b1.fn(); // ["1", "2", "3", "4"] // b2上的修改影響了b1
b2.fn(); // ["1", "2", "3", "4"] // b1上的修改影響了b2
*導致缺陷原因:引用型別,屬性變數儲存的是地址指標而非實際的值,這個指標指向了一塊用來儲存實際內容的地址。例項化後,所有例項中變數儲存了同一個指標,均指向同一個地址,當任何一個例項通過指標修改地址的內容(並非重新賦予新的指標地址或者修改指標指向)時,其他例項的也會受到影響。
二、借用建構函式方式
為了解決“原型鏈方式”繼承的缺陷,引入的一種“繼承”方案。
思路:通過call/apply,在子建構函式中呼叫父類的建構函式
function A() {
this.prop = [`1`,"2"];
this.fn2 = function () {
console.log(this.prop);
}
}
A.prototype.fn = function (){
console.log(this.prop);
}
function B() {
A.call(this); // 通過call/apply,在子建構函式中呼叫父類的建構函式
}
var b1 = new B();
var b2 = new B();
b1.fn2(); // ["1", "2"]
b2.fn2(); // ["1", "2"]
b1.prop.push(`3`);
b2.prop.push(`4`);
b1.fn2(); // ["1", "2", "3"]
b2.fn2(); // ["1", "2", "4"]
b1.fn(); // 提示異常:b1.fn is not a function
console.log(b1 instanceof B); // true
console.log(b1 instanceof A); // false
console.log(b1 instanceof Object); // true
缺陷:由於“繼承”過程中,A僅充當普通函式被呼叫,使得父建構函式A原型無法與形成子建構函式B構成原形鏈關係。因此無法形成繼承關係:”b1 instanceof A”結果為false,B的例項b1亦無法呼叫A原型中的方法。實際意義上,這種不屬於繼承。
三、組合繼承
結合“原型鏈方式”和“借用建構函式方式”的有點,進行改進的一種繼承方式。
思路:原型上的屬性和方法通過“原型鏈方式”繼承;父建構函式內的屬性和方法通過“借用建構函式方式”繼承
function A() {
this.prop = [`1`,"2"];
}
A.prototype.fn = function (){
console.log(this.prop);
}
function B() {
A.call(this); // 借用建構函式方式
}
B.prototype = new A(); // 原型鏈方式
var b1 = new B();
var b2 = new B();
b1.fn(); // ["1", "2"]
b2.fn(); // ["1", "2"]
b1.prop.push(`3`);
b2.prop.push(`4`);
b1.fn(); // ["1", "2", "3"]
b2.fn(); // ["1", "2", "4"]
console.log(b1 instanceof B); // true
console.log(b1 instanceof A); // true
console.log(b1 instanceof Object); // true
缺陷:子建構函式的原型出現一套冗餘“父建構函式非原型上的屬性和方法”。上述程式碼在執行“A.call(this);”時候,會給this(即將從B返回給b1賦值的物件)新增一個“prop”屬性;在執行“B.prototype = new A();”時,又會通過例項化的形式給B的原型賦值一次“prop”屬性。顯然,由於例項屬性方法的優先順序高於原型上的屬性方法,絕大多數情況下,原型上的“prop”是不會被訪問到的。
四、寄生組合式繼承
為了解決“組合繼承”中子建構函式的原型鏈出現冗餘的屬性和方法,引入的一種繼承方式。
思路:在組合繼承的基礎上,通過Object.create的方式實現原型鏈方式
function A() {
this.prop = [`1`,"2"];
}
A.prototype.fn = function (){
console.log(this.prop);
}
function B() {
A.call(this);
}
B.prototype = Object.create(A.prototype); // Object.create的方式實現原型鏈方式
var b1 = new B();
var b2 = new B();
b1.fn(); // ["1", "2"]
b2.fn(); // ["1", "2"]
b1.prop.push(`3`);
b2.prop.push(`4`);
b1.fn(); // ["1", "2", "3"]
b2.fn(); // ["1", "2", "4"]
console.log(b1 instanceof B); // true
console.log(b1 instanceof A); // true
console.log(b1 instanceof Object); // true
最後補充
1、因為子建構函式的例項自身沒有constructor屬性,當我們訪問例項的constructor屬性時,實際是訪問原型的constructor屬性,該屬性應該指向(子)建構函式。但是上述例子中,程式碼均會指向父建構函式。為了與ECMAScript規範保持一致,在所有的“原型鏈繼承”後,應當將原型的constructor屬性指向子建構函式本身:
B.prototype = ....
--> B.prototype.constructor = B; <--
...
2、Object.create是ECMAScript 5中加入的一個函式,這個函式的功能是:將入參(需為一個物件)作為原型,建立並返回一個新的(只有原型的)的物件。此功能等價於:
function object(o){
function F(){}
F. prototype = o;
return new F();
} // 來源於《JavaScript高階程式設計(第3版)》