說在前面:
爲了使代碼更爲簡潔方便理解, 本文中的代碼均將「非核心實現」部分的代碼移出。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版)》