面向?qū)ο螅ㄈ?/h2>

內(nèi)容承接 面向?qū)ο螅ǘ?/a>

繼承

ECMAScript中描述了原型鏈的概念,并將原型鏈作為實(shí)現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。

原型鏈的基本概念

簡單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。

那么,假如我們讓原型對(duì)象等于另一個(gè)類型的實(shí)例,結(jié)果會(huì)怎么樣呢?顯然,此時(shí)的原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。

  function SuperType() {
    this.property = true
  }
  
  SuperType.prototype.getSuperValue = function() {
    return this.property
  }
  function SubType() {
    this.subproperty = false
  }
  //繼承了 SuperType
  SubType.prototype = new SuperType()
  SubType.prototype.getSubValue = function () {
    return this.subproperty
  }
  
  const instance = new SubType()
  alert(instance.getSuperValue()) //true

以上代碼定義了兩個(gè)類型: SuperTypeSubType。每個(gè)類型分別有一個(gè)屬性和一個(gè)方法。它們的主要區(qū)別是SubType 繼承了 SuperType ,而繼承是通過創(chuàng)建SuperType的實(shí)例,并將該實(shí)例賦給SubType.prototype實(shí)現(xiàn)的。

實(shí)現(xiàn)繼承的本質(zhì)是重寫原型對(duì)象,代之以一個(gè)新類型的實(shí)例。

原型搜索機(jī)制

當(dāng)以讀取模式訪問一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在通過原型鏈實(shí)現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。就拿上面的例子來說,調(diào)用instance.getSuperValue() 會(huì)經(jīng)歷三個(gè)搜索步驟:

  • 搜索實(shí)例;
  • 搜索 SubType.prototype
  • 搜索SuperType.prototype,最后一步才會(huì)找到該方法。在找不到屬性或方法的情況下,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來。

細(xì)節(jié)一:別忘記默認(rèn)的原型

所有引用類型默認(rèn)都繼承了Object,而這個(gè)繼承也是通過原型鏈實(shí)現(xiàn)的。大家要記住,所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,因此默認(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指Object.prototype。

如何確定原型和實(shí)例的關(guān)系
1. instanceof

可以通過兩種方式來確定原型和實(shí)例之間的關(guān)系。第一種方式是使用instanceof操作符,只要用這個(gè)操作符來測試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會(huì)返回true。以下幾行代碼就說明了這一點(diǎn)。

  alert(instance instanceof Object) //true
  alert(instance instanceof SuperType) //true
  alert(instance instanceof SubType) //true
2. isPrototypeOf()

第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實(shí)例的原型,因此isPrototypeOf()方法也會(huì)返回 true,如下所示。

alert(Object.prototype.isPrototypeOf(instance))  //true
alert(SuperType.prototype.isPrototypeOf(instance))  //true
alert(SubType.prototype.isPrototypeOf(instance))  //true

細(xì)節(jié)二:謹(jǐn)慎地定義方法

子類型有時(shí)候需要重寫超類型中的某個(gè)方法,或者需要添加超類型中不存在的某個(gè)方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。來看下面的例子。

function SuperType() {
    this.property = true
  }

  SuperType.prototype.getSuperValue = function () {
    return this.property
  }

  function SubType() {
    this.subproperty = false
  }

  //繼承了 SuperType
  SubType.prototype = new SuperType()
  
  // 添加新方法
  SubType.prototype.getSubValue = function () {
    return this.subproperty
  }
  
  // 重寫超類型中的方法
  SubType.prototype.getSuperValue = function () {
    return false
  }
  
  const instance = new SubType()
  alert(instance.getSuperValue()) //false

第一個(gè)方法 getSubValue()被添加到了SubType中。第二個(gè)方法 getSuperValue()是原型鏈中已經(jīng)存在的一個(gè)方法,但重寫這個(gè)方法將會(huì)屏蔽原來的那個(gè)方法。換句話說,當(dāng)通過SubType 的實(shí)例調(diào)用getSuperValue()時(shí),調(diào)用的就是這個(gè)重新定義的方法;但通過SuperType的實(shí)例調(diào)用 getSuperValue()時(shí),還會(huì)繼續(xù)調(diào)用原來的那個(gè)方法。

這里要格外注意的是,必須在用SuperType的實(shí)例替換原型之后,再定義這兩個(gè)方法。

細(xì)節(jié)三:不使用對(duì)象字面量創(chuàng)建原型方法

在通過原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@樣做就會(huì)重寫原型鏈,如下面的例子所示。

 function SuperType() {
    this.property = true
  }

  SuperType.prototype.getSuperValue = function () {
    return this.property
  }

  function SubType() {
    this.subproperty = false
  }

  //繼承了 SuperType
  SubType.prototype = new SuperType()

  // 使用字面量添加新方法,會(huì)導(dǎo)致上一行代碼無效
  SubType.prototype = {
    getSubValue: function () {
      return this.subproperty
    },
    someOtherMethod: function () {
      return false;
    }
  }
  const instance = new SubType()
  alert(instance.getSuperValue()) //error!

原型鏈的問題

在通過原型來實(shí)現(xiàn)繼承時(shí),原型實(shí)際上會(huì)變成另一個(gè)類型的實(shí)例。于是,原先的實(shí)例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。

  function SuperType() {
    this.colors = ["red", "blue", "green"]
  }

  function SubType() {
  }

  //繼承了 SuperType
  SubType.prototype = new SuperType()
  const instance1 = new SubType()
  instance1.colors.push("black")
  alert(instance1.colors) // red,blue,green,black"
  const instance2 = new SubType()
  alert(instance2.colors) // "red,blue,green,black"

當(dāng) SubType通過原型鏈繼承了SuperType 之后,SubType.prototype就變成了SuperType的一個(gè)實(shí)例,因此它也擁有了一個(gè)它自己的colors屬性——就跟專門創(chuàng)建了一個(gè) SubType.prototype.colors 屬性一樣。但結(jié)果是什么呢?

結(jié)果是SubType的所有實(shí)例都會(huì)共享這一個(gè)colors 屬性。而我們對(duì) instance1.colors的修改能夠通過instance2.colors反映出來,就已經(jīng)充分證實(shí)了這一點(diǎn)。

原型鏈的第二個(gè)問題是:在創(chuàng)建子類型的實(shí)例時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。實(shí)際上,應(yīng)該說是沒有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。有鑒于此,再加上前面剛剛討論過的由于原型中包含引用類型值所帶來的問題,實(shí)踐中很少會(huì)單獨(dú)使用原型鏈。

實(shí)現(xiàn)繼承的方式

方式一:借用構(gòu)造函數(shù)

在解決原型中包含引用類型值所帶來問題的過程中,開發(fā)人員開始使用一種叫做借用構(gòu)造函數(shù)(constructor stealing)的技術(shù)(有時(shí)候也叫做偽造對(duì)象或經(jīng)典繼承)。即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。別忘了,函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對(duì)象,因此通過使用 apply()call()方法也可以在(將來)新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù),如下所示:

 function SuperType() {
    this.colors = ["red", "blue", "green"]
  }

  function SubType() {
    // 繼承了 SuperType
    SuperType.call(this)
  }

  const instance1 = new SubType()
  instance1.colors.push("black")
  alert(instance1.colors)   // "red,blue,green,black"
  const instance2 = new SubType()
  alert(instance2.colors)   // "red,blue,green"

通過使用call()方法(或 apply()方法也可以),我們實(shí)際上是在(未來將要)新創(chuàng)建的SubType實(shí)例的環(huán)境下調(diào)用了 SuperType構(gòu)造函數(shù)。這樣一來,就會(huì)在新 SubType對(duì)象上執(zhí)行SuperType() 函數(shù)中定義的所有對(duì)象初始化代碼。

優(yōu)點(diǎn): 傳遞參數(shù)

相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大的優(yōu)勢,即可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)??聪旅孢@個(gè)例子。

 function SuperType(name) {
    this.name = name
  }

  function SubType() {
    //繼承了 SuperType,同時(shí)還傳遞了參數(shù)
    SuperType.call(this, "Nicholas")
    //實(shí)例屬性
    this.age = 29
  }

  const instance = new SubType()
  alert(instance.name) //"Nicholas";
  alert(instance.age) //29
缺點(diǎn):函數(shù)復(fù)用

如果僅僅是借用構(gòu)造函數(shù),那么也將無法避免構(gòu)造函數(shù)模式存在的問題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。而且,在超類型的原型中定義的方法,對(duì)子類型而言也是不可見的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式??紤]到這些問題,借用構(gòu)造函數(shù)的技術(shù)也是很少單獨(dú)使用的。

方式二:組合繼承

組合繼承(combination inheritance),有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長的一種繼承模式。

其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,既通過在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性。

 function SuperType(name) {
    this.name = name
    this.colors = ["red", "blue", "green"]
  }

  SuperType.prototype.sayName = function () {
    alert(this.name)
  }

  function SubType(name, age) {
    //繼承屬性
    SuperType.call(this, name)
    this.age = age
  }

  //繼承方法
  SubType.prototype = new SuperType()
  SubType.prototype.constructor = SubType
  SubType.prototype.sayAge = function () {
    alert(this.age)
  }
  const instance1 = new SubType("Nicholas", 29)
  instance1.colors.push("black")
  alert(instance1.colors) //"red,blue,green,black"
  instance1.sayName(); //"Nicholas";
  instance1.sayAge(); //29
  
  const instance2 = new SubType("Greg", 27)
  alert(instance2.colors) //"red,blue,green"
  instance2.sayName() //"Greg";
  instance2.sayAge() //27

SubType 構(gòu)造函數(shù)在調(diào)用SuperType構(gòu)造函數(shù)時(shí)傳入了 name 參數(shù),緊接著又定義了它自己的屬性age。然后,將SuperType的實(shí)例賦值給 SubType的原型,然后又在該新原型上定義了方法sayAge()。這樣一來,就可以讓兩個(gè)不同的 SubType 實(shí)例既分別擁有自己屬性——包括colors屬性,又可以使用相同的方法了。

組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為JavaScript中最常用的繼承模式。而且, instanceofisPrototypeOf() 也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。

方式三:原型式繼承

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

function object(o) {
    function F() {
    }

    F.prototype = o
    return new F()
}

object() 函數(shù)內(nèi)部,先創(chuàng)建了一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回了這個(gè)臨時(shí)類型的一個(gè)新實(shí)例。

從本質(zhì)上講,object() 對(duì)傳入其中的對(duì)象執(zhí)行了一次淺復(fù)制。

 function object(o) {
    function F() {
    }

    F.prototype = o
    return new F()
  }
  const person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  }
  
  const anotherPerson = object(person)
  anotherPerson.name = "Greg"
  anotherPerson.friends.push("Rob")
  
  const yetAnotherPerson = object(person)
  yetAnotherPerson.name = "Linda"
  yetAnotherPerson.friends.push("Barbie")
  alert(person.friends) //"Shelby,Court,Van,Rob,Barbie"

克羅克福德主張的這種原型式繼承,要求你必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。如果有這么一個(gè)對(duì)象的話,可以把它傳遞給object()函數(shù),然后再根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。

可以作為另一個(gè)對(duì)象基礎(chǔ)的是person對(duì)象,于是我們把它傳入到object()函數(shù)中,然后該函數(shù)就會(huì)返回一個(gè)新對(duì)象。這個(gè)新對(duì)象將person作為原型,所以它的原型中就包含一個(gè)基本類型值屬性和一個(gè)引用類型值屬性。這意味著person.friends不僅屬于person所有,而且也會(huì)被anotherPerson以及yetAnotherPerson 共享。

Object.create()

ECMAScript 5通過新增 Object.create()方法規(guī)范化了原型式繼承。這個(gè)方法接收兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和(可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,Object.create()object()方法的行為相同。

 const person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  }
  
  const anotherPerson = Object.create(person)
  anotherPerson.name = "Greg"
  anotherPerson.friends.push("Rob")
  
  const yetAnotherPerson = Object.create(person)
  yetAnotherPerson.name = "Linda"
  yetAnotherPerson.friends.push("Barbie")
  
  alert(person.friends) //"Shelby,Court,Van,Rob,Barbie"

Object.create()方法的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性。

 const person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  }
  
  const anotherPerson = Object.create(person, {
    name: {
      value: "Greg"
    }
  })
  
  alert(anotherPerson.name)  //"Greg"

支持Object.create()方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+Chrome。

方式四:寄生式繼承

寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個(gè)僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象,最后再像真地是它做了所有工作一樣返回對(duì)象。以下代碼示范了寄生式繼承模式。

   function createAnother(original) {
    const clone = object(original) //通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
    clone.sayHi = function () { //以某種方式來增強(qiáng)這個(gè)對(duì)象
      alert("hi")
    }
    return clone //返回這個(gè)對(duì)象
  }

在這個(gè)例子中,createAnother() 函數(shù)接收了一個(gè)參數(shù),也就是將要作為新對(duì)象基礎(chǔ)的對(duì)象。然后,把這個(gè)對(duì)象(original)傳遞給object()函數(shù),將返回的結(jié)果賦值給clone 。再為 clone對(duì)象添加一個(gè)新方法sayHi(),最后返回clone對(duì)象??梢韵裣旅孢@樣來使用 createAnother() 函數(shù):

 function createAnother(original) {
    const clone = object(original) //通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
    clone.sayHi = function () { //以某種方式來增強(qiáng)這個(gè)對(duì)象
      alert("hi")
    }
    return clone //返回這個(gè)對(duì)象
  }
  const person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  }
  const anotherPerson = createAnother(person)
  anotherPerson.sayHi() //"hi"

這個(gè)例子中的代碼基于person 返回了一個(gè)新對(duì)象—— anotherPerson。新對(duì)象不僅具有person的所有屬性和方法,而且還有自己的 sayHi()方法。

使用寄生式繼承來為對(duì)象添加函數(shù),會(huì)由于不能做到函數(shù)復(fù)用而降低效率;這一點(diǎn)與構(gòu)造函數(shù)模式類似。

方式五:寄生組合式繼承

組合繼承是JavaScript最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會(huì)調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時(shí)候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部。

 function SuperType(name) {
    this.name = name
    this.colors = ["red", "blue", "green"]
  }

  SuperType.prototype.sayName = function () {
    alert(this.name)
  };

  function SubType(name, age) {
    SuperType.call(this, name) // 第二次調(diào)用 SuperType()
    this.age = age
  }

  SubType.prototype = new SuperType() // 第一次調(diào)用 SuperType()
  SubType.prototype.constructor = SubType
  SubType.prototype.sayAge = function () {
    alert(this.age)
  }

在第一次調(diào)用SuperType 構(gòu)造函數(shù)時(shí),SubType.prototype會(huì)得到兩個(gè)屬性:namecolors;它們都是SuperType 的實(shí)例屬性,只不過現(xiàn)在位于SubType的原型中。當(dāng)調(diào)用 SubType構(gòu)造函數(shù)時(shí),又會(huì)調(diào)用一次SuperType 構(gòu)造函數(shù),這一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性namecolors 。于是,這兩個(gè)屬性就屏蔽了原型中的兩個(gè)同名屬性。

所謂寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要的無非就是超類型原型的一個(gè)副本而已。本質(zhì)上,就是使用寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型。寄生組合式繼承的基本模式如下所示。

function inheritPrototype(subType, superType){
    const prototype = object(superType.prototype) //創(chuàng)建對(duì)象
    prototype.constructor = subType //增強(qiáng)對(duì)象
    subType.prototype = prototype //指定對(duì)象
}

這個(gè)示例中的 inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡單形式。這個(gè)函數(shù)接收兩個(gè)參數(shù):子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。

在函數(shù)內(nèi)部,第一步是創(chuàng)建超類型原型的一個(gè)副本。第二步是為創(chuàng)建的副本添加 constructor 屬性,從而彌補(bǔ)因重寫原型而失去的默認(rèn)的constructor 屬性。最后一步,將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型。這樣,我們就可以用調(diào)用inherit-Prototype()函數(shù)的語句,去替換前面例子中為子類型原型賦值的語句了,例如:

 function inheritPrototype(subType, superType) {
    const prototype = object(superType.prototype) //創(chuàng)建對(duì)象
    prototype.constructor = subType //增強(qiáng)對(duì)象
    subType.prototype = prototype //指定對(duì)象
  }

  function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
  }

  SuperType.prototype.sayName = function () {
    alert(this.name)
  }

  function SubType(name, age) {
    SuperType.call(this, name)
    this.age = age
  }

  inheritPrototype(SubType, SuperType)
  SubType.prototype.sayAge = function () {
    alert(this.age)
  }

這個(gè)例子的高效率體現(xiàn)在它只調(diào)用了一次 SuperType 構(gòu)造函數(shù),并且因此避免了在 SubType.prototype上面創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用instanceofisPrototypeOf()。開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。

參考書籍

《JavaScript高級(jí)程序設(shè)計(jì)(第3版)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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