JavaScript面向?qū)ο?二.三.四)

前面, 我們討論了很多種場景對(duì)象的方式: 從Object到字面量, 再到工廠模式, 再到構(gòu)造函數(shù).

最終我們發(fā)現(xiàn), 構(gòu)造函數(shù)是比較理想的一種方式, 但是它也存在問題.

為了最終解決這個(gè)問題, 我們需要學(xué)習(xí)一個(gè)新的知識(shí): 原型(prototype).

一. 理解原型模式

1.1. 什么是原型呢?

你需要先知道一個(gè)事實(shí):

我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype(原型)屬性

這個(gè)屬性是一個(gè)指針蜂挪,指向一個(gè)對(duì)象

而這個(gè)對(duì)象的作用是存放這個(gè)類型創(chuàng)建的所有實(shí)例共享的屬性和方法谱秽。

指向的這個(gè)對(duì)象, 就是我們的所謂的原型對(duì)象.

原型對(duì)象的作用:

使用原型對(duì)象的好處是可以讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。

換句話說,不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息篇梭,而是可以將這些信息直接添加到原型對(duì)象中。

我們來看看原型對(duì)象的使用:

//?創(chuàng)建對(duì)象的構(gòu)造函數(shù)

functionPerson(){}

//?通過原型對(duì)象來設(shè)置一些屬性和值

Person.prototype.name?="Coderwhy"

Person.prototype.age?=18

Person.prototype.height?=1.88

Person.prototype.sayHello?=function(){

alert(this.name)

}

//?創(chuàng)建兩個(gè)對(duì)象,?并且調(diào)用方法

varperson1?=newPerson()

varperson2?=newPerson()

person1.sayHello()//?Coderwhy

person2.sayHello()//?Coderwhy

代碼解析:

在上面的代碼中, 我們沒有給實(shí)例對(duì)象單獨(dú)設(shè)置屬性和方法, 而是直接設(shè)置給了原型對(duì)象.

而原型對(duì)象的作用是可以讓所有的對(duì)象來共享這些屬性和方法.

因此, 我們調(diào)用sayHello()方法時(shí), 它們打印的結(jié)果是一樣的, 它們是共享的.

1.2. 深入原型對(duì)象

原型對(duì)象的創(chuàng)建:

無論什么時(shí)候泛粹,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù)埃儿,就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象掩宜。

原型上的constructor屬性:

默認(rèn)情況下蔫骂,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)的指針牺汤。

用我們上面的例子來說, Person.prototype.constructor指向Person辽旋。

也就是原型對(duì)象自身來說, 只有一個(gè)constructor屬性, 而其他屬性可以由我們添加或者從Object中繼承.

新的實(shí)例創(chuàng)建時(shí), 原型對(duì)象在哪里呢?

當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)內(nèi)部屬性,該屬性的指針, 指向構(gòu)造函數(shù)的原型對(duì)象补胚。

這個(gè)屬性是_proto_

簡單說, 每個(gè)實(shí)例中, 其實(shí)也會(huì)有一個(gè)屬性, 該屬性是指向原型對(duì)象的.

//?原型對(duì)象中有一個(gè)屬性:?constructor屬性

//?屬性指向Person函數(shù)

console.log(Person.prototype.constructor);//?Person函數(shù)

//?對(duì)象實(shí)例也有一個(gè)屬性指向原型

console.log(person1.__proto__);//?原型對(duì)象

console.log(Person.prototype);//?原型對(duì)象

console.log(person1.__proto__?===?Person.prototype);//?true

我們通過一個(gè)圖來解釋上面的概念:

img

解析:

上面的圖解析了Person構(gòu)造函數(shù)码耐、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系

Person.prototype指向原型對(duì)象, 而Person.prototype.constructor又指回了Person.

原型對(duì)象中除了包含constructor屬性之外,還包括后來添加的其他屬性溶其。

Person的每個(gè)實(shí)例——personl和person2都包含一個(gè)內(nèi)部屬性_proto_骚腥,該屬性也指向原型對(duì)象;

對(duì)象搜索屬性和方法的過程:

每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí)瓶逃,都會(huì)執(zhí)行一次搜索束铭,也就是要找到給定名稱的屬性。

搜索首先從對(duì)象實(shí)例本身開始

如果在實(shí)例中找到了具有給定名字的屬性厢绝,則返回該屬性的值契沫;

如果沒有找到,則繼續(xù)搜索指針指向的原型對(duì)象昔汉,在原型對(duì)象中查找具有給定名字的屬性

如果在原型對(duì)象中找到了這個(gè)屬性懈万,則返回該屬性的值。

也就是說靶病,在我們調(diào)用personl.sayHello()的時(shí)候会通,會(huì)先后執(zhí)行兩次搜索。

現(xiàn)在我們也能理解, 為什么所有的實(shí)例中都包含一個(gè)constructor屬性, 這是因?yàn)槟J(rèn)所有的原型對(duì)象中都包含了該屬性.

可以通過__proto__來修改原型的值(通常不會(huì)這樣修改, 知道即可)

你可以理解為什么person1修改了name后, person2也會(huì)修改嗎?

通過上面的圖, 自己再來理解一下吧.

person1.sayHello()//?Coderwhy

person2.sayHello()//?Coderwhy

person1.__proto__.name?="Kobe"

person1.sayHello()//?Kobe

person2.sayHello()//?Kobe

但是要注意下面的情況:

當(dāng)我們給person1.name進(jìn)行賦值時(shí), 其實(shí)在給person1實(shí)例添加一個(gè)name屬性.

這個(gè)時(shí)候再次訪問時(shí), 就不會(huì)訪問原型中的name屬性了.

//?創(chuàng)建兩個(gè)對(duì)象,?并且調(diào)用方法

varperson1?=newPerson()

varperson2?=newPerson()

person1.sayHello()//?Coderwhy

person2.sayHello()//?Coderwhy

//?給person1實(shí)例添加屬性

person1.name?="Kobe"

person1.sayHello()//?Kobe,?來自實(shí)例

person2.sayHello()//?Coderwhy,?來自原型

通過hasOwnProperty判斷屬性屬于實(shí)例還是原型.

//?判斷屬性屬于誰

alert(person1.hasOwnProperty("name"))//?true

alert(person2.hasOwnProperty("name"))//?false

1.3. 簡潔的原型語法

簡潔語法概述:

如果按照前面的做法, 每添加一個(gè)原型屬性和方法, 都要敲一遍Person.prototype.

為了減少不必要的輸入, 另外也為了更好的封裝性, 更常用的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來重寫整個(gè)原型對(duì)象.

字面量重寫原型對(duì)象:

//?定義Person構(gòu)造函數(shù)

functionPerson(){}

//?重寫Person的原型屬性

Person.prototype?=?{

name:"Coderwhy",

age:18,

height:1.88,

sayHello:function(){

alert(this.name)

}

}

注意:

我們將Person.prototype賦值了一個(gè)新的對(duì)象字面量, 最終結(jié)果和原來是一樣的娄周;

但是: constructor屬性不再指向Person了涕侈;

前面我們說過, 每創(chuàng)建一個(gè)函數(shù), 就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象, 這個(gè)對(duì)象也會(huì)自動(dòng)獲取constructor屬性;

而我們這里相當(dāng)于給prototype重新賦值了一個(gè)對(duì)象, 那么這個(gè)新對(duì)象的constructor屬性, 會(huì)指向Object構(gòu)造函數(shù), 而不是Person構(gòu)造函數(shù)了昆咽;

//?創(chuàng)建Person對(duì)象

varperson?=newPerson()

alert(person.constructor?===Object)//?true

alert(person.constructor?===?Person)//?false

alert(personinstanceofPerson)//?true

如果在某些情況下, 我們確實(shí)需要用到constructor的值, 可以手動(dòng)的給constructor賦值即可

//?定義Person構(gòu)造函數(shù)

functionPerson(){}

//?重寫Person的原型屬性

Person.prototype?=?{

constructor:?Person,

name:"Coderwhy",

age:18,

height:1.88,

sayHello:function(){

alert(this.name)

}

}

//?創(chuàng)建Person對(duì)象

varperson?=newPerson()

alert(person.constructor?===Object)//?false

alert(person.constructor?===?Person)//?true

alert(personinstanceofPerson)//?true

上面的方式雖然可以, 但是也會(huì)造成constructor的[[Enumerable]]特性被設(shè)置了true.

默認(rèn)情況下, 原生的constructor屬性是不可枚舉的.

如果希望解決這個(gè)問題, 就可以使用我們前面介紹的Object.defineProperty()函數(shù)了.

//?定義Person構(gòu)造函數(shù)

functionPerson(){}

//?重寫Person的原型屬性

Person.prototype?=?{

name:"Coderwhy",

age:18,

height:1.88,

sayHello:function(){

alert(this.name)

}

}

Object.defineProperty(Person.prototype,"constructor",?{

enumerable:false,

value:?Person

})

1.4. 修改原型屬性

考慮下面的代碼執(zhí)行是否會(huì)有問題:

//?定義Person構(gòu)造函數(shù)

functionPerson(){}

//?創(chuàng)建Person的對(duì)象

varperson?=newPerson()

//?給Person的原型添加方法

Person.prototype.sayHello?=function(){

alert("Hello?JavaScript")

}

//?調(diào)用方法

person.sayHello()

代碼解析:

我們發(fā)現(xiàn)代碼的執(zhí)行沒有任何問題.

因?yàn)樵趧?chuàng)建person的時(shí)候, person的__proto__也是指向的Person.prototype.

所以, 當(dāng)動(dòng)態(tài)的修改了Person.prototype中的sayHello屬性時(shí), person中也可以獲取到該屬性

圖解上面的過程:

img

我們?cè)賮砜聪旅娴拇a會(huì)不會(huì)有問題:

//?定義Person構(gòu)造函數(shù)

functionPerson(){}

//?創(chuàng)建Person的對(duì)象

varperson?=newPerson()

//?給Person的原型添加方法

Person.prototype?=?{

constructor:?Person,

sayHello:function(){

alert("Hello?JavaScript")

}

}

//?調(diào)用方法

person.sayHello()

代碼解析:

代碼是不能正常運(yùn)行的. 因?yàn)镻erson的prototype指向了一個(gè)新的對(duì)象.

而最初我們創(chuàng)建的person依然指向原來的原型對(duì)象, 原來的原型對(duì)象沒有sayHello()函數(shù).

當(dāng)然, 如果再次之后, 再創(chuàng)建的Person對(duì)象, 是可以調(diào)用sayHello()的, 但是再次之前創(chuàng)建的, 沒有該方法.

圖解上面的過程:

img

1.5. 原型對(duì)象問題

原型對(duì)象也有一些缺點(diǎn):

首先, 它不再有為構(gòu)造函數(shù)傳遞參數(shù)的環(huán)節(jié), 所有實(shí)例在默認(rèn)情況下都將有相同的屬性值.

另外, 原型中所有的屬性是被很多實(shí)例共享的, 這種共享對(duì)于函數(shù)來說非常適合, 對(duì)于基本屬性通常情況下也不會(huì)有問題. (因?yàn)橥ㄟ^person.name直接修改時(shí), 會(huì)在實(shí)例上重新創(chuàng)建該屬性名, 不會(huì)在原型上修改. 除非使用person.__proto__.name修改).

但是, 對(duì)于引用類型的實(shí)例, 就必然會(huì)存在問題.

考慮下面代碼的問題:

//?定義Person構(gòu)造函數(shù)

functionPerson(){}

//?設(shè)置Person原型

Person.prototype?=?{

constructor:?Person,

name:"Coderwhy",

age:18,

height:1.88,

hobby:?["Basketball","Football"],

sayHello:function(){

alert("Hello?JavaScript")

}

}

//?創(chuàng)建兩個(gè)person對(duì)象

varperson1?=newPerson()

varperson2?=newPerson()

alert(person1.hobby)//?Basketball,Football

alert(person2.hobby)//?Basketball,Football

person1.hobby.push("tennis")

alert(person1.hobby)//?Basketball,Football,tennis

alert(person2.hobby)//?Basketball,Football,tennis

OK, 我們會(huì)發(fā)現(xiàn), 我們明明給person1添加了一個(gè)愛好, 但是person2也被添加到一個(gè)愛好.

因?yàn)樗鼈兪枪蚕淼耐粋€(gè)數(shù)組.

但是, 我們希望每個(gè)人有屬于自己的愛好, 而不是所有的Person愛好都相同.

二. 組合構(gòu)造函數(shù)和原型模式

創(chuàng)建自定義類型的最常見方式驾凶,就是組合使用構(gòu)造函數(shù)模式與原型模式。

構(gòu)造函數(shù)模式用于定義實(shí)例屬性掷酗,而原型模式用于定義方法和共享的屬性调违。

結(jié)果,每個(gè)實(shí)例都會(huì)有自己的一份實(shí)例屬性的副本泻轰,但同時(shí)又共享著對(duì)方法的引用技肩,最大限度地節(jié)省了內(nèi)存。

另外浮声,這種混成模式還支持向構(gòu)造函數(shù)傳遞參數(shù)虚婿;可謂是集兩種模式之長。

組合構(gòu)造函數(shù)和原型模式的代碼

//?創(chuàng)建Person構(gòu)造函數(shù)

functionPerson(name,?age,?height){

this.name?=?name

this.age?=?age

this.height?=?height

this.hobby?=?["Basketball","Football"]

}

//?重新Peron的原型對(duì)象

Person.prototype?=?{

constructor:?Person,

sayHello:function(){

alert("Hello?JavaScript")

}

}

//?創(chuàng)建對(duì)象

varperson1?=newPerson("Coderwhy",18,1.88)

varperson2?=newPerson("Kobe",30,1.98)

//?測試是否共享了函數(shù)

alert(person1.sayHello?==?person2.sayHello)//?true

//?測試引用類型是否存在問題

person1.hobby.push("tennis")

alert(person1.hobby)

alert(person2.hobby)

如果理解了原型, 上面的代碼非常好理解.

person1和person2各有一份自己的屬性, 但是方法是共享的.

事實(shí)上, 還有一些其他的變種模式來實(shí)現(xiàn)基于對(duì)象的封裝. 但是這種方式是最常用的, 因此我們這里不再展開討論其他的模式. 后續(xù)需要我們?cè)偕钊胗懻?

JavaScript面向?qū)ο笤斀猓ㄈ?/p>

繼承是面向?qū)ο笾蟹浅V匾奶匦?

ES5中和類的實(shí)現(xiàn)一樣, 不能直接實(shí)現(xiàn)繼承. 實(shí)現(xiàn)繼承主要是依靠原型鏈來實(shí)現(xiàn)的泳挥。

一. 原型鏈

原型鏈?zhǔn)荅S5中實(shí)現(xiàn)繼承的主要手段, 因此相對(duì)比較重要, 我們需要深入理解原型鏈.

1.1. 深入理解原型鏈

先來回顧一下構(gòu)造函數(shù)然痊、原型和實(shí)例的關(guān)系:

每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象, 通過prototype指針指向該原型對(duì)象.

原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針, 通過constructor指針, 指向構(gòu)造函數(shù)

而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針, 該內(nèi)部指針我們通常使用__proto__來描述.

思考如下情況:

我們知道, 可以通過Person.prototype = {}的方式來重寫原型對(duì)象.

假如, 我們后面賦值的不是一個(gè){}, 而是另外一個(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í)例與原型的鏈條。這就是所謂原型鏈的基本概念躬它。

有些抽象, 我們通過代碼來理解:

//?創(chuàng)建Person構(gòu)造函數(shù)

functionPerson(){

}

//?設(shè)置Animal的原型

Person.prototype?=?{

}

我們將代碼修改成原型鏈的形式:

//?1.創(chuàng)建Animal的構(gòu)造函數(shù)

functionAnimal(){

this.animalProperty?="Animal"

}

//?2.給Animal的原型中添加一個(gè)方法

Animal.prototype.animalFunction?=function(){

alert(this.animalProperty)

}

//?3.創(chuàng)建Person的構(gòu)造函數(shù)

functionPerson(){

this.personProperty?="Person"

}

//?4.給Person的原型對(duì)象重新賦值

Person.prototype?=newAnimal()

//?5.給Person添加屬于自己的方法

Person.prototype.personFunction?=function(){

alert(this.personProperty)

}

//?6.創(chuàng)建Person的實(shí)例

varperson?=newPerson()

person.animalFunction()

person.personFunction()

代碼解析:

代碼有一些復(fù)雜, 但是如果你希望學(xué)習(xí)好原型鏈, 必須耐心去看一看上面的代碼, 你會(huì)發(fā)現(xiàn)其實(shí)都是我們學(xué)習(xí)過的.

重點(diǎn)我們來看第4步代碼: 給Person.prototype賦值了一個(gè)Animal的實(shí)例. 也就是Person的原型變成了Animal的實(shí)例.

Animal實(shí)例本身有一個(gè)__proto__可以指向Animal的原型.

那么, 我們來思考一個(gè)問題: 如果現(xiàn)在搜索一個(gè)屬性或者方法, 這個(gè)時(shí)候會(huì)按照什么順序搜索呢?

第一步, 在person實(shí)例中搜索, 搜索到直接返回或者調(diào)用函數(shù). 如果沒有執(zhí)行第二步.

第二步, 在Person的原型中搜索, Person的原型是誰? Animal的實(shí)例. 所以會(huì)在Animal的實(shí)例中搜索, 無論是屬性還是方法, 如果搜索到則直接返回或者執(zhí)行. 如果沒有, 執(zhí)行第三步.

第三步, 在Animal的原型中搜索, 搜索到返回或者執(zhí)行, 如果沒有, 搜索結(jié)束. (當(dāng)然其實(shí)還有Object, 但是先不考慮)

畫圖解析可能更加清晰:

當(dāng)代碼執(zhí)行到第3步(上面代碼的序號(hào))的時(shí)候, 如圖所示:

img

當(dāng)代碼執(zhí)行第4步(上面代碼的序號(hào))時(shí), 發(fā)生了如圖所示的變化

注意圖片中的紅色線, 原來指向的是誰, 現(xiàn)在指向的是誰.

img

代碼繼續(xù)執(zhí)行

Person.prototype.personFunction = function (){}

當(dāng)執(zhí)行第5步, 也就是給Person的原型賦值了一個(gè)函數(shù)時(shí), 事實(shí)上在給new Animal(Animal的實(shí)例)賦值了一個(gè)新的方法.

img

代碼繼續(xù)執(zhí)行, 我們創(chuàng)建了一個(gè)Person對(duì)象

創(chuàng)建Person對(duì)象, person對(duì)象會(huì)有自己的屬性, personProperty.

另外, person對(duì)象有一個(gè)__prototype__指向Person的原型.

Person的原型是誰呢? 就是我們之前的new Animal(Animal的一個(gè)實(shí)例), 所以會(huì)指向它.

原型鏈簡單總結(jié):

通過實(shí)現(xiàn)原型鏈腾啥,本質(zhì)上擴(kuò)展了本章前面介紹的原型搜索機(jī)制。

當(dāng)以讀取模式訪問一個(gè)實(shí)例屬性時(shí)冯吓,首先會(huì)在實(shí)例中搜索該屬性倘待。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型组贺。在通過原型鏈實(shí)現(xiàn)繼承的情況下延柠,搜索過程就得以沿著原型鏈繼續(xù)向上。

在找不到屬性或方法的情況下锣披,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來。

1.2. 原型和實(shí)例的關(guān)系

如果我們希望確定原型和實(shí)例之間的關(guān)系, 有兩種方式:

第一種方式是使用instanceof操作符贿条,只要用這個(gè)操作符來測試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù)雹仿,結(jié)果就會(huì)返回true。

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

instanceof操作符

//?instanceof

alert(personinstanceofObject)//?true

alert(personinstanceofAnimal)//?true

alert(personinstanceofPerson)//?true

isPrototypeOf()函數(shù)

//?isPrototypeOf函數(shù)

alert("isPrototypeOf函數(shù)函數(shù)")

alert(Object.prototype.isPrototypeOf(person))//?true

alert(Animal.prototype.isPrototypeOf(person))//?true

alert(Person.prototype.isPrototypeOf(person))//?true

1.3. 添加新的方法

添加新的方法

在第5步操作中, 我們?yōu)樽宇愋吞砑恿艘粋€(gè)新的方法. 但是這里有一個(gè)注意點(diǎn).

無論是子類中添加新的方法, 還是對(duì)父類中方法進(jìn)行重寫. 都一定要將添加方法的代碼, 放在替換原型語句之后.

否則, 我們添加的方法將會(huì)無效.

錯(cuò)誤代碼引起的代碼:

//?1.定義Animal的構(gòu)造函數(shù)

functionAnimal(){

this.animalProperty?="Animal"

}

//?2.給Animal添加方法

Animal.prototype.animalFunction?=function(){

alert(this.animalProperty)

}

//?3.定義Person的構(gòu)造函數(shù)

functionPerson(){

this.personProperty?="Person"

}

//?4.給Person添加方法

Person.prototype.personFunction?=function(){

alert(this.personProperty)

}

//?5.給Person賦值新的原型對(duì)象

Person.prototype?=newAnimal()

//?6.創(chuàng)建Person對(duì)象,?并且調(diào)用方法

varperson?=newPerson()

person.personFunction()//?不會(huì)有任何彈窗,?因?yàn)檎也坏皆摲椒?/p>

代碼解析:

執(zhí)行上面的代碼不會(huì)出現(xiàn)任何的彈窗, 因?yàn)槲覀兲砑拥姆椒ㄊ菬o效的, 被賦值的新的原型覆蓋了.

正確的辦法是將第4步和第5步操作換一下位置即可.

總結(jié)

其實(shí)這個(gè)問題沒什么好說的, 只要你理解了原型鏈(好好看看我上面畫的圖, 或者自己畫一下圖)

但是, 切記在看圖的過程中一樣掃過, 因?yàn)檫@會(huì)讓你錯(cuò)過很多細(xì)節(jié), 對(duì)原型鏈的理解就會(huì)出現(xiàn)問題.

1.4. 原型鏈的問題

原型鏈對(duì)于繼承來說:

原型鏈似乎對(duì)初學(xué)JavaScript原型的人來說, 已經(jīng)算是比較高明的設(shè)計(jì)技巧了, 有些人理解起來都稍微有些麻煩.

但是, 這種設(shè)計(jì)還存在一些缺陷, 不是最理性的解決方案. (但是后續(xù)的解決方案也是依賴原型鏈, 無論如何都需要先理解它)

原型鏈存在的問題:

原型鏈存在最大的問題是關(guān)于引用類型的屬性.

通過上面的原型實(shí)現(xiàn)了繼承后, 子類的person對(duì)象繼承了(可以訪問)Animal實(shí)例中的屬性(animalProperty).

但是如果這個(gè)屬性是一個(gè)引用類型(比如數(shù)組或者其他引用類型), 就會(huì)出現(xiàn)問題.

引用類型的問題代碼:

//?1.定義Animal的構(gòu)造函數(shù)

functionAnimal(){

this.colors?=?["red","green"]

}

//?2.給Animal添加方法

Animal.prototype.animalFunction?=function(){

alert(this.colors)

}

//?3.定義Person的構(gòu)造函數(shù)

functionPerson(){

this.personProperty?="Person"

}

//?4.給Person賦值新的原型對(duì)象

Person.prototype?=newAnimal()

//?5.給Person添加方法

Person.prototype.personFunction?=function(){

alert(this.personProperty)

}

//?6.創(chuàng)建Person對(duì)象,?并且調(diào)用方法

varperson1?=newPerson()

varperson2?=newPerson()

alert(person1.colors)//?red,green

alert(person2.colors)//?red,green

person1.colors.push("blue")

alert(person1.colors)//?red,green,blue

alert(person2.colors)//?red,green,blue

代碼解析:

我們查看第6步的操作

創(chuàng)建了兩個(gè)對(duì)象, 并且查看了它們的colors屬性

修改了person1中的colors屬性, 添加了一個(gè)新的顏色blue

再次查看兩個(gè)對(duì)象的colors屬性, 會(huì)發(fā)現(xiàn)person2的colors屬性也發(fā)生了變化

兩個(gè)實(shí)例應(yīng)該是相互獨(dú)立的, 這樣的變化如果我們不制止將會(huì)在代碼中引發(fā)一些列問題.

原型鏈的其他問題:

在創(chuàng)建子類型的實(shí)例時(shí)邑商,不能向父類型的構(gòu)造函數(shù)中傳遞參數(shù)。

實(shí)際上凡蚜,應(yīng)該說是沒有辦法在不影響所有對(duì)象實(shí)例的情況下人断,給父類型的構(gòu)造函數(shù)傳遞參數(shù)。

從而可以修改父類型中屬性的值, 在創(chuàng)建構(gòu)造函數(shù)的時(shí)候就確定一個(gè)值.

二. 經(jīng)典繼承

為了解決原型鏈繼承中存在的問題, 開發(fā)人員提供了一種新的技術(shù): constructor stealing(有很多名稱: 借用構(gòu)造函數(shù)或經(jīng)典繼承或偽造對(duì)象), steal是偷竊的意思, 但是這里可以翻譯成借用.

2.1. 經(jīng)典繼承的思想

經(jīng)典繼承的做法非常簡單: 在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用父類型構(gòu)造函數(shù).

因?yàn)楹瘮?shù)可以在任意的時(shí)刻被調(diào)用

因此通過apply()和call()方法也可以在新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù).

經(jīng)典繼承代碼如下:

//?創(chuàng)建Animal的構(gòu)造函數(shù)

functionAnimal(){

this.colors?=?["red","green"]

}

//?創(chuàng)建Person的構(gòu)造函數(shù)

functionPerson(){

//?繼承Animal的屬性

Animal.call(this)

//?給自己的屬性賦值

this.name?="Coderwhy"

}

//?創(chuàng)建Person對(duì)象

varperson1?=newPerson()

varperson2?=newPerson()

alert(person1.colors)//?red,greem

alert(person2.colors)//?red,greem

person1.colors.push("blue")

alert(person1.colors)//?red,green,blue

alert(person2.colors)//?red,green

代碼解析:

我們通過在Person構(gòu)造函數(shù)中, 使用call函數(shù), 將this傳遞進(jìn)去.

這個(gè)時(shí)候, 當(dāng)Animal中有相關(guān)屬性初始化時(shí), 就會(huì)在this對(duì)象上進(jìn)行初始化操作.

這樣就實(shí)現(xiàn)了類似于繼承Animal屬性的效果.

這個(gè)時(shí)候, 我們也可以傳遞參數(shù), 修改上面的代碼:

//?創(chuàng)建Animal構(gòu)造函數(shù)

functionAnimal(age){

this.age?=?age

}

//?創(chuàng)建Person構(gòu)造函數(shù)

functionPerson(name,?age){

Animal.call(this,?age)

this.name?=?name

}

//?創(chuàng)建Person對(duì)象

varperson?=newPerson("Coderwhy",18)

alert(person.name)

alert(person.age)

2.2. 經(jīng)典繼承的問題

經(jīng)典繼承的問題:

對(duì)于經(jīng)典繼承理解比較深入, 你已經(jīng)能發(fā)現(xiàn): 經(jīng)典繼承只有屬性的繼承, 無法實(shí)現(xiàn)方法的繼承.

因?yàn)檎{(diào)用call函數(shù), 將this傳遞進(jìn)去, 只能將父構(gòu)造函數(shù)中的屬性初始化到this中.

但是如果函數(shù)存在于父構(gòu)造函數(shù)的原型對(duì)象中, this中是不會(huì)有對(duì)應(yīng)的方法的.

回顧原型鏈和經(jīng)典繼承:

原型鏈存在的問題是引用類型問題和無法傳遞參數(shù), 但是方法可以被繼承

經(jīng)典繼承是引用類型沒有問題, 也可以傳遞參數(shù), 但是方法無法被繼承.

怎么辦呢? 將兩者結(jié)合起來怎么樣?

三. 組合繼承

如果你認(rèn)識(shí)清楚了上面兩種實(shí)現(xiàn)繼承的方式存在的問題, 就可以很好的理解組合繼承了.

組合繼承(combination inheritance, 有時(shí)候也稱為偽經(jīng)典繼承), 就是將原型鏈和經(jīng)典繼承組合在一起, 從而發(fā)揮各自的優(yōu)點(diǎn).

3.1. 組合繼承的思想

組合繼承:

組合繼承就是發(fā)揮原型鏈和經(jīng)典繼承各自的優(yōu)點(diǎn)來完成繼承的實(shí)現(xiàn).

使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承.

通過經(jīng)典繼承實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承, 以及可以在構(gòu)造函數(shù)中傳遞參數(shù).

組合繼承的代碼:

//?1.創(chuàng)建構(gòu)造函數(shù)的階段

//?1.1.創(chuàng)建Animal的構(gòu)造函數(shù)

functionAnimal(age){

this.age?=?age

this.colors?=?["red","green"]

}

//?1.2.給Animal添加方法

Animal.prototype.animalFunction?=function(){

alert("Hello?Animal")

}

//?1.3.創(chuàng)建Person的構(gòu)造函數(shù)

functionPerson(name,?age){

Animal.call(this,?age)

this.name?=?name

}

//?1.4.給Person的原型對(duì)象重新賦值

Person.prototype?=newAnimal(0)

//?1.5.給Person添加方法

Person.prototype.personFunction?=function(){

alert("Hello?Person")

}

//?2.驗(yàn)證和使用的代碼

//?2.1.創(chuàng)建Person對(duì)象

varperson1?=newPerson("Coderwhy",18)

varperson2?=newPerson("Kobe",30)

//?2.2.驗(yàn)證屬性

alert(person1.name?+"-"+?person1.age)//?Coderwhy,18

alert(person2.name?+"-"+?person2.age)//?Kobe,30

//?2.3.驗(yàn)證方法的調(diào)用

person1.animalFunction()//?Hello?Animal

person1.personFunction()//?Hello?Person

//?2.4.驗(yàn)證引用屬性的問題

person1.colors.push("blue")

alert(person1.colors)//?red,green,blue

alert(person2.colors)//?red,green

代碼解析:

根據(jù)前面學(xué)習(xí)的知識(shí), 結(jié)合當(dāng)前的代碼, 大家應(yīng)該可以理解上述代碼的含義.

但是我還是建議大家一定要多手動(dòng)自己來敲代碼, 來理解其中每一個(gè)步驟.

記住: 看懂, 聽懂不一定真的懂, 自己可以寫出來, 才是真的懂了.

3.2. 組合繼承的分析

組合繼承是JavaScript最常用的繼承模式之一.

如果你理解到這里, 點(diǎn)到為止, 那么組合來實(shí)現(xiàn)繼承只能說問題不大.

但是它依然不是很完美, 存在一些問題不大的問題.(不成問題的問題, 基本一詞基本可用, 但基本不用)

組合繼承存在什么問題呢?

組合繼承最大的問題就是無論在什么情況下, 都會(huì)調(diào)用兩次父類構(gòu)造函數(shù).

一次在創(chuàng)建子類原型的時(shí)候

另一次在子類構(gòu)造函數(shù)內(nèi)部(也就是每次創(chuàng)建子類實(shí)例的時(shí)候).

另外, 如果你仔細(xì)按照我的流程走了上面的每一個(gè)步驟, 你會(huì)發(fā)現(xiàn): 所有的子類實(shí)例事實(shí)上會(huì)擁有兩份父類的屬性

一份在當(dāng)前的實(shí)例自己里面(也就是person本身的), 另一份在子類對(duì)應(yīng)的原型對(duì)象中(也就是person.__proto__里面)

當(dāng)然, 這兩份屬性我們無需擔(dān)心訪問出現(xiàn)問題, 因?yàn)槟J(rèn)一定是訪問實(shí)例本身這一部分的.

怎么解決呢?

看起來組合繼承也不是非常完美的解決方案, 雖然也可以應(yīng)用.

JavaScript面向?qū)ο笤斀猓ㄋ模?/p>

在上一篇中, 我們討論了ES5中, 實(shí)現(xiàn)繼承的一些方式.

在最后, 我們說了組合繼承是相對(duì)完美的解決方案, 但是它也存在一些問題.

這篇文章, 我們就通過某種新的模式, 給出一種目前使用最多, 也是我們最終的解決方案.

一. 原型式繼承

1.1. 原型式繼承的思想

原型式繼承的淵源

這種模式要從道格拉斯·克羅克福德(Douglas Crockford, 著名的前端大師, JSON的創(chuàng)立者)在2006年寫的一篇文章說起: Prototypal Inheritance in JavaScript(在JS中使用原型式繼承)

在這篇文章中, 它介紹了一種繼承方法, 而且這種繼承方法不是通過構(gòu)造函數(shù)來實(shí)現(xiàn)的.

為了理解這種方式, 我們先再次回顧一下JavaScript想實(shí)現(xiàn)繼承的目的: 重復(fù)利用另外一個(gè)對(duì)象的屬性和方法.

原型式繼承的核心函數(shù):

//?封裝object()函數(shù)

functionobject(o){

functionF(){}

F.prototype?=?o

returnnewF()

}

代碼解析:

在object()函數(shù)內(nèi)部, 先創(chuàng)建一個(gè)臨時(shí)的構(gòu)造函數(shù).

然后將傳遞的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型

最后返回了這個(gè)臨時(shí)類型的一個(gè)新的實(shí)例.

事實(shí)上, object()對(duì)傳入的對(duì)象執(zhí)行了一次淺復(fù)制.

1.2. 原型式繼承的使用

使用原型式繼承:

//?使用原生式繼承

varperson?=?{

name:"Coderwhy",

colors:?["red","green"]

}

//?通過person去創(chuàng)建另外一個(gè)對(duì)象

varperson1?=?object(person)

person1.name?="Kobe"

person1.colors.push("blue")

alert(person1.name)//?Kobe

alert(person1.colors)//?red,green,blue

alert(person.name)//?Coderwhy

alert(person.colors)//?red,green,blue

代碼解析:

這種方式和我們傳統(tǒng)意義上理解的繼承有些不同. 它做的事情是通過一個(gè)對(duì)象去創(chuàng)建另外一個(gè)對(duì)象.(利用person去創(chuàng)建person1)

當(dāng)然, person1中繼承過來的屬性是放在了自己的原型對(duì)象中的.

也可以給person1自己再次添加name屬性, 這個(gè)時(shí)候name才是在實(shí)例本身中.

但是如果是修改或者添加引用類型的內(nèi)容, 還是會(huì)引起連鎖反應(yīng).

可能暫時(shí)你看不到這些代碼的意義, 但是這些代碼是我們后續(xù)終極方案的前提思想, 所以先看看和練習(xí)一下這些代碼.

針對(duì)這種思想, ES5中新增了Object.create()方法來規(guī)范化了原型式繼承.

也就是上面的代碼可以修改成這樣.(只是將object函數(shù)修改成了Object.create)

//?使用原生式繼承

varperson?=?{

name:"Coderwhy",

colors:?["red","green"]

}

//?通過person去創(chuàng)建另外一個(gè)對(duì)象

varperson1?=Object.create(person)

person1.name?="Kobe"

person1.colors.push("blue")

alert(person1.name)//?Kobe

alert(person1.colors)//?red,green,blue

alert(person.name)//?Coderwhy

alert(person.colors)//?red,green,blue

Object.create()還可以傳入第二個(gè)參數(shù):

第二個(gè)參數(shù)用于每個(gè)屬性的自定義描述.

比如person1的name我們希望修改成"Kobe", 就可以這樣來做

//?使用原型式繼承

varperson?=?{

name:"Coderwhy",

colors:?["red","green"]

}

//?通過person去創(chuàng)建另外一個(gè)對(duì)象

varperson1?=Object.create(person,?{

name:?{

value:"Kobe"

}

})

person1.colors.push("blue")

alert(person1.name)//?Kobe

alert(person1.colors)//?red,green,blue

alert(person.name)//?Coderwhy

alert(person.colors)//?red,green,blue

1.3. 原型式繼承的問題

原型式繼承的的優(yōu)點(diǎn)和缺點(diǎn):

如果我們只是希望一個(gè)對(duì)象和另一個(gè)對(duì)象保持類似的情況下, 原型式繼承完全可以勝任, 這是它的優(yōu)勢.

但是, 原型式繼承依然存在屬性共享的問題, 就像使用原型鏈一樣.

二. 寄生式繼承

2.1. 寄生式繼承的思想

寄生式(Parasitic)繼承

寄生式(Parasitic)繼承是與原型式繼承緊密相關(guān)的一種思想, 并且同樣由道格拉斯·克羅克福德(Douglas Crockford)提出和推廣的

寄生式繼承的思路是結(jié)合原型類繼承和工廠模式的一種方式.

即創(chuàng)建一個(gè)封裝繼承過程的函數(shù), 該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象, 最后再將這個(gè)對(duì)象返回.

寄生式函數(shù)多增加了一個(gè)核心函數(shù):

//?封裝object函數(shù)

functionobject(o){

functionF(){}

F.prototype?=?o

returnnewF()

}

//?封裝創(chuàng)建新對(duì)象的函數(shù)

functioncreateAnother(original){

varclone?=?object(original)

clone.sayHello?=function(){

alert("Hello?JavaScript")

}

returnclone

}

2.2. 寄生式繼承的應(yīng)用

我們來使用一下寄生式繼承

//?person對(duì)象

varperson?=?{

name:"Coderwhy",

colors:?["red","green"]

}

//?新的對(duì)象

varperson1?=?createAnother(person)

person1.sayHello()

代碼解讀:

我們基于person對(duì)象, 創(chuàng)建了另外一個(gè)對(duì)象person1.

在最新的person1對(duì)象中, 不僅會(huì)擁有person的屬性和方法, 而且還有自己定義的方法.

2.3. 寄生式繼承的問題

寄生式繼承存在的問題:

寄生式繼承和原型式繼承存在一樣的問題, 引用類型會(huì)共享. (因?yàn)槭窃谠褪嚼^承基礎(chǔ)上的一種封裝)

另外寄生式繼承還存在函數(shù)無法復(fù)用的問題, 因?yàn)槊看蝐reateAnother一個(gè)新的對(duì)象, 都需要重新定義新的函數(shù).

三. 寄生組合式繼承

3.1. 寄生組合式繼承的思想

寄生組合式繼承

現(xiàn)在我們來回顧一下之前提出的比較理想的組合繼承

組合繼承是比較理想的繼承方式, 但是存在兩個(gè)問題:

問題一: 構(gòu)造函數(shù)會(huì)被調(diào)用兩次: 一次在創(chuàng)建子類型原型對(duì)象的時(shí)候, 一次在創(chuàng)建子類型實(shí)例的時(shí)候.

問題二: 父類型中的屬性會(huì)有兩份: 一份在原型對(duì)象中, 一份在子類型實(shí)例中.

事實(shí)上, 我們現(xiàn)在可以利用寄生式繼承將這兩個(gè)問題給解決掉.

你需要先明確一點(diǎn): 當(dāng)我們?cè)谧宇愋偷臉?gòu)造函數(shù)中調(diào)用父類型.call(this, 參數(shù))這個(gè)函數(shù)的時(shí)候, 就會(huì)將父類型中的屬性和方法復(fù)制一份到了子類型中. 所以父類型本身里面的內(nèi)容, 我們不再需要.

這個(gè)時(shí)候, 我們還需要獲取到一份父類型的原型對(duì)象中的屬性和方法.

能不能直接讓子類型的原型對(duì)象 = 父類型的原型對(duì)象呢?

不要這么做, 因?yàn)檫@么做意味著以后修改了子類型原型對(duì)象的某個(gè)引用類型的時(shí)候, 父類型原生對(duì)象的引用類型也會(huì)被修改.

我們使用前面的寄生式思想就可以了.

寄生組合式的核心代碼:

//?定義object函數(shù)

functionobject(o){

functionF(){}

F.prototype?=?o

returnnewF()

}

//?定義寄生式核心函數(shù)

functioninhreitPrototype(subType,?superType){

varprototype?=?object(superType.prototype)

prototype.constructor?=?subType

subType.prototype?=?prototype

}

3.2. 寄生組合式繼承的應(yīng)用

直接給出使用的代碼, 也是我們以后使用繼承的終極方式

//?定義Animal構(gòu)造函數(shù)

functionAnimal(age){

this.age?=?age

this.colors?=?["red","green"]

}

//?給Animal添加方法

Animal.prototype.animalFunction?=function(){

alert("Hello?Animal")

}

//?定義Person構(gòu)造函數(shù)

functionPerson(name,?age){

Animal.call(this,?age)

this.name?=?name

}

//?使用寄生組合式核心函數(shù)

inhreitPrototype(Person,?Animal)

//?給Person添加方法

Person.prototype.personFunction?=function(){

alert("Hello?Person")

}

代碼的優(yōu)點(diǎn):

這種方式的高效體現(xiàn)在現(xiàn)在它只調(diào)用了一次Animal的構(gòu)造函數(shù).

并且也避免了在原型上面多出的多余屬性, 而且原型之間不會(huì)產(chǎn)生任何的干擾(子類型原型和父類型原型之間).

在ES5中, 普遍認(rèn)為寄生組合式繼承是最理想的繼承范式.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末朝蜘,一起剝皮案震驚了整個(gè)濱河市恶迈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谱醇,老刑警劉巖暇仲,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異副渴,居然都是意外死亡奈附,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門煮剧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斥滤,“玉大人将鸵,你說我怎么就攤上這事≈械” “怎么了咨堤?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漩符。 經(jīng)常有香客問我一喘,道長,這世上最難降的妖魔是什么嗜暴? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任凸克,我火速辦了婚禮,結(jié)果婚禮上闷沥,老公的妹妹穿的比我還像新娘萎战。我一直安慰自己,他們只是感情好舆逃,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布蚂维。 她就那樣靜靜地躺著,像睡著了一般路狮。 火紅的嫁衣襯著肌膚如雪虫啥。 梳的紋絲不亂的頭發(fā)上笨枯,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天枢泰,我揣著相機(jī)與錄音,去河邊找鬼毅否。 笑死砸抛,一個(gè)胖子當(dāng)著我的面吹牛评雌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播直焙,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼景东,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了奔誓?” 一聲冷哼從身側(cè)響起耐薯,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丝里,沒想到半個(gè)月后曲初,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杯聚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年臼婆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幌绍。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颁褂,死狀恐怖故响,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颁独,我是刑警寧澤彩届,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站誓酒,受9級(jí)特大地震影響樟蠕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜靠柑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一寨辩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歼冰,春花似錦靡狞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腮恩,卻和暖如春蕾各,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背庆揪。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妨托,地道東北人缸榛。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像兰伤,于是被迫代替她去往敵國和親内颗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351