1 理解對(duì)象
1-1 屬性的類型
屬性分兩種:數(shù)據(jù)屬性和訪問器屬性
-
數(shù)據(jù)屬性
數(shù)據(jù)屬性包含一個(gè)保存數(shù)據(jù)值的位置宾茂。值會(huì)從這個(gè)位置讀取,也會(huì)寫入到這個(gè)位置拴还,數(shù)據(jù)屬性有4個(gè)特性來描述它們的行為跨晴。
-
[[Configurable]]
:表示屬性是否可以通過delete刪除并重新定義,是否可以修改它的特性片林,以及是否可以把它改為訪問器屬性端盆。默認(rèn)為true -
[[Enumerable]]
:表示屬性是否可枚舉(通過for-in循環(huán)返回),默認(rèn)為true -
[[Writable]]
:表示屬性的值是否可修改费封,默認(rèn)為true -
[[Value]]
:屬性實(shí)際的值焕妙,默認(rèn)是undefined
注意點(diǎn):雖然可以對(duì)同一個(gè)屬性多次調(diào)用
Object.defineProperty()
,但在把configurable
設(shè)為false之后就會(huì)受限制了 -
-
訪問器屬性
訪問器屬性不包含數(shù)據(jù)值孝偎。相反访敌,它們包含一個(gè)獲取函數(shù)(getter)和一個(gè)設(shè)置函數(shù)(setter),訪問器屬性有4個(gè)特性來描述它們的行為
-
[[Configurable]]
:表示屬性是否可以通過delete刪除并重新定義,是否可以修改它的特性衣盾,以及是否可以把它改為數(shù)據(jù)屬性寺旺。默認(rèn)為true -
[[Enumberable]]
:表示屬性是否可枚舉(通過for-in循環(huán)返回)爷抓,默認(rèn)為true -
[[Get]]
:獲取函數(shù),在讀取屬性時(shí)調(diào)用阻塑,默認(rèn)為undefined -
[[Set]]
:設(shè)置函數(shù)蓝撇,在寫入屬性時(shí)調(diào)用,默認(rèn)為undefined
-
1-2 定義多個(gè)屬性
在一個(gè)對(duì)象上同時(shí)定義多個(gè)屬性時(shí)陈莽,使用Object.defineProperties()
方法渤昌,區(qū)別于Object.definedProperty()
方法一次只能定義或修改多個(gè)屬性,具體看MDN文檔
1-3 讀取屬性的特性
使用Object.getOwnPropertyDescription(obj, prop)
方法可以獲取指定屬性的屬性描述符走搁,也就是屬性的特性独柑。接收兩個(gè)參數(shù):屬性所在的對(duì)象和要取得其描述符的屬性名。
Object.getOwnPropertyDescriptions(obj)
方法可用來獲取一個(gè)對(duì)象的所有屬性的屬性描述符私植。接收一個(gè)參數(shù):需要獲取的對(duì)象
1-4 合并對(duì)象
ES6
中使用Object.assign(target, source)
方法進(jìn)行對(duì)象的合并忌栅,返回值是目標(biāo)對(duì)象。這個(gè)方法實(shí)際上是對(duì)每個(gè)源對(duì)象執(zhí)行的是淺復(fù)制曲稼。
1-5 對(duì)象標(biāo)識(shí)及相等判定
為了解決 === 操作符判定特殊情況帶來的問題索绪,ES6
新增了Object.is()
// ===
console.log(+0 === -0) // true
console.log(+0 === 0) // true
console.log(-0 === 0) // true
console.log(NaN === NaN) // false
// Object.is()
console.log(Object.is(+0 === -0)) // false
console.log(Object.is(+0 === 0)) // true
console.log(Object.is(-0 === 0)) // false
console.log(Object.is(NaN, NaN)) // true
2 創(chuàng)建對(duì)象
2-1 工廠模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name)
}
return o;
}
let person1 = createPerson('xiaoming', 10, 'student');
let person2 = createPerson('zhangsan', 20, 'doctor');
console.log(person1 instanceof Person); // false 不能識(shí)別對(duì)象的類型
弊端:這里,函數(shù)每次調(diào)用都會(huì)返回一個(gè)新的對(duì)象贫悄, 這種方法可以解決創(chuàng)建多個(gè)類似對(duì)象的問題瑞驱,但是沒有解決對(duì)象標(biāo)識(shí)問題(即新創(chuàng)建的對(duì)象是什么類型),構(gòu)造函數(shù)模式可以解決這個(gè)問題窄坦。
2-2 構(gòu)造函數(shù)模式
ECMAScript
中的構(gòu)造函數(shù)是用于創(chuàng)建特定類型對(duì)象的唤反。
前面的例子使用構(gòu)造函數(shù)可以這么寫:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name)
}
}
let person1 = new Person('xiaoming', 10, 'student');
let person2 = new Person('zhangsan', 20, 'doctor');
這里的代碼和前面使用工廠函數(shù)創(chuàng)建的例子基本是一樣的,只是有以下區(qū)別:
- 沒有顯示地創(chuàng)建對(duì)象
- 屬性和方法直接賦值給this
- 沒有返回值
為什么鸭津?可以看到我們?cè)趧?chuàng)建實(shí)例地時(shí)候使用了new操作符拴袭。那使用new時(shí),內(nèi)部執(zhí)行了以下的操作:
- 創(chuàng)建一個(gè)新的空對(duì)象
- 這個(gè)對(duì)象內(nèi)部的
__proto__
屬性(這里應(yīng)該是[[Prototype]]特性曙博,具體為什么__proto__
可以訪問到?下面會(huì)解釋到)指向構(gòu)造函數(shù)(即Person)的prototype屬性- 構(gòu)造函數(shù)內(nèi)部的this指向這個(gè)新創(chuàng)建的空對(duì)象
- 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼怜瞒,也就是不斷地給this賦值父泳,不斷給this添加屬性
- 返回this對(duì)象(即新創(chuàng)建的對(duì)象)
instanceof
操作符是用來確定對(duì)象類型最可靠的方式。相比于工廠模式吴汪,可識(shí)別對(duì)象的類型是一個(gè)很大的好處惠窄。
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
弊端:構(gòu)造函數(shù)內(nèi)部定義的方法會(huì)在每個(gè)實(shí)例上都創(chuàng)建一遍,上面的例子中漾橙,
person1
和person2
中都有名為sayName()
的方法杆融,因?yàn)槭亲鐾患拢詻]必要?jiǎng)?chuàng)建兩次霜运。這個(gè)問題可以通過原型模式來解決脾歇。
2-3 原型模式
每個(gè)函數(shù)都會(huì)創(chuàng)建一個(gè)prototype
屬性蒋腮,這個(gè)屬性是一個(gè)對(duì)象。
使用原型對(duì)象的好處是:在它上面定義的屬性和方法可以被實(shí)例共享藕各。原來在構(gòu)造函數(shù)中直接賦值給對(duì)象實(shí)例的值池摧,可以直接賦值給他們的原型,如下:
function Person() {}
Person.prototype.name = 'xiaoming';
Person.prototype.age = 10;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
console.log(this.name);
}
let person1 = new Person();
let person2 = new Person();
console.log(person1.name) // xiaoming
console.log(person1.sayName == person2.sayName) // true
雖然構(gòu)造函數(shù)中什么都沒有激况,但是卻可以訪問得到相應(yīng)得屬性和方法作彤,而且使用定義在原型上的屬性和方法是共享給所有的實(shí)例的(即所有實(shí)例都可以訪問得到,也不會(huì)存在重復(fù)創(chuàng)建的問題)
- 理解原型
無論何時(shí)乌逐,只要?jiǎng)?chuàng)建一個(gè)函數(shù)竭讳,這個(gè)函數(shù)就存在一個(gè)
prototype
屬性(指向原型對(duì)象)。默認(rèn)情況下浙踢,所有原型對(duì)象都有一個(gè)名為constructor
的屬性绢慢,指回對(duì)應(yīng)的構(gòu)造函數(shù)。比如上面的例子Person.prototype.constructor
指回Person
每次調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例成黄,這個(gè)實(shí)例內(nèi)部存在一個(gè)
[[Prototype]]
特性呐芥,會(huì)指向構(gòu)造函數(shù)的原型對(duì)象。由于腳本中沒有訪問這個(gè)[[Prototype]]
特性的標(biāo)準(zhǔn)方式奋岁,但Firefox
思瘟,Safari
,Chrome
中會(huì)在每個(gè)對(duì)象上暴露__proto__
屬性闻伶,通過這個(gè)屬性可以訪問實(shí)例對(duì)象的原型滨攻。(這也為上面將new操作符時(shí)說為什么可以通過__proto__
訪問的到原型做了解釋)關(guān)鍵在于理解這一點(diǎn):實(shí)例與構(gòu)造函數(shù)原型之間有直接的聯(lián)系,但實(shí)例與構(gòu)造函數(shù)之間沒有
看圖:
- 原型層級(jí)
在通過對(duì)象訪問屬性時(shí)蓝翰,會(huì)按照這個(gè)屬性的名稱開始搜索光绕。搜索開始于對(duì)象實(shí)例本身。如果在這個(gè)實(shí)例上發(fā)現(xiàn)了給定的名稱畜份,則返回該名稱對(duì)應(yīng)的值诞帐。如果沒有找到這個(gè)屬性,則搜索會(huì)沿著指針進(jìn)入原型對(duì)象爆雹,然后在原型對(duì)象上找到屬性后停蕉,再返回對(duì)應(yīng)的值。
雖然可以通過實(shí)例讀取原型對(duì)象上的值钙态,但不可能通過實(shí)例重寫這些值慧起。如果在實(shí)例上添加了一個(gè)與原型對(duì)象中同名的屬性,那就會(huì)在實(shí)例上創(chuàng)建這個(gè)屬性册倒,這個(gè)屬性會(huì)遮住原型對(duì)象上的屬性蚓挤。即使在實(shí)例上把這個(gè)屬性設(shè)置為null,也不會(huì)恢復(fù)它和原型的聯(lián)系,不過灿意,使用delete操作符可以完全刪除實(shí)例上的這個(gè)屬性估灿,從而讓標(biāo)識(shí)符解析過程能夠繼續(xù)搜索原型對(duì)象。
hasOwnProperty()方法
用于確定某個(gè)屬性是在實(shí)例上還是在原型對(duì)象上脾歧。會(huì)在屬性存在于調(diào)用它的對(duì)象實(shí)例上時(shí)返回true甲捏,即如果該屬性是存在于實(shí)例上時(shí),返回true鞭执,反之返回false司顿。function Person() {} Person.prototype.name = 'xiaoming'; let person1 = new Person() person1.name = 'lucy' console.log(person1.hasOwnProperty('name')) // true delete person1.name // 刪除實(shí)例上的name屬性 console.log(person1.hasOwnProperty('name')) // false
- 原型和in操作符
有兩種方式使用in操作符:
for-in循環(huán)中使用
- for-in中使用in操作符時(shí),遍歷對(duì)象的所有可枚舉屬性
- 要想獲得對(duì)象上所有可枚舉的實(shí)例屬性兄纺,可以使用
Object.keys()
方法大溜。(接收一個(gè)對(duì)象作為參數(shù),返回所有可枚舉屬性組成的字符串?dāng)?shù)組)Object.getOwnPropertyNames()
方法返回的是所有實(shí)例屬性估脆,無論是否可枚舉钦奋;Object.getOwnPropertySymbols()
類似;單獨(dú)使用時(shí)疙赠,in操作符會(huì)在可以通過對(duì)象訪問指定屬性時(shí)返回true付材,無論該屬性是在實(shí)例上還是在原型上。
如果要確定某個(gè)屬性是否存在于原型上圃阳,則可以像這樣同時(shí)使用
hasOwnProperty
和in
操作符function hasPrototypeProperty(object, name) { return !Object.hasOwnProperty(name) && (name in Object) }
- 屬性枚舉順序
- 順序不確定:
for-in
循環(huán)厌衔,Object.keys()
,取決于JavaScript
引擎捍岳,可能因?yàn)g覽器而異- 順序確定:
Object.getOwnPropertyNames()
富寿、Object.getOwnPropertySymbols()
、Object.assign()
锣夹,先以升序枚舉數(shù)值鍵页徐,再按定義的順序插入枚舉字符串和符號(hào)鍵。(數(shù)字鍵優(yōu)先银萍,并且升序排列变勇,和定義屬性的順序無關(guān),次之是字符串和符號(hào)鍵贴唇,這兩種就按照定義屬性的順序來插入)
2-4 對(duì)象迭代
ESMAScript2017
新增了兩個(gè)靜態(tài)方法贰锁。用于迭代對(duì)象,這兩個(gè)方法執(zhí)行對(duì)象的淺復(fù)制滤蝠,都會(huì)忽略符號(hào)屬性。
-
Object.values()
:返回的是對(duì)象 值的數(shù)組 -
Object.entries()
:返回的是 鍵/值對(duì)的數(shù)組
- 其他原型語法
function Person() {} Person.prototype = { name: 'xiaoming', sayName() { console.log(this.name); } }
看上面的代碼授嘀,在直接通過一個(gè)包含所有屬性和方法的對(duì)象來重寫原型時(shí)物咳,要注意,這樣重寫后蹄皱,
Person.prototype
的constructor
屬性就不指向Person
了览闰,而是指向Object芯肤。如果我們想依靠constructor
屬性來識(shí)別類型,那怎么辦压鉴?那就重新指定一下function Person() {} Person.prototype = { constructor: Person, name: 'xiaoming', sayName() { console.log(this.name) } }
好了崖咨,但是有個(gè)問題,以這種方式恢復(fù)
constructor
屬性它是一個(gè)[[Enumerable]]
為true
的屬性油吭,而原生的constructor屬性
默認(rèn)是不可枚舉的击蹲。因此我們得用Object.definedProperty()
方法來定義constructor
屬性:function Person() {} Person.prototype = { name: 'xiaoming', sayName() { console.log(this.name) } } // 恢復(fù)constructor屬性 Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person })
這樣就可以完美恢復(fù)
constructor
屬性了。
- 原型的動(dòng)態(tài)性
注意給原型添加屬性和方法和重寫整個(gè)原型是完全兩回事婉宰。
先看個(gè)例子:給原型添加屬性和方法
let friend = new Person() Person.prototype.sayHi = function() { console.log('Hi') } friend.sayHi() // Hi
雖然我們是在實(shí)例化之后才給原型添加
sayHi()
方法的歌豺,為什么實(shí)例可以直接訪問到該方法?這是因?yàn)閚ew的時(shí)候?qū)嵗?code>[[Prototype]]指針就已經(jīng)指向
Person.prototype
了心包,所以無論我們后面怎么給原型對(duì)象添加屬性类咧,實(shí)例都能夠訪問得到。再看看這個(gè)例子:重寫整個(gè)原型
let friend = new Person() Person.prototype = { constructor: Person, name: 'xiaoming', sayName() { console.log(this.name) } } friend.sayName(); // 報(bào)錯(cuò)
為什么蟹腾?
這也是剛剛上面說的痕惋,實(shí)例的
[[Prototype]]
指針是在new的時(shí)候被賦值為Person.prototype
的,而上面的代碼因?yàn)橹貙懥嗽屯拗常喈?dāng)于又創(chuàng)建了一個(gè)新的對(duì)象值戳, 而這時(shí)實(shí)例指向的還是最初的原型對(duì)象,上面并沒有sayName()
方法珊随,所以報(bào)錯(cuò)重寫構(gòu)造函數(shù)上的原型之后再創(chuàng)建的實(shí)例才會(huì)引用新的原型述寡。而在此之前創(chuàng)建的實(shí)例仍然會(huì)引用最 初的原型。
- 原生對(duì)象原型
盡管可以像修改自定義對(duì)象原型一樣修改原生對(duì)象原型叶洞,隨時(shí)添加方法鲫凶,但不推薦在產(chǎn)品環(huán)境中修改原生對(duì)象原型。這樣做很可能造成誤會(huì)衩辟,而且可能引發(fā)命名沖突(比如一個(gè)名稱在某個(gè)瀏覽器實(shí)現(xiàn)中不存在螟炫,在另一個(gè)實(shí)現(xiàn)中卻存在)。另外還有可能意外重寫原生的方法艺晴。推薦的做法是創(chuàng)建一個(gè)自定義的類昼钻,繼承原生類型。
- 原型的問題
存在的問題:
- 弱化了向構(gòu)造函數(shù)傳遞初始化參數(shù)的能力封寞,會(huì)導(dǎo)致所有的實(shí)例默認(rèn)都取得相同的屬性值然评。
- 原型上的方法和屬性都是所有實(shí)例共享的,這對(duì)于方法來說比較合適狈究,但是對(duì)于屬性來說就不是特別好碗淌。如果屬性是原始類型,那還好,可以通過實(shí)例上添加同名屬性來覆蓋原型上地屬性亿眠。但是碎罚,如果屬性是引用類型,那么當(dāng)我們修改了某個(gè)實(shí)例上的該屬性纳像,(由于指針指向是相同的)那么這樣就影響了其他實(shí)例上的屬性荆烈,這是不合理的。
所以實(shí)際開發(fā)中通常不單獨(dú)使用原型模式竟趾。
3 繼承
繼承分為接口繼承和**實(shí)現(xiàn)繼承**憔购,實(shí)現(xiàn)繼承是`ECMAScript`唯一支持的繼承方式,而這主要是通過原型鏈實(shí)現(xiàn)的潭兽。
3-1 原型鏈繼承
原型鏈繼承就是 **使子類的原型指向父類的構(gòu)造出來的實(shí)例對(duì)象**
SubType.prototype = new SuperType()
- 默認(rèn)原型
任何函數(shù)的默認(rèn)原型都是
Object
的實(shí)例倦始,這意味著這個(gè)實(shí)例有一個(gè)內(nèi)部指針指向Object.prototype
,所以自定義類型能夠繼承如toString()
,valueOf()
這些方法山卦。
- 原型與繼承的關(guān)系
原型與實(shí)例的關(guān)系可以通過兩種方式來確定:
instanceof
操作符:(實(shí)例 instanceof 構(gòu)造函數(shù))
如果一個(gè)實(shí)例的原型鏈中出現(xiàn)過相應(yīng)的構(gòu)造函數(shù)鞋邑,則返回true
isPrototypeOf()方法
:(構(gòu)造函數(shù).prototype.isPrototypeOf(需要檢測的實(shí)例對(duì)象))
原型鏈中的每個(gè)原型都可以調(diào)用這個(gè)方法,用于檢測實(shí)例對(duì)象是否存在于另一個(gè)對(duì)象的原型鏈上账蓉,是則返回true
弊端:如果父類構(gòu)造函數(shù)中存在引用值會(huì)導(dǎo)致子類的原型中也存在著引用值(因?yàn)樽宇惖脑褪潜毁x值為父類的一個(gè)實(shí)例對(duì)象)枚碗,所以子類的所有實(shí)例都會(huì)共享存在的引用值。
3-2 盜用(借用)構(gòu)造函數(shù)繼承
為了解決原型包含引用值導(dǎo)致的繼承問題铸本,我們可以使用“盜用構(gòu)造函數(shù)繼承”
基本思路:在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)肮雨,可以使用
call()
和apply()
方法以新創(chuàng)建的對(duì)象為上下文執(zhí)行構(gòu)造函數(shù)。function SuperType(name) { this.name = name this.colors = ['red', 'green'] this.sayName = function() { console.log(this.name) } } function SubType() { SuperType.call(this, 'xiaoming') // 繼承SuperType并傳參 } let instance1 = new SubType() instance1.colors.push('blue') console.log(instance1.colors) // ['red', 'green', 'blue'] let instance2 = new SubType() console.log(instance2.colors) // ['red', 'green'] // 通過使用call()/apply()方法, SuperType構(gòu)造函數(shù)在SubType的實(shí)例創(chuàng)建的新對(duì)象的上下文中執(zhí)行了箱玷,相當(dāng)于新的SubType對(duì)象上運(yùn)行了SuperType函數(shù)中所有初始化代碼怨规。結(jié)果就是每個(gè)實(shí)例都會(huì)有自己的colors和name屬性。
優(yōu)點(diǎn):可以在子類構(gòu)造函數(shù)中向父類構(gòu)造函數(shù)傳參
缺點(diǎn):也是構(gòu)造函數(shù)模式的缺點(diǎn):就是必須在構(gòu)造函數(shù)中定義方法锡足,因此函數(shù)不能重用
3-3 組合繼承
組合繼承綜合了原型鏈和盜用(借用)構(gòu)造函數(shù)繼承波丰,將兩者的優(yōu)點(diǎn)集中了起來。
基本思路:使用原型鏈繼承原型上的屬性和方法舶得,而通過盜用構(gòu)造函數(shù)繼承實(shí)例屬性掰烟。
function SuperType(name) { this.name = name this.colors = ['red', 'blue'] } SuperType.prototype.sayName = function() { console.log(this.name) } function SubType(name, age) { SuperType.call(this, name) // 借用構(gòu)造函數(shù)繼承 讓SubType的每個(gè)實(shí)例都擁有name 和 colors屬性,相互之間不受影響 this.age = age } SubType.prototype = new SuperType() // 原型鏈繼承父類 SubType.prototype.sayAge = function() { console.log(this.age) } let instance1 = new SubType('xiaoming', 10) instance1.colors.push('yellow') console.log(instance1.colors) // ['red', 'blue', 'yellow'] instance1.sayName() // xiaoming instance1.sayAge() // 10 let instance2 = new SubType('lucy', 20) console.log(instance2.colors) // ['red', 'blue'] instance2.sayName() // lucy instance2.sayAge() // 20
優(yōu)點(diǎn):組合繼承彌補(bǔ)了原型鏈和盜用構(gòu)造函數(shù)的不足沐批,是
JavaScript
中使用最多的繼承模式纫骑,而且組合繼承也保留了instanceof
操作符和isPrototypeOf()
方法識(shí)別合成對(duì)象的能力弊端:存在效率問題,就是父類構(gòu)造函數(shù)始終會(huì)被調(diào)用兩次:一次是在賦值給子類原型時(shí)調(diào)用九孩,另一次是在子類構(gòu)造函數(shù)中調(diào)用先馆。
3-4 原型式繼承
基本思路:即使不自定義類型也可以通過原型實(shí)現(xiàn)對(duì)象之間的信息共享
function object(o) { function F() {} // 創(chuàng)建一個(gè)臨時(shí)構(gòu)造函數(shù)F F.prototype = o // 構(gòu)造函數(shù)F的原型指向o,說明F的實(shí)例對(duì)象能夠訪問到o的屬性和方法 return new F() // 返回構(gòu)造函數(shù)F的實(shí)例對(duì)象 } let person = { name: 'xiaoming', friends: ['xxx', 'yyy']} let anotherPerson = object(person) // 返回一個(gè)對(duì)象躺彬,這個(gè)對(duì)象的[[Prototype]]指針指向o anotherPerson.friends.push('zzz') let yetAnotherPerson = object(person) yetANotherPerson.friends.push('hhh') console.log(person) // ['xxx', 'yyy', 'zzz', 'hhh'] // 實(shí)際上磨隘,object()是對(duì)傳入的對(duì)象執(zhí)行了一次淺復(fù)制
適用場景:
- 你有一個(gè)對(duì)象缤底,想在它的基礎(chǔ)上再創(chuàng)建一個(gè)新對(duì)象。你需要先把這個(gè)對(duì)象傳入
object()
番捂,然后再對(duì)返回的對(duì)象做相應(yīng)的修改- 適合不需要單獨(dú)創(chuàng)建構(gòu)造函數(shù),但仍然需要在對(duì)象間共享信息的場合江解。
Object.create()
方法將原型式繼承的概念規(guī)范化了设预。這個(gè)方法接收兩個(gè)參數(shù):第一個(gè)參數(shù):作為新對(duì)象原型的對(duì)象;第二個(gè)參數(shù)(可選):給新對(duì)象定義額外屬性的對(duì)象犁河。當(dāng)只有一個(gè)參數(shù)時(shí)鳖枕,Objcet.create()
和object()
方法效果相同
Object.create()
的第二個(gè)參數(shù)與Object.definedProperties()
的第二個(gè)參數(shù)一樣:每個(gè)新增的屬性都通過各自的描述符來描述。以這種方式添加的屬性會(huì)遮蔽原型對(duì)象上的同名屬性桨螺。弊端:屬性中包含的引用值類型始終會(huì)在各個(gè)實(shí)例之間共享宾符,跟適用原型模式是一樣的。
3-5 寄生式繼承
寄生式繼承與原型式繼承比較接近灭翔。
基本思路:創(chuàng)建一個(gè)實(shí)現(xiàn)繼承的函數(shù)魏烫,以某種方式增強(qiáng)對(duì)象,然后返回這個(gè)對(duì)象
function object(o) { function F() {} F.prototype = o return new F() } function createAnother(original) { let clone = object(original) // 通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象 clone.sayHi = function() { // 以某種方式增強(qiáng)這個(gè)對(duì)象 console.log('hi') // 返回這個(gè)對(duì)象 } return clone } let person = { name: 'xiaoming', friends: ['xxx', 'yyy'] } let anotherPerson = createAnother(person) anotherPerson.sayHi() // 'hi'
弊端:通過寄生式繼承給對(duì)象添加函數(shù)會(huì)導(dǎo)致函數(shù)難以復(fù)用肝箱,與構(gòu)造函數(shù)模式類似哄褒。(即每次創(chuàng)建實(shí)例都要重復(fù)創(chuàng)建方法)
3-6 寄生式組合繼承
前面說到組合繼承其實(shí)存在性能問題:父類構(gòu)造函數(shù)最終會(huì)被調(diào)用兩次。(第一次是在給子類原型賦值時(shí)調(diào)用煌张;第二次是在子類構(gòu)造函數(shù)里面調(diào)用)寄生式組合繼承可以解決這個(gè)問題呐赡。
繼承方法:組合繼承(原型鏈繼承+借用構(gòu)造函數(shù)繼承)+ 寄生式繼承
基本思路:不通過調(diào)用父類構(gòu)造函數(shù)來給子類原型賦值,而是通過取得父類原型的一個(gè)副本
function object(o) { function F() {} F.prototype = o // 這里由于直接用對(duì)象賦值的形式重寫原型對(duì)象骏融,所以constructor的指向發(fā)生改變链嘀,指向該對(duì)象o return new F() } function inheritPrototype(subType, superType) { let prototype = object(superType.prototype) // 返回父類構(gòu)造函數(shù)的一個(gè)副本 prototype.constructor = subType // 修改constructor的指向 subType.prototype = prototype // } function SuperType(name) { this.name = name this.colors = ['red', 'yellow'] } SuperType.prototype.sayName = function() { console.log(this.name) } function SubType(name, age) { SuperType.call(this, name) this.age = age } // SubType.prototype = new SuperType() inheritPrototype(SubType, SuperType) SubType.prototype.sayAge = function() { console.log(this.age) }
這樣的話就只調(diào)用一次父類構(gòu)造函數(shù),這樣效率更高档玻。而且原型鏈保持不變怀泊,因此
instanceof
操作符和isPrototypeOf()
方法有效,所以寄生式組合繼承可以算是引用類型繼承的最佳方式窃肠。
4 類
ES6
引入一個(gè)class
關(guān)鍵字具有定義類的能力包个,是一個(gè)語法糖。class
背后使用的仍然是原型和構(gòu)造函數(shù)的概念冤留。
4-1 類定義
定義類有兩種主要方式:類聲明和類表達(dá)式
類聲明:
Class Person {}
類表達(dá)式:
const Animal = class {}
類聲明不能提升
4-2 類構(gòu)造函數(shù)
constructor
關(guān)鍵字用于在類定義塊的內(nèi)部創(chuàng)建類的構(gòu)造函數(shù)碧囊。方法名constructor
會(huì)告訴解釋器在使用new
操作符創(chuàng)建類的新實(shí)例時(shí),應(yīng)該調(diào)用這個(gè)函數(shù)纤怒。
- 實(shí)例化
調(diào)用類構(gòu)造函數(shù)時(shí)必須使用
new
操作符糯而,否則會(huì)報(bào)錯(cuò)。而普通構(gòu)造函數(shù)如果不使用new
泊窘,那就會(huì)以全局的this
(通常是window
)作為內(nèi)部對(duì)象類構(gòu)造函數(shù)實(shí)例化之后熄驼,它會(huì)變?yōu)槠胀ǖ膶?shí)例方法(但是它作為類構(gòu)造函數(shù)像寒,仍然需要使用new調(diào)用)
class Person { // Person:類標(biāo)識(shí)符 constructor() {} // constructor:類構(gòu)造函數(shù) } let p1 = new Person() let p2 = new p1.constructor()
4-3 實(shí)例、原型和類成員
類的語法可以非常方便地定義應(yīng)該存在于實(shí)例上的成員瓜贾、應(yīng)該存在于原型上的成員诺祸,以及應(yīng)該存在于類本身的成員
- 實(shí)例成員
每次通過
new
調(diào)用類標(biāo)識(shí)符時(shí),都會(huì)執(zhí)行類構(gòu)造函數(shù)祭芦】瓯浚可以為新創(chuàng)建的實(shí)例(this)
添加“自有”屬性。沒有限制是什么屬性龟劲。構(gòu)造函數(shù)執(zhí)行完畢后地梨,仍然可以給實(shí)例繼續(xù)添加新成員胖腾。
每個(gè)實(shí)例都對(duì)應(yīng)一個(gè)唯一的成員對(duì)象,這意味著所有成員都不會(huì)在原型上共享。
- 原型方法與訪問器
為了在實(shí)例間共享方法腰素,類定義語法把在類塊中定義的方法作為原型方法匙监。
class Person { constructor(name) { // 添加到this上面的所有內(nèi)容都會(huì)存在于不同的實(shí)例上面 this.name = name } // 在類塊中定義的所有內(nèi)容都會(huì)定義在類的原型上 locate() { console.log('prototype') } } let p1 = new Person('Jack') let p2 = new Person('May') console.log(p1.name) // Jack console.log(p2.name) // May p1.locate() // prototype p2.locate() // prototype
類方法等同于對(duì)象屬性琐馆,因此可以使用字符串瀑粥,符號(hào)或者計(jì)算的值作為鍵。
類定義也支持獲取和設(shè)置訪問器审胸。語法與行為跟普通對(duì)象一樣
class Person() { set name(newName) { this.name_ = newName } get name() { return this.name_ } }
- 靜態(tài)類方法
可以在類上定義靜態(tài)方法亥宿,與原型成員類似,靜態(tài)成員每個(gè)類上只能有一個(gè)砂沛。使用
static
關(guān)鍵字作為前綴烫扼,this
引用類自身。class Person () { ... 省略代碼 // 定義在類本身上 static locate() { console.log('class') } }
- 非函數(shù)原型和類成員的添加
雖然類定義不顯示支持在原型上或類上添加成員數(shù)據(jù)碍庵,但在類定義的外部映企,可以通過手動(dòng)來添加。
- 迭代器與生成器方法
4-4 繼承
- 繼承基礎(chǔ)
ES6
類支持單繼承静浴。使用extends
關(guān)鍵字堰氓,不僅可以繼承一個(gè)類,也可以繼承普通的構(gòu)造函數(shù)派生類都會(huì)通過原型鏈訪問到類和原型上定義的方法苹享。
this
的值會(huì)反映調(diào)用相應(yīng)方法的實(shí)例或者類双絮。
- 構(gòu)造函數(shù),
HomeObject
得问,super()
super
關(guān)鍵字只能在派生類中使用囤攀,而且僅限于構(gòu)造函數(shù),實(shí)例方法和靜態(tài)方法內(nèi)部宫纬。
在構(gòu)造函數(shù)中使用
super
可以調(diào)用繼承的父類的構(gòu)造函數(shù)class Vehicle { constructor() { this.hasEngine = true } } class Bus extends Vehicle { constructor() { super() // 相當(dāng)于super.constructor() console.log(this.hasEngine) // true console.log(this) // Bus { hasEngine: true } } } new Bus()
在靜態(tài)方法中使用
super
可以調(diào)用繼承的父類上定義的靜態(tài)方法class Vehicle { static identify() { console.log('vehicle') } } class Bus extends Vehicle { static identify() { super.identify() } } Bus.identify() // vehicle
使用
super
注意事項(xiàng):
super
只能在派生類構(gòu)造函數(shù)和靜態(tài)方法中使用焚挠。不能單獨(dú)引用
super
關(guān)鍵字,要么用它調(diào)用構(gòu)造函數(shù)漓骚,要么用它引用靜態(tài)方法調(diào)用
super()
會(huì)調(diào)用父類構(gòu)造函數(shù)蝌衔,并將父類構(gòu)造函數(shù)中返回的實(shí)例賦值給子類中的this
class Father { constructor() { this.name = 'xiaoming' } } class Child extends Father { constructor() { super() console.log(this) // Child { name: 'xiaoming' } } } let c1 = new Child() console.log(c1) // Child { name: 'xiaoming' }
super()
的行為如同調(diào)用構(gòu)造函數(shù)榛泛,如果需要給父類構(gòu)造函數(shù)傳參噩斟,則需要手動(dòng)傳入曹锨。class Father { constructor(name) { this.name = name } } class Child extends Father { constructor(name) { super(name) } } let c1 = new Child('Jack')
- 如果沒有派生類中沒有定義類構(gòu)造函數(shù)佳遂,在實(shí)例化派生類時(shí)會(huì)自動(dòng)調(diào)用
super()
营袜,而且會(huì)自動(dòng)傳入所有傳給派生類的參數(shù)class Father { constructor(name) { this.name = name } } class Child extends Father {} let c1 = new Child('Jack')
在派生類構(gòu)造函數(shù)中,不能在調(diào)用
super()
之前引用this
如果在派生類中顯式定義了構(gòu)造函數(shù)丑罪,則要么必須在其中調(diào)用
super()
荚板,要么必須在其中返回一個(gè)對(duì)象class Father { constructor() { this.name = 'Jack' } } class Child extends Father { constructor() { // 顯示定義了構(gòu)造函數(shù) super(); } } // 或者 class Child extends Father { constructor() { return {} } }
3.抽象基類
可供其他類繼承, 但本身不會(huì)被實(shí)例化吩屹」蛄恚可以通過
new.target
來實(shí)現(xiàn)。另外可以通過抽象基類構(gòu)造函數(shù)中進(jìn)行檢查煤搜,可以要求派生類必須定義某個(gè)方法免绿。// 抽象基類 class Father { constructor() { if (new.target === Father) { throw new Error('Father cannot be directly instantiated') } if (!this.foo) { throw new Error('Inheriting class must define foo()') } console.log("success") } } // 派生類 class Child extends Father { foo() {} } new Child() // class Child {} // success new Father() // class Father {} // Error: Father cannot be directly instantiated