第三章:對(duì)象

特別說(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能扒、numberboolean辫狼、null初斑、和 undefined)自身 不是 objectnull 有時(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" 基本類型。

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

無(wú)論使用字面還是構(gòu)造形式,Object戒财、ArrayFunction捺弦、和 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(){..}

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)綁定,而非像 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ù)导披,比如 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),但這不是一個(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 的拷貝堂竟,但 bcd 屬性僅僅是引用玻佩,它們指向被拷貝對(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 —— 屬性的性質(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)容要比 value2 多得多擎宝。它還包含另外三個(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:falsewritable 總是可以沒(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: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(..)凤跑,然后遞歸地迭代所有它引用的(目前還沒(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ì)大致檢查:

  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è)屬性定義為擁有 getter 或 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è)缦瓤吹椒啡疲?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ù)組匹中,不論能不能枚舉夏漱。

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)引用這樣特殊的屬性是越。另外,盡管這個(gè)名稱有這樣的暗示碌上,但 @@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è)怪異之處的原因超出了我們要在這里討論的范圍箩言,但是它源自于 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ì)象列表(擁有 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)鳖宾。

許多人聲稱“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ì)伺通,比如 writableconfigurable箍土。另外,對(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ù)酿箭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末立莉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子七问,更是在濱河造成了極大的恐慌蜓耻,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件械巡,死亡現(xiàn)場(chǎng)離奇詭異刹淌,居然都是意外死亡饶氏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門有勾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)疹启,“玉大人,你說(shuō)我怎么就攤上這事蔼卡『把拢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵雇逞,是天一觀的道長(zhǎng)荤懂。 經(jīng)常有香客問(wèn)我,道長(zhǎng)塘砸,這世上最難降的妖魔是什么节仿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮掉蔬,結(jié)果婚禮上廊宪,老公的妹妹穿的比我還像新娘。我一直安慰自己女轿,他們只是感情好箭启,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蛉迹,像睡著了一般册烈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婿禽,一...
    開(kāi)封第一講書(shū)人閱讀 51,287評(píng)論 1 301
  • 那天赏僧,我揣著相機(jī)與錄音,去河邊找鬼扭倾。 笑死淀零,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膛壹。 我是一名探鬼主播驾中,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼模聋!你這毒婦竟也來(lái)了肩民?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤链方,失蹤者是張志新(化名)和其女友劉穎持痰,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體祟蚀,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡工窍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年割卖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片患雏。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹏溯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出淹仑,到底是詐尸還是另有隱情丙挽,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布匀借,位于F島的核電站颜阐,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏怀吻。R本人自食惡果不足惜瞬浓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一初婆、第九天 我趴在偏房一處隱蔽的房頂上張望蓬坡。 院中可真熱鬧,春花似錦磅叛、人聲如沸屑咳。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)兆龙。三九已至,卻和暖如春敲董,著一層夾襖步出監(jiān)牢的瞬間紫皇,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工腋寨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留聪铺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓萄窜,卻偏偏與公主長(zhǎng)得像铃剔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子查刻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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