一、JavaScript 的繼承
許多面向對象語言都支持兩種繼承的方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。在 JavaScript 中由於函數沒有簽名也就無法實現接口繼承,而只支持實現繼承,而且實現繼承主要通過原型鏈來實現的。
先引述下官方文檔對於原型鏈的描述:其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。要理解這個概念要先弄清楚構造函數,原型,和實例的關系:每個構造函數(只要是函數)都有一個 prototype 屬性該屬性指向一個對象(這個對象就是構造函數的原型對象);原型對象(只要是對象)中都有一個 constructor 屬性該屬性指向一個構造函數;而實例中都包含一個指向原型對象的內部指針 `Prototype`。說白了就是原型鏈的構建是通過將一個類型的實例賦值給另一個構造函數的原型實現的。這樣子類型就可以訪問定義在超類型上的所有屬性和方法了。每個對象都有自己的原型對象,以原型對象為模板從原型對象中繼承屬性和方法,原型對象也可以有自己的原型並從中繼承屬性和方法,一層一層,以此類推,這種關系被稱為原型鏈它解釋了為何一個對象會擁有定義在其他對象上的屬性和方法。
二、JavaScript實現繼承的方式
1、原型鏈繼承
2、構造函數繼承
3、組合繼承
4、原型式繼承
5、寄生式繼承
6、寄生組合式繼承
1、原型鏈繼承
// 實現原型鏈的一種基本模式 function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } // 繼承,用 SuperType 類型的一個實例來重寫 SubType 類型的原型對象 SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; }; var instance = new SubType(); alert(instance.getSuperValue()); // true
其中,SubType 繼承了 SuperType,而繼承是通過創建 SuperType 的實例,並將該實例賦值給 SubType 的原型實現的。
實現的本質是重寫子類型的原型對象,代之以一個新類型的實例。子類型的新原型對象中有一個內部屬性 `Prototype` 指向了 SuperType 的原型,還有一個從 SuperType 原型中繼承過來的屬性 constructor 指向了 SuperType 構造函數。
最終的原型鏈是這樣的:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型,SuperType 的原型又指向 Object 的原型(所有函數的默認原型都是 Object 的實例,因此默認原型都會包含一個內部指針,指向 Object.prototype)
原型鏈的缺點:
1、在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性,並且會被所有的實例共享。這樣理解:在超類型構造函數中定義的引用類型值的實例屬性,會在子類型原型上變成原型屬性被所有子類型實例所共享
2、在創建子類型的實例時,不能向超類型的構造函數中傳遞參數
2、借用構造函數繼承(也稱偽造對象或經典繼承)
// 在子類型構造函數的內部調用超類型構造函數;使用 apply() 或 call() 方法將父對象的構造函數綁定在子對象上 function SuperType(){ // 定義引用類型值屬性 this.colors = ["red","green","blue"]; } function SubType(){ // 繼承 SuperType,在這里還可以給超類型構造函數傳參 SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push("purple"); alert(instance1.colors); // "red,green,blue,purple" var instance2 = new SubType(); alert(instance2.colors); // "red,green,blue"
通過使用 apply() 或 call() 方法,我們實際上是在將要創建的 SubType 實例的環境下調用了 SuperType 構造函數。這樣一來,就會在新 SubType 對象上執行 SuperType() 函數中定義的所有對象初始化代碼。結果 SubType 的每個實例就都會具有自己的 colors 屬性的副本了
借用構造函數的優點是解決了原型鏈實現繼承存在的兩個問題
借用構造函數的缺點是方法都在構造函數中定義,因此函數復用就無法實現了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式
3、組合繼承(也稱偽經典繼承)
// 將原型鏈和借用構造函數的技術組合到一塊。使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能夠保證每個實例都有自己的屬性。 function SuperType(name){ this.name = name; this.colors = ["red","green","blue"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ // 借用構造函數方式繼承屬性 SuperType.call(this,name); this.age = age; } // 原型鏈方式繼承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("luochen",22); instance1.colors.push("purple"); alert(instance1.colors); // "red,green,blue,purple" instance1.sayName(); instance1.sayAge(); var instance2 = new SubType("tom",34); alert(instance2.colors); // "red,green,blue" instance2.sayName(); instance2.sayAge();
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為 javascript 中最常用的繼承模式。而且,使用 instanceof 操作符和 isPrototype() 方法也能夠用於識別基於組合繼承創建的對象。
但它也有自己的不足 -- 無論在什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。
4、原型式繼承
// 借助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。 1、自定義一個函數來實現原型式繼承 function object(o){ function F(){} F.prototype = o; return new F(); }
在 object() 函數內部,先創建一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回這個臨時類型的一個新實例。實質上,object() 對傳入其中的對象執行了一次淺復制
使用 Object.create() 方法實現原型式繼承
這個方法接收兩個參數:一是用作新對象原型的對象和一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,此方法與 object() 方法作用一致。 在傳入第二個參數的情況下,指定的任何屬性都會覆蓋原型對象上的同名屬性。
var person = { name: "luochen", colors: ["red","green","blue"] }; var anotherPerson1 = Object.create(person,{ name: { value: "tom" } }); var anotherPerson2 = Object.create(person,{ name: { value: "jerry" } }); anotherPerson1.colors.push("purple"); alert(anotherPerson1.name); // "tom" alert(anotherPerson2.name); // "jerry" alert(anotherPerson1.colors); // "red,green,blue,purple" alert(anotherPerson2.colors); // "red,green,bule,purple";
只是想讓一個對象與另一個對象類似的情況下,原型式繼承是完全可以勝任的。但是缺點是:包含引用類型值的屬性始終都會共享相應的值
5、寄生式繼承
// 創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后返回這個對象 function createPerson(original){ var clone = Object.create(original); // 通過 Object.create() 函數創建一個新對象 clone.sayGood = function(){ // 增強這個對象 alert("hello world!!!"); }; return clone; // 返回這個對象 }
在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。此模式的缺點是做不到函數復用
6、寄生組合式繼承
// 通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型 function SuperType(name){ this.name = name; this.colors = ["red","green","blue"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ SuperType.call(this,name); this.age = age; } // 創建超類型原型的一個副本 var anotherPrototype = Object.create(SuperType.prototype); // 重設因重寫原型而失去的默認的 constructor 屬性 anotherPrototype.constructor = SubType; // 將新創建的對象賦值給子類型的原型 SubType.prototype = anotherPrototype; SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("luochen",22); instance1.colors.push("purple"); alert(instance1.colors); // "red,green,blue,purple" instance1.sayName(); instance1.sayAge(); var instance2 = new SubType("tom",34); alert(instance2.colors); // "red,green,blue" instance2.sayName(); instance2.sayAge();
這個例子的高效率體現在它只調用一次 SuperType 構造函數,並且因此避免了在 SubType.prototype 上面創建不必要,多余的屬性。與此同時,原型鏈還能保持不變;因此還能夠正常使用 instance 操作符和 isPrototype() 方法