第三章:對(duì)象

特別說明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

在第一和第二章中,我們講解了 this 綁定如何根據(jù)函數(shù)調(diào)用的調(diào)用點(diǎn)指向不同的對(duì)象。但究竟什么是對(duì)象,為什么我們需要指向它們?這一章我們就來詳細(xì)探索一下對(duì)象。

語法

對(duì)象來自于兩種形式:聲明(字面)形式,和構(gòu)造形式。

一個(gè)對(duì)象的字面語法看起來像這樣:

var myObj = {
    key: value
    // ...
};

構(gòu)造形式看起來像這樣:

var myObj = new Object();
myObj.key = value;

構(gòu)造形式和字面形式的結(jié)果是完全同種類的對(duì)象。唯一真正的區(qū)別在于你可以向字面聲明一次性添加一個(gè)或多個(gè)鍵/值對(duì),而對(duì)于構(gòu)造形式,你必須一個(gè)一個(gè)地添加屬性。

注意: 像剛才展示的那樣使用“構(gòu)造形式”來創(chuàng)建對(duì)象是極其少見的。你很有可能總是想使用字面語法形式。這對(duì)大多數(shù)內(nèi)建的對(duì)象也一樣(后述)。

類型

對(duì)象是大多數(shù) JS 程序依賴的基本構(gòu)建塊兒。它們是 JS 的六種主要類型(在語言規(guī)范中稱為“語言類型”)中的一種:

  • string
  • number
  • boolean
  • null
  • undefined
  • object

注意 簡(jiǎn)單基本類型stringnumber、boolean、null、和 undefined)自身 不是 objectnull 有時(shí)會(huì)被當(dāng)成一個(gè)對(duì)象類型,但是這種誤解源自于一個(gè)語言中的 Bug,它使得 typeof null 錯(cuò)誤地(而且令人困惑地)返回字符串 "object"。實(shí)際上,null 是它自己的基本類型。

一個(gè)常見的錯(cuò)誤論斷是“JavaScript中的一切都是對(duì)象”。這明顯是不對(duì)的。

對(duì)比來看,存在幾種特殊的對(duì)象子類型,我們可以稱之為 復(fù)雜基本類型。

function 是對(duì)象的一種子類型(技術(shù)上講,叫做“可調(diào)用對(duì)象”)。函數(shù)在 JS 中被稱為“頭等(first class)”類型,是因?yàn)樗鼈兓旧暇褪瞧胀ǖ膶?duì)象(附帶有可調(diào)用的行為語義),而且它們可以像其他普通的對(duì)象那樣被處理。

數(shù)組也是一種形式的對(duì)象,帶有特別的行為。數(shù)組在內(nèi)容的組織上要稍稍比一般的對(duì)象更加結(jié)構(gòu)化。

內(nèi)建對(duì)象

有幾種其他的對(duì)象子類型,通常稱為內(nèi)建對(duì)象。對(duì)于其中的一些來說,它們的名稱看起來暗示著它們和它們對(duì)應(yīng)的基本類型有著直接的聯(lián)系,但事實(shí)上,它們的關(guān)系更復(fù)雜,我們一會(huì)兒就開始探索。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

如果你依照和其他語言的相似性來看的話,比如 Java 語言的 String 類,這些內(nèi)建類型有著實(shí)際類型的外觀,甚至是類(class)的外觀,

但是在 JS 中,它們實(shí)際上僅僅是內(nèi)建的函數(shù)。這些內(nèi)建函數(shù)的每一個(gè)都可以被用作構(gòu)造器(也就是一個(gè)可以通過 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)單地說,我們可以通過借用基本的默認(rèn) toString() 方法來考察內(nèi)部子類型,而且你可以看到它揭示了 strObject 實(shí)際上是一個(gè)由 String 構(gòu)造器創(chuàng)建的對(duì)象。

基本類型值 "I am a string" 不是一個(gè)對(duì)象,它是一個(gè)不可變的基本字面值。為了對(duì)它進(jìn)行操作,比如檢查它的長度,訪問它的各個(gè)獨(dú)立字符內(nèi)容等等,都需要一個(gè) String 對(duì)象。

幸運(yùn)的是,在必要的時(shí)候語言會(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ì)象,所以這些屬性/方法的訪問可以工作。

當(dāng)使用如 42.359.toFixed(2) 這樣的方法時(shí),同樣的強(qiáng)制轉(zhuǎn)換也發(fā)生在數(shù)字基本字面量 42 和包裝對(duì)象 new Nubmer(42) 之間。同樣的還有 Boolean 對(duì)象和 "boolean" 基本類型。

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

無論使用字面還是構(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)我們說“內(nèi)容”時(shí),似乎暗示著這些值 實(shí)際上 存儲(chǔ)在對(duì)象內(nèi)部,但那只不過是表面現(xiàn)象。引擎會(huì)根據(jù)自己的實(shí)現(xiàn)來存儲(chǔ)這些值,而且通常都不是把它們存儲(chǔ)在容器對(duì)象 內(nèi)部。在容器內(nèi)存儲(chǔ)的是這些屬性的名稱,它們像指針(技術(shù)上講,叫 引用(reference))一樣指向值存儲(chǔ)的地方。

考慮下面的代碼:

var myObject = {
    a: 2
};

myObject.a;     // 2

myObject["a"];  // 2

為了訪問 myObject位置 a 的值,我們需要使用 .[ ] 操作符。.a 語法通常稱為“屬性(property)”訪問,而 ["a"] 語法通常稱為“鍵(key)”訪問。在現(xiàn)實(shí)中,它們倆都訪問相同的 位置,而且會(huì)拿出相同的值,2,所以這些術(shù)語可以互換使用。從現(xiàn)在起,我們將使用最常見的術(shù)語 —— “屬性訪問”。

兩種語法的主要區(qū)別在于,. 操作符后面需要一個(gè) 標(biāo)識(shí)符(Identifier) 兼容的屬性名,而 [".."] 語法基本可以接收任何兼容 UTF-8/unicode 的字符串作為屬性名。舉個(gè)例子,為了引用一個(gè)名為“Super-Fun!”的屬性,你不得不使用 ["Super-Fun!"] 語法訪問,因?yàn)?Super-Fun! 不是一個(gè)合法的 Identifier 屬性名。

而且,由于 [".."] 語法使用字符串的 來指定位置,這意味著程序可以動(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[..] 屬性訪問語法是十分有用的,比如 myObject[prefix + name]。但是當(dāng)使用字面對(duì)象語法聲明對(duì)象時(shí)則沒有什么幫助。

ES6 加入了 計(jì)算型屬性名,在一個(gè)字面對(duì)象聲明的鍵名稱位置,你可以指定一個(gè)表達(dá)式,用 [ ] 括起來:

var prefix = "foo";

var myObject = {
    [prefix + "bar"]: "hello",
    [prefix + "baz"]: "world"
};

myObject["foobar"]; // hello
myObject["foobaz"]; // world

計(jì)算型屬性名 的最常見用法,可能是用于 ES6 的 Symbol,我們將不會(huì)在本書中涵蓋關(guān)于它的細(xì)節(jié)。簡(jiǎn)單地說,它們是新的基本數(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)

有些開發(fā)者喜歡在討論對(duì)一個(gè)對(duì)象的屬性訪問時(shí)做一個(gè)區(qū)別,如果這個(gè)被訪問的值恰好是一個(gè)函數(shù)的話。因?yàn)檫@誘使人們認(rèn)為函數(shù) 屬于 這個(gè)對(duì)象,而且在其他語言中,屬于對(duì)象(也就是“類”)的函數(shù)被稱作“方法”,所以相對(duì)于“屬性訪問”,我們常能聽到“方法訪問”。

有趣的是,語言規(guī)范也做出了同樣的區(qū)別。

從技術(shù)上講,函數(shù)絕不會(huì)“屬于”對(duì)象,所以,說一個(gè)偶然在對(duì)象的引用上被訪問的函數(shù)就自動(dòng)地成為了一個(gè)“方法”,看起來有些像是牽強(qiáng)附會(huì)。

有些函數(shù)內(nèi)部確實(shí)擁有 this 引用,而且 有時(shí) 這些 this 引用指向調(diào)用點(diǎn)的對(duì)象引用。但這個(gè)用法確實(shí)沒有使這個(gè)函數(shù)比其他函數(shù)更像“方法”,因?yàn)?this 是在運(yùn)行時(shí)在調(diào)用點(diǎn)動(dòng)態(tài)綁定的,這使得它與這個(gè)對(duì)象的關(guān)系至多是間接的。

每次你訪問一個(gè)對(duì)象的屬性都是一個(gè) 屬性訪問,無論你得到什么類型的值。如果你 恰好 從屬性訪問中得到一個(gè)函數(shù),它也沒有魔法般地在那時(shí)成為一個(gè)“方法”。一個(gè)從屬性訪問得來的函數(shù)沒有任何特殊性(隱含的 this 綁定的情況在剛才已經(jīng)解釋過了)。

舉個(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 只不過是同一個(gè)函數(shù)的兩個(gè)分離的引用,它們中的任何一個(gè)都不意味著這個(gè)函數(shù)很特別或被其他對(duì)象所“擁有”。如果上面的 foo() 定義里面擁有一個(gè) this 引用,那么 myObject.someFoo隱含綁定 將會(huì)是這個(gè)兩個(gè)引用間 唯一 可以觀察到的不同。它們中的任何一個(gè)都沒有稱為“方法”的道理。

也許有人會(huì)爭(zhēng)辯,函數(shù) 變成了方法,不是在定義期間,而是在調(diào)用的執(zhí)行期間,根據(jù)它是如何在調(diào)用點(diǎn)被調(diào)用的(是否帶有一個(gè)環(huán)境對(duì)象引用 —— 細(xì)節(jié)見第二章)。即便是這種解讀也有些牽強(qiáng)。

可能最安全的結(jié)論是,在 JavaScript 中,“函數(shù)”和“方法”是可以互換使用的。

注意: ES6 加入了 super 引用,它通常是和 class(見附錄A)一起使用的。super 的行為方式(靜態(tài)綁定,而非像 this 一樣延遲綁定),給了這種說法更多的權(quán)重:一個(gè)被 super 綁定到某處的函數(shù)比起“函數(shù)”更像一個(gè)“方法”。但是同樣地,這僅僅是微妙的語義上的(和機(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(){ .. } 聲明語法介紹一種ES6的簡(jiǎn)化語法。

數(shù)組

數(shù)組也使用 [ ] 訪問形式,但正如上面提到的,在存儲(chǔ)值的方式和位置上它們的組織更加結(jié)構(gòu)化(雖然仍然在存儲(chǔ)值的 類型 上沒有限制)。數(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"

注意,添加命名屬性(不論是使用 . 還是 [ ] 操作符語法)不會(huì)改變數(shù)組的 length 所報(bào)告的值。

可以 把一個(gè)數(shù)組當(dāng)做普通的鍵/值對(duì)象使用,并且從不添加任何數(shù)字下標(biāo),但這不是一個(gè)好主意,因?yàn)閿?shù)組對(duì)它本來的用途有著特定的行為和優(yōu)化方式,普通對(duì)象也一樣。使用對(duì)象來存儲(chǔ)鍵/值對(duì),而用數(shù)組在數(shù)字下標(biāo)上存儲(chǔ)值。

小心: 如果你試圖在一個(gè)數(shù)組上添加屬性,但是屬性名 看起來 像一個(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)開發(fā)者們初次拿起 Javascript 語言時(shí),最常需要的特性就是如何復(fù)制一個(gè)對(duì)象??雌饋響?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ù)制而不是僅保留引用?,F(xiàn)在由于循環(huán)引用,我們得到了一個(gè)無限循環(huán)復(fù)制的問題。

我們應(yīng)當(dāng)檢測(cè)循環(huán)引用并打破循環(huán)遍歷嗎(不管位于深處的,沒有完全復(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所有這些刁鉆的問題?不同的 JS 框架都各自挑選自己的解釋并且做出自己的選擇。但是哪一種(如果有的話)才是 JS 應(yīng)當(dāng)作為標(biāo)準(zhǔn)采用的呢?長久以來,沒有明確答案。

一個(gè)解決方案是,JSON 安全的對(duì)象(也就是,可以被序列化為一個(gè) JSON 字符串,之后還可以被重新解析為擁有相同的結(jié)構(gòu)和值的對(duì)象)可以簡(jiǎn)單地這樣 復(fù)制

var newObj = JSON.parse( JSON.stringify( someObj ) );

當(dāng)然,這要求你保證你的對(duì)象是 JSON 安全的。對(duì)于某些情況,這沒什么大不了的。而對(duì)另一些情況,這還不夠。

同時(shí),淺拷貝相當(dāng)易懂,而且沒有那么多問題,所以 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ì)象上(僅通過 = 賦值)。它還會(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 語言沒有給出直接的方法,讓你的代碼可以考察或描述屬性性質(zhì)間的區(qū)別,比如屬性是否為只讀。

在 ES5 中,所有的屬性都用 屬性描述符(Property Descriptors) 來描述。

考慮這段代碼:

var myObject = {
    a: 2
};

Object.getOwnPropertyDescriptor( myObject, "a" );
// {
//    value: 2,
//    writable: true,
//    enumerable: true,
//    configurable: true
// }

正如你所見,我們普通的對(duì)象屬性 a 的屬性描述符(稱為“數(shù)據(jù)描述符”,因?yàn)樗鼉H持有一個(gè)數(shù)據(jù)值)的內(nèi)容要比 value2 多得多。它還包含另外三個(gè)性質(zhì):writableenumerable、和 configurable

當(dāng)我們創(chuàng)建一個(gè)普通屬性時(shí),可以看到屬性描述符的各種性質(zhì)的默認(rèn)值,同時(shí)我們可以用 Object.defineProperty(..) 來添加新屬性,或使用期望的性質(zhì)來修改既存的屬性(如果它是 configurable 的?。?/p>

舉例來說:

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

如你所見,我們對(duì) value 的修改悄無聲息地失敗了。如果我們?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)單地說,你可以觀察到 writable:false 意味著值不可改變,和你定義一個(gè)空的 setter 是有些等價(jià)的。實(shí)際上,你的空 setter 在被調(diào)用時(shí)需要扔出一個(gè) TypeError,來和 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 無關(guān),如果你試圖改變一個(gè)不可配置屬性的描述符定義,就會(huì)發(fā)生 TypeError。要小心:如你所看到的,將 configurable 設(shè)置為 false一個(gè)單向操作,不可撤銷!

注意: 這里有一個(gè)需要注意的微小例外:即便屬性已經(jīng)是 configurable:false,writable 總是可以沒有錯(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

如你所見,最后的 delete 調(diào)用(無聲地)失敗了,因?yàn)槲覀儗?a 屬性設(shè)置成了不可配置。

delete 僅用于直接從目標(biāo)對(duì)象移除該對(duì)象的(可以被移除的)屬性。如果一個(gè)對(duì)象的屬性是某個(gè)其他對(duì)象/函數(shù)的最后一個(gè)現(xiàn)存的引用,而你 delete 了它,那么這就移除了這個(gè)引用,于是現(xiàn)在那個(gè)沒有被任何地方所引用的對(duì)象/函數(shù)就可以被作為垃圾回收。但是,將 delete 當(dāng)做一個(gè)像其他語言(如 C/C++)中那樣的釋放內(nèi)存工具是 恰當(dāng)?shù)摹?code>delete 僅僅是一個(gè)對(duì)象屬性移除操作 —— 沒有更多別的含義。

可枚舉性(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)在這樣的枚舉中,即使它依然完全是可以訪問的。設(shè)置為 true 會(huì)使它出現(xiàn)。

所有普通的用戶定義屬性都默認(rèn)是可 enumerable 的,正如你通常希望的那樣。但如果你有一個(gè)特殊的屬性,你想讓它對(duì)枚舉隱藏,就將它設(shè)置為 enumerable:false。

我們一會(huì)兒就更加詳細(xì)地演示可枚舉性,所以在大腦中給這個(gè)話題上打一個(gè)書簽。

不可變性(Immutability)

有時(shí)我們希望將屬性或?qū)ο螅ㄓ幸饣驘o意地)設(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ì)象是不那么常見的。有些特殊情況當(dāng)然需要,但作為一個(gè)普通的設(shè)計(jì)模式,如果你發(fā)現(xiàn)自己想要 封?。╯eal)凍結(jié)(freeze) 你所有的對(duì)象,那么你可能想要退一步來重新考慮你的程序設(shè)計(jì),讓它對(duì)對(duì)象值的潛在變化更加健壯。

對(duì)象常量(Object Constant)

通過將 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ì)無聲地失敗。在 strict mode 下,它會(huì)拋出 TypeError

封?。⊿eal)

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ù)訪問”屬性設(shè)置為 writable:false,所以它們的值不可改變。

這種方法是你可以從對(duì)象自身獲得的最高級(jí)別的不可變性,因?yàn)樗柚谷魏螌?duì)對(duì)象或?qū)ο笾睂賹傩缘母淖儯m然,就像上面提到的,任何被引用的對(duì)象的內(nèi)容不受影響)。

你可以“深度凍結(jié)”一個(gè)對(duì)象:在這個(gè)對(duì)象上調(diào)用 Object.freeze(..),然后遞歸地迭代所有它引用的(目前還沒有受過影響的)對(duì)象,然后也在它們上面調(diào)用 Object.freeze(..)。但是要小心,這可能會(huì)影響其他你并不打算影響的(共享的)對(duì)象。

[[Get]]

關(guān)于屬性訪問如何工作有一個(gè)重要的細(xì)節(jié)。

考慮下面的代碼:

var myObject = {
    a: 2
};

myObject.a; // 2

myObject.a 是一個(gè)屬性訪問,但是它并不是看起來那樣,僅僅在 myObject 中尋找一個(gè)名為 a 的屬性。

根據(jù)語言規(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)求的名稱 沒能 找到屬性,[[Get]] 的算法定義了另一個(gè)重要的行為。我們會(huì)在第五章來解釋 接下來 會(huì)發(fā)生什么(遍歷 [[Prototype]] 鏈,如果有的話)。

[[Get]] 操作的一個(gè)重要結(jié)果是,如果它通過任何方法都不能找到被請(qǐng)求的屬性的值,那么它會(huì)返回 undefined。

var myObject = {
    a: 2
};

myObject.b; // undefined

這個(gè)行為和你通過標(biāo)識(shí)符名稱來引用 變量 不同。如果你引用了一個(gè)在可用的詞法作用域內(nèi)無法解析的變量,其結(jié)果不是像對(duì)象屬性那樣返回 undefined,而是拋出一個(gè) ReferenceError

var myObject = {
    a: undefined
};

myObject.a; // undefined

myObject.b; // undefined

的角度來說,這兩個(gè)引用沒有區(qū)別 —— 它們的結(jié)果都是 undefined。然而,在 [[Get]] 操作的底層,雖然不明顯,但是比起處理引用 myObject.a,處理 myObject.b 的操作要多做一些潛在的“工作”。

如果僅僅考察結(jié)果的值,你無法分辨一個(gè)屬性是存在并持有一個(gè) undefined 值,還是因?yàn)閷傩愿? 存在所以 [[Get]] 無法返回某個(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]] 來設(shè)置或創(chuàng)建這個(gè)屬性。但是實(shí)際情況卻有一些微妙的不同。

調(diào)用 [[Put]] 時(shí),它根據(jù)幾個(gè)因素表現(xiàn)不同的行為,包括(影響最大的)屬性是否已經(jīng)在對(duì)象中存在了。

如果屬性存在,[[Put]] 算法將會(huì)大致檢查:

  1. 這個(gè)屬性是訪問器描述符嗎(見下一節(jié)"Getters 與 Setters")?如果是,而且是 setter,就調(diào)用 setter。
  2. 這個(gè)屬性是 writablefalse 數(shù)據(jù)描述符嗎?如果是,在非 strict mode 下無聲地失敗,或者在 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)的語言特性,覆蓋整個(gè)對(duì)象(不僅是每個(gè)屬性)的默認(rèn) [[Put]][[Get]] 操作是可能的。這超出了我們要在這本書中討論的范圍,但我們會(huì)在后面的“你不懂 JS”系列中涵蓋此內(nèi)容。

ES5 引入了一個(gè)方法來覆蓋這些默認(rèn)操作的一部分,但不是在對(duì)象級(jí)別而是針對(duì)每個(gè)屬性,就是通過 getters 和 setters。Getter 是實(shí)際上調(diào)用一個(gè)隱藏函數(shù)來取得值的屬性。Setter 是實(shí)際上調(diào)用一個(gè)隱藏函數(shù)來設(shè)置值的屬性。

當(dāng)你將一個(gè)屬性定義為擁有 getter 或 setter 或兩者兼?zhèn)?,那么它的定義就成為了“訪問器描述符”(與“數(shù)據(jù)描述符”相對(duì))。對(duì)于訪問器描述符,它的 valuewritable 性質(zhì)因沒有意義而被忽略,取而代之的是 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

不管是通過在字面對(duì)象語法中使用 get a() { .. },還是通過使用 defineProperty(..) 明確定義,我們都在對(duì)象上創(chuàng)建了一個(gè)沒有實(shí)際持有值的屬性,訪問它們將會(huì)自動(dòng)地對(duì) getter 函數(shù)進(jìn)行隱藏的函數(shù)調(diào)用,其返回的任何值就是屬性訪問的結(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ò)誤而是無聲地將賦值廢棄。就算這里有一個(gè)合法的 setter,我們的自定義 getter 將返回值硬編碼為僅返回 2,所以賦值操作是沒有意義的。

為了使這個(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è)例子中的單純慣例,并不意味著它的行為有什么特別之處 —— 它和其他普通屬性沒有區(qū)別。

存在性(Existence)

我們?cè)缦瓤吹?,?myObject.a 這樣的屬性訪問可能會(huì)得到一個(gè) undefined 值,無論是它明確存儲(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ì)象遍歷的更高層中(詳見第五章)。相比之下,hasOwnProperty(..) 僅僅 檢查 myObject 是否擁有屬性,但 不會(huì) 查詢 [[Prototype]] 鏈。我們會(huì)在第五章詳細(xì)講解 [[Prototype]] 時(shí),回來討論這個(gè)兩個(gè)操作重要的不同。

通過委托到 Object.prototype,所有的普通對(duì)象都可以訪問 hasOwnProperty(..)(詳見第五章)。但是創(chuàng)建一個(gè)不鏈接到 Object.prototype 的對(duì)象也是可能的(通過 Object.create(null) —— 詳見第五章)。這種情況下,像 myObject.hasOwnProperty(..) 這樣的方法調(diào)用將會(huì)失敗。

在這種場(chǎng)景下,一個(gè)進(jìn)行這種檢查的更健壯的方式是 Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基本的 hasOwnProperty(..) 方法而且使用 明確的 this 綁定(詳見第二章)來對(duì)我們的 myObject 實(shí)施這個(gè)方法。

注意: in 操作符看起來像是要檢查一個(gè)值在容器中的存在性,但是它實(shí)際上檢查的是屬性名的存在性。在使用數(shù)組時(shí)注意這個(gè)區(qū)別十分重要,因?yàn)槲覀儠?huì)有很強(qiáng)的沖動(dòng)來進(jìn)行 4 in [2, 4, 6] 這樣的檢查,但是這總是不像我們想象的那樣工作。

枚舉(Enumeration)

先前,在學(xué)習(xí) enumerable 屬性描述符性質(zhì)時(shí),我們簡(jiǎn)單地解釋了"可枚舉性(enumerability)"的含義。現(xiàn)在,讓我們來更加詳細(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í)際上 存在,而且擁有可以訪問的值,但是它不出現(xiàn)在 for..in 循環(huán)中(然而令人詫異的是,它的 in 操作符的存在性檢查通過了)。這是因?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)下)沒有與 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

但是這并沒有迭代所有的值,而是迭代了所有的下標(biāo),然后由你使用索引來引用值,比如 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 語句,它們可以在迭代執(zhí)行到末尾之前將它結(jié)束掉。

如果你使用 for..in 循環(huán)在一個(gè)對(duì)象上進(jìn)行迭代,你也只能間接地得到值,因?yàn)樗鼘?shí)際上僅僅迭代對(duì)象的所有可枚舉屬性,讓你自己手動(dòng)地去訪問屬性來得到值。

注意: 與以有序數(shù)字的方式(for 循環(huán)或其他迭代器)迭代數(shù)組的下標(biāo)比較起來,迭代對(duì)象屬性的順序是 不確定 的,而且可能會(huì)因 JS 引擎的不同而不同。對(duì)于需要跨平臺(tái)環(huán)境保持一致的問題,不要依賴 觀察到的順序,因?yàn)檫@個(gè)順序是不可靠的。

但是如果你想直接迭代值,而不是數(shù)組下標(biāo)(或?qū)ο髮傩裕┠??ES6 加入了一個(gè)有用的 for..of 循環(huán)語法,用來迭代數(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è)在語言規(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 來手動(dòng)迭代一個(gè)數(shù)組,來看看它是怎么工作的:

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 來取得一個(gè)對(duì)象的 @@iterator 內(nèi)部屬性。我們?cè)诒菊轮泻?jiǎn)單地提到過 Symbol 的語義(見“計(jì)算型屬性名”),同樣的原理也適用于這里。你總是希望通過 Symbol 名稱,而不是它可能持有的特殊的值,來引用這樣特殊的屬性。另外,盡管這個(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)這樣做)來得到 done:true,以使自己知道迭代已經(jīng)完成。這個(gè)怪異之處的原因超出了我們要在這里討論的范圍,但是它源自于 ES6 生成器(generator)函數(shù)的語義。

雖然數(shù)組可以在 for..of 循環(huán)中自動(dòng)迭代,但普通的對(duì)象 沒有內(nèi)建的 @@iterator。這種故意省略的原因要比我們將在這里解釋的更復(fù)雜,但一般來說,為了未來的對(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(..) 來自定義我們的 @@iterator(很大程度上是因?yàn)槲覀兛梢詫⑺付椴豢擅杜e的),但是通過將 Symbol 作為一個(gè) 計(jì)算型屬性名(在本章前面的部分討論過),我們也可以直接聲明它,比如 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ù)雜的迭代方法,只要你覺得合適。對(duì)于操作用戶自定義對(duì)象來說,自定義迭代器與 ES6 的 for..of 循環(huán)相組合,是一個(gè)新的強(qiáng)大的語法工具。

舉個(gè)例子,一個(gè) Pixel(像素) 對(duì)象列表(擁有 xy 的坐標(biāo)值)可以根據(jù)距離原點(diǎn) (0,0) 的直線距離決定它的迭代順序,或者過濾掉那些“太遠(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í)別符等等)的“無窮”迭代器,雖然你可能不會(huì)將這樣的迭代器用于一個(gè)沒有邊界的 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 );

    // 不要超過邊界!
    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è)方面說)基本類型之一。對(duì)象有子類型,包括 function,還可以被行為特化,比如 [object Array] 作為內(nèi)部的標(biāo)簽表示子類型數(shù)組。

對(duì)象是鍵/值對(duì)的集合。通過 .propName["propName"] 語法,值可以作為屬性訪問。不管屬性什么時(shí)候被訪問,引擎實(shí)際上會(huì)調(diào)用內(nèi)部默認(rèn)的 [[Get]] 操作(在設(shè)置值時(shí)調(diào)用 [[Put]] 操作),它不僅直接在對(duì)象上查找屬性,在沒有找到時(shí)還會(huì)遍歷 [[Prototype]] 鏈(見第五章)。

屬性有一些可以通過屬性描述符控制的特定性質(zhì),比如 writableconfigurable。另外,對(duì)象擁有它的不可變性(它們的屬性也有),可以通過使用 Object.preventExtensions(..)、Object.seal(..)、和 Object.freeze(..) 來控制幾種不同等級(jí)的不可變性。

屬性不必非要包含值 —— 它們也可以是帶有 getter/setter 的“訪問器屬性”。它們也可以是可枚舉或不可枚舉的,這控制它們是否會(huì)在 for..in 這樣的循環(huán)迭代中出現(xiàn)。

你也可以使用 ES6 的 for..of 語法,在數(shù)據(jù)結(jié)構(gòu)(數(shù)組,對(duì)象等)中迭代 ,它尋找一個(gè)內(nèi)建或自定義的 @@iterator 對(duì)象,這個(gè)對(duì)象由一個(gè) next() 方法組成,通過這個(gè) next() 方法每次迭代一個(gè)數(shù)據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容