寫在前面 都知道,當我們讀取一個對象的屬性或方法的時候,會優先在這個對象上面找,如果在這個對象上找不到就會遍歷他的原型,還沒找到? >原型的原型,又沒找到?-->繼續往上。。。 這便是原型鏈的功用。下麵,我探討了一下原型鏈的使用與擴展,依靠原型鏈實現繼承。 至於什麼是繼承? 我的理解是,一個對象可以 ...
寫在前面
都知道,當我們讀取一個對象的屬性或方法的時候,會優先在這個對象上面找,如果在這個對象上找不到就會遍歷他的原型,還沒找到?--->原型的原型,又沒找到?-->繼續往上。。。
這便是原型鏈的功用。下麵,我探討了一下原型鏈的使用與擴展,依靠原型鏈實現繼承。
至於什麼是繼承? 我的理解是,一個對象可以直接使用另一個對象的屬性和方法。
本文結構:
- 直接使用原型鏈
- 借用構造函數
- 組合繼承
- 原型式繼承
- 寄生式繼承
- 寄生組合式繼承
其中的繼承方式層層遞進,不斷進化完善缺點。
進化過程: 1 → 2 → 3↘
→ → 6
4 → 5 ↗
一、直接使用原型鏈
1.回顧我的上一遍文章中寫的,構造函數、原型、實例之間的關係::
每一個構造函數都有prototype指針指向一個原型對象,原型對象有一個constructor指針指向構造函數,而實例通過[[prototype]]指針共用一個原型對象。
2.原型鏈繼承的實現:把一個構造函數的原型對象等於另一種類型(另一種構造函數)的實例。
function SuperType() { this.property = 'aaa'; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType() { this.subproperty = 'bbb'; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function() { return this.subproperty; } var instance = new SubType(); console.log(instance)
關係圖解
執行的結果:SubType.prototype 指向了一個 SuperType 的實例。並通過 SubType.prototype.getSubValue 為實例添加了方法。
當最終實例 instance 調用 getSuperValue 方法的時候。 先遍歷實例instance對象,沒找到--->搜索instance.prototype(也是SuperType的一個實例),還沒找到--->搜索instnce.prototype.prototype (也是SuperType.prototype);
3.註意:
3.1 別忘記預設的原型
原型鏈可以無限長(當然為了性能,不要搞太長)。但是去到最後必然是 Object.prototype
3.2 確定原型與實例的關係:原型鏈中的出現的原型都算該實例的原型
instance instanceOf SubType // true
SuperType.isPrototypeOf(instance) // true
3.3 謹慎地定義方法: 因為是修改了SubType的原型對象,因此必須在替換之後才定義原型上的方法: SubType.prototype.getSubTypeValue() 並且不能使用字面量,不然又換了一個新對象。
4.直接使用原型鏈繼承的優缺點
SuperType構造函數中若使用了引用類型值。就會放映在SubType的原型上。在原型上出現引用類型,容易被實例修改。影響所有的實例。
使用 SubType 創建對象的時候,不能再給SuperType傳遞參數了。因為SubType.prototype 已經是一個SuperType的實例了。
二、借用構造函數實現繼承
為解決直接使用原型鏈繼承中不能給 SuperType 傳遞參數的問題以及引用類型值問題而生。
function SuperType(name) { this.colors = ['red','blue','green']; this.name = name; } function SubType(name) { SuperType.call(this, name); } var instance1 = new SubType('jody'); var instance2 = new SubType('miaowwwww'); instance1.colors.push('black'); console.log(instance1) console.log(instance2); console.log(instance2.name)
首先要知道,構造函數只不過是在特定的環境(this)中執行代碼,併為它定義屬型而已。因此,大可以在SubType中執行以下SuperType,這樣一來,SubType 就獲取 SuperType 的屬性和方法。
優點:解決了原型鏈繼承的引用類型問題,以及 傳遞參數的問題
缺點:若僅僅使用借用構造函數,無法避免構造函數模式的問題,(方法都在實例中,方法的復用性降低了),並且instance1 並不是SuperType 的一個實例,無法使用instanceOf,isPrototypeOf.
三、組合繼承
鑒於借用構造函數繼承的缺點,當然不會僅僅借用構造函數啦。
組合繼承又稱偽經典繼承,結合了原型鏈和借用構造函數 兩種技術。(相信,大家看完借用構造函數的缺點就已經想到了)
function SuperType(name) { this.colors = ['red','blue','green']; this.name = name; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); this.age = age; } // 繼承方法、 SubType.prototype = new SuperType(); // 為了使用SuperType.prototype上的方法,所以要添加 SubType.prototype.constructor = SubType; //new 的時候要調用這個constructor?並沒有,這個屬性只是標記這個對象是這個類型,多一個判斷方法而已。這裡僅僅是補全替換prototype導致的constructor的缺失 SubType.prototype.sayAge = function() { // 在SubType原型上,同時也是SuperType的實例 console.log(this.age); } var instance1 = new SubType('jody', 22); console.log(instance1)
這種方式確實完美解決了傳遞參數,引用類型,公共方法重用的缺點。同時也可以被 instanceOf 和 isPrototypeOf() 識別。
但是它產生了新的問題:SubType實例含有(colors,name)屬性,SubType.prototype也有(colors,name)這部分屬性冗餘了。(這個問題將在 六、組合式繼承中解決)
四、原型式繼承
// 原型式繼承即通過object函數實現 function object(o) { function F() {}; F.prototype = o; return new F(); } // 實例 var person = { name: 'miaowwwww', friends: ['aaa', 'bbb'] }; var person1 = object(person); person1.name = 'jody'; person1.friends.push('ccc'); console.log(person1)
原型式繼承跟原型鏈是一個理念(把原型指向另一種類型的對象(實例))。把空構造函數的原型指向一個對象,然後創建空構造函數的實例,並放回。
4.1 object函數在es5 中得到了實現:Object.create(obj, defineProperties) :第二個參數而可選,設置屬性,及其描述,描述預設false
var person2 = Object.create(person, { name:{ value: 'dd', writable: false }, age: { value: 22 } }) console.log(person2)
4.2 缺點:跟原型鏈一樣,是原型上的引用類型值
五、寄生式繼承
5.1 寄生式是在原型式的基礎上的改進。 —_— 只是把定義實例屬性的邏輯放到一個函數裡面了而已
function craateAnother(original) { var clone = Object.create(original); clone.sayName = function() { console.log(this.name); }; return clone; } var person = { name: 'jody', friends: ['aa','bb']}; var person1 = createAnother(person);
5.2 缺點:公共的屬性和方法(如:sayName)不能復用。
六、寄生組合式繼承
這是 三、組合繼承 和 五、寄生式繼承的技術組合。(組合繼承使用寄生式繼承修改了,組合繼承的缺點。
1.先貼出組合繼承的代碼理解一下:
組合繼承的缺點:兩次調用SuperType(), 導致的 SubType.prototype產生了冗餘的(name,colors...)等SuperType的屬性。
function SuperType(name) { this.name = name; this.colors = ['aa','bb']; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); // 第二次執行SuperType this.age = age; } SubType.prototype = new SuperType(); //第一次執行SuperType SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }
既然知道 SubType.prototype = new SuperType(); 只是為了獲取SuperType.prototype。 那麼可以繞過執行SuperType,直接獲取SuperType.prototype。(這便是寄生式的作用)
2 寄生式繼承的用處 : 使用Object.create() 創建一個空的實例,並且該實例的prototype 指向了 SuperType.prototype. (乾凈,無冗餘屬性)
function inheritPrototype(subType, superType) { var emptyInstance = Object.create(superType.prototype); emptyInstance.constructor = subType; //補充constructor的缺失 subType.prototype = emptyInstance; }
3.寄生組合式繼承代碼
function inheritPrototype(subType, superType) { var prototype = Object.create(superType.prototype); prototype.constructor = subType; //補充constructor的缺失 subType.prototype = prototype; }
function SuperType(name) { this.name = name; this.colors = ['aa','bb']; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); // SubType.prototype = SuperType.prototype; //不可以這麼做,因為sayAge會添加在SuperType.prototype中 SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); } var instance = new SubType('jody', 22); console.log(instance) console.log(instance.name)
至此: 寄生組合式繼承被認為是引用類型最理想的繼承範式。
備註:(接受指導與批評)
本文是閱讀高級程式設計(第三版)P162~P174 的理解與總結。