博客內(nèi)容:
- 什么是面向?qū)ο?/li>
- 為什么要面向?qū)ο?/li>
- 面向?qū)ο缶幊痰奶匦院驮瓌t
- 理解對(duì)象屬性
- 創(chuàng)建對(duì)象
- 繼承
什么是面向?qū)ο?/h3>
面向?qū)ο蟪绦蛟O(shè)計(jì)即OOP(Object-oriented programming)崩溪,其中兩個(gè)最重要的概念就是對(duì)象和類。
JS中的對(duì)象都是基于一個(gè)引用類型創(chuàng)建的,這個(gè)引用類型可以是原生類型哥攘,也可以是開發(fā)人員定義的類型。
對(duì)象是無序?qū)傩缘募希扇舾蓚€(gè)“鍵值對(duì)”(key-value)構(gòu)成,屬性包含基本值莉掂、對(duì)象或函數(shù)。類是具備了某些功能和屬性的抽象模型千扔,實(shí)際應(yīng)用中需要對(duì)類進(jìn)行實(shí)例化憎妙,類在實(shí)例化之后就是對(duì)象库正。
而JavaScript語言沒有“類”,而改用構(gòu)造函數(shù)(constructor)作為對(duì)象基本結(jié)構(gòu)的模板尚氛。構(gòu)造函數(shù)專門用來生成對(duì)象诀诊,一個(gè)構(gòu)造函數(shù)可生成多個(gè)對(duì)象,這些對(duì)象都有相同的結(jié)構(gòu)阅嘶。
為什么要面向?qū)ο?/h3>
為什么要用面向?qū)ο螅嫦驅(qū)ο笠驗(yàn)榉庋b载迄,繼承讯柔,多態(tài)的特征使程序更易于擴(kuò)展,維護(hù)护昧,復(fù)用魂迄,原因很多。
比如我們畫了一個(gè)三角形惋耙,在另外一個(gè)環(huán)境中我們也要畫這個(gè)三角形捣炬,那么我們只需要將三角形這個(gè)對(duì)象及形狀父級(jí)對(duì)象引入,剩下關(guān)于三角形的操作都是三角形這個(gè)對(duì)象的內(nèi)部實(shí)現(xiàn)绽榛。維護(hù)起來也只是去查看該對(duì)象的該方法湿酸,比在整個(gè)環(huán)境中找三角形函數(shù)要好很多。
就前端開發(fā)來說我個(gè)人覺得有兩個(gè)優(yōu)點(diǎn):
- 將松散的JS代碼進(jìn)行整合灭美,便于后期的維護(hù)推溃。
- 讓我們的代碼適應(yīng)更多的業(yè)務(wù)邏輯。
面向?qū)ο缶幊痰奶匦院驮瓌t
前面說過届腐,面向?qū)ο笥腥筇卣魈玻庋b,繼承犁苏,多態(tài)硬萍。
- 封裝性:將一個(gè)類的使用和實(shí)現(xiàn)分開,隱藏對(duì)象的屬性和實(shí)現(xiàn)細(xì)節(jié)围详,僅對(duì)外提供公共訪問方式朴乖,提高代碼復(fù)用性和安全性。
- 繼承性:子類自動(dòng)繼承其父級(jí)類中的屬性和方法短曾,并可以添加新的屬性和方法或者對(duì)部分屬性和方法進(jìn)行重寫寒砖,繼承增加了代碼的復(fù)用性,讓類與類之間產(chǎn)生了聯(lián)系嫉拐,提供了多態(tài)的前提哩都。
- 多態(tài)性:子類繼承了來自父級(jí)類中的屬性和方法,并對(duì)其中部分方法進(jìn)行重寫婉徘。(比如函數(shù)的length和數(shù)組的length都繼承自對(duì)象但作用不同)漠嵌,提高了代碼的擴(kuò)展性和可維護(hù)性咐汞。
面向?qū)ο笤瓌t:
- 開閉原則:
對(duì)擴(kuò)展開放:應(yīng)用的需求改變時(shí)我們可以對(duì)模塊進(jìn)行擴(kuò)展,使其具有滿足改變的新行為儒鹿。
對(duì)修改封閉:對(duì)模塊行為進(jìn)行擴(kuò)展是化撕,不必改變模塊的源碼或二進(jìn)制代碼。 - 接口隔離:
不要依賴用不到的接口约炎。
看到這里覺得概念很模糊植阴,沒有關(guān)系,后面會(huì)講清楚圾浅。
理解對(duì)象屬性
屬性類型
首先創(chuàng)建一個(gè)對(duì)象掠手,對(duì)象字面量是創(chuàng)建對(duì)象的首選模式,簡單直觀狸捕。
【例1】:
var person = {
name: 'dot',
sex: 'female',
age: 2,
sayName: function () {
console.log(this.name)
}
}
person對(duì)象有3個(gè)屬性喷鸽,分別是name,sex灸拍,age做祝,有一個(gè)sayName方法,JavaScript通過各自的屬性值來定義它們的行為鸡岗。
ECMAScript中有兩種屬性:數(shù)據(jù)屬性和訪問器屬性混槐。
- 數(shù)據(jù)屬性:
數(shù)據(jù)屬性包含數(shù)據(jù)值的位置,在這個(gè)位置可以讀寫值纤房,有四個(gè)描述其行為的特性:
- Configurable:表示能否通過delete刪除屬性從而重新定義屬性
- Enumberable:表示能否通過for-in循環(huán)枚舉屬性
- Writable:表示能否修改屬性的值
- Value:包含這個(gè)屬性的數(shù)據(jù)值纵隔,從這個(gè)位置讀取屬性或?qū)⑿轮祵懭脒@個(gè)位置,默認(rèn)值是undefined炮姨。
直接在對(duì)象上定義的屬性捌刮,其Configurable、Enumberable舒岸、Writable默認(rèn)值均為true绅作,在調(diào)用Object.defineProperty()
方法創(chuàng)建新屬性時(shí)默認(rèn)值均為false,如果調(diào)用Object.defineProperty()
方法只是為了修改已定義的屬性的這三個(gè)特性蛾派,就沒有這個(gè)限制俄认。
【例1】中,以name屬性為例洪乍,name是直接在person對(duì)象上定義的屬性眯杏,它的Configurable、Enumberable壳澳、Writable值均為true岂贩,而Value特性被設(shè)置為'dot',對(duì)name值做的任何修改都將反映在這個(gè)位置上巷波。
Object.defineProperty()
方法用于修改屬性默認(rèn)的特性萎津,這個(gè)方法接收3個(gè)參數(shù):屬性所屬對(duì)象卸伞,屬性名,描述符對(duì)象锉屈,其中荤傲,描述符對(duì)象是由一組花括號(hào)包含的鍵值對(duì),鍵是Configurable颈渊、Enumberable遂黍、Writable、Value中的一個(gè)或多個(gè)俊嗽,設(shè)置對(duì)應(yīng)值可以修改相應(yīng)的特性值妓湘。
以Configurable為例做個(gè)演示,如下所示:
【例2】:
var person = {}
//-----1-----
Object.defineProperty(person, 'name', {
configurable: true,
value: 'dot'
})
console.log(person.name)//dot
//-----2-----
Object.defineProperty(person, 'name', {
value: 'dotttttttt'
})
console.log(person.name)//dotttttttt
//-----3-----
Object.defineProperty(person, 'name', {
configurable: false,
value: 'dooooot'
})
console.log(person.name)//dooooot
delete person.name
console.log(person.name)//dooooot
//-----4-----
Object.defineProperty(person, 'name', {
configurable: true,
value: 'dolby'
})
console.log(person.name)//TypeError: Cannot redefine property: name
按第1-第4部分依次解析代碼:
- 首先定義了一個(gè)空對(duì)象person乌询,因調(diào)用
Object.defineProperty()
方法創(chuàng)建新屬性時(shí),person對(duì)象的name屬性的configurable默認(rèn)值為false豌研,所以先設(shè)置configurable值為true妹田,表示name的值是可以被重寫的,接著value: 'dot'
將name屬性賦值為'dot'鬼佣,所以控制臺(tái)打印person.name
為dot
- 前面已經(jīng)設(shè)置過
configurable: true
,所以繼續(xù)重寫name的值也是生效的霜浴,控制臺(tái)打印person.name
為dotttttttt
- 設(shè)置configurable值為false晶衷,并將name重寫為 'dooooot',實(shí)際上是先將name值重寫為'dooooot'阴孟,再來執(zhí)行
configurable: false
晌纫,表明name的值不可以再被重寫了,重寫實(shí)際上分為兩步:刪除舊的屬性值永丝,添加新的屬性值锹漱,所以控制臺(tái)第一次打印person.name
得到'dooooot',接著delete person.name
慕嚷,不能被重寫了即不能再刪除和添加哥牍,所以delete操作不生效,再次打印person.name
仍舊得到'dooooot' - 這里要說明一個(gè)概念喝检,一旦把屬性定義為不可配置的嗅辣,就不能再將其定義為可配置的了。第三步中設(shè)置configurable值為false挠说,這里再將其設(shè)置為true澡谭,是無效的且會(huì)拋出錯(cuò)誤,說明name屬性是不可重新定義的纺涤。
多數(shù)情況下可能都沒有必要用到Object.defineProperty()
方法译暂,但理解這些概念對(duì)理解JavaScript對(duì)象非常有用抠忘。
- 訪問器屬性:
訪問器屬性不包含數(shù)據(jù)值,包含一對(duì)可選的get和set函數(shù)外永。
讀取訪問器屬性時(shí)調(diào)用get函數(shù)崎脉,get函數(shù)返回有效值;寫入訪問器屬性時(shí)調(diào)用set函數(shù)并傳入新值伯顶,set函數(shù)負(fù)責(zé)處理數(shù)據(jù)囚灼。
訪問器屬性包含四個(gè)特性:
- Configurable:表示能否通過delete刪除屬性從而重新定義屬性
- Enumberable:表示能否通過for-in循環(huán)枚舉屬性
- Get:讀取屬性時(shí)調(diào)用的函數(shù),默認(rèn)值為undefined
- Set:寫入屬性時(shí)調(diào)用的函數(shù)祭衩,默認(rèn)值為undefined
訪問器屬性必須用Object.defineProperty()
來定義
【例3】:
var person = {
_year: 2017,
age: 2
}
Object.defineProperty(person, 'year', {
get: function () {
return this._age
},
set: function (newValue) {
if (newValue > 2017) {
this._year = newValue
this.age += newValue - 2017
}
}
})
person.year = 2019
console.log(person.age)//4
以上代碼創(chuàng)建了一個(gè)person對(duì)象并有_year
和age
屬性灶体,_year
前的下劃線表示只能通過對(duì)象方法訪問,訪問其屬性year
包含一個(gè)get函數(shù)和一個(gè)set函數(shù)掐暮,get函數(shù)返回_year
的值2017蝎抽,set函數(shù)接收新值2019并通過計(jì)算得到新的年齡。因此路克,通過修改年份year會(huì)導(dǎo)致_year
變?yōu)?019樟结,age
變?yōu)?,這就是使用訪問器屬性的常見方式——設(shè)置一個(gè)屬性的值導(dǎo)致其他屬性發(fā)生變化精算。
不一定非要同是指定get和set函數(shù)瓢宦,只設(shè)置get表示屬性只讀,只設(shè)置set表示屬性只寫灰羽。
定義多個(gè)屬性
顧名思義驮履,用Object.defineProperties()
方法,可通過描述符一次定義多個(gè)屬性廉嚼,與Object.defineProperty()
方法接收的參數(shù)上稍有差別玫镐,本質(zhì)沒區(qū)別。
Object.defineProperties()
接收兩個(gè)參數(shù)前鹅,一個(gè)是要添加和修改的屬性所屬對(duì)象摘悴,一個(gè)是描述符。
【例4】:
var person = {}
Object.defineProperties(person, {
_year: {
writable: true,
value: 2017
},
age: {
writable: true,
value: 2
},
year: {
get: function () {
return this._year
},
set: function (newValue) {
if (newValue > 2017) {
this._year = newValue
this.age += newValue - 2017
}
}
}
})
person.year = 2019
console.log(person.age)//4
以上代碼在person對(duì)象上同時(shí)定義了兩個(gè)數(shù)據(jù)屬性(_year和age)舰绘,一個(gè)訪問器屬性year蹂喻。
讀取屬性的特性
使用Object.getOwnPropertyDescriptor()方法可取得給定屬性的描述符對(duì)象。這個(gè)方法接收兩個(gè)參數(shù):屬性所屬對(duì)象捂寿,要讀取其描述符的屬性名稱口四。返回的是一個(gè)對(duì)象,如果是數(shù)據(jù)屬性秦陋,這個(gè)對(duì)象的屬性有configurable蔓彩、enumberable、writable和value,如果是訪問其屬性赤嚼,這個(gè)對(duì)象的屬性有configurable旷赖、enumberable、get和set更卒。
【例5】:
var person = {}
Object.defineProperties(person, {
_year: {
value: 2017
},
age: {
value: 2
},
year: {
get: function () {
return this._year
},
set: function (newValue) {
if (newValue > 2017) {
this._year = newValue
this.age += newValue - 2017
}
}
}
})
var descriptor = Object.getOwnPropertyDescriptor(person, '_year')
console.log(descriptor.value)//2017
console.log(descriptor.configurable)//false
console.log(typeof descriptor.get)//undefined
var descriptor = Object.getOwnPropertyDescriptor(person, 'year')
console.log(descriptor.value)//undefined
console.log(descriptor.enumerable)//false
console.log(typeof descriptor.get)//function
- 首先讀取person對(duì)象中
_year
屬性的描述符等孵,打印值為初始值2017,通過Object.defineProperties()方法創(chuàng)建的屬性其Configurable蹂空、Enumberable俯萌、Writable特性也都是false,所以打印descriptor.configurable
值為false上枕,_year
為數(shù)據(jù)屬性咐熙,沒有g(shù)et方法,所以typeof descriptor.get
打印undefined - 接著讀取person對(duì)象中
year
屬性的描述符辨萍,打印值為初始值undefined棋恼,通過Object.defineProperties()方法創(chuàng)建的屬性其Configurable、Enumberable锈玉、Writable特性也都是false蘸泻,所以打印descriptor.enumerable
值為false,year
是訪問器屬性且有g(shù)et方法嘲玫,所以typeof descriptor.get
是一個(gè)函數(shù)
創(chuàng)建對(duì)象
前面已經(jīng)講過,JavaScript語言沒有“類”并扇,而改用構(gòu)造函數(shù)Constructor作為對(duì)象基本結(jié)構(gòu)的模板去团。我們可以采用下列模式創(chuàng)建對(duì)象。
- 工廠模式:
使用簡單函數(shù)創(chuàng)建對(duì)象并為對(duì)象添加屬性和方法穷蛹,最終返回對(duì)象土陪,工廠模式已被構(gòu)造函數(shù)模式取代。 - 構(gòu)造函數(shù)模式:
可創(chuàng)建自定義引用類型肴熏,可以像創(chuàng)建內(nèi)置對(duì)象實(shí)例一樣使用new操作符鬼雀,這種模式的缺點(diǎn)是無法實(shí)現(xiàn)復(fù)用,也沒有封裝性可言蛙吏,而函數(shù)與對(duì)象具有松散耦合的關(guān)系源哩,不能復(fù)用的話將面向?qū)ο缶蜎]有什么意義。 - 原型模式:
使用構(gòu)造函數(shù)的prototype屬性來指定共享的屬性和方法 - 組合使用構(gòu)造函數(shù)模式與原型模式
使用構(gòu)造函數(shù)定義實(shí)例屬性鸦做,使用原型定義共享的屬性和方法
還有幾種創(chuàng)建對(duì)象的方式如動(dòng)態(tài)原型模式励烦、寄生構(gòu)造函數(shù)模式、穩(wěn)妥構(gòu)造函數(shù)模式等泼诱,因?yàn)橛玫貌欢嗨栽诖瞬蛔鼋榻B坛掠。
工廠模式
工廠模式是創(chuàng)建對(duì)象,為其添加屬性和方法并返回對(duì)象的一種設(shè)計(jì)模式。
【例6】:
function createPerson(name, sex, age) {
var o = new Object()
o.name = name
o.sex = sex
o.age = age
o.sayName = function () {
console.log(this.name)
}
return o
}
var person1 = createPerson('dot', 'female', 2)
var person2 = createPerson('dolby', 'male', 3)
看代碼會(huì)發(fā)現(xiàn)屉栓,每一次調(diào)用createPerson函數(shù)舷蒲,每次都會(huì)返回包含三個(gè)屬性一個(gè)方法的對(duì)象,無法做到對(duì)象識(shí)別友多,于是出現(xiàn)了構(gòu)造函數(shù)模式牲平。
構(gòu)造函數(shù)模式
構(gòu)造函數(shù)可以創(chuàng)建特定類型的對(duì)象,Object夷陋、Array欠拾、RegExp等是原生構(gòu)造函數(shù),運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中并擁有相應(yīng)的方法骗绕,如下所示:
此外我們也可以創(chuàng)建自定義的構(gòu)造函數(shù)藐窄,從而自定義對(duì)象類型的屬性和方法,可用構(gòu)造函數(shù)模式將【例6】重寫如下:
【例7】:
function Person(name, sex, age) {//這里的Person就是構(gòu)造函數(shù)
this.name = name//運(yùn)行時(shí)才知道this指向什么酬土,定義的時(shí)候永遠(yuǎn)不清楚
this.sex = sex//運(yùn)行時(shí)才知道this指向什么荆忍,定義的時(shí)候永遠(yuǎn)不清楚
this.age = age//運(yùn)行時(shí)才知道this指向什么,定義的時(shí)候永遠(yuǎn)不清楚
this.sayName = function () {//運(yùn)行時(shí)才知道this指向什么撤缴,定義的時(shí)候永遠(yuǎn)不清楚
console.log(this.name)
}
}
var person1 = new Person('dot', 'female', 2)//person1就是Person的實(shí)例
var person2 = new Person('dolby', 'male', 3)//person2也是Person的實(shí)例
這個(gè)例子中刹枉,Person()函數(shù)取代了createPerson()函數(shù),不同之處在于:
- 沒有顯示地創(chuàng)建對(duì)象
- 直接將屬性和方法賦給了this對(duì)象
- 沒有return語句
此外屈呕,函數(shù)名Person使用的是大寫字母P微宝,按照慣例,構(gòu)造函數(shù)始終都應(yīng)以大寫字母開頭虎眨,而普通函數(shù)應(yīng)以小寫字母開頭蟋软,這樣做沒有什么特殊作用,只是為了區(qū)分普通函數(shù)與構(gòu)造函數(shù)嗽桩,構(gòu)造函數(shù)本身也是函數(shù)岳守,只是能用于創(chuàng)建對(duì)象而已。
要?jiǎng)?chuàng)建 Person構(gòu)造函數(shù)的實(shí)例碌冶,必須使用new操作符湿痢,以這種形式調(diào)用構(gòu)造函數(shù)實(shí)際上會(huì)經(jīng)歷一下四個(gè)階段:
- 創(chuàng)建一個(gè)新對(duì)象
- 將構(gòu)造函數(shù)的作用域賦給新對(duì)象,這樣this就指向了這個(gè)新對(duì)象
- 執(zhí)行構(gòu)造函數(shù)中的代碼為這個(gè)新對(duì)象添加屬性
- 返回新對(duì)象
可以實(shí)現(xiàn)一個(gè)create函數(shù)扑庞,模擬原生的new操作符譬重,有興趣可以看看:
【例8】:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype = {
prototype: 'type'
}
function create(constructor) {
var args = Array.prototype.slice.call(arguments, 1)//將傳進(jìn)來的參數(shù)轉(zhuǎn)化為數(shù)組并借用數(shù)組的slice方法獲取索引為1(包括索引1的項(xiàng))的項(xiàng)開始的所有項(xiàng),索引為0的項(xiàng)是傳進(jìn)來的constructor
var obj = {}//創(chuàng)建一個(gè)空對(duì)象
obj.__proto__ = constructor.prototype//將新創(chuàng)建的對(duì)象的原型指向構(gòu)造函數(shù)的原型
var res = constructor.apply(obj, args)//apply接受兩個(gè)參數(shù)罐氨,第一個(gè)是this害幅,第二個(gè)是參數(shù)數(shù)組,res是調(diào)用Person后得到的結(jié)果對(duì)象
if (typeof res === 'object' && res !== null) {
return res
}
return obj
}
var test = create(Person, 'dot', 2)
console.log(test)//{name:'dot,age:2}
console.log(test.prototype)//type
【例7】中岂昭,person1和person2分別保存著Person的一個(gè)不同的實(shí)例以现,這兩個(gè)對(duì)象都有一個(gè)constructor屬性狠怨,指向Person,如下所示:
console.log(person1.constructor === Person)//true
console.log(person2.constructor === Person)//true
constructor屬性最初是用來標(biāo)識(shí)對(duì)象類型的邑遏,但提到檢測(cè)對(duì)象類型佣赖,還是instanceof操作符靠譜,我們?cè)凇纠?】中創(chuàng)建的對(duì)象既是Object的實(shí)例记盒,同時(shí)也是Person的實(shí)例憎蛤,這一點(diǎn)通過instanceof操作符可以驗(yàn)證:
console.log(person1 instanceof Object)//true
console.log(person1 instanceof Person)//true
console.log(person2 instanceof Object)//true
console.log(person2 instanceof Person)//true
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實(shí)例標(biāo)識(shí)為一種特定的類型,這正是構(gòu)造函數(shù)模式取代了工廠模式的原因纪吮,person1與person2之所以同時(shí)是Object的實(shí)例俩檬,是因?yàn)樗袑?duì)象均繼承自O(shè)bject,后面會(huì)講到碾盟,以構(gòu)造函數(shù)模式定義的構(gòu)造函數(shù)是定義在瀏覽器的window對(duì)象上的棚辽。
必須要提到的是this屈藐,【例7】中的this.name
联逻,this并不是指Person對(duì)象检痰,而是調(diào)用構(gòu)造函數(shù)Person的新實(shí)例包归,然而this遠(yuǎn)遠(yuǎn)不止這里的那么簡單,實(shí)際運(yùn)用過程中會(huì)猜到無數(shù)的坑铅歼,這里不多講箫踩,而且我也不太懂。
構(gòu)造函數(shù)也是函數(shù)谭贪,不存在定義構(gòu)造函數(shù)的特殊語法,任何函數(shù)只要通過new操作符來調(diào)用锦担,那它就可以作為構(gòu)造函數(shù)俭识,而任何函數(shù)如果不通過new操作符調(diào)用,那它就是普通函數(shù)洞渔,【例7】中定義的函數(shù)可以通過下列幾種方式來調(diào)用:
【例9】:
//方法1
var person = new Person('dot', 'female', 2)
person.sayName()//dot
//方法2
Person('dolby', 'male', 3)
window.sayName()//dolby
//方法3
var o = new Object()
Person.call(o, 'dooot', 'female', 4)
o.sayName()//dooot
- 方法1中展示了構(gòu)造函數(shù)Person的典型用法套媚,用new操作符創(chuàng)建新對(duì)象。
- 方法2展示的就是調(diào)用普通函數(shù)得到的結(jié)果磁椒,在瀏覽器全局中調(diào)用一個(gè)函數(shù)時(shí)堤瘤,this對(duì)象總是指向window對(duì)象,因此調(diào)用函數(shù)之后可通過window對(duì)象來調(diào)用sayName()而且返回了結(jié)果浆熔,其實(shí)沒有那么難以理解本辐,分析一下
Person('dolby', 'male', 3)
,在全局中調(diào)用慎皱,改寫代碼為Person.call(context, 'dolby', 'male', 3)
,this 是你call一個(gè)函數(shù)時(shí)傳的context夺欲。
瀏覽器里有一條規(guī)則:
如果你傳的 context 是 null 或者 undefined万细,那么 window 對(duì)象就是默認(rèn)的 context(嚴(yán)格模式下默認(rèn) context 是 undefined)腰素,因此上面的this對(duì)象指向window對(duì)象。
可能我沒有講清楚,方方老師的this 的值到底是什么?一次說清楚這篇文章非常淺顯易懂汁展,可以經(jīng)常閱讀公罕、經(jīng)常閱讀铲汪、經(jīng)常閱讀帽揪,重要的事說三遍查邢。 - 方法3就是將我以上說的context換乘對(duì)象o,所以this指向了對(duì)象o,于是調(diào)用函數(shù)后可通過對(duì)象o來調(diào)用sayName()并返回了結(jié)果芥备。
構(gòu)造函數(shù)模式存在的問題:
每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍日月,前面的例子中,person1和person2都有一個(gè)sayName()方法燎斩,但不是同一個(gè)Function的實(shí)例荡碾,因?yàn)槊慷x一個(gè)函數(shù)劳殖,就是實(shí)例化一個(gè)對(duì)象玫膀,不同實(shí)例上的同名函數(shù)是不相等的
console.log(person1.sayName === person2.sayName)//false
有this對(duì)象在,創(chuàng)建兩個(gè)一模一樣的Function實(shí)例沒有必要,我們可以把sayName方法轉(zhuǎn)移到構(gòu)造函數(shù)外來解決重復(fù)實(shí)例化的問題。
【例9】:
function Person(name, sex, age) {
this.name = name
this.sex = sex
this.age = age
this.sayName = sayName
}
function sayName() {
console.log(this.name)
}
var person1 = new Person('dot', 'female', 2)
var person2 = new Person('dolby', 'male', 3)
person1.sayName()//dot
以上代碼解決了兩個(gè)函數(shù)做同一件事的問題,但這樣一來,全局作用域定義的函數(shù)實(shí)際上只能被某個(gè)特定的對(duì)象調(diào)用,這讓全局作用域變得名不副實(shí)错览,而且如果對(duì)象需要定義很多個(gè)方法羞海,我們就要?jiǎng)?chuàng)建很多個(gè)全局函數(shù)却邓,于是我們自定義的引用類型就沒有絲毫封裝性可言了,不符合面向?qū)ο蟮脑O(shè)計(jì)原則院水,于是出現(xiàn)了下面的原型模式腊徙。
原型模式
我們創(chuàng)建的每一個(gè)函數(shù)都有一個(gè)prototype(原型)屬性简十,這個(gè)屬性是一個(gè)指向某個(gè)對(duì)象的指針,這個(gè)對(duì)象的用途是可以包含由特定類型的所有實(shí)例共享的屬性和方法撬腾。換句話說,我們不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息半沽,而是直接將這些信息添加到原型對(duì)象中
【例10】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)//dot
}
var person1 = new Person()
person1.sayName()//dot
var person2 = new Person()
person2.sayName()//dot
console.log(person1.sayName() === person2.sayName())//true
我們將構(gòu)造函數(shù)Person變成了空函數(shù)蜜暑,與構(gòu)造函數(shù)模式不同的是袍患,person1余person2訪問的是同一組屬性和同一個(gè)sayName()函數(shù)了巫糙。
要理解原型模式的工作原理,必須先理解原型對(duì)象的性質(zhì)冕茅。
- 理解原型對(duì)象:
- 只要?jiǎng)?chuàng)建了一個(gè)新函數(shù)鲤桥,就會(huì)為該函數(shù)創(chuàng)建一個(gè)prototype屬性辩恼,這個(gè)屬性指向函數(shù)的原型對(duì)象匈挖,默認(rèn)情況下肴捉,所有的原型對(duì)象都會(huì)自定獲得一個(gè)constructor屬性垒迂,指向它的構(gòu)造函數(shù)楷拳。
拿【例10】來說,Person.prototype.constructor
指向Person
吏奸,通過這個(gè)構(gòu)造函數(shù)欢揖,我們可以繼續(xù)為原型對(duì)象添加屬性和方法。 - 創(chuàng)建了自定義的構(gòu)造函數(shù)之后奋蔚,其原型對(duì)象默認(rèn)只會(huì)有constructor屬性她混,其他方法都是由Object繼承得來的,繼承后面會(huì)講泊碑。
- 當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后坤按,該實(shí)例的內(nèi)部將包含一個(gè)
__proto__
指針指向構(gòu)造函數(shù)的原型對(duì)象。
要明確最重要的一點(diǎn)是馒过,這個(gè)連接存在于實(shí)例與原型對(duì)象之間臭脓,而不是實(shí)例與構(gòu)造函數(shù)之間。
以【例10】為例可以畫出以上文字解析對(duì)應(yīng)的原型圖
從圖中可看出腹忽,person1和person2都不包含屬性和方法来累,但我們?yōu)槭裁纯梢哉{(diào)用sayName()方法砂轻,這是通過查找對(duì)象屬性的過程來實(shí)現(xiàn)的睦优。
我們可以通過兩個(gè)方法來驗(yàn)證實(shí)例的與原型對(duì)象之間是否存在某種聯(lián)系。
- isPrototypeOf()微王,直譯過來就是“是***的原型嗎”
console.log(Person.prototype.isPrototypeOf(person1))//true
console.log(Person.prototype.isPrototypeOf(person2))//true
- Object.getPrototypeOf()蔼夜,取得對(duì)象的原型兼耀,此方法在利用原型實(shí)現(xiàn)繼承中很重要
console.log(Object.getPrototypeOf(person1) === Person.prototype)//true
console.log(Object.getPrototypeOf(person2).name)//dot
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性瘤运,搜索遵循從內(nèi)向外的原則窍霞,首先從對(duì)象實(shí)例本身開始,找到了就返回該屬性值拯坟,沒有找到就繼續(xù)搜索指針指向的原型對(duì)象但金,找了了就返回該屬性值。也就是說上例中郁季,當(dāng)我們調(diào)用person1.sayName()方法時(shí)冷溃,會(huì)先后執(zhí)行兩次搜索,通過person2調(diào)用sayName()方法也是這樣梦裂,這正是多個(gè)實(shí)例共享原型的屬性和方法的基本原理似枕。
我們可以通過實(shí)例去訪問但不能重寫原型對(duì)象中的屬性和方法,在實(shí)例中定義與原型對(duì)象上相同的屬性或方法只會(huì)存在于實(shí)例中年柠,并且會(huì)覆蓋原本取得的原型中的屬性凿歼。
【例11】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.name = 'dolby'
console.log(person1.name)//dolby
console.log(person2.name)//dot
上例體現(xiàn)了屬性在原型中的搜索機(jī)制。
在實(shí)例中添加同名屬性只會(huì)阻止該實(shí)例訪問原型中的同名屬性冗恨,但不會(huì)修改原型中的同名屬性答憔。使用delete操作度可以完全刪除實(shí)例屬性以恢復(fù)其指向原型的連接。
【例12】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.name = 'dolby'
console.log(person1.name)//dolby掀抹,來自實(shí)例
delete person1.name//刪除實(shí)例屬性虐拓,恢復(fù)實(shí)例與原型的連接
console.log(person1.name)//dot,來自原型
console.log(person2.name)//dot傲武,來自原型
使用hasOwnProperty()方法可以檢測(cè)一個(gè)屬性到底是存在于原型中還是存在于實(shí)例中侯嘀,這個(gè)方法從Object繼承得來,只在屬性存在于對(duì)象實(shí)例中才返回true谱轨。
【例13】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
console.log(person1.hasOwnProperty('name'))//false
person1.name = 'dolby'
console.log(person1.name)//dolby戒幔,來自實(shí)例
console.log(person1.hasOwnProperty('name'))//true
console.log(person2.name)//dot,來自原型
console.log(person2.hasOwnProperty('name'))//false
delete person1.name//刪除實(shí)例屬性土童,恢復(fù)實(shí)例與原型的連接
console.log(person1.name)//dot诗茎,來自原型
console.log(person1.hasOwnProperty('name'))//false
通過使用hasOwnProperty()方法,什么時(shí)候訪問的是實(shí)例屬性献汗,什么時(shí)候訪問的是原型屬性就一清二楚了敢订。
- 原型與in操作符
有兩種使用方式使用in操作符,單獨(dú)使用和在for-in循環(huán)中使用罢吃。
- 單獨(dú)使用時(shí)楚午,in操作符會(huì)在通過對(duì)象能夠訪問到給定屬性時(shí)返回true,無論該屬性存在于實(shí)例還是原型中尿招。
【例13】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
console.log(person1.hasOwnProperty('name'))//false
console.log('name' in person1)//true
person1.name = 'dolby'
console.log(person1.name)//dolby矾柜,來自實(shí)例
console.log(person1.hasOwnProperty('name'))//true
console.log('name' in person1)//true
console.log(person2.name)//dot阱驾,來自原型
console.log(person2.hasOwnProperty('name'))//false
console.log('name' in person2)//true
delete person1.name//刪除實(shí)例屬性,恢復(fù)實(shí)例與原型的連接
console.log(person1.name)//dot怪蔑,來自原型
console.log(person1.hasOwnProperty('name'))//false
console.log('name' in person1)//true
以上代碼執(zhí)行過程中里覆,name屬性要么是直接在對(duì)象上訪問到的,要么是通過原型訪問到的缆瓣,所以調(diào)用'name' in person
始終返回true喧枷,同時(shí)使用hasOwnProperty()和in操作符就可以確定屬性到底存在于原型還是實(shí)例中
【例14】:
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object)
}
in操作符只要能訪問到屬性就返回true,而hasOwnProperty()只有在屬性存在于實(shí)例中才返回true弓坞,所以只要in操作符返回true隧甚,hasOwnProperty()返回false就可以確定屬性存在于原型中。
所以以上hasPrototypeProperty(object, name){}
函數(shù)的意思是渡冻,object上的屬性name來自原型嗎戚扳?
用法如下:
【例15】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)
}
var person = new Person()
console.log(hasPrototypeProperty(person, 'name'))//true
person.name = 'dolby'
console.log(hasPrototypeProperty(person, 'name'))//false
- 使用for-in循環(huán)時(shí),返回的是所有能夠通過對(duì)象訪問的可枚舉的屬性菩帝,其中既包括實(shí)例中的屬性,也包括原型中的屬性茬腿,屏蔽了原型中的不可枚舉的屬性(將enumberable標(biāo)記為false的屬性)的實(shí)例屬性也會(huì)在for-in循環(huán)中被返回呼奢。
要去的對(duì)象上所有的額可枚舉屬性,可以使用Object.keys()方法切平,這個(gè)方法接收一個(gè)對(duì)象作為參數(shù)握础,返回一個(gè)包含所有可枚舉屬性的字符串?dāng)?shù)組
【例16】:
function Person() {
}
Person.prototype.name = 'dot'
Person.prototype.sex = 'female'
Person.prototype.age = 2
Person.prototype.sayName = function () {
console.log(this.name)
}
var keys=Object.keys(Person.prototype)
console.log(keys)//[ 'name', 'sex', 'age', 'sayName' ]
var person1 = new Person()
person1.name = 'dolby'
person1.age=3
var person1Keys=Object.keys(person1)
console.log(person1Keys)//[ 'name', 'age' ]
如果想得到所有的實(shí)例屬性,無論其是否可枚舉悴品,可以使用Object.getOwnPropertyNames()方法
【例17】:
var keys=Object.getOwnPropertyNames(Person.prototype)
console.log(keys)//[ 'constructor', 'name', 'sex', 'age', 'sayName' ]
其中禀综,constructor屬性是不可枚舉的,Object.keys()方法和Object.getOwnPropertyNames()方法都可以替代for-in循環(huán)苔严。
- 更簡單的原型語法
前面寫的例子代碼量都太多了定枷,我們可以從視覺上更好的封裝原型的功能,用對(duì)象字面量形式重寫整個(gè)原型對(duì)象
【例18】:
function Person() {
}
Person.prototype = {
name: 'dot',
sex: 'female',
age: 2,
sayName: function () {
console.log(this.name)
}
}
var friend=new Person()
console.log(friend instanceof Object)//true
console.log(friend instanceof Person)//true
console.log(friend.constructor === Person)//false
console.log(friend.constructor === Object)//true
用對(duì)象字面量形式重寫的新對(duì)象與前面代碼的寫法沒有什么不同届氢,只有一點(diǎn)欠窒,constructor屬性不再指向其構(gòu)造函數(shù)了,前面講過退子,每創(chuàng)建一個(gè)函數(shù)都會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象岖妄,這個(gè)對(duì)象也會(huì)自動(dòng)獲得constructor屬性,我們?cè)谶@里用對(duì)象字面量的方式重寫了對(duì)象寂祥,也完全重寫了默認(rèn)的prototype對(duì)象荐虐,因此constructor屬性也就變成了新對(duì)象的constructor屬性,指向Object構(gòu)造函數(shù)丸凭。盡管instanceof操作符還能返回正確結(jié)果福扬,但通過constructor已經(jīng)無法確定對(duì)象的類型了腕铸。
如果constructor的值很重要,可以將其顯式地設(shè)置為適當(dāng)值忧换。
【例19】:
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'dot',
sex: 'female',
age: 2,
sayName: function () {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
var friend = new Person()
console.log(friend instanceof Object)//true
console.log(friend instanceof Person)//true
console.log(friend.constructor === Person)//true
- 原型的動(dòng)態(tài)性
先看一下以下兩個(gè)例子:
【例20】:
function Person() {
}
var friend = new Person()
Person.prototype.sayHi = function () {
console.log('hi')
}
friend.sayHi()//hi
【例21】:
function Person() {
}
var friend = new Person()
Person.prototype = {
constructor: Person,
name: 'dot',
sex: 'female',
age: 2,
sayName: function () {
console.log(this.name)
}
}
friend.sayName()//TypeError:friend.sayName is not a function
從【例20】可以得出的結(jié)論是恬惯,我們對(duì)原型對(duì)象所做的任何修改都能立即從實(shí)例上反映出來,即便是先創(chuàng)建了實(shí)例后修改原型也是如此亚茬,原因可歸結(jié)為實(shí)例與原型間的松散連接關(guān)系酪耳,實(shí)例與原型間的連接不過是一個(gè)指針,而不是一個(gè)副本刹缝,所以可以動(dòng)態(tài)地查找碗暗。
那么【例21】為什么會(huì)報(bào)錯(cuò)呢?因?yàn)椤纠?1】實(shí)際上是重寫了整個(gè)原型對(duì)象梢夯,從而切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系言疗,構(gòu)造函數(shù)指向新的原型對(duì)象,而實(shí)例仍指向原本的原型對(duì)象颂砸,而原本的原型對(duì)象中不存在sayName()方法噪奄,所以調(diào)用會(huì)報(bào)錯(cuò)。
什么樣的情況下調(diào)用sayName()方法會(huì)返回我們希望看到的值呢人乓?
只要在重寫原型對(duì)象之后再定義實(shí)例就好了
【例22】
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'dot',
sex: 'female',
age: 2,
sayName: function () {
console.log(this.name)
}
}
var friend = new Person()
friend.sayName()//dot
可以畫圖演示重寫原型對(duì)象前后的指針指向
- 原生對(duì)象的原型
原生的引用類型也是采用原型模式創(chuàng)建的勤篮,所有原生引用類型(Array、String等)都在其構(gòu)造函數(shù)的原型上定義了方法色罚,可以像修改自定義對(duì)象的原型一樣修改原生對(duì)象的原型碰缔,因此可以隨時(shí)添加方法,
通過原生對(duì)象的原型不僅可以取得默認(rèn)方法的引用戳护,也可以定義新方法
【例23】
Array.prototype.shuffle = function () {
var arr = this
for (var i = arr.length - 1; i >= 0; i--) {
var randomIdx = Math.floor(Math.random() * (i + 1))
var itemAtIdx = arr[randomIdx]
arr[randomIdx] = arr[i]
arr[i] = itemAtIdx
}
return arr
}
var tempArr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(tempArr.shuffle())//[ 5, 9, 6, 8, 4, 7, 3, 1, 2 ]
以上代碼中創(chuàng)建了一個(gè)shuffle方法金抡,用于隨機(jī)排列數(shù)組中的元素,我們將該方法掛載到Array對(duì)象的原型下腌且,當(dāng)前環(huán)境下的任何數(shù)組都可以調(diào)用該方法梗肝。如:
var tempArr=[1, 2, 3, 4, 5]
console.log(tempArr.shuffle())//[3, 2, 1, 4, 5]
盡管可以這樣做,但不推薦修改原生對(duì)象的原型铺董,如果因?yàn)槟硞€(gè)實(shí)現(xiàn)中缺少某個(gè)方法统捶,就在原生對(duì)象的原型中添加這個(gè)方法,那么當(dāng)在另一個(gè)支持該方法的實(shí)現(xiàn)中運(yùn)行代碼是柄粹,可能導(dǎo)致命名沖突喘鸟,而且這樣做也可能意外導(dǎo)致重寫原生方法。
- 原型對(duì)象的問題:
原型模式省略了為構(gòu)造函數(shù)初始化參數(shù)這一環(huán)節(jié)驻右,導(dǎo)致所有的實(shí)例在默認(rèn)情況下都取得相同的屬性值什黑,但這不算什么,最大的問題是由其共享的本質(zhì)導(dǎo)致的堪夭。
【例24】:
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'dot',
sex: 'female',
age: 2,
fav: ['milk', 'orange', 'apple'],
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.fav.push('meat')
console.log(person1.fav)//[ 'milk', 'orange', 'apple', 'meat' ]
console.log(person2.fav)//[ 'milk', 'orange', 'apple', 'meat' ]
console.log(person1.fav === person2.fav)//true
問題還是很突出的愕把,這里的結(jié)果讓人瞬間想起了最基本的概念拣凹,基本類型值按值傳遞,引用類型值按引用傳遞恨豁。因?yàn)閜erson1.fav與person2.fav指向的是內(nèi)存中的同一地址嚣镜,所以對(duì)person1.fav的修改在person2.fav中也會(huì)反映出來,而實(shí)例一般都要擁有屬于自己的全部屬性橘蜜,所以原型模式還是存在比較嚴(yán)重的缺陷菊匿。
組合使用構(gòu)造函數(shù)模式與原型模式
組合使用構(gòu)造函數(shù)模式與原型模式是創(chuàng)建自定義類型的最常見方式。
分工:構(gòu)造函數(shù)模式用于定義實(shí)例屬性计福,原型模式用于定義共享的屬性和方法跌捆。
結(jié)果:每個(gè)實(shí)例都有自己的一份實(shí)例屬性副本,但同時(shí)又可以共享對(duì)方法的引用象颖,最大限度節(jié)省了內(nèi)存佩厚,集兩種模式之長,是使用最廣泛说订,認(rèn)同度最高的一種創(chuàng)建自定義類型的方法抄瓦。
用組合模式改寫前面的代碼
【例25】:
function Person(name, age) {
this.name = name
this.age = age
this.fav = ['milk', 'orange', 'apple']
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person('dot', 2)
var person2 = new Person('dolby', 3)
person1.fav.push('meat')
console.log(person1.fav)//[ 'milk', 'orange', 'apple', 'meat' ]
console.log(person2.fav)//[ 'milk', 'orange', 'apple' ]
console.log(person1.fav === person2.fav)//false
console.log(person1.sayName === person2.sayName)//true
上例中,實(shí)例屬性都在構(gòu)造函數(shù)Person中定義陶冷,所有實(shí)例的共享屬性和方法都是在原型中定義的钙姊,修改了person1.fav后并不會(huì)影響到person2.fav,因?yàn)樗鼈兎謩e引用了不同的數(shù)組埃叭。這種方法用于創(chuàng)建對(duì)象可以說是非常完美了摸恍。
現(xiàn)在可以深究一下悉罕,prototype 是什么赤屋?有什么特性?
- 每個(gè)函數(shù)都有
prototype
這個(gè)屬性壁袄,對(duì)應(yīng)值是原型對(duì)象类早,prototype被設(shè)計(jì)出來就是用來公用的,它是js繼承機(jī)制的靈魂嗜逻。 - 每個(gè)對(duì)象都有個(gè)內(nèi)部屬性
__proto__
涩僻,每個(gè)實(shí)例的__proto__
指向創(chuàng)建它的構(gòu)造函數(shù)的prototype
- 一切函數(shù)都是由 Function 這個(gè)函數(shù)創(chuàng)建的,所以
Function.prototype === 被創(chuàng)建的函數(shù).__proto__
- 一切函數(shù)的原型對(duì)象都是由 Object 這個(gè)函數(shù)創(chuàng)建的栈顷,所以
Object.prototype === 一切函數(shù).prototype.__proto__
那么來畫上面代碼的原型圖練手吧~
繼承
JavaScript主要通過原型鏈實(shí)現(xiàn)繼承逆日,原型鏈的構(gòu)建是通過將一個(gè)類型的實(shí)例賦值給另一個(gè)構(gòu)造函數(shù)的原型實(shí)現(xiàn)的,這樣一來萄凤,子類型就可以訪問超類型的所有屬性和方法室抽,原型鏈的問題是對(duì)象實(shí)例共享所有繼承的屬性和方法,因此不適宜單獨(dú)使用靡努。
解決方法是在子類型構(gòu)造函數(shù)內(nèi)部調(diào)用超類型構(gòu)造函數(shù)坪圾,可以做到每個(gè)實(shí)例都擁有一套自己的屬性晓折,同時(shí)保證只使用構(gòu)造函數(shù)模式來定義類型。
有六種繼承模式:
- 構(gòu)造函數(shù)繼承:
在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類構(gòu)造函數(shù)兽泄,通過使用call()和apply()方法可以在新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù)漓概。
優(yōu)點(diǎn):子類的每個(gè)實(shí)例都有自己的屬性(name),不會(huì)相互影響病梢。
缺點(diǎn):沒有實(shí)現(xiàn)父類方法的復(fù)用胃珍。 - 原型鏈繼承:
每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象包含一個(gè)指向構(gòu)造函數(shù)的指針飘千,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針堂鲜。讓一個(gè)構(gòu)造函數(shù)的原型對(duì)象等于另一個(gè)構(gòu)造函數(shù)的實(shí)例,以此類推實(shí)現(xiàn)原型鏈繼承
優(yōu)點(diǎn):父類的方法(getName)得到了復(fù)用护奈。
缺點(diǎn):所有的實(shí)例會(huì)共享通過原型鏈繼承的屬性缔莲,在一個(gè)實(shí)例中改變了,會(huì)在另一個(gè)實(shí)例中反映出來 - 組合繼承:
使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承霉旗,借用構(gòu)造函數(shù)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承痴奏。
優(yōu)點(diǎn):既實(shí)現(xiàn)了函數(shù)復(fù)用,又保證每個(gè)實(shí)例具有自己的屬性厌秒。
缺點(diǎn):無論什么情況下都會(huì)調(diào)用兩次超類型的構(gòu)造函數(shù)(SuperType)读拆,一次在創(chuàng)建子類型原型的時(shí)候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部鸵闪。 - 原型式繼承:
借助原型基于已有的對(duì)象創(chuàng)建新對(duì)象檐晕,同時(shí)在創(chuàng)建的過程中加以修改,達(dá)到了繼承原有屬性和方法并可以添加新屬性和方法的目的蚌讼。
優(yōu)點(diǎn):不用創(chuàng)建自定義類型辟灰,新對(duì)象上具有原來的屬性和方法,又可以增加新的屬性和方法且不會(huì)影響到原來的對(duì)象篡石。
缺點(diǎn):如果原有對(duì)象包含有引用類型值芥喇,值會(huì)被所有實(shí)例共享,改變一個(gè)就全變了 - 寄生式繼承:
與原型繼承很相似凰萨,也是基于某個(gè)對(duì)象或某些信息創(chuàng)建一個(gè)對(duì)象继控,然后增強(qiáng)并返回對(duì)象
優(yōu)點(diǎn):可解決組合繼承模式多次調(diào)用超類型構(gòu)造函數(shù)導(dǎo)致的低效率問題
缺點(diǎn):不能做到函數(shù)復(fù)用 - 寄生組合式繼承:
不必為了制定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要的無非就是超類型原型的一個(gè)副本而已胖眷。
本質(zhì)上就是使用寄生式繼承來繼承超類型的原型武通,再將結(jié)果指定給子類型的原型。
構(gòu)造函數(shù)繼承
基本思想:在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類構(gòu)造函數(shù)珊搀,通過使用call()和apply()方法可以在新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù)冶忱。
function Parent() {
this.name = 'parent'
this.sayName = function () {
console.log(this.name)
}
}
function Child() {
Parent.call(this)
this.type = 'child'
}
var qinghua = new Parent()
console.log(qinghua)//Parent { name: 'parent', sayName: ? }
var dot = new Child()
console.log(dot)//Child {name: "parent", sayName: ?, type: "child"}
console.log(dot.fn())//parent
原型鏈繼承
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function subType() {
this.property = false;
}
//繼承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.property;
}
var instance = new SubType();
console.log(instance.getSuperValue());//true
原型式繼承
基本思想:借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象,還不必因此創(chuàng)建自定義類型食棕。
【例26】:
function object(o){
function F(){}
F.prototype=o
return new F()
}
var person={
name:'dot',
friends:['a','b','c']
}
var anotherPerson=object(person)
anotherPerson.name='dolby'
anotherPerson.friends.push('d')
var anotherPerson2=object(person)
anotherPerson2.name='dooot'
anotherPerson2.friends.push('e')
console.log(person.friends)//[ 'a', 'b', 'c', 'd', 'e' ]
console.log(anotherPerson.friends)//[ 'a', 'b', 'c', 'd', 'e' ]
console.log(anotherPerson2.friends)//[ 'a', 'b', 'c', 'd', 'e' ]
原型式繼承要求必須有一個(gè)對(duì)象作為另一個(gè)對(duì)象的基礎(chǔ)朗和,如果有這么個(gè)對(duì)象错沽,就將它傳遞給object()函數(shù),然后根據(jù)具體需求得到的對(duì)象加以修改眶拉,上例中千埃,person.friends不僅屬于person所有,而且也會(huì)被anotherPerson和anotherPerson2共享忆植,他們因用的是內(nèi)存中的同一片地址放可,所以改變其中一個(gè),另兩個(gè)的值也會(huì)動(dòng)態(tài)改變朝刊。
ECMAScript5新增了Object.create()方法規(guī)范化了原型式繼承耀里,這個(gè)方法接收兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和可選的為新對(duì)象定義額外屬性的對(duì)象。Object.create()方法實(shí)際上就是創(chuàng)建一個(gè)接收的參數(shù)的副本拾氓。
在只傳入一個(gè)參數(shù)的情況下冯挎,Object.create()方法與【例26】中的object()函數(shù)行為相同。
【例27】:
var person = {
name: 'dot',
friends: ['a', 'b', 'c']
}
var anotherPerson = Object.create(person)
anotherPerson.name = 'dolby'
anotherPerson.friends.push('d')
var anotherPerson2 = Object.create(person)
anotherPerson2.name = 'dooot'
anotherPerson2.friends.push('e')
console.log(person.friends)//[ 'a', 'b', 'c', 'd', 'e' ]
console.log(anotherPerson.friends)//[ 'a', 'b', 'c', 'd', 'e' ]
console.log(anotherPerson2.friends)//[ 'a', 'b', 'c', 'd', 'e' ]
Object.create()的第二個(gè)參數(shù)與Object.defineProperties()的第二個(gè)參數(shù)格式相同咙鞍,每個(gè)屬性都是通過自己的描述符定義的房官,以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性。
【例28】:
var person = {
name: 'dot',
friends: ['a', 'b', 'c']
}
var anotherPerson = Object.create(person, {
name: {
value: 'dolby'
}
})
console.log(anotherPerson.name)//dolby
寄生式繼承
寄生式繼承是與原型式繼承緊密相關(guān)的一種思路续滋。創(chuàng)建一個(gè)僅用于封裝繼承過程的函數(shù)翰守,該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象,最后再像是真的是它做了所有工作一樣返回該對(duì)象疲酌。
【例29】:
function object(o){
function F(){}
F.prototype=o
return new F()
}
function createAnother(original) {
var clone = object(original)//通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
clone.sayHi = function () {//增強(qiáng)對(duì)象
console.log('hi')
}
return clone//返回這個(gè)對(duì)象
}
var person={
name:'dot',
friends: ['a', 'b', 'c']
}
var anotherPerson=createAnother(person)//基于person返回了一個(gè)新對(duì)象anotherPerson
anotherPerson.sayHi()//新對(duì)象anotherPerson不僅有person對(duì)象的所有屬性和方法蜡峰,還有自己的sayName()方法
在主要考慮對(duì)象而不是自定義類型和構(gòu)造函數(shù)的情況下,寄生式繼承也也是一種有用的模式朗恳,任何能夠返回新對(duì)象的函數(shù)都適用于此模式湿颅。
而使用寄生式繼承來為對(duì)象添加函數(shù)也不能做到函數(shù)復(fù)用。
組合繼承
也叫做經(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í)例具有自己的屬性冀自。
【例30】:
function SuperType(name) {
this.name = name
this.friend = ['a', 'b', 'c']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)//繼承屬性揉稚,第二次調(diào)用SuperType
this.age = age
}
SubType.prototype = new SuperType()//第一次調(diào)用SuperTy
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
console.log(this.age)
}
var instance1 = new SubType('dot', 2)
instance1.friend.push('d')
console.log(instance1.friend)//[ 'a', 'b', 'c', 'd' ]
instance1.sayName()//dot
instance1.sayAge()//2
var instance2 = new SubType('dolby', 3)
console.log(instance2.friend)//[ 'a', 'b', 'c']
instance2.sayName()//dolby
instance2.sayAge()//3
上例中,SuperType構(gòu)造函數(shù)定義了兩個(gè)屬性熬粗,name和friend搀玖,SuperType的原型定義了一個(gè)方法sayName(),SubType構(gòu)造函數(shù)通過調(diào)用SuperType構(gòu)造函數(shù)傳入了參數(shù)name屬性驻呐,接著定義了自己的age屬性灌诅,然后將SuperType的實(shí)例賦給SubType的原型芳来,又在該原型上定義了sayAge()方法。這樣就可以讓兩個(gè)不同的SubType實(shí)例分別用有自己的屬性猜拾,包括friend屬性即舌,又可以使用相同的方法。
組合繼承是JS中最常用的繼承模式挎袜,但它有一個(gè)缺點(diǎn)就是顽聂,無論什么情況下都會(huì)調(diào)用兩次超類型的構(gòu)造函數(shù)(SuperType),一次在創(chuàng)建子類型原型的時(shí)候盯仪,另一次是在子類型構(gòu)造函數(shù)內(nèi)部紊搪。
還是看【例30】中的代碼,里面有標(biāo)識(shí)兩次調(diào)用SuperType的時(shí)機(jī)全景,第一次調(diào)用SuperType構(gòu)造函數(shù)時(shí)耀石,SubType.prototype會(huì)得到SuperType的實(shí)例屬性name和friend;第二次調(diào)用發(fā)生在調(diào)用SubType構(gòu)造函數(shù)時(shí)爸黄,這一次在新對(duì)象上創(chuàng)建了實(shí)例屬性name和friend娶牌,接著這兩個(gè)屬性就屏蔽了原型中的兩個(gè)同名屬性。解決調(diào)用兩次超類型構(gòu)造函數(shù)而導(dǎo)致的效率低下的問題的辦法就是寄生組合式繼承馆纳。
寄生組合式繼承
寄生組合式繼承的思路是:不必為了制定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù)诗良,我們所需要的無非就是超類型原型的一個(gè)副本而已。
本質(zhì)上就是使用寄生式繼承來繼承超類型的原型鲁驶,再將結(jié)果指定給子類型的原型鉴裹。
【例31】:
function Person(name, sex) {
this.name = name
this.sex = sex
}
Person.prototype.sayName = function () {
console.log(this.name)
}
function Male(name, sex, age) {
Person.call(this, name, sex)
this.age = age
}
Male.prototype = Object.create(Person.prototype);
Male.prototype.sayAge = function () {
console.log(this.age)
}
var dot = new Male('dot', 'male', 2)
dot.sayName()//dot
上例貌似沒問題了,但有個(gè)問題钥弯,我們知道prototype對(duì)象有一個(gè)屬性constructor指向其類型径荔,而我們復(fù)制的SuperType的prototype,這時(shí)候constructor屬性指向是不對(duì)的脆霎,導(dǎo)致我們判斷類型出錯(cuò)
console.log(Male.prototype.constructor)//[function: Person]
console.log(dot.constructor)//[function: Person]
console.log(Male.prototype.constructor === dot.constructor)//true
所以我們還需要修改一下constructor的指向总处,通過一個(gè)inherit函數(shù)可以做到這一點(diǎn),看以下代碼:
【例32】:
function inherit(superType, subType) {
var _prototype = superType.prototype
_prototype.constructor = subType//修改constructor指向
subType.prototype = _prototype
}
function Person(name, sex) {
this.name = name
this.sex = sex
}
Person.prototype.sayName = function () {
console.log(this.name)
}
function Male(name, sex, age) {
Person.call(this, name, sex)
this.age = age
}
inherit(Person, Male)//Male繼承Person
// 在繼承函數(shù)之后寫自己的方法睛蛛,否則會(huì)被覆蓋
Male.prototype.sayAge = function () {
console.log(this.age)
}
var dot = new Male('dot', 'male', 2)
dot.sayName()//dot
console.log(Male.prototype.constructor)//[function: Male]
console.log(dot.constructor)//[function: Male]
console.log(Male.prototype.constructor === dot.constructor)//true
現(xiàn)在就沒有問題了鹦马,可以說很完美。
那么來試一下畫出以下代碼繼承的原型圖:
Object.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Function.__proto__ === Function.prototype
Object.prototype.__proto__ === null
【例33】
//定義一個(gè)繼承的函數(shù)忆肾,確保指針指向正確
function inherit(SuperType, SubType) {
var _prototype = Object.create(SuperType.prototype)
_prototype.constructor = SubType
SubType.prototype = _prototype
}
//定義一個(gè)爺爺輩構(gòu)造函數(shù)荸频,有一個(gè)實(shí)例屬性name
function Person(name) {
this.name = name
}
//爺爺輩構(gòu)造函數(shù)的原型上有一個(gè)sayName方法
Person.prototype.sayName = function () {
console.log(`i am ${this.name}`)
}
//定義一個(gè)爸爸輩構(gòu)造函數(shù),有兩個(gè)實(shí)例屬性name和skill
function Developer(name, skill) {
Person.call(this, name)
this.skill = skill
}
//調(diào)用繼承函數(shù)客冈,讓爸爸繼承爺爺?shù)膶傩院头椒?inherit(Person, Developer)
//爸爸在繼承之后定義自己的saySkill方法旭从,以免被覆蓋
Developer.prototype.saySkill = function () {
console.log(`i have a skill ${this.skill}`)
}
////定義一個(gè)兒子輩構(gòu)造函數(shù),有三個(gè)實(shí)例屬性name、skill和悦、frontendSkill
function FEDeveloper(name, skill, frontendSkill) {
Developer.call(this, name, skill)
this.frontendSkill = frontendSkill
}
//調(diào)用繼承函數(shù)退疫,讓兒子繼承爸爸的屬性和方法
inherit(Developer, FEDeveloper)
//兒子在繼承之后定義自己的sayFESkill方法,以免被覆蓋
FEDeveloper.prototype.sayFESkill = function () {
console.log(`i have a frontendSkill ${this.frontendSkill}`)
}
//定義一個(gè)爺爺?shù)膶?shí)例并傳入實(shí)參鸽素,將這個(gè)實(shí)例賦值給變量person1
var person1 = new Person('dooot')
//定義一個(gè)爸爸的實(shí)例并傳入實(shí)參蹄咖,將這個(gè)實(shí)例賦值給變量developer
var developer = new Developer('dot', 'node')
//定義一個(gè)兒子的實(shí)例并傳入實(shí)參,將這個(gè)實(shí)例賦值給變量fe
var fe = new FEDeveloper('dolb', 'program', 'css')
console.log(fe.__proto__ === FEDeveloper.prototype)//true
console.log(fe.constructor === FEDeveloper)//true
console.log(FEDeveloper.prototype.constructor === FEDeveloper)//true
console.log(FEDeveloper.prototype.__proto__ === Developer.prototype)//true
console.log(developer.constructor === Developer)//true
console.log(Developer.prototype.__proto__ === Person.prototype)//true
注:畫圖不易付鹿,本文所有圖片澜汤,禁止轉(zhuǎn)載,謝謝舵匾。
參考資料:
- 特別感謝:JavaScript高級(jí)程序設(shè)計(jì)(第3版)
- this 的值到底是什么俊抵?一次說清楚
- 談?wù)勀銓?duì)原型、原型鏈坐梯、 Function徽诲、Object 的理解
- js中call、apply吵血、bind那些事
- mdn bind()
- 深入理解javascript原型和閉包(6)—繼承
- Javascript中bind()方法的使用與實(shí)現(xiàn)