3.1 語(yǔ)法
- 對(duì)象的文字語(yǔ)法:
var myObj = {
key: value
// ...
};
- 構(gòu)造形式:
var myObj = new Object();
myObj.key = value;
3.2 類(lèi)型
- 在 JavaScript 中一共有六種主要類(lèi)型:
string、boolean、number、null酥泛、undefined今豆、object
。 - 注意柔袁,簡(jiǎn)單基本類(lèi)型(
string呆躲、boolean、number捶索、null插掂、undefined
)本身并不是對(duì)象,不要與String腥例、Number辅甥、Boolean
搞混。 - null 有時(shí)會(huì)被當(dāng)作一種對(duì)象類(lèi)型燎竖,但是這其實(shí)只是語(yǔ)言本身的一個(gè) bug璃弄,即對(duì) null 執(zhí)行 typeof null 時(shí)會(huì)返回字符串 "object"。實(shí)際上构回,null 本身是基本類(lèi)型夏块。
- 有一種常見(jiàn)的錯(cuò)誤說(shuō)法是“JavaScript 中萬(wàn)物皆是對(duì)象”,這顯然是錯(cuò)誤的纤掸。
- 實(shí)際上脐供,JavaScript 中有許多特殊的對(duì)象子類(lèi)型,我們可以稱(chēng)之為復(fù)雜基本類(lèi)型茁肠。
- 函數(shù)就是對(duì)象的一個(gè)子類(lèi)型(從技術(shù)角度來(lái)說(shuō)就是“可調(diào)用的對(duì)象”)患民。JavaScript 中的函數(shù)是“一等公民”缩举,因?yàn)樗鼈儽举|(zhì)上和普通的對(duì)象一樣(只是可以調(diào)用)垦梆,所以可以像操作其他對(duì)象一樣操作函數(shù)(比如當(dāng)作另一個(gè)函數(shù)的參數(shù))。
- 數(shù)組也是對(duì)象的一種類(lèi)型仅孩,具備一些額外的行為托猩。數(shù)組中內(nèi)容的組織方式比一般的對(duì)象要 稍微復(fù)雜一些。
內(nèi)置對(duì)象
- JavaScript 中還有一些對(duì)象子類(lèi)型辽慕,通常被稱(chēng)為內(nèi)置對(duì)象京腥。有些內(nèi)置對(duì)象的名字看起來(lái)和 簡(jiǎn)單基礎(chǔ)類(lèi)型一樣,不過(guò)實(shí)際上它們的關(guān)系更復(fù)雜溅蛉。
-
內(nèi)置對(duì)象:
String公浪、Number、Boolean船侧、Object欠气、Function、Array镜撩、Data预柒、RegExp、Error
。
在 JavaScript 中宜鸯,它們實(shí)際上是一些內(nèi)置函數(shù)憔古。這些內(nèi)置函數(shù)可以當(dāng)作構(gòu)造函數(shù) (由 new 產(chǎn)生的函數(shù)調(diào)用)來(lái)使用,從而可以構(gòu)造一個(gè)對(duì)應(yīng)子類(lèi)型的新對(duì)象淋袖。
舉例來(lái)說(shuō):
var strPrimitive = "I am a string";
typeof strPrimitive; // "string" strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 檢查 sub-type 對(duì)象
Object.prototype.toString.call( strObject ); // [object String]
原始值 "I am a string" 并不是一個(gè)對(duì)象鸿市,它只是一個(gè)字面量,并且是一個(gè)不可變的值即碗。 如果要在這個(gè)字面量上執(zhí)行一些操作灸芳,比如獲取長(zhǎng)度、訪(fǎng)問(wèn)其中某個(gè)字符等拜姿,那需要將其轉(zhuǎn)換為 String 對(duì)象烙样。
幸好,在必要時(shí)語(yǔ)言會(huì)自動(dòng)把字符串字面量轉(zhuǎn)換成一個(gè) String 對(duì)象蕊肥,也就是說(shuō)你并不需要顯式創(chuàng)建一個(gè)對(duì)象谒获。
例如:
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
使用以上兩種方法,我們都可以直接在字符串字面量上訪(fǎng)問(wèn)屬性或者方法壁却,之所以可以這 樣做批狱,是因?yàn)橐孀詣?dòng)把字面量轉(zhuǎn)換成 String 對(duì)象,所以可以訪(fǎng)問(wèn)屬性和方法展东。
- 因此赔硫,
1.對(duì)于字符串字面量(string)、數(shù)值字面量(number)盐肃、布爾字面量(boolean)
來(lái)說(shuō)爪膊,我們都可以直接在其上面訪(fǎng)問(wèn)屬性或者方法,之所以可以這樣做砸王,是因?yàn)橐孀詣?dòng)把字面量轉(zhuǎn)換成String粹舵、Number互婿、Boolean對(duì)象
,所以可以訪(fǎng)問(wèn)屬性和方法。
2.null赵刑、undefined
沒(méi)有對(duì)應(yīng)的構(gòu)造形式槽袄,它們只有文字形式芹关。相反皿桑,Date
只有構(gòu)造(new Date(..)),沒(méi)有文字形式嘹朗。
3.對(duì)于Object师妙、Array、Function骡显、RegExp
來(lái)說(shuō)疆栏,無(wú)論使用文字形式還是構(gòu)造形式曾掂,它們都是對(duì)象,不是字面量壁顶。
4.Error
對(duì)象很少在代碼中顯式創(chuàng)建珠洗,一般是在拋出異常時(shí)被自動(dòng)創(chuàng)建。也可以使用 new Error(..) 這種構(gòu)造形式來(lái)創(chuàng)建若专,不過(guò)一般來(lái)說(shuō)用不著许蓖。
3.3 內(nèi)容(對(duì)象的屬性)
- 對(duì)象的內(nèi)容是由一些存儲(chǔ)在特定命名位置的(任意類(lèi)型的)值組成的,我們稱(chēng)之為屬性调衰。
- 需要強(qiáng)調(diào)的一點(diǎn)是膊爪,當(dāng)我們說(shuō)“內(nèi)容”時(shí),似乎在暗示這些值實(shí)際上被存儲(chǔ)在對(duì)象內(nèi)部嚎莉, 但是這只是它的表現(xiàn)形式米酬。在引擎內(nèi)部,這些值的存儲(chǔ)方式是多種多樣的趋箩,一般并不會(huì)存在對(duì)象容器內(nèi)部赃额。存儲(chǔ)在對(duì)象容器內(nèi)部的是這些屬性的名稱(chēng),它們就像指針(從技術(shù)角度來(lái)說(shuō)就是引用)一樣叫确,指向這些值真正的存儲(chǔ)位置跳芳。
- 在對(duì)象中,屬性名永遠(yuǎn)都是字符串竹勉。如果你使用 string(字面量)以外的其他值作為屬性 名飞盆,那它首先會(huì)被轉(zhuǎn)換為一個(gè)字符串。即使是數(shù)字也不例外次乓,雖然在數(shù)組下標(biāo)中使用的的 確是數(shù)字吓歇,但是在對(duì)象屬性名中數(shù)字會(huì)被轉(zhuǎn)換成字符串,所以當(dāng)心不要搞混對(duì)象和數(shù)組中數(shù)字的用法:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
3.3.1 可計(jì)算屬性名
ES6 增加了可計(jì)算屬性名檬输,可以在文字形式中使用 [] 包裹一個(gè)表達(dá)式來(lái)當(dāng)作屬性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
可計(jì)算屬性名最常用的場(chǎng)景可能是 ES6 的符號(hào)(Symbol)照瘾。簡(jiǎn)單來(lái)說(shuō),Symbol是一種新的基礎(chǔ)數(shù)據(jù)類(lèi)型丧慈,包含一個(gè)不透明且無(wú)法預(yù)測(cè)的值(從技術(shù)角度來(lái)說(shuō)就是一個(gè)字符串)。一般來(lái)說(shuō)你不會(huì)用到符號(hào)的實(shí)際值(因?yàn)槔碚撋蟻?lái)說(shuō)在不 同的 JavaScript 引擎中值是不同的)主卫,所以通常你接觸到的是符號(hào)的名稱(chēng)逃默,比如 Symbol. Something(這個(gè)名字是我編的):
var myObject = {
[Symbol.Something]: "hello world"
}
3.3.4 復(fù)制對(duì)象
- Array:
slice
、concat
簇搅、Array.from()
- Object:
Object.assign()
完域、JSON.parse(JSON.stringify(obj))
不過(guò)使用JSON.parse(JSON.stringify(obj))
的話(huà),undefined
瘩将、任意的函數(shù)
吟税、symbol
在序列化過(guò)程中會(huì)被忽略(出現(xiàn)在非數(shù)組對(duì)象的屬性中時(shí))或者被轉(zhuǎn)換成null
(出現(xiàn)在數(shù)組中時(shí))
3.3.5 屬性描述符
writable(可寫(xiě))
凹耙、 enumerable(可枚舉)
、 configurable(可配置)
肠仪。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
在創(chuàng)建普通屬性時(shí)屬性描述符會(huì)使用默認(rèn)值肖抱,我們也可以使用 Object.defineProperty(..)
來(lái)添加一個(gè)新屬性或者修改一個(gè)已有屬性(如果它是 configurable)并對(duì)特性進(jìn)行設(shè)置。
舉例來(lái)說(shuō):
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
1. Writable
writable 決定是否可以修改屬性的值异旧。
思考下面的代碼:
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫(xiě)意述!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
如你所見(jiàn),我們對(duì)于屬性值的修改靜默失斔庇肌(silently failed)了荤崇。如果在嚴(yán)格模式下,這種方法會(huì)出錯(cuò):
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫(xiě)潮针!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
TypeError 錯(cuò)誤表示我們無(wú)法修改一個(gè)不可寫(xiě)的屬性术荤。
2. Configurable
只要屬性是可配置的,就可以使用 defineProperty(..)
方法來(lái)修改屬性描述符:
var myObject = {
a:2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置每篷!
enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty( myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
} ); // TypeError
最后一個(gè) defineProperty(..) 會(huì)產(chǎn)生一個(gè) TypeError 錯(cuò)誤喜每,不管是不是處于嚴(yán)格模式,嘗 試修改一個(gè)不可配置的屬性描述符都會(huì)出錯(cuò)雳攘。注意:如你所見(jiàn)带兜,把 configurable 修改成 false 是單向操作,無(wú)法撤銷(xiāo)吨灭!
要注意有一個(gè)小小的例外:即便屬性是 configurable:false刚照, 我們還是可以 把 writable 的狀態(tài)由 true 改為 false,但是無(wú)法由 false 改為 true喧兄。
除了無(wú)法修改无畔,configurable:false 還會(huì)禁止刪除這個(gè)屬性:
var myObject = {
a:2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
} );
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
如你所見(jiàn),最后一個(gè) delete 語(yǔ)句(靜默)失敗了吠冤,因?yàn)閷傩允遣豢膳渲玫摹?/p>
3. Enumerable
這個(gè)描述符控制的是屬性是否會(huì)出現(xiàn)在對(duì)象的屬性枚舉中浑彰,比如說(shuō) for..in
循環(huán)。如果把 enumerable 設(shè)置成 false拯辙,這個(gè)屬性就不會(huì)出現(xiàn)在枚舉中郭变,雖然仍然可以正常訪(fǎng)問(wèn)它。相對(duì)地涯保,設(shè)置成 true 就會(huì)讓它出現(xiàn)在枚舉中诉濒。
用戶(hù)定義的所有的普通屬性默認(rèn)都是 enumerable,這通常就是你想要的夕春。但是如果你不希 望某些特殊屬性出現(xiàn)在枚舉中未荒,那就把它設(shè)置成 enumerable:false。
3.3.6 不變性
1. 對(duì)象常量
結(jié)合 writable:false
和 configurable:false
就可以創(chuàng)建一個(gè)真正的常量屬性(不可修改及志、 重定義或者刪除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
2. 禁止擴(kuò)展
如果你想禁止一個(gè)對(duì)象添加新屬性并且保留已有屬性片排,可以使用 Object.prevent Extensions(..)
:
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
在非嚴(yán)格模式下寨腔,創(chuàng)建屬性 b 會(huì)靜默失敗。在嚴(yán)格模式下率寡,將會(huì)拋出 TypeError 錯(cuò)誤迫卢。
3. 密封
Object.seal(..)
會(huì)創(chuàng)建一個(gè)“密封”的對(duì)象,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.preventExtensions(..)
并把所有現(xiàn)有屬性標(biāo)記為 configurable:false
勇劣。
所以靖避,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現(xiàn)有屬性(雖然可以修改屬性的值)比默。
4. 凍結(jié)
Object.freeze(..)
會(huì)創(chuàng)建一個(gè)凍結(jié)對(duì)象幻捏,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.seal(..)
并把所有“數(shù)據(jù)訪(fǎng)問(wèn)”屬性標(biāo)記為 writable:false
,這樣就無(wú)法修改它們的值命咐。
這個(gè)方法是你可以應(yīng)用在對(duì)象上的級(jí)別最高的不可變性
篡九,它會(huì)禁止對(duì)于對(duì)象本身及其任意直接屬性的修改(不過(guò)就像我們之前說(shuō)過(guò)的,這個(gè)對(duì)象引用的其他對(duì)象是不受影響的)醋奠。
你可以“深度凍結(jié)”一個(gè)對(duì)象榛臼,具體方法為,首先在這個(gè)對(duì)象上調(diào)用 Object.freeze(..)
窜司, 然后遍歷它引用的所有對(duì)象并在這些對(duì)象上調(diào)用 Object.freeze(..)
沛善。但是一定要小心,因 為這樣做有可能會(huì)在無(wú)意中凍結(jié)其他(共享)對(duì)象塞祈。
3.3.7 [[Get]]
在語(yǔ)言規(guī)范中金刁,myObject.a
在 myObject
上實(shí)際上是實(shí)現(xiàn)了 [[Get]]
操作(有點(diǎn)像函數(shù)調(diào) 用:[[Get]]()
)。對(duì)象默認(rèn)的內(nèi)置 [[Get]]
操作首先在對(duì)象中查找是否有名稱(chēng)相同的屬性议薪, 如果找到就會(huì)返回這個(gè)屬性的值尤蛮。
然而,如果沒(méi)有找到名稱(chēng)相同的屬性斯议,按照 [[Get]]
算法的定義會(huì)執(zhí)行另外一種非常重要的行為遍歷可能存在的 [[Prototype]] 鏈产捞, 也就是原型鏈。
如果無(wú)論如何都沒(méi)有找到名稱(chēng)相同的屬性哼御,那 [[Get]]
操作會(huì)返回值 undefined
3.3.8 [[Put]]
[[Put]]
被觸發(fā)時(shí)坯临,實(shí)際的行為取決于許多因素,包括對(duì)象中是否已經(jīng)存在這個(gè)屬性(這是最重要的因素)艇搀。
如果已經(jīng)存在這個(gè)屬性尿扯,[[Put]]
算法大致會(huì)檢查下面這些內(nèi)容。
- 屬性是否是訪(fǎng)問(wèn)描述符(參見(jiàn)3.3.9節(jié))?如果是并且存在setter就調(diào)用setter焰雕。
- 屬性的數(shù)據(jù)描述符中writable是否是false?如果是,在非嚴(yán)格模式下靜默失敗芳杏,在嚴(yán)格模式下拋出 TypeError 異常矩屁。
- 如果都不是辟宗,將該值設(shè)置為屬性的值。
如果對(duì)象中不存在這個(gè)屬性吝秕,[[Put]] 操作會(huì)更加復(fù)雜泊脐。我們會(huì)在第 5 章討論 [[Prototype]] 時(shí)詳細(xì)進(jìn)行介紹。
3.3.9 Getter和Setter
對(duì)象默認(rèn)的 [[Put]] 和 [[Get]] 操作分別可以控制屬性值的設(shè)置和獲取烁峭。
在 ES5 中可以使用 getter 和 setter 部分改寫(xiě)默認(rèn)操作容客,但是只能應(yīng)用在單個(gè)屬性上,無(wú)法 應(yīng)用在整個(gè)對(duì)象上约郁。getter 是一個(gè)隱藏函數(shù)缩挑,會(huì)在獲取屬性值時(shí)調(diào)用。setter 也是一個(gè)隱藏 函數(shù)鬓梅,會(huì)在設(shè)置屬性值時(shí)調(diào)用供置。
當(dāng)你給一個(gè)屬性定義 getter、setter 或者兩者都有時(shí)绽快,這個(gè)屬性會(huì)被定義為“訪(fǎng)問(wèn)描述符”(和“數(shù)據(jù)描述符”相對(duì))芥丧。對(duì)于訪(fǎng)問(wèn)描述符來(lái)說(shuō),JavaScript 會(huì)忽略它們的 value 和 writable 特性坊罢,取而代之的是關(guān)心 set 和 get(還有 configurable 和 enumerable)特性续担。
var myObject = {
// 給 a 定義一個(gè) getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目標(biāo)對(duì)象
"b", // 屬性名
{ // 描述符
// 給 b 設(shè)置一個(gè) getter
get: function(){
return this.a * 2
},
// 確保 b 會(huì)出現(xiàn)在對(duì)象的屬性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
不管是對(duì)象文字語(yǔ)法中的get a(){..}
,還是 defineProperty(..)
中的顯式定義活孩,二者都會(huì)在對(duì)象中創(chuàng)建一個(gè)不包含值的屬性物遇,對(duì)于這個(gè)屬性的訪(fǎng)問(wèn)會(huì)自動(dòng)調(diào)用一個(gè)隱藏函數(shù),它的返回值會(huì)被當(dāng)作屬性訪(fǎng)問(wèn)的返回值诱鞠。
為了讓屬性更合理挎挖,還應(yīng)當(dāng)定義 setter,和你期望的一樣航夺,setter 會(huì)覆蓋單個(gè)屬性默認(rèn)的 [Put]操作蕉朵。通常來(lái)說(shuō) getter 和 setter 是成對(duì)出現(xiàn)的(只定義一個(gè)的話(huà) 通常會(huì)產(chǎn)生意料之外的行為):
var myObject = {
// 給 a 定義一個(gè) getter
get a() {
return this._a_;
},
// 給 a 定義一個(gè) setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
3.3.10 存在性
-
in 操作符
會(huì)檢查屬性是否在對(duì)象及其 [[Prototype]] 原型鏈中
-
hasOwnProperty(..)
只會(huì)檢查屬性是否在 myObject 對(duì)象中
,不會(huì)檢查 [[Prototype]] 鏈
1. 枚舉
-
for..in
循環(huán)可以用來(lái)遍歷對(duì)象的可枚舉屬性列表(包括 [[Prototype]] 鏈)阳掐。 -
propertyIsEnumerable(..)
會(huì)檢查給定的屬性名是否直接存在于對(duì)象中(而不是在原型鏈 上)并且滿(mǎn)足 enumerable:true始衅。 -
Object.keys(..)
會(huì)返回一個(gè)數(shù)組,包含所有可枚舉屬性缭保,只會(huì)查找對(duì)象直接包含的屬性汛闸。 -
Object.getOwnPropertyNames(..)
會(huì)返回一個(gè)數(shù)組,包含所有屬性艺骂,無(wú)論它們是否可枚舉诸老,只會(huì)查找對(duì)象直接包含的屬性。
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 讓 a 像普通屬性一樣可以枚舉
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 讓 b 不可枚舉
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
3.4 遍歷
-
for..in
循環(huán)可以用來(lái)遍歷對(duì)象的可枚舉屬性列表(包括 [[Prototype]] 鏈)钳恕。 -
forEach(..)
别伏、every(..)
蹄衷、some(..)
-
for..of
,循環(huán)每次調(diào)用 myObject 迭代器對(duì)象的 next() 方法時(shí)厘肮,內(nèi)部的指針都會(huì)向前移動(dòng)并 返回對(duì)象屬性列表的下一個(gè)值愧口。
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator](); // 使用 ES6 中的符號(hào) Symbol.iterator 來(lái)獲取對(duì)象的 @@iterator 內(nèi)部屬 性。
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
注:和數(shù)組不同类茂,普通的對(duì)象沒(méi)有內(nèi)置的 @@iterator耍属,所以無(wú)法自動(dòng)完成 for..of 遍歷。
第4章 混合對(duì)象“類(lèi)”
類(lèi)巩检、繼承厚骗、實(shí)例化、多態(tài)
4.1.1 “類(lèi)”設(shè)計(jì)模式
- 面向?qū)ο笤O(shè)計(jì)模式碴巾,比如迭代器模式溯捆、觀察者模式、工廠(chǎng)模式厦瓢、單例模式提揍,等等。
- 最好使用類(lèi)把過(guò)程化風(fēng)格的“意大利面代碼”轉(zhuǎn)換成結(jié)構(gòu)清晰煮仇、組織良好的代碼劳跃。
4.2 類(lèi)的機(jī)制
- 一個(gè)類(lèi)就是一張藍(lán)圖。為了獲得真正可以交互的對(duì)象浙垫,我們必須按照類(lèi)來(lái)建造(也可以說(shuō)實(shí)例化)一個(gè)東西刨仑,這個(gè)東西通常被稱(chēng)為實(shí)例。這個(gè)對(duì)象就是類(lèi)中描述的所有特性的一份副本夹姥。
4.3.1 多態(tài)
多態(tài)是說(shuō)父類(lèi)的通用行為可以被子類(lèi)用更特殊的行為重寫(xiě)杉武。
多態(tài)并不表示子類(lèi)和父類(lèi)有關(guān)聯(lián),子類(lèi)得到的只是父類(lèi)的一份副本辙售。類(lèi)的繼承其實(shí)就是復(fù)制轻抱。
第5章 原型
5.1 [[Prototype]]
- 幾乎所有的對(duì)象在創(chuàng)建時(shí)
[[Prototype]]
屬性都會(huì)被賦予一個(gè)非空的值。 - 對(duì)于默認(rèn)的
[[Get]]
操作來(lái)說(shuō)旦部,如果無(wú)法在對(duì)象本身找到需要的屬性祈搜,就會(huì)繼續(xù)訪(fǎng)問(wèn)對(duì)象的[[Prototype]]
鏈。這個(gè)過(guò)程會(huì)持續(xù)到找到匹配的屬性名或者查找完整條[[Prototype]]
鏈士八。如果是后者的話(huà)容燕,[[Get]]
操作的返回值是undefined
。 - 使用
for..in
遍歷對(duì)象時(shí)婚度,使用in
操作符來(lái)檢查屬性在對(duì)象中是否存在時(shí)蘸秘,同樣會(huì)查找對(duì)象的整條原型鏈
5.1.1 [[Prototype]] 的“盡頭”
所有普通的 [[Prototype]] 鏈最終都會(huì)指向內(nèi)置的 Object.prototype,所以它包含 JavaScript 中許多通用的功能。
5.1.2 屬性設(shè)置和屏蔽
如果 foo 不直接存在于 myObject 中而是存在于原型鏈上層時(shí) myObject.foo = "bar" 會(huì)出現(xiàn)的三種情況秘血。
- 如果在[[Prototype]]鏈上層存在名為foo的普通數(shù)據(jù)訪(fǎng)問(wèn)屬性(參見(jiàn)第3章)并且沒(méi) 有被標(biāo)記為只讀(writable:false)味抖,那就會(huì)直接在 myObject 中添加一個(gè)名為 foo 的新 屬性评甜,它是屏蔽屬性灰粮。
- 如果在[[Prototype]]鏈上層存在foo,但是它被標(biāo)記為只讀(writable:false)忍坷,那么 無(wú)法修改已有屬性或者在 myObject 上創(chuàng)建屏蔽屬性粘舟。如果運(yùn)行在嚴(yán)格模式下,代碼會(huì) 拋出一個(gè)錯(cuò)誤佩研。否則柑肴,這條賦值語(yǔ)句會(huì)被忽略⊙恚總之晰骑,不會(huì)發(fā)生屏蔽。
- 如果在[[Prototype]]鏈上層存在foo并且它是一個(gè)setter(參見(jiàn)第3章)绊序,那就一定會(huì) 調(diào)用這個(gè) setter硕舆。foo 不會(huì)被添加到(或者說(shuō)屏蔽于)myObject,也不會(huì)重新定義 foo 這 個(gè) setter骤公。
有些情況下會(huì)隱式產(chǎn)生屏蔽抚官,一定要當(dāng)心。思考下面的代碼:
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隱式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
盡管 myObject.a++ 看起來(lái)應(yīng)該(通過(guò)委托)查找并增加 anotherObject.a 屬性阶捆,但是別忘了 ++ 操作相當(dāng)于 myObject.a = myObject.a + 1凌节。因此 ++ 操作首先會(huì)通過(guò) [[Prototype]] 查找屬性 a 并從 anotherObject.a 獲取當(dāng)前屬性值 2,然后給這個(gè)值加 1洒试,接著用 [[Put]] 將值 3 賦給 myObject 中新建的屏蔽屬性 a倍奢,天吶!
修改委托屬性時(shí)一定要小心。如果想讓 anotherObject.a 的值增加垒棋,唯一的辦法是 anotherObject.a++卒煞。
5.2 “類(lèi)”
5.2.1 “類(lèi)”函數(shù)
所有的函數(shù)默認(rèn)都會(huì)擁有一個(gè)名為 prototype
的公有并且不可枚舉的屬性,它會(huì)指向另一個(gè)對(duì)象:
function Foo() {
// ...
}
Foo.prototype; // { }
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
new Foo() 只是間接完成我們的目標(biāo):一個(gè)關(guān)聯(lián)到其他對(duì)象的新對(duì)象捕犬。
5.2.2 “構(gòu)造函數(shù)”
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
// a.constructor 只是通過(guò)默認(rèn)的 [[Prototype]] 委托指向 Foo
Foo.prototype 默認(rèn)有一個(gè)公有并且不可枚舉的屬性 .constructor跷坝,這個(gè)屬性引用的是對(duì)象關(guān)聯(lián)的函數(shù)(本例中是 Foo)。此外碉碉,我們可以看到通過(guò)“構(gòu)造函數(shù)”調(diào)用 new Foo() 創(chuàng)建的對(duì)象也有一個(gè) .constructor 屬性柴钻,指向 “創(chuàng)建這個(gè)對(duì)象的函數(shù)”。
實(shí)際上 a 本身并沒(méi)有 .constructor 屬性垢粮。而且贴届,雖然 a.constructor 確實(shí)指向 Foo 函數(shù),但是這個(gè)屬性并不是表示 a 由 Foo“構(gòu)造”。實(shí)際上毫蚓,.constructor 引用同樣被委托給了 Foo.prototype占键,而 Foo.prototype.constructor 默認(rèn)指向 Foo。a.constructor 只是通過(guò)默認(rèn)的 [[Prototype]] 委托指向 Foo元潘,這和“構(gòu)造”毫無(wú)關(guān)系畔乙。
5.3 (原型)繼承
- 下面這段代碼使用的就是典型的“原型風(fēng)格”:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 我們創(chuàng)建了一個(gè)新的 Bar.prototype 對(duì)象并關(guān)聯(lián)到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );
// 注意!現(xiàn)在沒(méi)有 Bar.prototype.constructor 了
// 如果你需要這個(gè)屬性的話(huà)可能需要手動(dòng)修復(fù)一下它
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
這段代碼的核心部分就是語(yǔ)句 Bar.prototype = Object.create( Foo.prototype )
。調(diào)用 Object.create(..)
會(huì)憑空創(chuàng)建一個(gè)“新”對(duì)象并把新對(duì)象內(nèi)部的 [[Prototype]] 關(guān)聯(lián)到你指定的對(duì)象(本例中是 Foo.prototype)翩概。
- ES6 添加了輔助函數(shù)
Object.setPrototypeOf(..)
牲距,可以用標(biāo)準(zhǔn)并且可靠的方法來(lái)修改對(duì)象的 [[Prototype]] 關(guān)聯(lián)。
我們來(lái)對(duì)比一下兩種把 Bar.prototype 關(guān)聯(lián)到 Foo.prototype 的方法:
// ES6 之前需要拋棄默認(rèn)的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開(kāi)始可以直接修改現(xiàn)有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
如果忽略掉 Object.create(..) 方法帶來(lái)的輕微性能損失(拋棄的對(duì)象需要進(jìn)行垃圾回 收)钥庇,它實(shí)際上比 ES6 及其之后的方法更短而且可讀性更高牍鞠。不過(guò)無(wú)論如何,這是兩種完全不同的語(yǔ)法评姨。
檢查“類(lèi)”關(guān)系
思考下面的代碼:
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
- 第一種方法是站在“類(lèi)”的角度來(lái)判斷:
a instanceof Foo; // true
instanceof
回答的問(wèn)題是:在 a 的整條 [[Prototype]] 鏈中是否有指向 Foo.prototype 的對(duì)象?
instanceof 操作符的左操作數(shù)是一個(gè)普通的對(duì)象难述,右操作數(shù)是一個(gè)函數(shù)。
可惜吐句,這個(gè)方法只能處理對(duì)象(a)和函數(shù)(帶 .prototype 引用的 Foo)之間的關(guān)系胁后。如 果你想判斷兩個(gè)對(duì)象(比如 a 和 b)之間是否通過(guò) [[Prototype]] 鏈關(guān)聯(lián),只用 instanceof 無(wú)法實(shí)現(xiàn)蕴侧。
- 第二種判斷 [[Prototype]] 反射的方法:
Foo.prototype.isPrototypeOf( a ); // true
isPrototypeOf(..)
回答的問(wèn)題是:在 a 的整條 [[Prototype]] 鏈中是否出現(xiàn)過(guò)Foo.prototype ?
- 我們也可以直接獲取一個(gè)對(duì)象的 [[Prototype]] 鏈择同。在 ES5 中,標(biāo)準(zhǔn)的方法是:
Object.getPrototypeOf( a )
可以驗(yàn)證一下净宵,這個(gè)對(duì)象引用是否和我們想的一樣:
Object.getPrototypeOf( a ) === Foo.prototype; // true
- 絕大多數(shù)(不是所有!)瀏覽器也支持一種非標(biāo)準(zhǔn)的方法來(lái)訪(fǎng)問(wèn)內(nèi)部 [[Prototype]] 屬性:
__proto__
a.__proto__ === Foo.prototype; // true
.proto 看起來(lái)很像一個(gè)屬性敲才,但是實(shí)際上它更像一個(gè) getter/setter:
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this ); },
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );
5.4 對(duì)象關(guān)聯(lián)
[[Prototype]] 機(jī)制就是存在于對(duì)象中的一個(gè)內(nèi)部鏈接,它會(huì)引用其他
對(duì)象择葡。
通常來(lái)說(shuō)紧武,這個(gè)鏈接的作用是:如果在對(duì)象上沒(méi)有找到需要的屬性或者方法引用,引擎就 會(huì)繼續(xù)在 [[Prototype]] 關(guān)聯(lián)的對(duì)象上進(jìn)行查找敏储。同理阻星,如果在后者中也沒(méi)有找到需要的 引用就會(huì)繼續(xù)查找它的 [[Prototype]],以此類(lèi)推已添。這一系列對(duì)象的鏈接被稱(chēng)為“原型鏈”妥箕。
5.4.1 創(chuàng)建關(guān)聯(lián)
Object.create(..)
- Object.create(..) 會(huì)創(chuàng)建一個(gè)新對(duì)象(bar)并把它關(guān)聯(lián)到我們指定的對(duì)象(foo),這樣 我們就可以充分發(fā)揮 [[Prototype]] 機(jī)制的威力(委托)并且避免不必要的麻煩(比如使用 new 的構(gòu)造函數(shù)調(diào)用會(huì)生成 .prototype 和 .constructor 引用)更舞。
- Object.create(null) 會(huì) 創(chuàng) 建 一 個(gè) 擁 有 空( 或 者 說(shuō) null)[[Prototype]] 鏈接的對(duì)象畦幢,這個(gè)對(duì)象無(wú)法進(jìn)行委托。由于這個(gè)對(duì)象沒(méi)有原型鏈缆蝉,所以 instanceof 操作符(之前解釋過(guò))無(wú)法進(jìn)行判斷宇葱,因此總是會(huì)返回 false瘦真。
- Object.create()的polyfill代碼(兼容舊IE)
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
}; }
- Object.create()的擴(kuò)展
Object.create(..) 的第二個(gè)參數(shù)指定了需要添加到新對(duì)象中的屬性名以及這些屬性的屬性描述符:
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject, {
b: {
enumerable: false,
writable: true,
configurable: false,
value: 3
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 4
}
});
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
myObject.hasOwnProperty( "c" ); // true
myObject.a; // 2
myObject.b; // 3
myObject.c; // 4
5.5 小結(jié)
如果要訪(fǎng)問(wèn)對(duì)象中并不存在的一個(gè)屬性,[[Get]] 操作就會(huì)查找對(duì)象內(nèi)部[[Prototype]] 關(guān)聯(lián)的對(duì)象黍瞧。這個(gè)關(guān)聯(lián)關(guān)系實(shí)際上定義了一條“原型鏈”(有點(diǎn)像嵌套的作用域鏈)诸尽,在查找屬性時(shí)會(huì)對(duì)它進(jìn)行遍歷。
所有普通對(duì)象都有內(nèi)置的 Object.prototype印颤,指向原型鏈的頂端(比如說(shuō)全局作用域)您机,如 果在原型鏈中找不到指定的屬性就會(huì)停止。toString()膀哲、valueOf() 和其他一些通用的功能 都存在于 Object.prototype 對(duì)象上往产,因此語(yǔ)言中所有的對(duì)象都可以使用它們。
關(guān)聯(lián)兩個(gè)對(duì)象最常用的方法是使用 new 關(guān)鍵詞進(jìn)行函數(shù)調(diào)用某宪,在調(diào)用的 4 個(gè)步驟(第 2 章)中會(huì)創(chuàng)建一個(gè)關(guān)聯(lián)其他對(duì)象的新對(duì)象。
使用 new 調(diào)用函數(shù)時(shí)會(huì)把新對(duì)象的 .prototype 屬性關(guān)聯(lián)到“其他對(duì)象”锐朴。帶 new 的函數(shù)調(diào)用 通常被稱(chēng)為“構(gòu)造函數(shù)調(diào)用”兴喂,盡管它們實(shí)際上和傳統(tǒng)面向類(lèi)語(yǔ)言中的類(lèi)構(gòu)造函數(shù)不一樣。
雖然這些 JavaScript 機(jī)制和傳統(tǒng)面向類(lèi)語(yǔ)言中的“類(lèi)初始化”和“類(lèi)繼承”很相似焚志,但 是 JavaScript 中的機(jī)制有一個(gè)核心區(qū)別衣迷,那就是不會(huì)進(jìn)行復(fù)制,對(duì)象之間是通過(guò)內(nèi)部的 [[Prototype]] 鏈關(guān)聯(lián)的酱酬。
出于各種原因壶谒,以“繼承”結(jié)尾的術(shù)語(yǔ)(包括“原型繼承”)和其他面向?qū)ο蟮男g(shù)語(yǔ)都無(wú) 法幫助你理解 JavaScript 的真實(shí)機(jī)制(不僅僅是限制我們的思維模式)。
相比之下膳沽,“委托”是一個(gè)更合適的術(shù)語(yǔ)汗菜,因?yàn)閷?duì)象之間的關(guān)系不是復(fù)制而是委托。
第6章 行為委托
類(lèi)模型(面向?qū)ο箫L(fēng)格)
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
委托模型(對(duì)象關(guān)聯(lián)風(fēng)格)
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
6.6 小結(jié)
在軟件架構(gòu)中你可以選擇是否使用類(lèi)和繼承設(shè)計(jì)模式挑社。大多數(shù)開(kāi)發(fā)者理所當(dāng)然地認(rèn)為類(lèi)是 唯一(合適)的代碼組織方式陨界,但是本章中我們看到了另一種更少見(jiàn)但是更強(qiáng)大的設(shè)計(jì)模式:行為委托。
行為委托認(rèn)為對(duì)象之間是兄弟關(guān)系痛阻,互相委托菌瘪,而不是父類(lèi)和子類(lèi)的關(guān)系。JavaScript 的 [[Prototype]] 機(jī)制本質(zhì)上就是行為委托機(jī)制阱当。也就是說(shuō)俏扩,我們可以選擇在 JavaScript 中努 力實(shí)現(xiàn)類(lèi)機(jī)制(參見(jiàn)第 4 和第 5 章),也可以擁抱更自然的 [[Prototype]] 委托機(jī)制弊添。
當(dāng)你只用對(duì)象來(lái)設(shè)計(jì)代碼時(shí)录淡,不僅可以讓語(yǔ)法更加簡(jiǎn)潔,而且可以讓代碼結(jié)構(gòu)更加清晰表箭。 對(duì)象關(guān)聯(lián)(對(duì)象之前互相關(guān)聯(lián))是一種編碼風(fēng)格赁咙,它倡導(dǎo)的是直接創(chuàng)建和關(guān)聯(lián)對(duì)象钮莲,不把它們抽象成類(lèi)。對(duì)象關(guān)聯(lián)可以用基于 [[Prototype]] 的行為委托非常自然地實(shí)現(xiàn)彼水。