JS中繼承方式總結

說在前面:
爲了使代碼更爲簡潔方便理解, 本文中的代碼均將「非核心實現」部分的代碼移出。segmentfault


1、原型鏈方式
關於原型鏈,可點擊《深刻淺出,JS原型鏈的工做原理》,本文再也不重複敘述。app

思路:讓子構造函數的原型等於父構造函數的實例函數

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

缺陷:若是父構造函數中的屬性爲引用類型,則子構造函數的實例會出現相互影響的狀況;this

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

*致使缺陷緣由:引用類型,屬性變量保存的是地址指針而非實際的值,這個指針指向了一塊用來保存實際內容的地址。實例化後,全部實例中變量保存了同一個指針,均指向同一個地址,當任何一個實例經過指針修改地址的內容(並不是從新賦予新的指針地址或者修改指針指向)時,其餘實例的也會受到影響。prototype


2、借用構造函數方式
爲了解決「原型鏈方式」繼承的缺陷,引入的一種「繼承」方案。設計

思路:經過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原型中的方法。實際意義上,這種不屬於繼承。code


3、組合繼承
結合「原型鏈方式」和「借用構造函數方式」的有點,進行改進的一種繼承方式。對象

思路:原型上的屬性和方法經過「原型鏈方式」繼承;父構造函數內的屬性和方法經過「借用構造函數方式」繼承繼承

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」是不會被訪問到的。


4、寄生組合式繼承
爲了解決「組合繼承」中子構造函數的原型鏈出現冗餘的屬性和方法,引入的一種繼承方式。

思路:在組合繼承的基礎上,經過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

最後補充
一、由於子構造函數的實例自身沒有constructor屬性,當咱們訪問實例的constructor屬性時,實際是訪問原型的constructor屬性,該屬性應該指向(子)構造函數。可是上述例子中,代碼均會指向父構造函數。爲了與ECMAScript規範保持一致,在全部的「原型鏈繼承」後,應當將原型的constructor屬性指向子構造函數自己:

B.prototype = ....
--> B.prototype.constructor = B; <--
    ...

二、Object.create是ECMAScript 5中加入的一個函數,這個函數的功能是:將入參(需爲一個對象)做爲原型,建立並返回一個新的(只有原型的)的對象。此功能等價於:

function object(o){ 
    function F(){}
    F. prototype = o; 
    return new F(); 
} // 來源於《JavaScript高級程序設計(第3版)》
相關文章
相關標籤/搜索