(ฅ´ω`ฅ) 來源《JavaScript高級程式設計第三版》,建議學習時間 2小時 ...
—————————————————————————————————————————————————————————
繼承 - ECMAScript只支持實現繼承(依靠原型鏈),不支持介面繼承(函數沒有簽名)
原型鏈
- 利用原型讓一個引用類型繼承另一個引用類型的屬性和方法,
- 構造函數、原型、實例的關係:每個構造函數都有一個原型對象,原型對象包含一個指向構造函數的指針。實例包含一個指向原型對象的內部指針,在創建實例之後即指向原型對象
- 而當A原型對象的指針指向B個原型對象時(此時A原型對象與B實例同級),就形成了一條原型鏈。
-
圖解:
原型搜索機制:當讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性,如果沒有找到該屬性則沿著原型鏈向上查找
在例子<Demo-1>中,調用instance.getSuperValue(),先搜索實例instance,再搜索SubType.prototype,再搜索SuperType.protorype,最後一步才找到該方法。
預設的原型:所有的引用類型預設都繼承了Object,所以預設原型的指針都會指向Object.prototype,完整的原型鏈如下:
instance → SubType.prototype → SuperType.prototype → Object.prototype
-
p.s.
必須替換掉實例的原型後才能給實例添加方法
不能使用對象字面量創建原型方法,這樣做會重寫原型鏈,如<Demo-3>
-
缺點:
包含引用類型值(Function Object Array)的原型屬性會被所有實例共用,在通過原型來實現繼承時,原型實際上會變成另一個類型的實例,所以原先的實例屬性就變成了現在的原型屬性了。<Demo-4>
在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。
// "use strict"; // Demo - 1 // SuperType 擁有一個屬性和一個方法 // SubType 擁有一個屬性和一個方法,又從SuperType那裡繼承了一個屬性一個方法 function SuperType(){ this.property = "111"; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = "222"; } // p.s.new操作之前,SubType.prototype指向的是function,不允許為function()定義.getSubValue方法,所以要將添加方法放在修改原型指向之後 // 操作之後SubType.prototype指向SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ // 必須在SubType替換原型之後才能定義 return this.subproperty; } var instance = new SubType(); console.log(instance.property); // 111 console.log(instance.getSuperValue()); // 111 console.log(instance.subproperty); // 222 console.log(instance.getSubValue()); // 222 console.log(instance.constructor); // f SuperType(){} 原本SubType中的constructor屬性被重寫 // 重寫SuperType.getSuperValue() // 如果要重寫這個方法,會屏蔽原來的方法 // 換句話說,當通過SubType的實例調用getSuperValue時調用的就是這個重新定義的方法,但通過SuperType的實例調用時還會繼續調用原來的方法 var beforeReWrite = new SuperType(); SuperType.prototype.getSuperValue = function(){ console.log("rewrite"); } console.log(instance.getSuperValue()); // rewrite,this.property = undefined console.log(SuperType.prototype.getSuperValue()); // rewrite,this.property = undefined console.log(beforeReWrite.getSuperValue()); // Demo - 2 // 確認原型和實例的關係 console.log(instance instanceof Object); // true console.log(instance instanceof SuperType); // true console.log(instance instanceof SubType); // true // 另一種方法 console.log(Object.prototype.isPrototypeOf(instance)); // true console.log(SuperType.prototype.isPrototypeOf(instance)); // true console.log(SubType.prototype.isPrototypeOf(instance)); // true // Demo - 3 function SuperType2(){ this.property = "1111"; } SuperType2.prototype.getSuperValue = function(){ return this.property; } function SubType2(){ this.subproperty = "2222"; } SubType2.prototype = new SuperType2(); SubType2.prototype = { getSubValue:function(){ return this.subproperty; }, someOtherMethod:function(){ return false; } } var instance2 = new SubType2(); console.log(instance2 instanceof Object); // true console.log(instance2 instanceof SuperType2); // false,原型鏈被切斷 console.log(instance2 instanceof SubType2); // true // console.log(instance2.getSuperValue()); // error // Demo - 4 function SuperType3(){ this.colors = ["red","blue","green"]; } function SubType3(){} SubType3.prototype = new SuperType3(); var instance3 = new SubType3(); instance3.colors.push("black"); console.log(instance3.colors); // ["red", "blue", "green", "black"] var instance4 = new SubType3(); console.log(instance4.colors); // ["red", "blue", "green", "black"]
借用構造函數(偽造對象 / 經典繼承)
- 在子類型構造函數的內部調用超類型構造函數
-
優點:
解決了單獨使用原型鏈共用引用類型值屬性的問題
可以在子類型構造函數中向超類型構造函數傳遞參數
-
缺點:
無法避免構造函數模式存在的問題:方法都在構造函數中定義,無法實現函數復用
// "use strict"; function SuperType(name) { this.name = name; this.colors = ["111", "222", "333"]; } function SubType() { SuperType.call(this, "name1"); this.age = 20; } var instance = new SubType(); instance.colors.push("444"); console.log(instance.colors); // ["111", "222", "333", "444"] console.log(instance.name); // name1 console.log(instance.age); // 20 var instance2 = new SubType(); console.log(instance2.colors); // ["111", "222", "333"]
組合繼承(偽經典繼承)
- 將原型鏈和借用構造函數組合,使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承
- 對應創建對象 <組合使用構造函數模式和原型模式>
- 優點:最常用
- 缺點:需要調用兩次超類型構造函數,一次在創建子函數原型時,另一次在子函數構造函數內部。調用子類型構造函數時需要重寫屬性
// "use strict"; function SuperType(name) { this.name = name; this.colors = ["111", "222", "333"]; } SuperType.prototype.sayName = function() { console.log(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() { console.log(this.age); } var instance1 = new SubType("hugh", 20); instance1.colors.push("444"); console.log(instance1.colors); // ["111", "222", "333", "444"] instance1.sayName(); // hugh instance1.sayAge(); // 20 var instance2 = new SubType("dong", 21); console.log(instance2.colors); // ["111", "222", "333"] instance2.sayName(); // dong instance2.sayAge(); // 21
原型式繼承
- 對應創建對象 <動態原型模式>
- 沒有使用嚴格意義上的構造函數,藉助已有的對象創建新對象
-
優點:
在不想創建構造函數,只想讓一個對象與另一個對象保持類似的情況下,原型式繼承完全可以勝任
-
缺點:
包含引用類型值的屬性始終都會共用,就像原型模式一樣
// "use strict"; function object(o){ function F(){} // 創建臨時性構造函數 F.prototype = o; // 將傳入的對象作為構造函數的原型 return new F(); // 返回臨時類型的一個新實例 } var person = { name:"hugh", friends:["111",'222','333'] }; var anotherPerson = object(person); anotherPerson.name = "dong"; anotherPerson.friends.push("444"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "hehe"; yetAnotherPerson.friends.push("555"); console.log(person.friends); // ["111", "222", "333", "444", "555"] console.log(person.name); // hugh console.log(anotherPerson.friends); // ["111", "222", "333", "444", "555"] console.log(anotherPerson.name); // dong console.log(yetAnotherPerson.friends); // ["111", "222", "333", "444", "555"] console.log(yetAnotherPerson.name); // hehe // 使用Object.create()規範化原型式繼承 // 以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性 var otherPerson1 = Object.create(person); otherPerson1.friends.push("666"); console.log(yetAnotherPerson.friends); // ["111", "222", "333", "444", "555", "666"] var otherPerson2 = Object.create(person,{ name:{ value:"test" } }); console.log(otherPerson2.name);
寄生式繼承
- 對應創建對象 <寄生構造函數 / 工廠模式>
- 創建一個僅用於封裝繼承過程的函數,在內部增強對象,最後返回對象
- 示範集成模式時使用的object()函數不是必須的,任何能夠返回新對象的函數都適用於此模式
- 使用場景:在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式
- 缺點:無法做到函數復用,類似於構造函數模式
// "use strict"; function object(o) { function F() {} // 創建臨時性構造函數 F.prototype = o; // 將傳入的對象作為構造函數的原型 return new F(); // 返回臨時類型的一個新實例 } function createAnother(original) { // 接收的函數作為新對象基礎的對象 var clone = object(original); clone.sayHi = function() { // 添加新方法 console.log('hi'); }; return clone; } var person = { name: "hugh", friends: ['111', '222', '333'] }; var person1 = createAnother(person); person1.sayHi(); console.log(person1.name); console.log(person1.friends);
寄生組合式繼承
-
優點:
最理想的繼承範式
解決組合繼承重寫屬性的問題,只調用了一次SuperType構造函數
避免了在SubType.prototype上創建不必要的屬性
原型鏈保持不變
能夠正常使用instanceof和isPrototypeOf()
"use strict"; function object(o) { function F() {} F.prototype = o; return new F(); } // 1.創建超類型原型的一個副本 // 2.為創建的副本添加constructor屬性,彌補因重寫原型而失去的屬性 // 3.將新創建的對象(即副本)賦值給子類型的原型 function inheritProtoType(subType,superType){ var prototype = object(superType.prototype); // 創建對象 prototype.constructor = subType; // 增強對象 subType.prototype = prototype; // 指定對象 } function SuperType(name){ this.name = name; this.colors= [1,2,3,4]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name,age){ SuperType.call(this,name); this.age = age; } inheritProtoType(SubType,SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); }