在這之前,先搞清楚下面這個問題:
function Father(){} Father.prototype.name = 'father'; Father.prototype.children = []; const child1 = new Father(); console.log('get1 ==',child1); // Father {} console.log('get ==',child1.name); // father console.log('get ==',child1.children); // [] child1.name = 'child1'; console.log('set ==',child1.name); // child1 child1.children.push('child2'); // child1.children = ['123']; console.log('set ==',child1.children); // ["child2"] console.log('get2 ==',child1) // Father {name: "child1"}
疑問:
(1)為什麼訪問 child1.name 的時候,值是原型上的 name 的值,而設定值之後,例項的原型上的name屬性未被修改,反而自己生成了一個name屬性?
(2) child1.children.push('child2') 與 child1.children = ['123']; 最終的結果為什麼會不同?為什麼 push 方法會導致原型上的 children 屬性也會改變?
參考:
簡單來說就是:
(1)查詢物件屬性的時候,會從本體物件開始查詢,如果有就返回本體上的屬性,因為原型鏈上的被遮蔽了。如果沒有就查原型鏈,直到原型鏈最高層null,找不到就返回 undefined。
(2)設定值的時候,如果該屬性沒有通過 Object.defineproperty 設定 setter 或者 writable 為 true,並且本體物件中沒有該屬性,並且是 ‘=’ 號賦值,那麼會直接在本體物件中新增該屬性。
(3)所以上面的 name 值,查詢的時候是 原型上的 name值。而設定的時候,符合 (2)的條件,所以直接在 child1 中新增 name屬性。如果 改成 child1.name++ ,結果也是本體物件中新增新屬性。因為 這句程式碼等價於 child1.name = child1.name + 1; 。是隱形的等號賦值哦。
(4) child1.children.push('child2') 由於不是等號賦值,那麼在 執行 child1.children 的時候,查詢到 children 之後,沒有 “=” 號賦值,而是 push ,所以操作的是 原型物件中的 children 屬性(引用屬性)。 而 child1.children = ['123'] 也符合(2)的條件。所以是本體物件新增該屬性。
(5)可以簡單的理解為如果本體物件上沒有該屬性, ‘=’ 號賦值之後,分配了新的記憶體地址,因此只有在本體上新增屬性,才能儲存賦的值。如果有該屬性,就是簡單的值替換。
上面的問題明白了之後,再來了解一下 js 的幾種繼承方式。文字有點多,一定要耐心看。幾種繼承方式是有關係的
1. 原型鏈繼承
function Father(){ this.name = 'father'; this.children = []; this.age = 30; } Father.prototype.say = function(){ if(this.children.length){ console.log('我的孩子:'+this.children.join()); }else{ console.log('我是單身狗') } } function CreateChild(name){ this.name = name || '未出生'; this.age = 0; // this.children = []; } CreateChild.prototype = new Father(); // 這句很關鍵,讓子類和父類連結起來,子類繼承了父類所有的屬性和方法,包括父類原型上的 let child1 = new CreateChild('張三'); let child2 = new CreateChild('張四'); child1.children.push('張小一') console.log(child1) console.log(child2.children)
結果:
特性:
(1)是通過覆蓋建構函式的 原型 prototype 來實現的。
(2)不能給父類傳參。
(3)子類繼承父類所有的屬性和方法,包括 原型 prototype 上的。
(4)這裡的繼承屬性和方法指的是:構造的子例項,本身上是沒有屬性的(除非自己初始有),只有原型上繼承的父類的屬性和方法,呼叫屬性或方法是根據原型鏈查詢的。
(5)如果父類有引用型別,子類沒有,那麼其中一個子類繼承來的引用型別修改後會影響所有的子類
說明:
(1)為什麼訪問 child2.children 的時候,也會出現 child1 的的 children 內容? 因為 訪問的時候,child2 本身是沒有children 這個屬性的,只有在原型上去找,剛好找到父級存在這個屬性,而這個屬性又是引用屬性,一處修改,所有引用的地方都更改了。所以最後結果也是 [‘張小一’];
(2)如果子類裡面,自己定義有 children 屬性。那麼相當於 生成的例項 child1,child2 有些屬性就是自己的建構函式上的,並不是繼承來的,所以,如果放開 this.children = [] 的註釋。你會看到 生成的例項中,父級的 children 屬性未變化。
(3)如果在 CreateChild.prototype = new Father() 之後再給 CreateChild.prototype = xxx 賦值的話,結果又會不一樣,原型直接被覆蓋了。
(4)這裡沒必要去修復 CreateChild 的 constructor 的指向。因為在 CreateChild.prototype = new Father() 之後,原始 CreateChild 上的原型屬性全都被覆蓋了,去修復也沒什麼作用。
(5)那如果我又 想給父類傳參 怎麼辦?借用建構函式繼承 就能實現這個需求
2. 借用建構函式繼承。
function Father(name){ this.name = name; this.children = ['張大大']; this.age = 30; } Father.prototype.say = function(){ if(this.children.length){ console.log('我的孩子:'+this.children.join()); }else{ console.log('我是單身狗') } } function CreateChild(name){ // this.children = ['張老大'] Father.call(this,name); // this.children = ['張老大'] } let child1 = new CreateChild('張三'); let child2 = new CreateChild('張四'); child1.children.push('張大一') child2.children.push('張小二') console.log(child1) console.log(child2)
結果:
特性:
(1)繼承父類的原始屬性和方法,不包括原型上的屬性和方法。這裡繼承的屬性和方法指的是:構造的例項,本身繼承的是父類的屬性和方法。原型上無任何變化。
(2)可以給父類傳參,但是不能用於例項化(new)父類的時候傳參。
(3)父類的引用屬性是獨立的, Father.call(this,name) 這段程式碼,相當於給 Father 方法的 this 繫結 為 CreateChild 函式中 this 的指向。然後給這個指向繫結屬性和方法,有點 new 的味道。因為 new 的作用是:內部新增一個物件,讓建構函式內部的 this 指向這個新物件,然後執行語句,為這個物件繫結屬性和方法,最後返回這個物件。
(4)每次初始化子類都會執行一次 Father 父類,不能複用(一次執行,多次使用)。且子類未用到原型
說明:
(1)最重要,最核心的就是 Father.call(this,name) 這段程式碼,給子類繫結了屬性和方法。
(2)如果放開 CreateChild 裡面的第一個或者第二個賦值註釋,都會因為程式碼執行的先後順序 ,原始的資料會被覆蓋。
(3)並不會繼承父類原型上的屬性和方法。因為 此時的 Father 只是當作普通函式執行,所以 prototype 原型上的屬性和方法訪問不了,因為 Father 並未使用建構函式的方式執行。
(4)如果我 既想繼承父類原型上的屬性和方法,又想給父類傳參 怎麼辦?那麼 組合繼承 就能滿足這個需求
3. 組合繼承
function Father(name){ this.name = name; this.children = []; this.age = 30; } Father.prototype.say = function(){ if(this.children.length){ console.log('我的孩子:'+this.children.join()); }else{ console.log('我是單身狗') } } Father.prototype.hobbies = ['woman']; function CreateChild(name){ Father.call(this,name) } CreateChild.prototype = new Father(); let child1 = new CreateChild('張三'); let child2 = new CreateChild('張四'); child1.children.push('張大一'); child1.hobbies.push('meet'); child2.children.push('張小二'); child2.hobbies.push('fruit') console.log(child1) console.log(child2) console.log(child2.hobbies)
結果:
特性:
(1)能繼承父類的所有屬性和方法,因為 Father.call(this,name) 這句程式碼。因此,構造例項本身就含有父類的屬性和方法。
(2)能繼承父類 prototype 原型上的屬性和方法,因為 CreateChild.prototype = new Father(); 這句程式碼 。因此,構造例項的原型上含有父類原型的屬性和方法
(3)能給父類傳參,但是不能用於例項化(new)父類的時候傳參。
(4)父類原型上如果有引用屬性,某一例項修改後,其它的例項也會受到影響。
(5)每建立一個例項,Father 函式會被執行一次。
說明:
(1)這種繼承方式,是第一,二中方式的 組合,所以叫組合繼承。囊括了這兩種方式的優缺點。
4. 原型式繼承
function extendChild(target){ function Fn(){}; Fn.prototype = target; return new Fn(); } function Father(){ this.name = 'father'; this.children = []; this.age = 30; } const FatherInstance = new Father(); const child1 = extendChild(FatherInstance); const child2 = extendChild(FatherInstance); child1.children.push('張大一'); child1.name = 'child1'; child2.children = ['張小二']; child2.name = 'child2'; console.log(child1) console.log(child2)
結果:
特性:
(1)通過覆蓋一個函式的原型,實現構造的例項的原型上繼承傳入的物件。構造的例項本身是沒有屬性和方法的。
(2)如果父類有引用屬性,那麼一個構造例項改變後,其它的例項也會改變。
(3)每次新增例項,都需要執行一次 new Fn()。
(4)主要的功能就是:基於已有的物件,去建立新物件,繼承已有物件的屬性和方法。
說明:
(1)如果看了文章最初的第一個問題,就會明白child1和child2的name,還有child2的children 屬性為什麼會新增到本體屬性上。
(2)細心的會發現,這種繼承方式,和 Object.create 的 polify 一樣一樣的,是同樣的原理,看mdn。
(3)我如果 想給所有例項新增 共同初始的 方法或者屬性,而又不影響父類 怎麼辦? 寄生式繼承 就能解決這個問題
5. 寄生式繼承
function extendChild(target){ function Fn(){}; Fn.prototype = target; return new Fn(); } function Father(){ this.name = 'father'; this.children = []; this.age = 30; } const FatherInstance = new Father(); function createChild(target){ var target = extendChild(target) target.name = 'target'; return target; } const child1 = createChild(FatherInstance); const child2 = createChild(FatherInstance); child1.children.push('張大一'); child1.name = 'child1'; child2.children = ['張小二']; child2.name = 'child2'; console.log(child1) console.log(child2)
結果:
特性:
(1)在原型式繼承上,多加了一個函式。
(2)可以例項化前,給所有例項新增公用的方法或屬性。不會影響父級
說明:
(1)和原型式繼承差不多,其它的沒看出來有什麼優缺點???
(2)組合繼承挺好的,就是父類多呼叫了,而寄生式繼承 只呼叫了一次,能不能把寄生式繼承的優點和組合繼承結合起來?所以 寄生組合式 就這麼來了
6. 寄生組合式繼承,這種繼承方式是最優的繼承方式
function Father(name){ this.name = name || 'father'; this.children = []; this.age = 30; } Father.prototype.say = function(){ console.log(this.name); } Father.prototype.hobbies = ['fruit'] function Child(name){ Father.call(this,name) // this.name = name || '未出生'; // this.age = 0; } function createChild(target){ var Fn = function(){}; Fn.prototype = target; return new Fn(); } function extendFn(Child,Father){ var instance = createChild(Father.prototype); Child.prototype = instance; } extendFn(Child,Father) let child1 = new Child('張三'); let child2 = new Child('張四'); child1.children.push('張大一') child1.hobbies.push('123') child1.name = 'child1' child2.children.push('張小二') child2.name = 'child2' console.log(child1) console.log(child2)
結果:
特性:
(1)和組合繼承的特性一樣。
(2)解決了 多次呼叫 父類的問題。
說明:
(1)createChild 方法 可以換成 Object.create。還可以省點程式碼,功能是一樣的。
7. es6 class類的繼承 extends
class Father{ constructor(name){ this.name = name || 'father'; this.children = []; } hobbies = ['fruit']; say(){ console.log(this.name) } } class Child extends Father{ constructor(name) { super(name); } } let child1 = new Child('child1'); let child2 = new Child('child2'); child1.children.push('child1'); child1.hobbies.push('apple') child2.hobbies = ['apple2'] console.log(child1) console.log(child2) console.log(child2.say())
結果:
特性:
(1)很方便用???
(2)子類會從父類繼承所有的屬性和方法,父類的引用屬性不共享。
總結:
(1)原型鏈繼承 —(優化)— > 借用建構函式繼承 —(優化)—> 組合繼承
(2)原型式繼承 —(優化)— > 寄生式繼承 —(優化)— > 寄生組合式繼承