(注1:如果有問題歡迎留言探討,一起學習!轉載請注明出處,喜歡可以點個贊哦!)
(注2:更多內容請查看我的目錄。)
1. 簡介
在前面兩節,我們花了大量的篇幅來介紹如何創建對象(JS入門難點解析10-創建對象)以及構造函數,原型對象和實例對象三者的定義和關系(JS入門難點解析11-構造函數,原型對象,實例對象)。如果你能好好理解體會這兩篇文章中的內容,那么對于本章所述的知識點,你將會感覺清晰易懂。
2. 關于繼承
在詳細講述繼承前,我們有必要理解繼承的概念和JS為什么要實現繼承。
關于繼承的概念,我們來看一段引自百度百科(百度百科-繼承性)的解釋:
“繼承”是面向對象軟件技術當中的一個概念。如果一個類A繼承自另一個類B,就把這個A稱為"B的子類",而把B稱為"A的父類"。繼承可以使得子類具有父類的各種屬性和方法,而不需要再次編寫相同的代碼。在令子類繼承父類的同時,可以重新定義某些屬性,并重寫某些方法,即覆蓋父類的原有屬性和方法,使其獲得與父類不同的功能。另外,為子類追加新的屬性和方法也是常見的做法。
通過繼承可以提高代碼復用性,方便地實現擴展,降低軟件維護的難度。我們知道,JavaScript是一種基于對象的腳本語言,而在ES6之前JS沒有類的概念。如何將所有的對象區分與聯系起來?如何更好地組織JS的代碼呢?
JS借鑒C++和Java使用new命令時調用"類"的構造函數(constructor)的思路,做了一個簡化的設計,在Javascript語言中,new命令后面跟的不是類,而是構造函數。構造函數中的this關鍵字,代表了新創建的實例對象。每一個實例對象,都有自己的屬性和方法的副本。而所有的實例對象共享同一個prototype對象,prototype對象就好像是實例對象的原型,而實例對象則好像"繼承"了prototype對象一樣。
當然,利用構造函數和原型鏈,只是其中一種思路。下面我們詳細介紹實現JS繼承的兩類四種方式和這幾種方式的組合,以及他們各自的優缺點。
3. 模擬類的繼承
正如第2節所述,JS的設計者為我們提供了一個最直接的思路。通過構造函數實例化對象,并通過原型鏈將實例對象關聯起來。
3.1 原型鏈繼承
基本思想:使用父類實例對象作為子類原型對象。
// demo3.1
// 聲明父類構造函數
function SuperType() {
this.superValue = 'super';
}
// 為父類原型對象添加方法
SuperType.prototype.getSuperValue = function() {
return this.superValue;
};
// 聲明子類構造函數
function SubType() {
this.subValue = 'sub';
}
// 將父類實例對象作為子類原型對象-關鍵就在這里
SubType.prototype = new SuperType();
// 為子類原型對象添加方法
SubType.prototype.getSubValue = function () {
return this.subValue;
};
// 新建子類實例對象
var instance = new SubType();
console.log(instance.superValue); // super
console.log(instance.getSuperValue()); // super
console.log(instance.subValue); // sub
console.log(instance.getSubValue()); // sub
其構造函數,實例對象和原型對象關系如下:
注意:
- 將父類實例對象賦值給子類構造函數的prototype 屬性以后,重寫了子類原型對象,此時新的子類原型對象是沒有屬于自己的constructor屬性的,而是繼承了SuperType.protoType的constructor屬性。
// 接代碼demo3.1
console.log(SubType.prototype.constructor === SubType); // false
console.log(SuperType.prototype.constructor === SuperType); // true
console.log(SubType.prototype.constructor === SuperType); // true
- 所有的對象默認都繼承了Object,這個繼承是通過原型鏈實現的。
// 接代碼demo3.1
console.log(SuperType.prototype.__proto__ === Object.prototype); // true
- 在對子類原型對象的屬性和方法進行改動(增加,刪除,重寫)時,需要在將父類實例對象賦值給子類構造函數的prototype 屬性以后。
// demo3.2
function SuperType() {
this.superValue = 'super';
}
SuperType.prototype.getSuperValue = function() {
return this.superValue;
};
function SubType() {
this.subValue = 'sub';
}
// 將父類實例對象作為子類原型對象之前為子類原型對象添加屬性
SubType.prototype.beforeNewSuperType = 'beforeNewSuperType';
// 將父類實例對象作為子類原型對象-關鍵就在這里
SubType.prototype = new SuperType();
// 將父類實例對象作為子類原型對象之后為子類原型對象添加屬性
SubType.prototype.afterNewSuperType = 'afterNewSuperType';
// 新建子類實例對象
var instance = new SubType();
// 新建子類實例對象后之后為子類原型對象添加屬性
SubType.prototype.afterNewSubType = 'afterNewSubType';
console.log(instance.beforeNewSuperType); // undefined
console.log(instance.afterNewSuperType); // afterNewSuperType
console.log(instance.afterNewSubType); // afterNewSubType
- 在對子類原型對象的屬性和方法進行改動時,不可以用對象字面量改寫子類原型對象。
// demo3.3
// 聲明父類構造函數
function SuperType() {
this.superValue = 'super';
}
// 為父類原型對象添加方法
SuperType.prototype.getSuperValue = function() {
return this.superValue;
};
// 聲明子類構造函數
function SubType() {
this.subValue = 'sub';
}
// 將父類實例對象作為子類原型對象-關鍵就在這里
SubType.prototype = new SuperType();
// 為子類原型對象添加方法
SubType.prototype= {
getSubValue : function () {
return this.subValue;
}
};
// 新建子類實例對象
var instance = new SubType();
console.log(instance.superValue); // undefined
console.log(instance.getSuperValue); // undefined
console.log(instance.subValue); // sub
console.log(instance.getSubValue()); // sub
優點:
當原型進行屬性和方法的改動時,對所有繼承實例能夠即時生效。(參見demo3.2)
方便判斷對象類型(這一塊以后會開單章詳細講述其原理)。
- 方法1:用instanceof操作符來判斷原型鏈中是否有某構造函數,操作符右邊必然是構造函數,而左邊是在該構造函數所處原型鏈位置之前的實例或者原型對象時會返回true。
- 方法2:用isPrototypeOf方法來判斷原型鏈中是否有某原型對象,方法調用者必然是原型對象,而參數是在該原型對象所處原型鏈位置之前的實例或者原型對象時時會返回true。
// 接demo3.1
// 方法一:用instanceof操作符來判斷
// 左邊實例右邊構造函數
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof Object); // true
// 左邊原型對象右邊構造函數
console.log(SubType.prototype instanceof SuperType); // true
// 方法二:用isPrototypeOf方法來判斷
// 調用者原型對象參數是實例
console.log(SubType.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true
// 調用者原型對象參數是原型對象
console.log(SuperType.prototype.isPrototypeOf(SubType.prototype)); // true
缺點:
- 父類構造函數的屬性,被子類原型擁有之后,由子類實例對象共享。
// demo3.4
// 聲明父類構造函數
function SuperType() {
this.value = [1, 2, 3];
}
// 聲明子類構造函數
function SubType() {
}
// 將父類實例對象作為子類原型對象-關鍵就在這里
SubType.prototype = new SuperType();
// 新建子類實例對象
var instance1 = new SubType();
var instance2 = new SubType();
console.log(instance1.value); // [1, 2, 3]
console.log(instance2.value); // [1, 2, 3]
instance1.value.push(4);
console.log(instance1.value); // [1, 2, 3, 4]
console.log(instance2.value); // [1, 2, 3, 4]
在創建繼承關系時,無法對父類型的構造函數傳參。理由同缺點1,如果傳參,會影響到所有的實例。
無法實現多繼承。因為將父類實例對象作為子類原型對象時,是一對一的。
3.2 借用構造函數繼承
基本思想:在子類構造函數內部調用父類構造函數。拋開父類的原型對象,直接通過在子類構造函數內部借用父類構造函數來增強子類構造函數,此時子類實例會擁有子類和父類定義的實例屬性與方法。
// demo3.5
// 聲明父類構造函數
function SuperType(value) {
this.superValue = value;
this.arr = [1, 2, 3];
}
// 聲明子類構造函數
function SubType() {
// 借用父類構造函數,繼承父類并且可以傳參-關鍵就在這里
SuperType.call(this, 'super');
this.subValue = 'sub';
}
// 新建子類實例對象
var instance1 = new SubType();
var instance2 = new SubType();
console.log(instance1.arr); // [1, 2, 3]
console.log(instance2.arr); // [1, 2, 3]
instance1.arr.push(4);
console.log(instance1.arr); // [1, 2, 3, 4]
console.log(instance2.arr); // [1, 2, 3]
優點:
由父類構造函數定義的實例屬性被子類實例繼承以后仍然是獨立的實例屬性。(參見demo3.5)
在創建繼承關系時,可以傳參。理由同優點1,傳參不會影響所有實例。(參見demo3.5)
可以實現多繼承。因為在子類構造函數內部可以借用多個父類構造函數。
缺點
父類原型定義的公共屬性和方法無法被繼承。
父類構造函數發生改動時,可能會影響到子類構造函數以及實例的構造方法,而且這種變動不會影響到之前已經生成的實例。
繼承關系難以判定,只能判斷實例與子類的直接繼承關系,實例與父類的繼承關系無法判定。
// 接demo3.5
console.log(instance1 instanceof SubType); // true
console.log(instance1 instanceof SuperType); // false
console.log(SubType.prototype.isPrototypeOf(instance1)); // true
console.log(SuperType.prototype.isPrototypeOf(instance1)); // false
- 方法都定義在構造函數內部,無法實現方法復用。
3.3 組合繼承(原型鏈 + 借用構造函數)—— 最常用的繼承模式
主要思路:利用原型鏈實現對父類原型屬性的繼承,借用構造函數實現對父類實例屬性的繼承。
// demo3.6
// 聲明父類構造函數
function SuperType(value) {
this.superValue = value;
this.arr = [1, 2, 3];
}
// 為父類原型對象添加方法
SuperType.prototype.getSuperValue = function() {
return this.superValue;
};
// 聲明子類構造函數
function SubType() {
// 借用父類構造函數,繼承父類并且可以傳參-第二次調用父類構造函數
SuperType.call(this, 'super');
this.subValue = 'sub';
}
// 將父類實例對象作為子類原型對象,第一次調用父類構造函數
SubType.prototype = new SuperType();
// 將子類原型對象的constructor屬性指向子類本身
SubType.prototype.constructor = SubType;
// 為子類原型對象添加方法
SubType.prototype.getSubValue = function () {
return this.subValue;
};
// 新建子類實例對象
var instance = new SubType();
console.log(instance.superValue); // super
console.log(instance.getSuperValue()); // super
console.log(instance.subValue); // sub
console.log(instance.getSubValue()); // sub
var instance2 = new SubType();
instance.arr.push(4);
console.log(instance.arr); // [1, 2, 3, 4]
console.log(instance2.arr); // [1, 2, 3]
優點:
擁有原型鏈繼承和借用構造函數繼承的所有優點,卻沒有兩者的缺點。
缺點:
調用了兩次父類構造函數,父類的實例屬性被復制了兩份,一份放在子類原型,一份放在子類實例,而且最后子類實例繼承自父類的實例屬性覆蓋了子類原型繼承自父類的實例屬性。
4. 委托繼承
委托繼承,并不需要使用者去調用構造函數。本質上其實是選一個原始對象作為其他對象的原型來繼承,這樣在其他對象中找不到的屬性和方法,會委托該原始對象去尋找,也就實現了繼承。
4.1 原型式繼承
主要思路:利用一個空的構造函數為橋梁,將一個對象作為原型創建新對象,這樣新生成的對象都可以通過原型鏈共享這個原型對象的屬性。
可以用如下函數來闡釋該思路:
// demo4.1
function object(o) {
function F() {} // 建一個空的構造函數
F.prototype = o; // 將F的原型對象指向o
return new F(); // 返回F的實例,這樣返回的實例原型即為傳入的o
}
下面我們來看一個具體的例子:
// 接demo4.1
// demo4.2
// person就是原始對象,用來作為其他新對象的原型對象
var person = {
name: 'ZhangSan',
hobbies: ['painting', 'running'],
friends: ['LiSi', 'WangWu']
};
var anotherPersonOne = object(person);
anotherPersonOne.name = 'LiSi';
anotherPersonOne.hobbies.push('singing');
anotherPersonOne.friends = ['ZhangSan', 'WangWu'];
var anotherPersonTwo = object(person);
anotherPersonTwo.name = 'WangWu';
anotherPersonTwo.hobbies.push('dancing');
anotherPersonTwo.friends = ['ZhangSan', 'LiSi'];
console.log(person.name); // 'ZhangSan'
console.log(person.hobbies); // ['painting', 'running', 'singing', 'dancing']
console.log(person.friends); // ['LiSi', 'WangWu']
console.log(anotherPersonOne.name); // 'LiSi'
console.log(anotherPersonOne.hobbies); // ['painting', 'running', 'singing', 'dancing']
console.log(anotherPersonOne.friends); // ['ZhangSan', 'WangWu']
console.log(anotherPersonTwo.name); // 'WangWu'
console.log(anotherPersonTwo.hobbies); // ['painting', 'running', 'singing', 'dancing']
console.log(anotherPersonTwo.friends); // ['ZhangSan', 'LiSi']
console.log(anotherPersonOne.__proto__ === person); // true
console.log(anotherPersonTwo.__proto__ === person); // true
注意:
ECMAScript5通過新增方法Object.create()方法規范化了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個可選的為新對象定義額外屬性的對象。其實就是一種語法糖,幫助我們實現繼承的同時,方便地定義了新對象的屬性。在只傳入一個參數的情況下,Object.create()和我們定義的object()方法效果相同。
// demo4.3
// person就是原始對象,用來作為其他新對象的原型對象
var person = {
name: 'ZhangSan',
hobbies: ['painting', 'running'],
friends: ['LiSi', 'WangWu']
};
var anotherPersonOne = Object.create(person, {
name: {
value: 'LiSi'
},
friends: {
value: ['ZhangSan', 'WangWu']
}});
anotherPersonOne.hobbies.push('singing');
var anotherPersonTwo = Object.create(person, {
name: {
value: 'WangWu'
},
friends: {
value: ['ZhangSan', 'LiSi']
}});
anotherPersonTwo.hobbies.push('dancing');
console.log(person.name); // 'ZhangSan'
console.log(person.hobbies); // ['painting', 'running', 'singing', 'dancing']
console.log(person.friends); // ['LiSi', 'WangWu']
console.log(anotherPersonOne.name); // 'ZhangSan'
console.log(anotherPersonOne.hobbies); // ['painting', 'running', 'singing', 'dancing']
console.log(anotherPersonOne.friends); // ['ZhangSan', 'WangWu']
console.log(anotherPersonTwo.name); // 'ZhangSan'
console.log(anotherPersonTwo.hobbies); // ['painting', 'running', 'singing', 'dancing']
console.log(anotherPersonTwo.friends); // ['ZhangSan', 'LiSi']
console.log(anotherPersonOne.__proto__ === person); // true
console.log(anotherPersonTwo.__proto__ === person); // true
優點:
不需要使用者調用構造函數,不必額外創建自定義類型。
支持傳參。
// 接demo4.1
// demo4.4
var superObj = {
init: function(value){
this.value = value;
},
getValue: function(){
return this.value;
}
}
var subObj = object(superObj);
subObj.init('sub');
console.log(subObj.getValue()); // 'sub'
- 可以用用isPrototypeOf方法來判斷繼承關系。
console.log(superObj.isPrototypeOf(subObj)); // true
缺點:
由于引用屬性是被共享的,對引用屬性的改動會影響到其他對象。(參見demo4.2)
無法用instanceof操作符來判斷繼承關系,因為沒有構造函數。
4.2 寄生式繼承
主要思路:在原型式繼承的基礎上,對返回的原型進行了增強。
// demo4.5
function object(o) {
function F() {} // 建一個空的構造函數
F.prototype = o; // 將F的原型對象指向o
return new F(); // 返回F的實例,這樣返回的實例原型即為傳入的o
}
function createAnother(obj) {
var clone = object(obj); // 通過調用函數來創建一個新對象
clone.favouriteColors = ['red']; // 以某種方式來增強這個對象
clone.sayHi = function() {
console.log('hi');
}
return clone; // 返回這個對象
}
var person = {
name: 'ZhangSan',
hobbies: ['painting', 'running'],
friends: ['LiSi', 'WangWu']
};
var anotherPersonOne = createAnother(person);
console.log(anotherPersonOne.favouriteColors); // ['red']
anotherPersonOne.sayHi(); // hi
var anotherPersonTwo = createAnother(person);
anotherPersonTwo.favouriteColors.push('white');
console.log(anotherPersonOne.favouriteColors); // ['red']
console.log(anotherPersonTwo.favouriteColors); // ['red', 'white']
注意:
有的人可能想到了,我們前面說過Object.create()在只有一個參數時與object效果相同。所以上述代碼可以寫成:
// demo4.6
function createAnother(obj) {
var clone = Object.create(obj, {
favouriteColors: {
value: ['red']
},
sayHi: {
value: function() {
console.log('hi');
}
}
});
return clone;
}
var person = {
name: 'ZhangSan',
hobbies: ['painting', 'running'],
friends: ['LiSi', 'WangWu']
};
var anotherPersonOne = createAnother(person);
console.log(anotherPersonOne.favouriteColors); // ['red']
anotherPersonOne.sayHi(); // hi
var anotherPersonTwo = createAnother(person);
anotherPersonTwo.favouriteColors.push('white');
console.log(anotherPersonOne.favouriteColors); // ['red']
console.log(anotherPersonTwo.favouriteColors); // ['red', 'white']
不過哪種寫法更優,需要使用者自己抉擇。
優點:
為原型添加屬性和方法更加方便。
新增加的屬性和方法是獨立的。(參見demo4.5和demo4.6)
缺點:
新增加的函數無法復用。
4.3 寄生組合式繼承(組合 + 寄生)—— 最完美的繼承模式
還記得使用最廣泛的組合繼承模式么,唯一的缺點就是需要兩次調用父類構造函數。而寄生模式不需要調用構造函數,那么想辦法將組合模式其中一次調用改成使用寄生模式即可。
基本思路:父類構造函數定義的實例屬性通過借用構造函數來繼承,而父類原型定義的共享屬性通過寄生模式來繼承。
// demo 4.6
// 寄生繼承方法,將父類原型復制一份給子類原型,并且將constructor變成指向子類原型
function inheritPrototype(subType, superType) {
var prototype = superType.prototype;
prototype.constructor = subType;
subType.prototype = prototype;
}
// 父類構造函數定義父類實例屬性
function SuperType(name) {
this.name = name;
this.colors = ['blue', 'green']
}
// 父類原型中定義公共方法
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);
};
var instanceOne = new SubType('張三', 22);
var instanceTwo = new SubType('李四', 26);
instanceOne.sayName(); // 張三
instanceOne.sayAge(); // 22
console.log(instanceOne.colors); // ['blue', 'green']
instanceTwo.colors.push('white');
console.log(instanceTwo.colors);// ['blue', 'green', 'white']
console.log(instanceOne.colors);// ['blue', 'green']
注意:
此時,是可以用instanceof操作符和isPrototypeOf方法來判斷繼承關系的,但是并不是從原型鏈找到父類原型來判斷的,而是子類原型和父類原型的引用是同一個對象。
// 接 demo4.6
console.log(instanceOne instanceof SubType); // true
console.log(instanceOne instanceof SuperType); // true
console.log(SubType.prototype.isPrototypeOf(instanceOne)); // true
console.log(SuperType.prototype.isPrototypeOf(instanceOne)); // true
console.log(SubType.prototype === SuperType.prototype); // true
優點:
近乎完美,父類的實例屬性不會出現在子類的原型而是獨立出現在各個子類實例,而父類的原型屬性被copy到了子類中,子類可以共享父類和子類原型定義的屬性。
缺點:
對子類原型的修改影響了父類原型,事實上現在他們使用的是同一個引用。
思考:
當然,為了解決該缺點,我們在inheritPrototype()方法中,可以將superType.prototype拷貝一份給subType.prototype,而不是指向同一個引用。但是如此一來,又會引發另一個缺點,那就是不能判斷實例與父類型的繼承關系。如何抉擇,可以根據實際需要來定。
6. 總結
其實理解繼承,主要是理解構造函數,實例屬性和原型屬性的關系。要想實現繼承,將不同的對象或者函數聯系起來,總共就以下幾種思路:
- 原型鏈:父類的實例當做子類的原型。如此子類的原型包含父類定義的實例屬性,享有父類原型定義的的屬性。
- 借用構造函數:子類直接使用父類的構造函數。如此子類的實例直接包含父類定義的實例屬性。
- 原型式:復制父類原型屬性給子類原型。如此,子類實例享有父類定義的原型屬性。
- 寄生式:思路與3一樣,只是利用工廠模式對復制的父類原型對象進行增強。
然后,1,2思路結合,實例屬性繼承用借用構造函數保證獨立性,方法繼承用原型鏈保證復用性,就是組合模式。
4,2思路結合,或者說3,4與1,2思路結合,實例屬性繼承用借用構造函數保證獨立性,方法繼承用原型復制增強的方式,就是寄生組合模式。
參考
JS入門難點解析10-創建對象
JS入門難點解析11-構造函數,原型對象,實例對象
javascript面向對象系列第三篇——實現繼承的3種形式
一張圖理解prototype、proto和constructor的三角關系
JS實現繼承的幾種方式
重新理解JS的6種繼承方式
Javascript繼承機制的設計思想
經典面試題:js繼承方式上
經典面試題:js繼承方式下
閑說繼承
Javascript中的幾種繼承方式比較
JS實現繼承的幾種方式詳述(推薦)
百度百科-面向對象程序設計
廖雪峰的官方網站-原型繼承
百度百科-javascript
百度百科-繼承性
BOOK-《JavaScript高級程序設計(第3版)》第6章
BOOK-《你不知道的JavaScript》 第2部分