JS中繼承方式總結

Lycop發表於2019-02-16

說在前面:
為了使程式碼更為簡潔方便理解, 本文中的程式碼均將“非核心實現”部分的程式碼移出。


一、原型鏈方式
關於原型鏈,可點選《深入淺出,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版)》

相關文章