本文摘錄及參考自:
1. Javascript繼承機(jī)制的設(shè)計(jì)思想
2. Javascript 面向?qū)ο缶幊蹋ㄒ唬悍庋b
3. Javascript面向?qū)ο缶幊蹋ǘ簶?gòu)造函數(shù)的繼承
4. Javascript面向?qū)ο缶幊蹋ㄈ悍菢?gòu)造函數(shù)的繼承
5. 繼承與原型鏈
6. JavaScript 中的繼承
7. JavaScript繼承方式詳解
8. JavaScript深入之繼承的多種方式和優(yōu)缺點(diǎn)
9. JavaScript深入之從原型到原型鏈
10. JavaScript深入之call和apply的模擬實(shí)現(xiàn)
11. JavaScript深入之new的模擬實(shí)現(xiàn)
12. JavaScript深入之創(chuàng)建對象
13. 深入理解JavaScrip面向?qū)ο蠛驮屠^承
繼承的由來
1. 原始模式
JavaScript的繼承從本質(zhì)上來說是通過“原型鏈”(prototype chain)實(shí)現(xiàn)的。
我們要?jiǎng)?chuàng)建Dog這一個(gè)對象,通過對Dog的封裝,原始模式可能是這樣的:
例:
var Dog ={
name:""
type: ""
}
var dogA = {}
dogA.name="測試1"
dogA.type="type"
var dogB ={}
dogB.name="測試2"
dogB.type="type"
可以看出,這樣生成實(shí)例的方式非常麻煩,而且實(shí)例與原型之間沒有聯(lián)系
2. new
JavaScript在一開始的設(shè)計(jì)中,并沒有引入“類”的概念(到ECMAScript6才引入class)。為了解決1中的問題,它引入了new關(guān)鍵字用來創(chuàng)建實(shí)例,new后面跟的不是類名,而是構(gòu)造函數(shù)(方法)
例:
function Dog(name){
this.name = name
}
var dogA = new Dog("測試"); // Dog {name:測試}
通過這種方式創(chuàng)建的dogA實(shí)例,有一個(gè)__proto__屬性(3會(huì)解釋這個(gè)屬性)指向原型。內(nèi)部使用this,該變量會(huì)綁定到實(shí)例對象上。
一般情況下,我們使用這種方式創(chuàng)建實(shí)例即可。但是現(xiàn)在有一個(gè)新的需求: 需要給Dog構(gòu)造函數(shù)添加一個(gè)公共的屬性type,所有通過該構(gòu)造函數(shù)創(chuàng)建的實(shí)例共享這個(gè)屬性。
例:
function Dog(name){
this.name = name;
this.type = "Dog"
}
var dogA = new Dog("測試A");
var dogB = new Dog("測試B");
console.log(dogA.type); // Dog
console.log(dogB.type); // Dog
dogA.type ="chanag type";
console.log(dogA.type); // change type
console.log(dogB.type); // Dog 期望是 change type
因此對于new 構(gòu)造函數(shù)的方法,存在無法共享屬性和方法的問題。
3. prototype
為了解決2中存在的問題, JavaScript引入了prototype屬性,這個(gè)屬性包含一個(gè)對象,我們稱這個(gè)對象為prototype對象。所有實(shí)例對象需要共享的屬性和方法,都放在這個(gè)對象里面;那些不需要共享的屬性和方法,就放在構(gòu)造函數(shù)里面。實(shí)例對象一旦創(chuàng)建,將自動(dòng)引用prototype對象的屬性和方法。也就是說,實(shí)例對象的屬性和方法,分成兩種,一種是本地的,另一種是引用的。
例:
function Dog(name){
this.name = name;
}
Dog.prototype={ type:"Dog" }
var dogA = new Dog("測試A");
var dogB = new Dog("測試B");
console.log(dogA.type); // Dog
console.log(dogB.type); // Dog
//dogA.type="change type"; //這種方式只是在dogA的實(shí)例上添加了一個(gè)屬性,并不是修改公共屬性
Dog.prototype.type = "change type"
//dogA.__proto__.type ="chanag type"; 與上面的方式效果相同
console.log(dogA.type); // change type
console.log(dogB.type); // change type
繼承的幾種方式
在了解了new 和 prototype之后,我們來看看JavaScript中繼承的幾種方式。
1. 構(gòu)造函數(shù)綁定
使用call或apply方法,將父對象的構(gòu)造函數(shù)綁定到子對象上。
例:
function Animal(){
this.species = "動(dòng)物";
}
function Cat(name,color){
Animal.apply(this,arguments); //Cat繼承Animal
this.name = name;
this.color = color;
}
var cat1 = new Cat("測試1",'紅色');
console.log(cat1);
輸出結(jié)果如下圖所示:

注意構(gòu)造函數(shù)綁定的方法,不會(huì)繼承 Animal.prototype上的屬性和方法
2. prototype模式(組合繼承)
該方式比較常見,如果“貓”的prototype對象,指向一個(gè)Animal實(shí)例,那么所有“貓”的實(shí)例,就能繼承Animal了。
例:
function Animal(){
this.species = "動(dòng)物";
}
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = new Animal();
var cat1 = new Cat("測試1",'黃色');
console.log(cat1);

注意,該方式創(chuàng)建的Cat實(shí)例的constructor指向的是Animal而非Cat。原來,任何一個(gè)prototype對象都有一個(gè)constructor屬性,指向它的構(gòu)造函數(shù),而我們讓Cat的prototype直接指向了一個(gè)Animal實(shí)例,因此Cat的構(gòu)造函數(shù)變成了Animal。這顯然會(huì)導(dǎo)致繼承鏈的紊亂(cat1明明是用構(gòu)造函數(shù)Cat生成的)
為了解決該問題,我們需要手動(dòng)將constructor屬性指回Cat構(gòu)造函數(shù)
例:
function Animal(){
this.species = "動(dòng)物";
}
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("測試1",'黃色');
console.log(cat1);

3. 直接繼承prototype
第二種方法中,每次都要新建一個(gè)Animal對象,比較浪費(fèi)內(nèi)存。因此我們可以將不變的屬性直接寫入Animal.prototype,然后讓Cat()跳過Animal(),直接繼承Animal.prototype。
例:
function Animal(){
}
Animal.prototype.species = "動(dòng)物"
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = Animal.prototype;
var cat1 = new Cat("測試1",'黃色');
console.log(cat1);

參照2. prototype模式 ,我們可以手動(dòng)把Cat.prototype的constructor指回Cat
Cat.prototype.constructor = Cat;

這種方法雖然效率高,但是也有缺點(diǎn) ,Cat.prototype和Animal.prototype現(xiàn)在指向了同一個(gè)對象,那么任何對Cat.prototype的修改,都會(huì)反映到Animal.prototype(注意,方法2也有同樣的問題,所有的Cat實(shí)例,共享了一個(gè)Animal實(shí)例)。 因此上面的代碼中,已經(jīng)將Animal.prototype對象的constructor屬性改成了Cat
Cat.prototype.constructor = Cat;
console.log(Animal.prototype.constructor); // Cat
4. 利用空對象作為中介(寄生組合式繼承)
第三種方法“直接繼承prototype”存在上述的缺點(diǎn),因此就有了第四種方法,利用一個(gè)空對象作為中介
function Animal(){}
Animal.prototype.species = "動(dòng)物"
function Cat(name,color){
this.name = name;
this.color = color;
}
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("測試1","黃色");
console.log(cat1);

我們將上面的方法,封裝成一個(gè)函數(shù),便于使用。(這實(shí)際上是ES5 Object.create的模擬實(shí)現(xiàn),具體參照6. Object.create創(chuàng)建)
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F(); //空對象,消耗較少
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
5. 拷貝繼承
我們也可以通過將父對象的所有屬性和方法拷貝進(jìn)子對象來實(shí)現(xiàn)繼承
例:
function Animal(){
}
Animal.prototype.species = "動(dòng)物";
function Cat(name,color){
this.name = name;
this.color = color;
}
function copyExtend(Child,Parent){
let p = Parent.prototype;
let c = Child.prototype;
// 注意constructor屬性不會(huì)被遍歷到
for(let i in p ){
c[i] = p[i];
}
}
copyExtend(Cat,Animal);
let cat1 = new Cat("測試1",'紅色');
console.log(cat1);

6. Object.create創(chuàng)建
ECMAScript 5 中引入了一個(gè)新方法: Object.create()。可以調(diào)用這個(gè)方法來創(chuàng)建一個(gè)新對象。新對象的原型就是調(diào)用create方法時(shí)傳入的第一個(gè)參數(shù)
例
function Animal(){}
Animal.prototype.species = "動(dòng)物";
let animal = new Animal();
let cat1= Object.create(animal);
cat1.name = "測試1";
cat1.color = "紅色";
console.log(cat1);

注意這種方式創(chuàng)建的對象,constructor是Animal。 而且,包含引用類型的屬性值始終都會(huì)共享相應(yīng)的值。
例:
function Animal(){
this.name = 'animal';
this.testArray=['test1','test2','test3']
}
Animal.prototype.species = "動(dòng)物";
let animal = new Animal();
let cat1= Object.create(animal);
cat1.name = 'cat1'
cat1.testArray.push("cat1");
console.log(cat1.name) ; // cat1
console.log(cat1.testArray); // ["test1", "test2", "test3", "cat1"]
let cat2 = Object.create(animal);
console.log(cat2.name ); //animal
console.log(cat2.testArray); // ["test1", "test2", "test3", "cat1"]
注意:修改cat1.name的值,cat2.name的值并未發(fā)生改變,并不是因?yàn)閏at1和cat2有獨(dú)立的 name 值,而是因?yàn)閏at1.name = 'cat1',給cat1添加了 name 值,并非修改了原型上的 name 值。
7. 使用Class關(guān)鍵字創(chuàng)建
ECMAScript6引入了一套新的關(guān)鍵字用來實(shí)現(xiàn)class。
例:
class Animal{
constructor(){
console.log("Animal Constructor");
}
}
Animal.prototype.species = "動(dòng)物";
class Cat extends Animal{
constructor(name,color){
super();
this.name = name;
this.color = color;
}
}
let cat1 = new Cat("測試1","紅色")
console.log(cat1);

總結(jié)
JavaScript中的繼承分為兩種:
- 原型鏈繼承(對象間的繼承): 借助已有的對象創(chuàng)建新的對象,將子類的原型指向父類,就相當(dāng)于加入了父類這條原型鏈(例如,方法2,3,4,6,7)
- 類式繼承(構(gòu)造函數(shù)間的繼承): 在子類構(gòu)造函數(shù)的內(nèi)部調(diào)用超類的構(gòu)造函數(shù)(例如1)。嚴(yán)格的類式繼承并不是很常見,一般都是組合著用