你不懂JS:this與對(duì)象原型 第三章:對(duì)象

官方中文版原文鏈接

感謝社區(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氢拥,booleannull谢床,和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"基本類型嗽仪。

nullundefined沒(méi)有對(duì)象包裝的形式荒勇,僅有它們的基本類型值。相比之下闻坚,Date的值 僅可以 由它們的構(gòu)造對(duì)象形式創(chuàng)建,因?yàn)樗鼈儧](méi)有對(duì)應(yīng)的字面形式兢孝。

無(wú)論使用字面還是構(gòu)造形式窿凤,ObjectArray跨蟹,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(){..}

someFoomyObject.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ù)柱衔,比如042樊破。

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幔崖,cd屬性僅僅是引用食店,它們指向被拷貝對(duì)象中引用的相同位置。一個(gè) 深拷貝(deep copy) 將不僅復(fù)制myObject赏寇,還會(huì)復(fù)制anotherObjectanotherArray吉嫩。但之后我們讓anotherArray擁有anotherObjectmyObject的引用,所以 那些 也應(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)容要比value2多得多。它還包含另外3個(gè)性質(zhì):writableenumerable,和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:falseconfigurable: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ì)大致檢查:

  1. 這個(gè)屬性是訪問(wèn)器描述符嗎(見(jiàn)下一節(jié)"Getters 與 Setters")毅贮?如果是,而且是setter尘奏,就調(diào)用setter滩褥。
  2. 這個(gè)屬性是writablefalse數(shù)據(jù)描述符嗎?如果是炫加,在非strict mode下無(wú)聲地失敗瑰煎,或者在strict mode下拋出TypeError
  3. 否則俗孝,像平常一樣設(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)器描述符殖属,它的valuewritable性質(zhì)沒(méi)有意義而被忽略,取而代之的是JS將會(huì)考慮屬性的setget性質(zhì)(還有configurableenumerable)瓦盛。

考慮下面的代碼:

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ù)組检盼,不論能不能枚舉。

inhasOwnProperty(..)區(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的SymbolSymbol.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)容可以迭代窘游。

注意值3done: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ì)象列表(擁有xy的坐標(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ì),比如writableconfigurable裆针。另外刨摩,對(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ù)阵子。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市胜蛉,隨后出現(xiàn)的幾起案子挠进,更是在濱河造成了極大的恐慌,老刑警劉巖誊册,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件领突,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡案怯,警方通過(guò)查閱死者的電腦和手機(jī)君旦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嘲碱,“玉大人金砍,你說(shuō)我怎么就攤上這事÷缶猓” “怎么了恕稠?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)扶欣。 經(jīng)常有香客問(wèn)我鹅巍,道長(zhǎng),這世上最難降的妖魔是什么料祠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任骆捧,我火速辦了婚禮,結(jié)果婚禮上髓绽,老公的妹妹穿的比我還像新娘凑懂。我一直安慰自己,他們只是感情好梧宫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布接谨。 她就那樣靜靜地躺著摆碉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脓豪。 梳的紋絲不亂的頭發(fā)上巷帝,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音扫夜,去河邊找鬼楞泼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛笤闯,可吹牛的內(nèi)容都是我干的堕阔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼颗味,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼超陆!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起浦马,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤时呀,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后晶默,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體谨娜,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年磺陡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了趴梢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡币他,死狀恐怖坞靶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情圆丹,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布躯喇,位于F島的核電站辫封,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏狸剃。R本人自食惡果不足惜悄窃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一少欺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧欣福,春花似錦、人聲如沸焦履。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至郑临,卻和暖如春栖博,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厢洞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工仇让, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人躺翻。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓丧叽,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親公你。 傳聞我的和親對(duì)象是個(gè)殘疾皇子踊淳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容