筆記:JavaScript繼承

本文摘錄及參考自:
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é)果如下圖所示:

image.png

注意構(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);

image.png

注意,該方式創(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);
image.png

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);
image.png

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

Cat.prototype.constructor = Cat;
image.png

這種方法雖然效率高,但是也有缺點(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);
image.png

我們將上面的方法,封裝成一個(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);
image.png

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);
image.png

注意這種方式創(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);
image.png

總結(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)格的類式繼承并不是很常見,一般都是組合著用
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第3章 基本概念 3.1 語法 3.2 關(guān)鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,543評論 0 21
  • ??面向?qū)ο螅∣bject-Oriented,OO)的語言有一個(gè)標(biāo)志,那就是它們都有類的概念,而通過類可以創(chuàng)建任意...
    霜天曉閱讀 2,265評論 0 6
  • 轉(zhuǎn)載地址:http://javascript.ruanyifeng.com/oop/prototype.html#...
    化城閱讀 447評論 0 1
  • 曾去太行昆山村,嶺秀寂靜少人聞。 偶有寫生學(xué)生過,雞犬之聲如鄉(xiāng)魂。 尋果誤入歧途路,突遇畫師點(diǎn)白云。 長想隱居此山...
    綠野V仙蹤閱讀 358評論 0 0
  • 摘記: 而在近百年之后的今天,你會(huì)發(fā)現(xiàn)那時(shí)困擾著女性的問題,好像到現(xiàn)在也沒解決。 你知道你應(yīng)該追求自由,可是仍然被...
    Miki璐閱讀 78評論 0 0

友情鏈接更多精彩內(nèi)容