JavaScript繼承新舊方法匯總

例子

我們生成兩個構造函數,后面的例子都是讓‘’貓‘’繼承‘’動物‘’的所有屬性和方法。

  • 動物(為了更好的理解各種繼承,這里給動物附上了基本類型和引用類型)
function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
}
function Cat(name, color) {    
    this.name = name   
    this.color = color
}

1.簡單的原型鏈

這可能是最簡單直觀的一種實現繼承方式了

1.1 實現方法

function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
}  
function Cat(name, color) {    
    this.name = name   
    this.color = color
}
Cat.prototype = new Animal //重點!!!!!
Cat.prototype.constructor = Cat
var cat1 = new Cat('小黃', '黃色')
console.log(cat1.species) // 動物
console.log(cat1.do) // [ '運動', '繁殖' ] 

1.2 核心

這種方法的核心就這一句話:Cat.prototype = new Animal 也就是拿父類實例來充當子類原型對象

1.3 優缺點

然而這個方法雖然簡單但是有一個很嚴重的問題:在我們修改一個實例的屬性時,其他的也隨之改變。

var cat1 = new Cat('小黃', '黃色')
var cat2 = new Cat('小白', '白色')
cat1.species = '哺乳動物'
cat1.do.push('呼吸')
console.log(cat1.species) // 哺乳動物
console.log(cat2.species) // 動物
console.log(cat1.do) // [ '運動', '繁殖', '呼吸' ]
console.log(cat2.do) // [ '運動', '繁殖', '呼吸' ]
  • 優點
  1. 容易實現
  • 缺點
    1. 修改cat1.do后cat2.do也變了,因為來自原型對象的引用屬性是所有實例共享的。
      可以這樣理解:執行cat1.do.push('呼吸');先對cat1進行屬性查找,找遍了實例屬性(在本例中沒有實例屬性),沒找到,就開始順著原型鏈向上找,拿到了cat1的原型對象,一搜身,發現有do屬性。于是給do末尾插入了'呼吸',所以sub2.do也變了

    2. 創建子類實例時,無法向父類構造函數傳參

1.4 繼承鏈的紊亂問題

Cat.prototype = new Animal

任何一個prototype對象都有一個constructor屬性,指向它的構造函數。如果沒有"Cat.prototype = new Animal();"這一行,Cat.prototype.constructor是指向Cat的。加了這一行以后,Cat.prototype.constructor指向Animal。

alert(Cat.prototype.constructor == Animal) //true

更重要的是,每一個實例也有一個constructor屬性,默認調用prototype對象的constructor屬性。因此,在運行"Cat.prototype = new Animal();"這一行之后,cat1.constructor也指向Animal!

alert(cat1.constructor == Cat.prototype.constructor) // true

這顯然會導致繼承鏈的紊亂(cat1明明是用構造函數Cat生成的),因此我們必須手動糾正,將Cat.prototype對象的constructor值改為Cat。

Cat.prototype.constructor = Cat

這是很重要的一點,編程時務必要遵守。下文都遵循這一點,即如果替換了prototype對象,那么,下一步必然是為新的prototype對象加上constructor屬性,并將這個屬性指回原來的構造函數。

2. 借用構造函數

使用call或apply方法,將父對象的構造函數綁定在子對象上,即在子對象構造函數中加一行:Animal.apply(this, arguments)

2.1 實現方法

function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
}  
function Cat(name, color) {  
    Animal.call(this, arguments) ///重點!!!!
    this.name = name   
    this.color = color
}
var cat1 = new Cat('小黃', '黃色')
console.log(cat1.species) // 動物
console.log(cat1.do) // [ '運動', '繁殖' ] 

2.2 核心

借父類的構造函數來增強子類實例,等于是把父類的實例屬性復制了一份給子類實例裝上了(完全沒有用到原型)

2.3 優缺點

var cat1 = new Cat('小黃', '黃色')
var cat2 = new Cat('小白', '白色')
cat1.species = '哺乳動物'
cat1.do.push('呼吸')
console.log(cat1.species) // 哺乳動物
console.log(cat2.species) // 動物
console.log(cat1.do) // [ '運動', '繁殖', '呼吸' ]
console.log(cat2.do) // [ '運動', '繁殖' ]
  • 優點:
  1. 解決了子類實例共享父類引用屬性的問題
  2. 創建子類實例時,可以向父類構造函數傳參
  • 缺點:
  1. 無法實現函數復用,過多的占用內存。
  2. 創建子類實例時,無法向父類構造函數傳參

3. 組合繼承(偽經典繼承)

將原型鏈和借用構造函數的技術組合起來,發揮二者之長:使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義的方法實現了函數復用,又能夠保證每個實例都有它自己的屬性。是實現繼承最常用的方式。

3.1 實現方法

function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
}  
function Cat(name, color) {  
    Animal.call(this, arguments)//重點!!!!
    this.name = name   
    this.color = color
}
Cat.prototype = new Animal//重點!!!!
Cat.prototype.constructor = Cat
var cat1 = new Cat('小黃', '黃色')
console.log(cat1.species) // 動物
console.log(cat1.do) // [ '運動', '繁殖' ] 

3.2 核心

把實例函數都放在原型對象上,以實現函數復用。同時還要保留借用構造函數方式的優點,通過Animal.call(this);繼承父類的基本屬性和引用屬性并保留能傳參的優點;通過Cat.prototype = new Animal繼承父類函數,實現函數復用。

3.3 優缺點

  • 優點:
  1. 不存在引用屬性共享問題
  2. 可傳參
  3. 函數可復用
  • 缺點:
  1. 子類原型上有一份多余的父類實例屬性,因為父類構造函數被調用了兩次,生成了兩份。(私有屬性一份,原型里面一份)

4. 原型式

道格拉斯·克羅克福德在2006年寫了一篇文章,Prototypal Inheritance in JavaScript(JavaScript中的原型式繼承)。在這篇文章中,他介紹了一種實現繼承的方法,這種方法并沒有使用嚴格意義上的構造函數。他的想法是借助原型可以基于已有的對象創建新對象,同時還不必因此創建自定義類型,為了達到這個目的,他給出了如下函數。

function object(o) {
    function F() {}
    f.prototype = o
    return new F()
}

在object()函數內部,先創建了一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺拷貝。

4.1 實現方法

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
} 
var Animal1 = new Animal 
var cat1 = object(Animal1)//重點!!!!

cat1.name = '小黃'
cat1.color = '黃色'

console.log(cat1.species) //動物
console.log(cat1.do) //["運動", "繁殖"]

4.2 核心

核心就是通過一個函數來得到一個空的新對象,再在空對象的基礎上添加需要的方法(實例屬性)

4.3 優缺點

  • 優點:
  1. 從已有對象衍生新對象,不需要創建自定義類型。
  • 缺點:
  1. 原型引用屬性會被所有實例共享,因為是用整個父類對象來充當了子類
    原型對象,所以這個缺陷無可避免
  2. 無法實現代碼復用

5. 寄生式

寄生式在我看來和原型式差別不大,只是把對空對象私有屬性的添加封裝成了一個函數。

5.1 實現方法

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
} 
function getCatObject(obj) {
    var clone = object(obj)//重點!!!!
    clone.name = '小黃'
    clone.color = '黃色'
    return clone
}

var cat1 = getCatObject(new Animal)

console.log(cat1.species) //動物
console.log(cat1.do) //["運動", "繁殖"]

5.2 核心

只是給原型式繼承套了一個殼子而已。
對于寄生式的理解:創建新對象 -> 增強 -> 返回該對象,這樣的過程叫寄生式繼承,新對象是如何創建的并不重要。

5.3 優缺點

  • 優點:
  1. 不需要創建自定義類型。
  • 缺點:
  1. 無法實現代碼復用

6. 寄生組合繼承

前面說過,組合繼承是JavaScript 最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。也就是會出現這種情況:



我們發現在私有屬性和原型里面都有name和do的屬性,這是因為調用了兩次構造函數造成的后果,這必然會過多占用內存。
寄生組合繼承完美的解決了這個問題。

6.1 實現方法

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Animal() {    
    this.species = "動物"
    this.do = ['運動', '繁殖'] 
} 
function Cat(name, color) {
    Animal.call(this, arguments)//重點!!!!
    this.name = name
    this.color = color
}


var proto = object(Animal.prototype)//重點!!!!
proto.constructor = Cat//重點!!!!
Cat.prototype = proto//重點!!!!

var cat1 = new Cat()

console.log(cat1.species) //動物
console.log(cat1.do) //["運動", "繁殖"]

6.2 核心

用object(Animal.prototype)切掉了原型對象上多余的那份父類實例屬性

6.3 優缺點

  • 優點:
  1. 幾乎完美
  • 缺點:
  1. 用起來有些麻煩,理論上沒有缺點。

7. ES5使用 Object.create 創建對象

ECMAScript 5 中引入了一個新方法:Object.create()
。可以調用這個方法來創建一個新對象。新對象的原型就是調用 create方法時傳入的第一個參數:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因為d沒有繼承Object.prototype

8. ES6使用 class 關鍵字

ECMAScript6 引入了一套新的關鍵字用來實現 class。使用基于類語言的開發人員會對這些結構感到熟悉,但它們是不一樣的。 JavaScript 仍然是基于原型的。這些新的關鍵字包括 class
, constructor
, static
, extends
, 和 super
.
例子如下:

class Animal {
    constructor(species, canDo) {
        this.species = '動物'
        this.canDo = ['運動', '繁殖'] 
    }
}

class Cat extends Animal {
    constructor(name, color) {
        super()
        this.name = name
        this.color = color
    }
}
var cat1 = new Cat('小黃', '黃色')
console.dir(cat1)

9. 參考文獻

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容