溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript實(shí)現(xiàn)繼承的方法是什么

發(fā)布時(shí)間:2022-11-03 09:41:42 來源:億速云 閱讀:121 作者:iii 欄目:web開發(fā)

這篇文章主要介紹了JavaScript實(shí)現(xiàn)繼承的方法是什么的相關(guān)知識,內(nèi)容詳細(xì)易懂,操作簡單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇JavaScript實(shí)現(xiàn)繼承的方法是什么文章都會有所收獲,下面我們一起來看看吧。

0. 繼承

繼承是面向?qū)ο缶幊讨杏懻撟疃嗟脑掝}。很多面向?qū)ο笳Z言都支持兩種繼承:接口繼承和實(shí)現(xiàn)繼承。前者只繼承方法簽名,后者繼承實(shí)際的方法。 接口繼承在 ECMAScript中 是不可能的,因?yàn)楹瘮?shù)沒有簽名。實(shí)現(xiàn)繼承是 ECMAScript 唯一支持的繼承方式,而這主要是通過原型鏈實(shí)現(xiàn)的。

1. 原型鏈繼承【方案一】

ECMA-262 把原型鏈定義為 ECMAScript 的主要繼承方式。其基本思想就是通過原型繼承多個(gè)引用類型的屬性和方法。重溫一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:

  • 每個(gè)構(gòu)造函數(shù)都有一個(gè)prototype屬性指向原型對象

  • 所有原型對象自動獲得一個(gè)名為 constructor 的屬性,指回與之關(guān)聯(lián)的構(gòu)造函數(shù)

而實(shí)例有一個(gè)內(nèi)部指針指向原型。如果原型是另一個(gè)類型的實(shí)例呢?那就意味著這個(gè)原型本身有一個(gè)內(nèi)部指針指向另一個(gè)原型,相應(yīng)的另一個(gè)原型也有一個(gè)指針指向另一個(gè)構(gòu)造函數(shù)。這樣就在實(shí)例和原型之間構(gòu)造了一條原型鏈。這就是原型鏈的基本構(gòu)想。

實(shí)現(xiàn)原型鏈繼承涉及如下代碼模式

// 定義 Person 構(gòu)造函數(shù)
function Person() {
  this.name = 'CoderBin'
}

// 給 Person 的原型上添加 getPersonValue 方法(原型方法)
Person.prototype.getPersonValue = function() {
  return this.name
}

// 定義 Student 構(gòu)造函數(shù)
function Student() {
  this.sno = '001'
}

// 繼承 Person — 將 Peson 的實(shí)例賦值給 Student 的原型
Student.prototype = new Person()

Student.prototype.getStudentValue = function() {
  return this.sno
}

// 實(shí)例化 Student
let stu = new Student()

console.log(stu.getPersonValue()) // CoderBin

1.1 代碼解讀

以上代碼定義了兩個(gè)構(gòu)造函數(shù):Person 和 Student。這兩個(gè)構(gòu)造函數(shù)分別定義了一個(gè)屬性和一個(gè)方法。

這兩個(gè)類型的主要區(qū)別是 Student 通過創(chuàng)建 Person 的實(shí)例并將其賦值給自己的原型 Student.prototype 實(shí)現(xiàn)了對 Person 的繼承。

這個(gè)賦值重寫了 Student 最初的原型,將其替換為 Person 的實(shí)例。這意味著 Person 實(shí)例可以訪問的所有屬性和方法也會存在于Student.prototype。這樣實(shí)現(xiàn)繼承之后,代碼緊接著又給Student.prototype,也就是這個(gè) Person 的實(shí)例添加了 一個(gè)新方法。最后又創(chuàng)建了 Student 的實(shí)例并調(diào)用了它繼承的getPersonValue()方法。

下圖展示了子類的實(shí)例與兩個(gè)構(gòu)造函數(shù)及其對應(yīng)的原型之間的關(guān)系:

JavaScript實(shí)現(xiàn)繼承的方法是什么

1.2 代碼核心解析

這個(gè)例子中實(shí)現(xiàn)繼承的關(guān)鍵,是 Student 沒有使用默認(rèn)原型,而是將其替換成了一個(gè)新的對象。這個(gè)新的對象恰好是 Person 的實(shí)例。這樣一來,Student 的實(shí)例不僅能從 Person 的實(shí)例中繼承屬性和方法,而且還與 Person 的原型掛上了鉤。于是 stu(通過內(nèi)部的 [[Prototype]] )指向Student.prototype,而Student.prototype(作為 Person 的實(shí)例又通過內(nèi)部的 [[Prototype]] )指向Person.prototype。

注意1:getPersonValue() 方法還在Person.prototype對象上,而 name 屬性則在Student.prototype上。這是因?yàn)?getPersonValue() 是一個(gè)原型方法,而 name 是一個(gè)實(shí)例屬性。Student.prototype現(xiàn)在是 Person 的一個(gè)實(shí)例,因此 name 才會存儲在它上面。

注意2:由于 Student.prototype 的 constructor 屬性被重寫為指向 Person,所以 stu.constructor 也指向 Person 。

1.3 默認(rèn)原型

實(shí)際上,原型鏈中還有一環(huán)。默認(rèn)情況下,所有引用類型都繼承自 Object ,這也是通過原型鏈實(shí)現(xiàn)的。任何函數(shù)的默認(rèn)原型都是一個(gè) Object 的實(shí)例,這意味著這個(gè)實(shí)例有一個(gè)內(nèi)部指針指向Object.prototype。這也是為什么自定義類型能夠繼承包括 toString() 、valueOf() 在內(nèi)的所有默認(rèn)方法的原因。因此前面的例子還有額外一層繼承關(guān)系。

下圖展示了完整的原型鏈。

JavaScript實(shí)現(xiàn)繼承的方法是什么

Student 繼承 Person ,而 Person 繼承 Object 。在調(diào)用 stu.toString() 時(shí),實(shí)際上調(diào)用的是保存在 Object.prototype 上的方法。

1.4 原型與繼承的關(guān)系

原型與實(shí)例的關(guān)系可以通過兩種方式來確定。

1.4.1 instanceof

第一種方式是使用instanceof操作符,如果一個(gè)實(shí)例的原型鏈中出現(xiàn)過相應(yīng)的構(gòu)造函數(shù),則instanceof返回 true 。如下例所示:

console.log(stu instanceof Object)    // true
console.log(stu instanceof Person)    // true
console.log(stu instanceof Student)   // true

從技術(shù)上講,stu 是 Object、Person 和 Student 的實(shí)例,因?yàn)?stu 的原型鏈中包含這些構(gòu)造函數(shù)的原型。結(jié)果就是 instanceof 對所有這些構(gòu)造函數(shù)都返回 true 。

1.4.2 isPrototypeOf()

確定這種關(guān)系的第二種方式是使用isPrototypeOf()方法。原型鏈中的每個(gè)原型都可以調(diào)用這個(gè)方法,如下例所示,只要原型鏈中包含這個(gè)原型,這個(gè)方法就返回 true 。

console.log(Object.prototype.isPrototypeOf(stu))    // true
console.log(Person.prototype.isPrototypeOf(stu))    // true
console.log(Student.prototype.isPrototypeOf(stu))   // true

1.5 關(guān)于方法

子類有時(shí)候需要覆蓋父類的方法,或者增加父類沒有的方法。為此, 這些方法必須在原型賦值之后再添加到原型上。來看下面的例子:

// 定義 Person 構(gòu)造函數(shù)
function Person() {
  this.name = 'CoderBin'
}

// 給 Person 的原型上添加 getPersonValue 方法(原型方法)
Person.prototype.getPersonValue = function() {
  return this.name
}

// 定義 Student 構(gòu)造函數(shù)
function Student() {
  this.sno = '001'
}

// 繼承 Person
Student.prototype = new Person()

// 新方法 —— 1
Student.prototype.getStudentValue = function() {
  return this.sno
}

// 覆蓋已有的方法 —— 2
Student.prototype.getPersonValue = function() {
  return 'Bin'
}

// 實(shí)例化 Student
let stu = new Student()

console.log(stu.getPersonValue()) // Bin

在上面的代碼中,注釋1、2的部分涉及兩個(gè)方法。

  • 第一個(gè)方法 getStudentValue() 是 Student 的新方法,

  • 第二個(gè)方法 getPersonValue() 是原型鏈上已經(jīng)存在但在這里被遮蔽的方法。

后面在 Student 實(shí)例上調(diào)用 getPersonValue() 時(shí)調(diào)用的是2這個(gè)方法。而 Person 的實(shí)例仍然會調(diào)用最初的方法。

重點(diǎn)一:上述兩個(gè)方法都是在把原型賦值為 Person 的實(shí)例之后定義的。

重點(diǎn)二:另一個(gè)要理解的重點(diǎn)是,以對象字面量方式創(chuàng)建原型方法會破壞之前的原型鏈,因?yàn)檫@相當(dāng)于重寫了原型鏈。下面是一個(gè)例子:

// 定義 Person 構(gòu)造函數(shù)
function Person() {
  this.name = 'CoderBin'
}

// 給 Person 的原型上添加 getPersonValue 方法(原型方法)
Person.prototype.getPersonValue = function() {
  return this.name
}

// 定義 Student 構(gòu)造函數(shù)
function Student() {
  this.sno = '001'
}

// 繼承 Person
Student.prototype = new Person()

// 通過對象字面量添加新方法,這會導(dǎo)致上一行無效!?。?
Student.prototype = {
  getStudentValue() {
    return this.sno
  },
  someOtherMethod() {
    return 'something'
  }
}

// 實(shí)例化 Student
let stu = new Student()

console.log(stu.getPersonValue())  // TypeError: stu.getPersonValue is not a function

在這段代碼中,子類的原型在被賦值為 Person 的實(shí)例后,又被一個(gè)對象字面量覆蓋了。覆蓋后的原型是一個(gè)Object 的實(shí)例,而不再是 Person 的實(shí)例。因此之前的原型鏈就斷了。Student 和 Person 之間也沒有關(guān)系了。

1.6 原型鏈繼承的缺陷

原型鏈雖然是實(shí)現(xiàn)繼承的強(qiáng)大工具,但它也有問題。

主要問題出現(xiàn)在原型中包含引用值的時(shí)候。前面在談到原型的問題時(shí)也提到過,原型中包含的引用值會在所有實(shí)例間共享,這也是為什么屬性通常會在構(gòu)造函數(shù)中定義而不會定義在原型上的原因。在使用原型實(shí)現(xiàn)繼承時(shí),原型實(shí)際上變成了另一個(gè)類型的實(shí)例【1】。這意味著原先的實(shí)例屬性搖身一變成為了原型屬性。下面的例子揭示了這個(gè)問題:

// 定義 Person 構(gòu)造函數(shù)
function Person() {
  this.letters = ['a', 'b', 'c']
}

// 定義 Student 構(gòu)造函數(shù)
function Student() {
  this.sno = '001'
}

// 繼承 Person
Student.prototype = new Person()

let stu1 = new Student()
let stu2 = new Student()

stu1.letters.push('d')

console.log(stu1.letters)  // ['a', 'b', 'c', 'd']
console.log(stu2.letters)  // ['a', 'b', 'c', 'd']

代碼解析: 在這個(gè)例子中,Person 構(gòu)造函數(shù)定義了一個(gè) letters 屬性,其中包含一個(gè)數(shù)組(引用值)。每個(gè) Person 的實(shí)例都會有自己的 letters 屬性,包含自己的數(shù)組。但是,當(dāng) Student 通過原型繼承 Person 后,Student.prototype變成了 Person 的一個(gè)實(shí)例,因而也獲得了自己的 letters 屬性。這類似于創(chuàng)建了Student.prototype.letters 屬性。最終結(jié)果是,Student 的所有實(shí)例都會共享這個(gè) letters 屬性。這一點(diǎn)通過 stu1.letters 上的修改也能反映到 stu2.letters 上就可以看出來。

原型鏈的第二個(gè)問題是,子類型在實(shí)例化時(shí)不能給父類型的構(gòu)造函數(shù)傳參【2】。事實(shí)上,我們無法在不影響所有對象實(shí)例的情況下把參數(shù)傳進(jìn)父類的構(gòu)造函數(shù)。再加上之前提到的原型中包含引用值的問題,就導(dǎo)致原型鏈基本不會被單獨(dú)使用。

2. 盜用構(gòu)造函數(shù)繼承【方案二】

為了解決原型包含引用值導(dǎo)致的繼承問題,一種叫作“盜用構(gòu)造函數(shù)” (constructor stealing)的技術(shù)在開發(fā)社區(qū)流行起來(這種技術(shù)有時(shí)也稱作“對象偽裝”或“經(jīng)典繼承”)?;舅悸泛芎唵危?strong>在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)。 因?yàn)楫吘购瘮?shù)就是在特定上下文中執(zhí)行代碼的簡單對象,所以可以使用apply()call()方法以新創(chuàng)建的對象為上下文執(zhí) 行構(gòu)造函數(shù)。來看下面的例子:

// 定義 Person 構(gòu)造函數(shù)
function Person() {
  this.letters = ['a', 'b', 'c']
}

// 定義 Student 構(gòu)造函數(shù)
function Student() {
  // 繼承 Person — 使用 call() 方法調(diào)用 Person 構(gòu)造函數(shù)
  Person.call(this)
}

let stu1 = new Student()
let stu2 = new Student()

stu1.letters.push('d')

console.log(stu1.letters)  // ['a', 'b', 'c', 'd']
console.log(stu2.letters)  // ['a', 'b', 'c'

代碼解析: 示例中繼承 Person 那一行代碼展示了盜用構(gòu)造函數(shù)的調(diào)用。通過使用call() (或 apply() )方法,Person 構(gòu)造函數(shù)在為 Student 的實(shí)例創(chuàng)建的新對象的上下文中執(zhí)行了。這相當(dāng)于新的 Student 對象上運(yùn)行了 Person() 函數(shù)中的所有初始化代碼。結(jié)果就是每個(gè)實(shí)例都會有自己的 letters 屬性。

2.1 傳遞參數(shù)

相比于使用原型鏈,盜用構(gòu)造函數(shù)的一個(gè)優(yōu)點(diǎn)就是可以在子類構(gòu)造函數(shù)中向父類構(gòu)造函數(shù)傳參。來看下面的例子:

// 定義 Person 構(gòu)造函數(shù)
function Person(name) {
  this.name = name
}

// 定義 Student 構(gòu)造函數(shù)
function Student(name) {
  // 繼承 Person
  Person.call(this, name)
  // 實(shí)例屬性
  this.age = 18
}

let stu = new Student('CoderBin')

console.log(stu.name)   // CoderBin
console.log(stu.age)     // 18

代碼解析:在這個(gè)例子中,Person 構(gòu)造函數(shù)接收一個(gè)參數(shù) name ,然后將它賦值給一個(gè)屬性。在 Student 構(gòu)造函數(shù)中調(diào)用 Person 構(gòu)造函數(shù)時(shí)傳入這個(gè)參數(shù),實(shí)際上會在 Student 的實(shí)例上定義 name 屬性。為確保 Person 構(gòu)造函數(shù)不會覆蓋 Student 定義的屬性,可以在調(diào)用父類構(gòu)造函數(shù)之后再給子類實(shí)例添加額外的屬性。

2.2 盜用構(gòu)造函數(shù)繼承的缺陷

盜用構(gòu)造函數(shù)的主要缺點(diǎn),也是使用構(gòu)造函數(shù)模式自定義類型的問題:必須在構(gòu)造函數(shù)中定義方法,因此函數(shù)不能重用。此外,子類也不能訪問父類原型上定義的方法,因此所有類型只能使用構(gòu)造函數(shù)模式。由于存在這些問題,盜用構(gòu)造函數(shù)基本上也不能單獨(dú)使用。

3. 組合繼承【方案三】

組合繼承 (有時(shí)候也叫偽經(jīng)典繼承)綜合了原型鏈和盜用構(gòu)造函數(shù),將兩者的優(yōu)點(diǎn)集中了起來。基本的思路是:使用原型鏈繼承原型上的屬性和方法,而通過盜用構(gòu)造函數(shù)繼承實(shí)例屬性。 這樣既可以把方法定義在原型上以實(shí)現(xiàn)重用,又可以讓每個(gè)實(shí)例都有自己的屬性。來看下面的例子:

// 定義 Person 構(gòu)造函數(shù)
function Person(name) {
  this.name = name
  this.letters = ['a', 'b', 'c']
}

// 在 Person 的原型上添加 sayName 方法
Person.prototype.sayName = function() {
  console.log(this.name + ' 你好~')
}

// 定義 Student 構(gòu)造函數(shù)
function Student(name, age) {
  // 繼承屬性
  Person.call(this, name)
  this.age = age
}

// 繼承方法
Student.prototype = new Person()

// 在 Student 的原型上添加 sayAge 方法
Student.prototype.sayAge = function() {
  console.log(this.age)
}

let stu1 = new Student('CoderBin', 18)
let stu2 = new Student('Bin', 23)

stu1.letters.push('d')

// 輸出 stu1 的信息
console.log(stu1.letters)   // [ 'a', 'b', 'c', 'd' ]
stu1.sayName()               // CoderBin 你好~
stu1.sayAge()                 // 18

// 輸出 stu2 的信息
console.log(stu2.letters)   // [ 'a', 'b', 'c']
stu2.sayName()               // Bin 你好~
stu2.sayAge()                 // 23

代碼解析:在這個(gè)例子中,Person 構(gòu)造函數(shù)定義了兩個(gè)屬性,name 和 letters ,而它的原型上也定義了一個(gè)方法叫 sayName() 。Student 構(gòu)造函數(shù)調(diào)用了 Person 構(gòu)造函數(shù),傳入了 name 參數(shù),然后又定義了自己的屬性 age 。

此外,Student.prototype 也被賦值為 Person 的實(shí)例。 原型賦值之后,又在這個(gè)原型上添加了新方法sayAge() 。這樣,就可以創(chuàng)建兩個(gè) Student 實(shí)例,讓這兩個(gè)實(shí)例都有自己的屬性,包括 letters , 同時(shí)還共享相同的方法。

最后:組合繼承彌補(bǔ)了原型鏈和盜用構(gòu)造函數(shù)的不足,是 JavaScript 中使用最多的繼承模式。而且組合繼承也保留了instanceof操作符和isPrototypeOf()方法識別合成對象的能力。

4. 原型式繼承【方案四】

2006年,Douglas Crockford(JSON之父) 寫了一篇文章:《JavaScript中的原型式繼承》(“Prototypal Inheritance in JavaScript”)。這篇文章介紹了 一種不涉及嚴(yán)格意義上構(gòu)造函數(shù)的繼承方法。他的出發(fā)點(diǎn)是即使不自定義類型也可以通過原型實(shí)現(xiàn)對象之間的信息共享。文章最終給出了一個(gè)函數(shù):

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

這個(gè)object() 函數(shù)會創(chuàng)建一個(gè)臨時(shí)構(gòu)造函數(shù),將傳入的對象賦值給這個(gè)構(gòu)造函數(shù)的原型,然后返回這個(gè)臨時(shí)類型的一個(gè)實(shí)例。

4.1 方法一:object

本質(zhì)上,object() 是對傳入的對象執(zhí)行了一次淺復(fù)制。 來看下面的例子:

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

let person = {
  name: 'CoderBin',
  letters: ['a', 'b', 'c']
}

let p1 = object(person)
let p2 = object(person)

p1.name = 'p1'
p1.letters.push('d')

p2.name = 'p2'
p2.letters.push('e')

console.log(person.letters)   // [ 'a', 'b', 'c', 'd', 'e' ]

代碼解析:在這個(gè)例子中,person 對象定義了另一個(gè)對象也應(yīng)該共享的信息,把它傳給 object() 之后會返回一個(gè)新對象。這個(gè)新對象的原型是 person ,意味著它的原型上既有原始值屬性又有引用值屬性。這也意味著 person.letters 不僅是 person 的屬性,也會跟 p1 和 p2 共享。這里實(shí)際上克隆了兩個(gè) person 。

Crockford推薦的原型式繼承適用于這種情況:你有一個(gè)對象,想在它的基礎(chǔ)上再創(chuàng)建一個(gè)新對象。你需要把這個(gè)對象先傳給 object() ,然后再對返回的對象進(jìn)行適當(dāng)修改。

4.2 方法二:Object.create()

ECMAScript5 通過增加Object.create()方法將原型式繼承的概念規(guī)范化了。這個(gè)方法接收兩個(gè)參數(shù):作為新對象原型的對象,以及給新對象定義額外屬性的對象(第二個(gè)可選)。在只有一個(gè)參數(shù)時(shí),Object.create() 與這里的object()方法效果相同:

let person = {
  name: 'CoderBin',
  letters: ['a', 'b', 'c']
}

let p1 = Object.create(person)
let p2 = Object.create(person)

p1.name = 'p1'
p1.letters.push('d')

p2.name = 'p2'
p2.letters.push('e')

console.log(person.letters)   // [ 'a', 'b', 'c', 'd', 'e' ]

Object.create()的第二個(gè)參數(shù)與Object.defineProperties()的第二個(gè)參數(shù)一樣:每個(gè)新增屬性都通過各自的描述符來描述。以這種方式添加的屬性會遮蔽原型對象上的同名屬性。比如:

let person = {
  name: 'CoderBin',
  letters: ['a', 'b', 'c']
}

let p1 = Object.create(person, {
  name: {
    value: 'CoderBin'
  }
})

console.log(p1.name)

原型式繼承非常適合不需要單獨(dú)創(chuàng)建構(gòu)造函數(shù),但仍然需要在對象間共享信息的場合。但要記住,屬性中包含的引用值始終會在相關(guān)對象間共享,跟使用原型模式是一樣的。

5. 寄生式繼承【方案五】

與原型式繼承比較接近的一種繼承方式是寄生式繼承 (parasitic inheritance),也是Crockford首倡的一種模式。寄生式繼承背后的思路類似于寄生構(gòu)造函數(shù)和工廠模式:創(chuàng)建一個(gè)實(shí)現(xiàn)繼承的函數(shù),以某種方式增強(qiáng)對象,然后返回這個(gè)對象?;镜募纳^承模式如下:

function inheritPrototype(o) {
  let clone = Object.create(o)  // 通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對象
  clone.sayHi = function() {     // 以某種方式增強(qiáng)這個(gè)對象
    console.log('Hi~')
  }
  return clone  // 返回這個(gè)對象
}

代碼解析:在這段代碼中,inheritPrototype() 函數(shù)接收一個(gè)參數(shù),就是新對象的基準(zhǔn)對象。這個(gè)對象 o 會被傳給Object.create()函數(shù),然后將返回的新對象賦值給 clone 。接著給 clone 對象添加一個(gè)新方法 sayHi() 。最后返回這個(gè)對象??梢韵裣旅孢@樣使用 inheritPrototype() 函數(shù):

let person = {
  name: 'CoderBin',
  letters: ['a', 'b', 'c']
}

let p1 = inheritPrototype(person)
p1.sayHi()  // Hi~

代碼解析:這個(gè)例子基于 person 對象返回了一個(gè)新對象。新返回的 p1 對象具有 person 的所有屬性和方法,還有一個(gè)新方法叫 sayHi() 。寄生式繼承同樣適合主要關(guān)注對象,而不在乎類型和構(gòu)造函數(shù)的場景。Object.create()函數(shù)不是寄生式繼承所必需的,任何返回新對象的函數(shù)都可以在這里使用。

注意: 通過寄生式繼承給對象添加函數(shù)會導(dǎo)致函數(shù)難以重用,與構(gòu)造函數(shù)模式類似。

6. 寄生式組合繼承【方案六】

組合繼承其實(shí)也存在效率問題。最主要的效率問題就是父類構(gòu)造函數(shù)始終會被調(diào)用兩次:一次在是創(chuàng)建子類原型時(shí)調(diào)用,另一次是在子類構(gòu)造函數(shù)中調(diào)用。本質(zhì)上,子類原型最終是要包含超類對象的所有實(shí)例屬性,子類構(gòu)造函數(shù)只要在執(zhí)行時(shí)重寫自己的原型就行了。

6.1 組合式繼承的缺陷

再來看一看這個(gè)組合繼承的例子:

// 定義 Person 構(gòu)造函數(shù)
function Person(name) {
  this.name = name
  this.letters = ['a', 'b', 'c']
}

// 在 Person 的原型上添加 sayName 方法
Person.prototype.sayName = function() {
  console.log(this.name)
}

// 定義 Student 構(gòu)造函數(shù)
function Student(name, age) {
  Person.call(this, name)   // 第一次調(diào)用 Person()
  this.age = age
}

Student.prototype = new Person()  // 第二次調(diào)用 Person()

// 讓 Student 的原型指回 Student
Student.prototype.constructor = Student

// 在 Student 的原型上添加 sayAge 方法
Student.prototype.sayAge = function() {
  console.log(this.age)
}

let stu = new Student('CoderBin', 18)

console.log(stu)
// 輸出:Student { name: 'CoderBin', letters: [ 'a', 'b', 'c' ], age: 18 }

console.log(Student.prototype)
// 輸出:
// Person {
//   name: undefined,
//   letters: [ 'a', 'b', 'c' ],      
//   constructor: [Function: Student],
//   sayAge: [Function (anonymous)]   
// }

代碼解析:代碼中注釋的部分是調(diào)用 Person 構(gòu)造函數(shù)的地方。在上面的代碼執(zhí)行后,Student.prototype上會有兩個(gè)屬性:name 和 letters 。它們都是 Person 的實(shí)例屬性,但現(xiàn)在成為了 Student 的原型屬性。在調(diào)用 Student 構(gòu)造函數(shù)時(shí),也會調(diào)用 Person 構(gòu)造函數(shù),這一次會在新對象上創(chuàng)建實(shí)例屬性 name 和 letters 。這兩個(gè)實(shí)例屬性會遮蔽原型上同名的屬性。

所以,執(zhí)行完上面的代碼后,有兩組 name 和 letters 屬性:一組在實(shí)例上,另一組在 Student 的原型上。這是調(diào)用兩次 Person 構(gòu)造函數(shù)的結(jié)果。

6.2 解決方法

寄生式組合繼承通過盜用構(gòu)造函數(shù)繼承屬性,但使用混合式原型鏈繼承方法?;舅悸肥?strong>不通過調(diào)用父類構(gòu)造函數(shù)給子類原型賦值,而是取得父類原型的一個(gè)副本。說到底就是使用寄生式繼承來繼承父類原型,然后將返回的新對象賦值給子類原型。寄生式組合繼承的基本模式如下所示:

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype)   // 創(chuàng)建對象
  prototype.constructor = subType                             // 增強(qiáng)對象
  subType.prototype = prototype                               // 賦值對象
}

代碼解析:這個(gè) inheritPrototype() 函數(shù)實(shí)現(xiàn)了寄生式組合繼承的核心邏輯。這個(gè)函數(shù)接收兩個(gè)參數(shù):子類構(gòu)造函數(shù)和父類構(gòu)造函數(shù)。在這個(gè)函數(shù)內(nèi)部,第一步是創(chuàng)建父類原型的一個(gè)副本。然后,給返回的prototype 對象設(shè)置 constructor 屬性,解決由于重寫原型導(dǎo)致默認(rèn) constructor 丟失的問題。最后將新創(chuàng)建的對象賦值給子類型的原型。如下例所示,調(diào)用 inheritPrototype() 就可以實(shí)現(xiàn)前面例子中的子類型原型賦值:

// 定義 Person 構(gòu)造函數(shù)
function Person(name) {
  this.name = name
  this.letters = ['a', 'b', 'c']
}

// 在 Person 的原型上添加 sayName 方法
Person.prototype.sayName = function() {
  console.log(this.name)
}

// 定義 Student 構(gòu)造函數(shù)
function Student(name, age) {
  Person.call(this, name)
  this.age = age
}
// 調(diào)用 inheritPrototype() 函數(shù),傳入 子類構(gòu)造函數(shù) 和 父類構(gòu)造函數(shù)
inheritPrototype(Student, Person)

// 在 Person 的原型上添加 sayAge 方法
Student.prototype.sayAge = function() {
  console.log(this.age)
}

let stu = new Student('CoderBin', 18)

console.log(stu)
// 輸出:Student { name: 'CoderBin', letters: [ 'a', 'b', 'c' ], age: 18 }

console.log(Student.prototype)
// 輸出
// Person {
//   constructor: [Function: Student],
//   sayAge: [Function (anonymous)]   
// }

這里只調(diào)用了一次 Person 構(gòu)造函數(shù),避免了Student.prototype上不必要也用不到的屬性,因此可以說這個(gè)例子的效率更高。而且,原型鏈仍然保持不變,因此instanceof操作符和isPrototypeOf()方法正常有效。寄生式組合繼承可以算是引用類型繼承的最佳模式。

關(guān)于“JavaScript實(shí)現(xiàn)繼承的方法是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“JavaScript實(shí)現(xiàn)繼承的方法是什么”知識都有一定的了解,大家如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI