感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券崇棠,享受所有官網(wǎng)優(yōu)惠咽袜,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
在第一和第二章中,我們講解了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的6中主要類型(在語(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è)函數(shù)可以和new
操作符一起調(diào)用——參照第二章),其結(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í)際上是由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"
基本類型轉(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)地將它轉(zhuǎn)換為String
對(duì)象,所以這些屬性/方法的訪問(wèn)可以工作颠印。
當(dāng)使用如42.359.toFixed(2)
這樣的方法時(shí)纲岭,同樣的轉(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)?code>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ì)象中逃顶,屬性名 總是 字符串。如果你使用字符串以外(基本)類型的值充甚,它會(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)有些像是延伸了語(yǔ)義栽渴。
有些函數(shù)確實(shí)擁有this
引用,而且 有時(shí) 這些this
引用指向調(diào)用點(diǎn)的對(duì)象引用稳懒。但這個(gè)用法真的沒(méi)有使這個(gè)函數(shù)比其他函數(shù)更像“方法”闲擦,因?yàn)?code>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)綁定诲祸,而非動(dòng)態(tài)綁定)浊吏,給了這種說(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)顺少,但這不是好主意,因?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)”并展示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
多得多。它還包含另外3個(gè)性質(zhì):writable
,enumerable
,和configurable
喧笔。
當(dāng)我們創(chuàng)建一個(gè)普通屬性時(shí),可以看到屬性描述符的各種性質(zhì)的默認(rèn)值龟再,我們可以用Object.defineProperty(..)
來(lái)添加新屬性书闸,或使用期望的性質(zhì)來(lái)修改既存的屬性(如果它是configurable
的!)利凑。
舉例來(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ì)修改為不同的值。
可寫(xiě)性(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ì)value
的修改悄無(wú)聲息地失敗了丙躏。如果我們?cè)?code>strict mode下進(jìn)行嘗試择示,會(huì)得到一個(gè)錯(cuò)誤:
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // not writable!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
這個(gè)TypeError
告訴我們,我們不能改變一個(gè)不可寫(xiě)屬性晒旅。
注意: 我們一會(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)?code>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)槲覀儗?code>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)存工具是不正確的。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]
在這段代碼中岸裙,我們假想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(..)
摊滔,然后遞歸地迭代所有它引用的對(duì)象(目前還沒(méi)有受過(guò)影響的)阴绢,然后在它們上也調(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
,而是拋出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è)屬性定義為擁有g(shù)etter或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è)缦瓤吹剑?code>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)椤癳numerable”基本上意味著“如果對(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)引用這樣特殊的屬性积糯。同時(shí),與這個(gè)名稱的含義無(wú)關(guān)碴犬,@@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è)特別之處的原因超出了我們要在這里討論的范圍寺庄,但是它來(lái)自于ES6生成器函數(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)哄孤。
許多人錯(cuò)誤地聲稱“Javascript中的一切都是對(duì)象”,這是不對(duì)的吹截。對(duì)象是6種(或7中瘦陈,看你從哪個(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í)的不可變性呻征。
屬性不必非要包含值——它們也可以是帶有g(shù)etter/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ù)阵子。