特別說(shuō)明本砰,為便于查閱矛物,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
在第一和第二章中霹琼,我們講解了 this
綁定如何根據(jù)函數(shù)調(diào)用的調(diào)用點(diǎn)指向不同的對(duì)象蓬抄。但究竟什么是對(duì)象丰嘉,為什么我們需要指向它們?這一章我們就來(lái)詳細(xì)探索一下對(duì)象嚷缭。
語(yǔ)法
對(duì)象來(lái)自于兩種形式:聲明(字面)形式饮亏,和構(gòu)造形式耍贾。
一個(gè)對(duì)象的字面語(yǔ)法看起來(lái)像這樣:
var myObj = {
key: value
// ...
};
構(gòu)造形式看起來(lái)像這樣:
var myObj = new Object();
myObj.key = value;
構(gòu)造形式和字面形式的結(jié)果是完全同種類的對(duì)象。唯一真正的區(qū)別在于你可以向字面聲明一次性添加一個(gè)或多個(gè)鍵/值對(duì)路幸,而對(duì)于構(gòu)造形式荐开,你必須一個(gè)一個(gè)地添加屬性。
注意: 像剛才展示的那樣使用“構(gòu)造形式”來(lái)創(chuàng)建對(duì)象是極其少見(jiàn)的简肴。你很有可能總是想使用字面語(yǔ)法形式晃听。這對(duì)大多數(shù)內(nèi)建的對(duì)象也一樣(后述)。
類型
對(duì)象是大多數(shù) JS 程序依賴的基本構(gòu)建塊兒砰识。它們是 JS 的六種主要類型(在語(yǔ)言規(guī)范中稱為“語(yǔ)言類型”)中的一種:
string
number
boolean
null
undefined
object
注意 簡(jiǎn)單基本類型 (string
能扒、number
、boolean
辫狼、null
初斑、和 undefined
)自身 不是 object
。null
有時(shí)會(huì)被當(dāng)成一個(gè)對(duì)象類型膨处,但是這種誤解源自于一個(gè)語(yǔ)言中的 Bug见秤,它使得 typeof null
錯(cuò)誤地(而且令人困惑地)返回字符串 "object"
。實(shí)際上灵迫,null
是它自己的基本類型秦叛。
一個(gè)常見(jiàn)的錯(cuò)誤論斷是“JavaScript中的一切都是對(duì)象”。這明顯是不對(duì)的瀑粥。
對(duì)比來(lái)看挣跋,存在幾種特殊的對(duì)象子類型,我們可以稱之為 復(fù)雜基本類型狞换。
function
是對(duì)象的一種子類型(技術(shù)上講避咆,叫做“可調(diào)用對(duì)象”)。函數(shù)在 JS 中被稱為“頭等(first class)”類型修噪,是因?yàn)樗鼈兓旧暇褪瞧胀ǖ膶?duì)象(附帶有可調(diào)用的行為語(yǔ)義)查库,而且它們可以像其他普通的對(duì)象那樣被處理。
數(shù)組也是一種形式的對(duì)象黄琼,帶有特別的行為樊销。數(shù)組在內(nèi)容的組織上要稍稍比一般的對(duì)象更加結(jié)構(gòu)化。
內(nèi)建對(duì)象
有幾種其他的對(duì)象子類型脏款,通常稱為內(nèi)建對(duì)象围苫。對(duì)于其中的一些來(lái)說(shuō),它們的名稱看起來(lái)暗示著它們和它們對(duì)應(yīng)的基本類型有著直接的聯(lián)系撤师,但事實(shí)上剂府,它們的關(guān)系更復(fù)雜,我們一會(huì)兒就開(kāi)始探索剃盾。
String
Number
Boolean
Object
Function
Array
Date
RegExp
Error
如果你依照和其他語(yǔ)言的相似性來(lái)看的話腺占,比如 Java 語(yǔ)言的 String
類淤袜,這些內(nèi)建類型有著實(shí)際類型的外觀,甚至是類(class)的外觀衰伯,
但是在 JS 中铡羡,它們實(shí)際上僅僅是內(nèi)建的函數(shù)。這些內(nèi)建函數(shù)的每一個(gè)都可以被用作構(gòu)造器(也就是一個(gè)可以通過(guò) new
操作符調(diào)用的函數(shù) —— 參照第二章)嚎研,其結(jié)果是一個(gè)新 構(gòu)建 的相應(yīng)子類型的對(duì)象蓖墅。例如:
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
// 考察 object 子類型
Object.prototype.toString.call( strObject ); // [object String]
我們會(huì)在本章稍后詳細(xì)地看到 Object.prototype.toString...
到底是如何工作的,但簡(jiǎn)單地說(shuō)临扮,我們可以通過(guò)借用基本的默認(rèn) toString()
方法來(lái)考察內(nèi)部子類型论矾,而且你可以看到它揭示了 strObject
實(shí)際上是一個(gè)由 String
構(gòu)造器創(chuàng)建的對(duì)象。
基本類型值 "I am a string"
不是一個(gè)對(duì)象杆勇,它是一個(gè)不可變的基本字面值贪壳。為了對(duì)它進(jìn)行操作,比如檢查它的長(zhǎng)度蚜退,訪問(wèn)它的各個(gè)獨(dú)立字符內(nèi)容等等闰靴,都需要一個(gè) String
對(duì)象。
幸運(yùn)的是钻注,在必要的時(shí)候語(yǔ)言會(huì)自動(dòng)地將 "string"
基本類型強(qiáng)制轉(zhuǎn)換為 String
對(duì)象類型蚂且,這意味著你幾乎從不需要明確地創(chuàng)建對(duì)象。JS 社區(qū)的絕大部分人都 強(qiáng)烈推薦 盡可能地使用字面形式的值幅恋,而非使用構(gòu)造的對(duì)象形式杏死。
考慮下面的代碼:
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
在這兩個(gè)例子中,我們?cè)谧址幕绢愋蜕险{(diào)用屬性和方法捆交,引擎會(huì)自動(dòng)地將它強(qiáng)制轉(zhuǎn)換為 String
對(duì)象淑翼,所以這些屬性/方法的訪問(wèn)可以工作。
當(dāng)使用如 42.359.toFixed(2)
這樣的方法時(shí)品追,同樣的強(qiáng)制轉(zhuǎn)換也發(fā)生在數(shù)字基本字面量 42
和包裝對(duì)象 new Nubmer(42)
之間玄括。同樣的還有 Boolean
對(duì)象和 "boolean"
基本類型。
null
和 undefined
沒(méi)有對(duì)象包裝的形式肉瓦,僅有它們的基本類型值遭京。相比之下,Date
的值 僅可以 由它們的構(gòu)造對(duì)象形式創(chuàng)建泞莉,因?yàn)樗鼈儧](méi)有對(duì)應(yīng)的字面形式洁墙。
無(wú)論使用字面還是構(gòu)造形式,Object
戒财、Array
、Function
捺弦、和 RegExp
(正則表達(dá)式)都是對(duì)象饮寞。在某些情況下孝扛,構(gòu)造形式確實(shí)會(huì)比對(duì)應(yīng)的字面形式提供更多的創(chuàng)建選項(xiàng)。因?yàn)閷?duì)象可以被任意一種方式創(chuàng)建幽崩,更簡(jiǎn)單的字面形式幾乎是所有人的首選苦始。僅僅在你需要使用額外的選項(xiàng)時(shí)使用構(gòu)建形式。
Error
對(duì)象很少在代碼中明示地被創(chuàng)建慌申,它們通常在拋出異常時(shí)自動(dòng)地被創(chuàng)建陌选。它們可以由 new Error(..)
構(gòu)造形式創(chuàng)建,但通常是不必要的蹄溉。
內(nèi)容
正如剛才提到的咨油,對(duì)象的內(nèi)容由存儲(chǔ)在特定命名的 位置 上的(任意類型的)值組成,我們稱這些值為屬性柒爵。
有一個(gè)重要的事情需要注意:當(dāng)我們說(shuō)“內(nèi)容”時(shí)役电,似乎暗示著這些值 實(shí)際上 存儲(chǔ)在對(duì)象內(nèi)部,但那只不過(guò)是表面現(xiàn)象棉胀。引擎會(huì)根據(jù)自己的實(shí)現(xiàn)來(lái)存儲(chǔ)這些值法瑟,而且通常都不是把它們存儲(chǔ)在容器對(duì)象 內(nèi)部。在容器內(nèi)存儲(chǔ)的是這些屬性的名稱唁奢,它們像指針(技術(shù)上講霎挟,叫 引用(reference))一樣指向值存儲(chǔ)的地方。
考慮下面的代碼:
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
為了訪問(wèn) myObject
在 位置 a
的值麻掸,我們需要使用 .
或 [ ]
操作符酥夭。.a
語(yǔ)法通常稱為“屬性(property)”訪問(wèn),而 ["a"]
語(yǔ)法通常稱為“鍵(key)”訪問(wèn)论笔。在現(xiàn)實(shí)中采郎,它們倆都訪問(wèn)相同的 位置,而且會(huì)拿出相同的值狂魔,2
蒜埋,所以這些術(shù)語(yǔ)可以互換使用。從現(xiàn)在起最楷,我們將使用最常見(jiàn)的術(shù)語(yǔ) —— “屬性訪問(wèn)”整份。
兩種語(yǔ)法的主要區(qū)別在于,.
操作符后面需要一個(gè) 標(biāo)識(shí)符(Identifier)
兼容的屬性名籽孙,而 [".."]
語(yǔ)法基本可以接收任何兼容 UTF-8/unicode 的字符串作為屬性名烈评。舉個(gè)例子,為了引用一個(gè)名為“Super-Fun!”的屬性犯建,你不得不使用 ["Super-Fun!"]
語(yǔ)法訪問(wèn)讲冠,因?yàn)?Super-Fun!
不是一個(gè)合法的 Identifier
屬性名。
而且适瓦,由于 [".."]
語(yǔ)法使用字符串的 值 來(lái)指定位置竿开,這意味著程序可以動(dòng)態(tài)地組建字符串的值谱仪。比如:
var wantA = true;
var myObject = {
a: 2
};
var idx;
if (wantA) {
idx = "a";
}
// 稍后
console.log( myObject[idx] ); // 2
在對(duì)象中,屬性名 總是 字符串否彩。如果你使用 string
以外的(基本)類型值疯攒,它會(huì)首先被轉(zhuǎn)換為字符串。這甚至包括在數(shù)組中常用于索引的數(shù)字列荔,所以要小心不要將對(duì)象和數(shù)組使用的數(shù)字搞混了敬尺。
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
計(jì)算型屬性名
如果你需要將一個(gè)計(jì)算表達(dá)式 作為 一個(gè)鍵名稱,那么我們剛剛描述的 myObject[..]
屬性訪問(wèn)語(yǔ)法是十分有用的贴浙,比如 myObject[prefix + name]
砂吞。但是當(dāng)使用字面對(duì)象語(yǔ)法聲明對(duì)象時(shí)則沒(méi)有什么幫助。
ES6 加入了 計(jì)算型屬性名悬而,在一個(gè)字面對(duì)象聲明的鍵名稱位置呜舒,你可以指定一個(gè)表達(dá)式,用 [ ]
括起來(lái):
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
計(jì)算型屬性名 的最常見(jiàn)用法笨奠,可能是用于 ES6 的 Symbol
袭蝗,我們將不會(huì)在本書(shū)中涵蓋關(guān)于它的細(xì)節(jié)。簡(jiǎn)單地說(shuō)般婆,它們是新的基本數(shù)據(jù)類型晤硕,擁有一個(gè)不透明不可知的值(技術(shù)上講是一個(gè) string
值)哪轿。你將會(huì)被強(qiáng)烈地不鼓勵(lì)使用一個(gè) Symbol
的 實(shí)際值 (這個(gè)值理論上會(huì)因 JS 引擎的不同而不同)稚虎,所以 Symbol
的名稱奸柬,比如 Symbol.Something
(這是個(gè)瞎編的名稱!)啤咽,才是你會(huì)使用的:
var myObject = {
[Symbol.Something]: "hello world"
};
屬性(Property) vs. 方法(Method)
有些開(kāi)發(fā)者喜歡在討論對(duì)一個(gè)對(duì)象的屬性訪問(wèn)時(shí)做一個(gè)區(qū)別晋辆,如果這個(gè)被訪問(wèn)的值恰好是一個(gè)函數(shù)的話。因?yàn)檫@誘使人們認(rèn)為函數(shù) 屬于 這個(gè)對(duì)象宇整,而且在其他語(yǔ)言中瓶佳,屬于對(duì)象(也就是“類”)的函數(shù)被稱作“方法”,所以相對(duì)于“屬性訪問(wèn)”鳞青,我們常能聽(tīng)到“方法訪問(wèn)”霸饲。
有趣的是,語(yǔ)言規(guī)范也做出了同樣的區(qū)別臂拓。
從技術(shù)上講厚脉,函數(shù)絕不會(huì)“屬于”對(duì)象,所以胶惰,說(shuō)一個(gè)偶然在對(duì)象的引用上被訪問(wèn)的函數(shù)就自動(dòng)地成為了一個(gè)“方法”傻工,看起來(lái)有些像是牽強(qiáng)附會(huì)。
有些函數(shù)內(nèi)部確實(shí)擁有 this
引用,而且 有時(shí) 這些 this
引用指向調(diào)用點(diǎn)的對(duì)象引用中捆。但這個(gè)用法確實(shí)沒(méi)有使這個(gè)函數(shù)比其他函數(shù)更像“方法”威鹿,因?yàn)?this
是在運(yùn)行時(shí)在調(diào)用點(diǎn)動(dòng)態(tài)綁定的,這使得它與這個(gè)對(duì)象的關(guān)系至多是間接的轨香。
每次你訪問(wèn)一個(gè)對(duì)象的屬性都是一個(gè) 屬性訪問(wèn),無(wú)論你得到什么類型的值幼东。如果你 恰好 從屬性訪問(wèn)中得到一個(gè)函數(shù)臂容,它也沒(méi)有魔法般地在那時(shí)成為一個(gè)“方法”。一個(gè)從屬性訪問(wèn)得來(lái)的函數(shù)沒(méi)有任何特殊性(隱含的 this
綁定的情況在剛才已經(jīng)解釋過(guò)了)根蟹。
舉個(gè)例子:
function foo() {
console.log( "foo" );
}
var someFoo = foo; // 對(duì) `foo` 的變量引用
var myObject = {
someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}
someFoo
和 myObject.someFoo
只不過(guò)是同一個(gè)函數(shù)的兩個(gè)分離的引用脓杉,它們中的任何一個(gè)都不意味著這個(gè)函數(shù)很特別或被其他對(duì)象所“擁有”。如果上面的 foo()
定義里面擁有一個(gè) this
引用简逮,那么 myObject.someFoo
的 隱含綁定 將會(huì)是這個(gè)兩個(gè)引用間 唯一 可以觀察到的不同球散。它們中的任何一個(gè)都沒(méi)有稱為“方法”的道理。
也許有人會(huì)爭(zhēng)辯散庶,函數(shù) 變成了方法蕉堰,不是在定義期間,而是在調(diào)用的執(zhí)行期間悲龟,根據(jù)它是如何在調(diào)用點(diǎn)被調(diào)用的(是否帶有一個(gè)環(huán)境對(duì)象引用 —— 細(xì)節(jié)見(jiàn)第二章)屋讶。即便是這種解讀也有些牽強(qiáng)。
可能最安全的結(jié)論是须教,在 JavaScript 中皿渗,“函數(shù)”和“方法”是可以互換使用的。
注意: ES6 加入了 super
引用轻腺,它通常是和 class
(見(jiàn)附錄A)一起使用的乐疆。super
的行為方式(靜態(tài)綁定,而非像 this
一樣延遲綁定)贬养,給了這種說(shuō)法更多的權(quán)重:一個(gè)被 super
綁定到某處的函數(shù)比起“函數(shù)”更像一個(gè)“方法”挤土。但是同樣地,這僅僅是微妙的語(yǔ)義上的(和機(jī)制上的)細(xì)微區(qū)別煤蚌。
就算你聲明一個(gè)函數(shù)表達(dá)式作為字面對(duì)象的一部分耕挨,那個(gè)函數(shù)都不會(huì)魔法般地 屬于 這個(gè)對(duì)象 —— 仍然僅僅是同一個(gè)函數(shù)對(duì)象的多個(gè)引用罷了。
var myObject = {
foo: function foo() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
注意: 在第六章中尉桩,我們會(huì)為字面對(duì)象的 foo: function foo(){ .. }
聲明語(yǔ)法介紹一種ES6的簡(jiǎn)化語(yǔ)法筒占。
數(shù)組
數(shù)組也使用 [ ]
訪問(wèn)形式,但正如上面提到的蜘犁,在存儲(chǔ)值的方式和位置上它們的組織更加結(jié)構(gòu)化(雖然仍然在存儲(chǔ)值的 類型 上沒(méi)有限制)翰苫。數(shù)組采用 數(shù)字索引,這意味著值被存儲(chǔ)的位置,通常稱為 下標(biāo)奏窑,是一個(gè)非負(fù)整數(shù)导披,比如 0
和 42
。
var myArray = [ "foo", 42, "bar" ];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
數(shù)組也是對(duì)象埃唯,所以雖然每個(gè)索引都是正整數(shù)撩匕,你還可以在數(shù)組上添加屬性:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
注意,添加命名屬性(不論是使用 .
還是 [ ]
操作符語(yǔ)法)不會(huì)改變數(shù)組的 length
所報(bào)告的值墨叛。
你 可以 把一個(gè)數(shù)組當(dāng)做普通的鍵/值對(duì)象使用止毕,并且從不添加任何數(shù)字下標(biāo),但這不是一個(gè)好主意漠趁,因?yàn)閿?shù)組對(duì)它本來(lái)的用途有著特定的行為和優(yōu)化方式扁凛,普通對(duì)象也一樣。使用對(duì)象來(lái)存儲(chǔ)鍵/值對(duì)闯传,而用數(shù)組在數(shù)字下標(biāo)上存儲(chǔ)值谨朝。
小心: 如果你試圖在一個(gè)數(shù)組上添加屬性,但是屬性名 看起來(lái) 像一個(gè)數(shù)字甥绿,那么最終它會(huì)成為一個(gè)數(shù)字索引(也就是改變了數(shù)組的內(nèi)容):
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
復(fù)制對(duì)象
當(dāng)開(kāi)發(fā)者們初次拿起 Javascript 語(yǔ)言時(shí)字币,最常需要的特性就是如何復(fù)制一個(gè)對(duì)象∶媒眩看起來(lái)應(yīng)該有一個(gè)內(nèi)建的 copy()
方法纬朝,對(duì)吧?但是事情實(shí)際上比這復(fù)雜一些骄呼,因?yàn)樵谀J(rèn)情況下共苛,復(fù)制的算法應(yīng)當(dāng)是什么,并不十分明確蜓萄。
例如隅茎,考慮這個(gè)對(duì)象:
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是拷貝!
c: anotherArray, // 又一個(gè)引用!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );
一個(gè)myObject
的 拷貝 究竟應(yīng)該怎么表現(xiàn)嫉沽?
首先辟犀,我們應(yīng)該回答它是一個(gè) 淺(shallow) 還是一個(gè) 深(deep) 拷貝?一個(gè) 淺拷貝(shallow copy) 會(huì)得到一個(gè)新對(duì)象绸硕,它的 a
是值 2
的拷貝堂竟,但 b
、c
和 d
屬性僅僅是引用玻佩,它們指向被拷貝對(duì)象中引用的相同位置出嘹。一個(gè) 深拷貝(deep copy) 將不僅復(fù)制 myObject
,還會(huì)復(fù)制 anotherObject
和 anotherArray
咬崔。但之后我們讓 anotherArray
擁有 anotherObject
和 myObject
的引用税稼,所以 那些 也應(yīng)當(dāng)被復(fù)制而不是僅保留引用》持龋現(xiàn)在由于循環(huán)引用,我們得到了一個(gè)無(wú)限循環(huán)復(fù)制的問(wèn)題郎仆。
我們應(yīng)當(dāng)檢測(cè)循環(huán)引用并打破循環(huán)遍歷嗎(不管位于深處的只祠,沒(méi)有完全復(fù)制的元素)?我們應(yīng)當(dāng)報(bào)錯(cuò)退出嗎扰肌?或者介于兩者之間抛寝?
另外,“復(fù)制”一個(gè)函數(shù)意味著什么曙旭,也不是很清楚墩剖。有一些技巧,比如提取一個(gè)函數(shù)源代碼的 toString()
序列化表達(dá)(這個(gè)源代碼會(huì)因?qū)崿F(xiàn)不同而不同夷狰,而且根據(jù)被考察的函數(shù)的類型,其結(jié)果甚至在所有引擎上都不可靠)郊霎。
那么我們?nèi)绾谓鉀Q所有這些刁鉆的問(wèn)題沼头?不同的 JS 框架都各自挑選自己的解釋并且做出自己的選擇。但是哪一種(如果有的話)才是 JS 應(yīng)當(dāng)作為標(biāo)準(zhǔn)采用的呢书劝?長(zhǎng)久以來(lái)进倍,沒(méi)有明確答案。
一個(gè)解決方案是购对,JSON 安全的對(duì)象(也就是猾昆,可以被序列化為一個(gè) JSON 字符串,之后還可以被重新解析為擁有相同的結(jié)構(gòu)和值的對(duì)象)可以簡(jiǎn)單地這樣 復(fù)制:
var newObj = JSON.parse( JSON.stringify( someObj ) );
當(dāng)然骡苞,這要求你保證你的對(duì)象是 JSON 安全的垂蜗。對(duì)于某些情況,這沒(méi)什么大不了的解幽。而對(duì)另一些情況贴见,這還不夠。
同時(shí)躲株,淺拷貝相當(dāng)易懂片部,而且沒(méi)有那么多問(wèn)題,所以 ES6 為此任務(wù)已經(jīng)定義了 Object.assign(..)
霜定。Object.assign(..)
接收 目標(biāo) 對(duì)象作為第一個(gè)參數(shù)档悠,然后是一個(gè)或多個(gè) 源 對(duì)象作為后續(xù)參數(shù)。它會(huì)在 源 對(duì)象上迭代所有的 可枚舉(enumerable)望浩,owned keys(直接擁有的鍵)辖所,并把它們拷貝到 目標(biāo) 對(duì)象上(僅通過(guò) =
賦值)。它還會(huì)很方便地返回 目標(biāo) 對(duì)象曾雕,正如下面你可以看到的:
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
注意: 在下一部分中奴烙,我們將討論“屬性描述符(property descriptors —— 屬性的性質(zhì))”并展示 Object.defineProperty(..)
的使用。然而在 Object.assign(..)
中發(fā)生的復(fù)制是單純的 =
式賦值,所以任何在源對(duì)象屬性的特殊性質(zhì)(比如 writable
)在目標(biāo)對(duì)象上 都不會(huì)保留 切诀。
屬性描述符(Property Descriptors)
在 ES5 之前揩环,JavaScript 語(yǔ)言沒(méi)有給出直接的方法,讓你的代碼可以考察或描述屬性性質(zhì)間的區(qū)別幅虑,比如屬性是否為只讀丰滑。
在 ES5 中,所有的屬性都用 屬性描述符(Property Descriptors) 來(lái)描述倒庵。
考慮這段代碼:
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
正如你所見(jiàn)褒墨,我們普通的對(duì)象屬性 a
的屬性描述符(稱為“數(shù)據(jù)描述符”,因?yàn)樗鼉H持有一個(gè)數(shù)據(jù)值)的內(nèi)容要比 value
為 2
多得多擎宝。它還包含另外三個(gè)性質(zhì):writable
郁妈、enumerable
、和 configurable
绍申。
當(dāng)我們創(chuàng)建一個(gè)普通屬性時(shí)噩咪,可以看到屬性描述符的各種性質(zhì)的默認(rèn)值,同時(shí)我們可以用 Object.defineProperty(..)
來(lái)添加新屬性极阅,或使用期望的性質(zhì)來(lái)修改既存的屬性(如果它是 configurable
的N改搿)。
舉例來(lái)說(shuō):
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
使用 defineProperty(..)
筋搏,我們手動(dòng)仆百、明確地在 myObject
上添加了一個(gè)直白的,普通的 a
屬性奔脐。然而俄周,你通常不會(huì)使用這種手動(dòng)方法,除非你想要把描述符的某個(gè)性質(zhì)修改為不同的值髓迎。
可寫性(Writable)
writable
控制著你改變屬性值的能力栈源。
考慮這段代碼:
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
如你所見(jiàn)竖般,我們對(duì) value
的修改悄無(wú)聲息地失敗了甚垦。如果我們?cè)?strict mode
下進(jìn)行嘗試,會(huì)得到一個(gè)錯(cuò)誤:
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫涣雕!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
這個(gè) TypeError
告訴我們艰亮,我們不能改變一個(gè)不可寫屬性。
注意: 我們一會(huì)兒就會(huì)討論 getters/setters挣郭,但是簡(jiǎn)單地說(shuō)迄埃,你可以觀察到 writable:false
意味著值不可改變,和你定義一個(gè)空的 setter 是有些等價(jià)的兑障。實(shí)際上侄非,你的空 setter 在被調(diào)用時(shí)需要扔出一個(gè) TypeError
蕉汪,來(lái)和 writable:false
保持一致。
可配置性(Configurable)
只要屬性當(dāng)前是可配置的逞怨,我們就可以使用相同的 defineProperty(..)
工具者疤,修改它的描述符定義。
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
最后的 defineProperty(..)
調(diào)用導(dǎo)致了一個(gè) TypeError驹马,這與 strict mode
無(wú)關(guān),如果你試圖改變一個(gè)不可配置屬性的描述符定義除秀,就會(huì)發(fā)生 TypeError糯累。要小心:如你所看到的,將 configurable
設(shè)置為 false
是 一個(gè)單向操作册踩,不可撤銷泳姐!
注意: 這里有一個(gè)需要注意的微小例外:即便屬性已經(jīng)是 configurable:false
,writable
總是可以沒(méi)有錯(cuò)誤地從 true
改變?yōu)?false
暂吉,但如果已經(jīng)是 false
的話不能變回 true
仗岸。
configurable:false
阻止的另外一個(gè)事情是使用 delete
操作符移除既存屬性的能力。
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)借笙,最后的 delete
調(diào)用(無(wú)聲地)失敗了,因?yàn)槲覀儗?a
屬性設(shè)置成了不可配置较锡。
delete
僅用于直接從目標(biāo)對(duì)象移除該對(duì)象的(可以被移除的)屬性业稼。如果一個(gè)對(duì)象的屬性是某個(gè)其他對(duì)象/函數(shù)的最后一個(gè)現(xiàn)存的引用,而你 delete
了它蚂蕴,那么這就移除了這個(gè)引用低散,于是現(xiàn)在那個(gè)沒(méi)有被任何地方所引用的對(duì)象/函數(shù)就可以被作為垃圾回收。但是骡楼,將 delete
當(dāng)做一個(gè)像其他語(yǔ)言(如 C/C++)中那樣的釋放內(nèi)存工具是 不 恰當(dāng)?shù)摹?code>delete 僅僅是一個(gè)對(duì)象屬性移除操作 —— 沒(méi)有更多別的含義熔号。
可枚舉性(Enumerable)
我們將要在這里提到的最后一個(gè)描述符性質(zhì)是 enumerable
(還有另外兩個(gè),我們將在一會(huì)兒討論 getter/setters 時(shí)談到)鸟整。
它的名稱可能已經(jīng)使它的功能很明顯了引镊,這個(gè)性質(zhì)控制著一個(gè)屬性是否能在特定的對(duì)象-屬性枚舉操作中出現(xiàn),比如 for..in
循環(huán)篮条。設(shè)置為 false
將會(huì)阻止它出現(xiàn)在這樣的枚舉中弟头,即使它依然完全是可以訪問(wèn)的。設(shè)置為 true
會(huì)使它出現(xiàn)涉茧。
所有普通的用戶定義屬性都默認(rèn)是可 enumerable
的赴恨,正如你通常希望的那樣。但如果你有一個(gè)特殊的屬性伴栓,你想讓它對(duì)枚舉隱藏伦连,就將它設(shè)置為 enumerable:false
雨饺。
我們一會(huì)兒就更加詳細(xì)地演示可枚舉性,所以在大腦中給這個(gè)話題上打一個(gè)書(shū)簽惑淳。
不可變性(Immutability)
有時(shí)我們希望將屬性或?qū)ο螅ㄓ幸饣驘o(wú)意地)設(shè)置為不可改變的额港。ES5 用幾種不同的微妙方式,加入了對(duì)此功能的支持汛聚。
一個(gè)重要的注意點(diǎn)是:所有 這些方法創(chuàng)建的都是淺不可變性锹安。也就是,它們僅影響對(duì)象和它的直屬屬性的性質(zhì)倚舀。如果對(duì)象擁有對(duì)其他對(duì)象(數(shù)組叹哭、對(duì)象、函數(shù)等)的引用痕貌,那個(gè)對(duì)象的 內(nèi)容 不會(huì)受影響风罩,任然保持可變。
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
在這段代碼中舵稠,我們假設(shè) myImmutableObject
已經(jīng)被創(chuàng)建超升,而且被保護(hù)為不可變。但是哺徊,為了保護(hù) myImmutableObject.foo
的內(nèi)容(也是一個(gè)對(duì)象 —— 數(shù)組)室琢,你將需要使用下面的一個(gè)或多個(gè)方法將 foo
設(shè)置為不可變。
注意: 在 JS 程序中創(chuàng)建完全不可動(dòng)搖的對(duì)象是不那么常見(jiàn)的落追。有些特殊情況當(dāng)然需要盈滴,但作為一個(gè)普通的設(shè)計(jì)模式,如果你發(fā)現(xiàn)自己想要 封咏文啤(seal) 或 凍結(jié)(freeze) 你所有的對(duì)象巢钓,那么你可能想要退一步來(lái)重新考慮你的程序設(shè)計(jì),讓它對(duì)對(duì)象值的潛在變化更加健壯疗垛。
對(duì)象常量(Object Constant)
通過(guò)將 writable:false
與 configurable:false
組合症汹,你可以實(shí)質(zhì)上創(chuàng)建了一個(gè)作為對(duì)象屬性的 常量(不能被改變,重定義或刪除)贷腕,比如:
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
防止擴(kuò)展(Prevent Extensions)
如果你想防止一個(gè)對(duì)象被添加新的屬性背镇,但另一方面保留其他既存的對(duì)象屬性,可以調(diào)用 Object.preventExtensions(..)
:
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
在非 strict mode
模式下泽裳,b
的創(chuàng)建會(huì)無(wú)聲地失敗芽世。在 strict mode
下,它會(huì)拋出 TypeError
诡壁。
封蛹闷啊(Seal)
Object.seal(..)
創(chuàng)建一個(gè)“封印”的對(duì)象,這意味著它實(shí)質(zhì)上在當(dāng)前的對(duì)象上調(diào)用 Object.preventExtensions(..)
妹卿,同時(shí)也將它所有的既存屬性標(biāo)記為 configurable:false
旺矾。
所以蔑鹦,你既不能添加更多的屬性,也不能重新配置或刪除既存屬性(雖然你依然 可以 修改它們的值)箕宙。
凍結(jié)(Freeze)
Object.freeze(..)
創(chuàng)建一個(gè)凍結(jié)的對(duì)象嚎朽,這意味著它實(shí)質(zhì)上在當(dāng)前的對(duì)象上調(diào)用 Object.seal(..)
,同時(shí)也將它所有的“數(shù)據(jù)訪問(wèn)”屬性設(shè)置為 writable:false
柬帕,所以它們的值不可改變哟忍。
這種方法是你可以從對(duì)象自身獲得的最高級(jí)別的不可變性,因?yàn)樗柚谷魏螌?duì)對(duì)象或?qū)ο笾睂賹傩缘母淖儯m然陷寝,就像上面提到的锅很,任何被引用的對(duì)象的內(nèi)容不受影響)。
你可以“深度凍結(jié)”一個(gè)對(duì)象:在這個(gè)對(duì)象上調(diào)用 Object.freeze(..)
凤跑,然后遞歸地迭代所有它引用的(目前還沒(méi)有受過(guò)影響的)對(duì)象爆安,然后也在它們上面調(diào)用 Object.freeze(..)
。但是要小心仔引,這可能會(huì)影響其他你并不打算影響的(共享的)對(duì)象扔仓。
[[Get]]
關(guān)于屬性訪問(wèn)如何工作有一個(gè)重要的細(xì)節(jié)。
考慮下面的代碼:
var myObject = {
a: 2
};
myObject.a; // 2
myObject.a
是一個(gè)屬性訪問(wèn)咖耘,但是它并不是看起來(lái)那樣翘簇,僅僅在 myObject
中尋找一個(gè)名為 a
的屬性。
根據(jù)語(yǔ)言規(guī)范儿倒,上面的代碼實(shí)際上在 myObject
上執(zhí)行了一個(gè) [[Get]]
操作(有些像 [[Get]]()
函數(shù)調(diào)用)版保。對(duì)一個(gè)對(duì)象進(jìn)行默認(rèn)的內(nèi)建 [[Get]]
操作,會(huì) 首先 檢查對(duì)象义桂,尋找一個(gè)擁有被請(qǐng)求的名稱的屬性,如果找到蹈垢,就返回相應(yīng)的值慷吊。
然而,如果按照被請(qǐng)求的名稱 沒(méi)能 找到屬性曹抬,[[Get]]
的算法定義了另一個(gè)重要的行為溉瓶。我們會(huì)在第五章來(lái)解釋 接下來(lái) 會(huì)發(fā)生什么(遍歷 [[Prototype]]
鏈,如果有的話)谤民。
但 [[Get]]
操作的一個(gè)重要結(jié)果是堰酿,如果它通過(guò)任何方法都不能找到被請(qǐng)求的屬性的值,那么它會(huì)返回 undefined
张足。
var myObject = {
a: 2
};
myObject.b; // undefined
這個(gè)行為和你通過(guò)標(biāo)識(shí)符名稱來(lái)引用 變量 不同触创。如果你引用了一個(gè)在可用的詞法作用域內(nèi)無(wú)法解析的變量,其結(jié)果不是像對(duì)象屬性那樣返回 undefined
为牍,而是拋出一個(gè) ReferenceError
哼绑。
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
從 值 的角度來(lái)說(shuō)岩馍,這兩個(gè)引用沒(méi)有區(qū)別 —— 它們的結(jié)果都是 undefined
。然而抖韩,在 [[Get]]
操作的底層蛀恩,雖然不明顯,但是比起處理引用 myObject.a
茂浮,處理 myObject.b
的操作要多做一些潛在的“工作”双谆。
如果僅僅考察結(jié)果的值,你無(wú)法分辨一個(gè)屬性是存在并持有一個(gè) undefined
值席揽,還是因?yàn)閷傩愿?不 存在所以 [[Get]]
無(wú)法返回某個(gè)具體值而返回默認(rèn)的 undefined
顽馋。但是,你很快就能看到你其實(shí) 可以 分辨這兩種場(chǎng)景驹尼。
[[Put]]
既然為了從一個(gè)屬性中取得值而存在一個(gè)內(nèi)部定義的 [[Get]]
操作趣避,那么很明顯應(yīng)該也存在一個(gè)默認(rèn)的 [[Put]]
操作。
這很容易讓人認(rèn)為新翎,給一個(gè)對(duì)象的屬性賦值程帕,將會(huì)在這個(gè)對(duì)象上調(diào)用 [[Put]]
來(lái)設(shè)置或創(chuàng)建這個(gè)屬性。但是實(shí)際情況卻有一些微妙的不同地啰。
調(diào)用 [[Put]]
時(shí)愁拭,它根據(jù)幾個(gè)因素表現(xiàn)不同的行為,包括(影響最大的)屬性是否已經(jīng)在對(duì)象中存在了亏吝。
如果屬性存在岭埠,[[Put]]
算法將會(huì)大致檢查:
- 這個(gè)屬性是訪問(wèn)器描述符嗎(見(jiàn)下一節(jié)"Getters 與 Setters")?如果是蔚鸥,而且是 setter惜论,就調(diào)用 setter。
- 這個(gè)屬性是
writable
為false
數(shù)據(jù)描述符嗎止喷?如果是馆类,在非strict mode
下無(wú)聲地失敗,或者在strict mode
下拋出TypeError
弹谁。 - 否則乾巧,像平常一樣設(shè)置既存屬性的值。
如果屬性在當(dāng)前的對(duì)象中還不存在预愤,[[Put]]
操作會(huì)變得更微妙和復(fù)雜沟于。我們將在第五章討論 [[Prototype]]
時(shí)再次回到這個(gè)場(chǎng)景,更清楚地解釋它植康。
Getters 與 Setters
對(duì)象默認(rèn)的 [[Put]]
和 [[Get]]
操作分別完全控制著如何設(shè)置既存或新屬性的值旷太,和如何取得既存屬性。
注意: 使用較先進(jìn)的語(yǔ)言特性销睁,覆蓋整個(gè)對(duì)象(不僅是每個(gè)屬性)的默認(rèn) [[Put]]
和 [[Get]]
操作是可能的。這超出了我們要在這本書(shū)中討論的范圍,但我們會(huì)在后面的“你不懂 JS”系列中涵蓋此內(nèi)容案训。
ES5 引入了一個(gè)方法來(lái)覆蓋這些默認(rèn)操作的一部分腾它,但不是在對(duì)象級(jí)別而是針對(duì)每個(gè)屬性,就是通過(guò) getters 和 setters。Getter 是實(shí)際上調(diào)用一個(gè)隱藏函數(shù)來(lái)取得值的屬性。Setter 是實(shí)際上調(diào)用一個(gè)隱藏函數(shù)來(lái)設(shè)置值的屬性。
當(dāng)你將一個(gè)屬性定義為擁有 getter 或 setter 或兩者兼?zhèn)湮ツ敲此亩x就成為了“訪問(wèn)器描述符”(與“數(shù)據(jù)描述符”相對(duì))。對(duì)于訪問(wèn)器描述符偶房,它的 value
和 writable
性質(zhì)因沒(méi)有意義而被忽略趁曼,取而代之的是 JS 將會(huì)考慮屬性的 set
和 get
性質(zhì)(還有 configurable
和 enumerable
)。
考慮下面的代碼:
var myObject = {
// 為 `a` 定義一個(gè) getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目標(biāo)對(duì)象
"b", // 屬性名
{ // 描述符
// 為 `b` 定義 getter
get: function(){ return this.a * 2 },
// 確保 `b` 作為對(duì)象屬性出現(xiàn)
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
不管是通過(guò)在字面對(duì)象語(yǔ)法中使用 get a() { .. }
棕洋,還是通過(guò)使用 defineProperty(..)
明確定義挡闰,我們都在對(duì)象上創(chuàng)建了一個(gè)沒(méi)有實(shí)際持有值的屬性,訪問(wèn)它們將會(huì)自動(dòng)地對(duì) getter 函數(shù)進(jìn)行隱藏的函數(shù)調(diào)用掰盘,其返回的任何值就是屬性訪問(wèn)的結(jié)果摄悯。
var myObject = {
// 為 `a` 定義 getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
因?yàn)槲覀儍H為 a
定義了一個(gè) getter,如果之后我們?cè)囍O(shè)置 a
的值愧捕,賦值操作并不會(huì)拋出錯(cuò)誤而是無(wú)聲地將賦值廢棄奢驯。就算這里有一個(gè)合法的 setter,我們的自定義 getter 將返回值硬編碼為僅返回 2
次绘,所以賦值操作是沒(méi)有意義的瘪阁。
為了使這個(gè)場(chǎng)景更合理,正如你可能期望的那樣邮偎,每個(gè)屬性還應(yīng)當(dāng)被定義一個(gè)覆蓋默認(rèn) [[Put]]
操作(也就是賦值)的 setter管跺。幾乎可確定,你將總是想要同時(shí)聲明 getter 和 setter(僅有它們中的一個(gè)經(jīng)常會(huì)導(dǎo)致意外的行為):
var myObject = {
// 為 `a` 定義 getter
get a() {
return this._a_;
},
// 為 `a` 定義 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
注意: 在這個(gè)例子中禾进,我們實(shí)際上將賦值操作([[Put]]
操作)指定的值 2
存儲(chǔ)到了另一個(gè)變量 _a_
中豁跑。_a_
這個(gè)名稱只是用在這個(gè)例子中的單純慣例,并不意味著它的行為有什么特別之處 —— 它和其他普通屬性沒(méi)有區(qū)別命迈。
存在性(Existence)
我們?cè)缦瓤吹椒啡疲?myObject.a
這樣的屬性訪問(wèn)可能會(huì)得到一個(gè) undefined
值火的,無(wú)論是它明確存儲(chǔ)著 undefined
還是屬性 a
根本就不存在壶愤。那么,如果這兩種情況的值相同馏鹤,我們還怎么區(qū)別它們呢征椒?
我們可以查詢一個(gè)對(duì)象是否擁有特定的屬性,而 不必 取得那個(gè)屬性的值:
var myObject = {
a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in
操作符會(huì)檢查屬性是否存在于對(duì)象 中湃累,或者是否存在于 [[Prototype]]
鏈對(duì)象遍歷的更高層中(詳見(jiàn)第五章)勃救。相比之下碍讨,hasOwnProperty(..)
僅僅 檢查 myObject
是否擁有屬性,但 不會(huì) 查詢 [[Prototype]]
鏈蒙秒。我們會(huì)在第五章詳細(xì)講解 [[Prototype]]
時(shí)勃黍,回來(lái)討論這個(gè)兩個(gè)操作重要的不同。
通過(guò)委托到 Object.prototype
晕讲,所有的普通對(duì)象都可以訪問(wèn) hasOwnProperty(..)
(詳見(jiàn)第五章)覆获。但是創(chuàng)建一個(gè)不鏈接到 Object.prototype
的對(duì)象也是可能的(通過(guò) Object.create(null)
—— 詳見(jiàn)第五章)。這種情況下瓢省,像 myObject.hasOwnProperty(..)
這樣的方法調(diào)用將會(huì)失敗弄息。
在這種場(chǎng)景下,一個(gè)進(jìn)行這種檢查的更健壯的方式是 Object.prototype.hasOwnProperty.call(myObject,"a")
勤婚,它借用基本的 hasOwnProperty(..)
方法而且使用 明確的 this
綁定(詳見(jiàn)第二章)來(lái)對(duì)我們的 myObject
實(shí)施這個(gè)方法摹量。
注意: in
操作符看起來(lái)像是要檢查一個(gè)值在容器中的存在性,但是它實(shí)際上檢查的是屬性名的存在性馒胆。在使用數(shù)組時(shí)注意這個(gè)區(qū)別十分重要缨称,因?yàn)槲覀儠?huì)有很強(qiáng)的沖動(dòng)來(lái)進(jìn)行 4 in [2, 4, 6]
這樣的檢查,但是這總是不像我們想象的那樣工作国章。
枚舉(Enumeration)
先前具钥,在學(xué)習(xí) enumerable
屬性描述符性質(zhì)時(shí),我們簡(jiǎn)單地解釋了"可枚舉性(enumerability)"的含義∫菏蓿現(xiàn)在骂删,讓我們來(lái)更加詳細(xì)地重新講解它。
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚舉四啰,如一般情況
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `b` 不可枚舉
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2
你會(huì)注意到宁玫,myObject.b
實(shí)際上 存在,而且擁有可以訪問(wèn)的值柑晒,但是它不出現(xiàn)在 for..in
循環(huán)中(然而令人詫異的是欧瘪,它的 in
操作符的存在性檢查通過(guò)了)。這是因?yàn)?“enumerable” 基本上意味著“如果對(duì)象的屬性被迭代時(shí)會(huì)被包含在內(nèi)”匙赞。
注意: 將 for..in
循環(huán)實(shí)施在數(shù)組上可能會(huì)給出意外的結(jié)果佛掖,因?yàn)槊杜e一個(gè)數(shù)組將不僅包含所有的數(shù)字下標(biāo),還包含所有的可枚舉屬性涌庭。所以一個(gè)好主意是:將 for..in
循環(huán) 僅 用于對(duì)象芥被,而為存儲(chǔ)在數(shù)組中的值使用傳統(tǒng)的 for
循環(huán)并用數(shù)字索引迭代。
另一個(gè)可以區(qū)分可枚舉和不可枚舉屬性的方法是:
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"]
propertyIsEnumerable(..)
測(cè)試一個(gè)給定的屬性名是否直 接存 在于對(duì)象上拴魄,并且是 enumerable:true
。
Object.keys(..)
返回一個(gè)所有可枚舉屬性的數(shù)組,而 Object.getOwnPropertyNames(..)
返回一個(gè) 所有 屬性的數(shù)組匹中,不論能不能枚舉夏漱。
in
和 hasOwnProperty(..)
區(qū)別于它們是否查詢 [[Prototype]]
鏈,而 Object.keys(..)
和 Object.getOwnPropertyNames(..)
都 只 考察直接給定的對(duì)象顶捷。
(當(dāng)下)沒(méi)有與 in
操作符的查詢方式(在整個(gè) [[Prototype]]
鏈上遍歷所有的屬性挂绰,如我們?cè)诘谖逭陆忉尩模┑葍r(jià)的、內(nèi)建的方法可以得到一個(gè) 所有屬性 的列表服赎。你可以近似地模擬一個(gè)這樣的工具:遞歸地遍歷一個(gè)對(duì)象的 [[Prototype]]
鏈扮授,在每一層都從 Object.keys(..)
中取得一個(gè)列表——僅包含可枚舉屬性。
迭代(Iteration)
for..in
循環(huán)迭代一個(gè)對(duì)象上(包括它的 [[Prototype]]
鏈)所有的可迭代屬性专肪。但如果你想要迭代值呢刹勃?
在數(shù)字索引的數(shù)組中,典型的迭代所有的值的辦法是使用標(biāo)準(zhǔn)的 for
循環(huán)嚎尤,比如:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 1 2 3
但是這并沒(méi)有迭代所有的值荔仁,而是迭代了所有的下標(biāo),然后由你使用索引來(lái)引用值芽死,比如 myArray[i]
乏梁。
ES5 還為數(shù)組加入了幾個(gè)迭代幫助方法,包括 forEach(..)
关贵、every(..)
遇骑、和 some(..)
。這些幫助方法的每一個(gè)都接收一個(gè)回調(diào)函數(shù)揖曾,這個(gè)函數(shù)將施用于數(shù)組中的每一個(gè)元素落萎,僅在如何響應(yīng)回調(diào)的返回值上有所不同。
forEach(..)
將會(huì)迭代數(shù)組中所有的值炭剪,并且忽略回調(diào)的返回值练链。every(..)
會(huì)一直迭代到最后,或者 當(dāng)回調(diào)返回一個(gè) false
(或“falsy”)值奴拦,而 some(..)
會(huì)一直迭代到最后媒鼓,或者 當(dāng)回調(diào)返回一個(gè) true
(或“truthy”)值。
這些在 every(..)
和 some(..)
內(nèi)部的特殊返回值有些像普通 for
循環(huán)中的 break
語(yǔ)句错妖,它們可以在迭代執(zhí)行到末尾之前將它結(jié)束掉绿鸣。
如果你使用 for..in
循環(huán)在一個(gè)對(duì)象上進(jìn)行迭代,你也只能間接地得到值暂氯,因?yàn)樗鼘?shí)際上僅僅迭代對(duì)象的所有可枚舉屬性潮模,讓你自己手動(dòng)地去訪問(wèn)屬性來(lái)得到值。
注意: 與以有序數(shù)字的方式(for
循環(huán)或其他迭代器)迭代數(shù)組的下標(biāo)比較起來(lái)株旷,迭代對(duì)象屬性的順序是 不確定 的再登,而且可能會(huì)因 JS 引擎的不同而不同。對(duì)于需要跨平臺(tái)環(huán)境保持一致的問(wèn)題晾剖,不要依賴 觀察到的順序锉矢,因?yàn)檫@個(gè)順序是不可靠的。
但是如果你想直接迭代值齿尽,而不是數(shù)組下標(biāo)(或?qū)ο髮傩裕┠毓了穑縀S6 加入了一個(gè)有用的 for..of
循環(huán)語(yǔ)法,用來(lái)迭代數(shù)組(和對(duì)象循头,如果這個(gè)對(duì)象有定義的迭代器):
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
for..of
循環(huán)要求被迭代的 東西 提供一個(gè)迭代器對(duì)象(從一個(gè)在語(yǔ)言規(guī)范中叫做 @@iterator
的默認(rèn)內(nèi)部函數(shù)那里得到)绵估,每次循環(huán)都調(diào)用一次這個(gè)迭代器對(duì)象的 next()
方法,循環(huán)迭代的內(nèi)容就是這些連續(xù)的返回值卡骂。
數(shù)組擁有內(nèi)建的 @@iterator
国裳,所以正如展示的那樣,for..of
對(duì)于它們很容易使用全跨。但是讓我們使用內(nèi)建的 @@iterator
來(lái)手動(dòng)迭代一個(gè)數(shù)組缝左,來(lái)看看它是怎么工作的:
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
注意: 我們使用一個(gè) ES6 的 Symbol
:Symbol.iterator
來(lái)取得一個(gè)對(duì)象的 @@iterator
內(nèi)部屬性。我們?cè)诒菊轮泻?jiǎn)單地提到過(guò) Symbol
的語(yǔ)義(見(jiàn)“計(jì)算型屬性名”)浓若,同樣的原理也適用于這里渺杉。你總是希望通過(guò) Symbol
名稱,而不是它可能持有的特殊的值挪钓,來(lái)引用這樣特殊的屬性是越。另外,盡管這個(gè)名稱有這樣的暗示碌上,但 @@iterator
本身 不是迭代器對(duì)象倚评, 而是一個(gè)返回迭代器對(duì)象的 方法 —— 一個(gè)重要的細(xì)節(jié)!
正如上面的代碼段揭示的馏予,迭代器的 next()
調(diào)用的返回值是一個(gè) { value: .. , done: .. }
形式的對(duì)象蔓纠,其中 value
是當(dāng)前迭代的值,而 done
是一個(gè) boolean
吗蚌,表示是否還有更多內(nèi)容可以迭代腿倚。
注意值 3
和 done:false
一起返回,猛地一看會(huì)有些奇怪蚯妇。你不得不第四次調(diào)用 next()
(在前一個(gè)代碼段的 for..of
循環(huán)會(huì)自動(dòng)這樣做)來(lái)得到 done:true
敷燎,以使自己知道迭代已經(jīng)完成。這個(gè)怪異之處的原因超出了我們要在這里討論的范圍箩言,但是它源自于 ES6 生成器(generator)函數(shù)的語(yǔ)義硬贯。
雖然數(shù)組可以在 for..of
循環(huán)中自動(dòng)迭代,但普通的對(duì)象 沒(méi)有內(nèi)建的 @@iterator
陨收。這種故意省略的原因要比我們將在這里解釋的更復(fù)雜饭豹,但一般來(lái)說(shuō)鸵赖,為了未來(lái)的對(duì)象類型,最好不要加入那些可能最終被證明是麻煩的實(shí)現(xiàn)拄衰。
但是 可以 為你想要迭代的對(duì)象定義你自己的默認(rèn) @@iterator
它褪。比如:
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
} );
// 手動(dòng)迭代 `myObject`
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用 `for..of` 迭代 `myObject`
for (var v of myObject) {
console.log( v );
}
// 2
// 3
注意: 我們使用了 Object.defineProperty(..)
來(lái)自定義我們的 @@iterator
(很大程度上是因?yàn)槲覀兛梢詫⑺付椴豢擅杜e的),但是通過(guò)將 Symbol
作為一個(gè) 計(jì)算型屬性名(在本章前面的部分討論過(guò))翘悉,我們也可以直接聲明它茫打,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }
。
每次 for..of
循環(huán)在 myObject
的迭代器對(duì)象上調(diào)用 next()
時(shí)妖混,迭代器內(nèi)部的指針將會(huì)向前移動(dòng)并返回對(duì)象屬性列表的下一個(gè)值(關(guān)于對(duì)象屬性/值迭代順序老赤,參照前面的注意事項(xiàng))。
我們剛剛演示的迭代制市,是一個(gè)簡(jiǎn)單的一個(gè)值一個(gè)值的迭代抬旺,當(dāng)然你可以為你的自定義數(shù)據(jù)結(jié)構(gòu)定義任意復(fù)雜的迭代方法,只要你覺(jué)得合適祥楣。對(duì)于操作用戶自定義對(duì)象來(lái)說(shuō)嚷狞,自定義迭代器與 ES6 的 for..of
循環(huán)相組合,是一個(gè)新的強(qiáng)大的語(yǔ)法工具荣堰。
舉個(gè)例子床未,一個(gè) Pixel(像素)
對(duì)象列表(擁有 x
和 y
的坐標(biāo)值)可以根據(jù)距離原點(diǎn) (0,0)
的直線距離決定它的迭代順序,或者過(guò)濾掉那些“太遠(yuǎn)”的點(diǎn)振坚,等等薇搁。只要你的迭代器從 next()
調(diào)用返回期望的 { value: .. }
返回值,并在迭代結(jié)束后返回一個(gè) { done: true }
值渡八,ES6 的 for..of
循環(huán)就可以迭代它啃洋。
其實(shí),你甚至可以生成一個(gè)永遠(yuǎn)不會(huì)“結(jié)束”屎鳍,并且總會(huì)返回一個(gè)新值(比如隨機(jī)數(shù)宏娄,遞增值,唯一的識(shí)別符等等)的“無(wú)窮”迭代器逮壁,雖然你可能不會(huì)將這樣的迭代器用于一個(gè)沒(méi)有邊界的 for..of
循環(huán)孵坚,因?yàn)樗肋h(yuǎn)不會(huì)結(jié)束,而且會(huì)阻塞你的程序窥淆。
var randoms = {
[Symbol.iterator]: function() {
return {
next: function() {
return { value: Math.random() };
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push( n );
// 不要超過(guò)邊界卖宠!
if (randoms_pool.length === 100) break;
}
這個(gè)迭代器會(huì)“永遠(yuǎn)”生成隨機(jī)數(shù),所以我們小心地僅從中取出 100 個(gè)值忧饭,以使我們的程序不被阻塞扛伍。
復(fù)習(xí)
JS 中的對(duì)象擁有字面形式(比如 var a = { .. }
)和構(gòu)造形式(比如 var a = new Array(..)
)。字面形式幾乎總是首選词裤,但在某些情況下刺洒,構(gòu)造形式提供更多的構(gòu)建選項(xiàng)鳖宾。
許多人聲稱“Javascript 中的一切都是對(duì)象”,這是不對(duì)的逆航。對(duì)象是六種(或七中鼎文,看你從哪個(gè)方面說(shuō))基本類型之一。對(duì)象有子類型纸泡,包括 function
,還可以被行為特化赖瞒,比如 [object Array]
作為內(nèi)部的標(biāo)簽表示子類型數(shù)組女揭。
對(duì)象是鍵/值對(duì)的集合。通過(guò) .propName
或 ["propName"]
語(yǔ)法栏饮,值可以作為屬性訪問(wèn)吧兔。不管屬性什么時(shí)候被訪問(wèn),引擎實(shí)際上會(huì)調(diào)用內(nèi)部默認(rèn)的 [[Get]]
操作(在設(shè)置值時(shí)調(diào)用 [[Put]]
操作)袍嬉,它不僅直接在對(duì)象上查找屬性境蔼,在沒(méi)有找到時(shí)還會(huì)遍歷 [[Prototype]]
鏈(見(jiàn)第五章)。
屬性有一些可以通過(guò)屬性描述符控制的特定性質(zhì)伺通,比如 writable
和 configurable
箍土。另外,對(duì)象擁有它的不可變性(它們的屬性也有)罐监,可以通過(guò)使用 Object.preventExtensions(..)
吴藻、Object.seal(..)
、和 Object.freeze(..)
來(lái)控制幾種不同等級(jí)的不可變性弓柱。
屬性不必非要包含值 —— 它們也可以是帶有 getter/setter 的“訪問(wèn)器屬性”沟堡。它們也可以是可枚舉或不可枚舉的,這控制它們是否會(huì)在 for..in
這樣的循環(huán)迭代中出現(xiàn)矢空。
你也可以使用 ES6 的 for..of
語(yǔ)法航罗,在數(shù)據(jù)結(jié)構(gòu)(數(shù)組,對(duì)象等)中迭代 值屁药,它尋找一個(gè)內(nèi)建或自定義的 @@iterator
對(duì)象粥血,這個(gè)對(duì)象由一個(gè) next()
方法組成,通過(guò)這個(gè) next()
方法每次迭代一個(gè)數(shù)據(jù)酿箭。