# 第三章:對象
在第一和第二章中,我們講解了?`this`?綁定如何根據函數調用的調用點指向不同的對象。但究竟什么是對象,為什么我們需要指向它們?這一章我們就來詳細探索一下對象。
## 語法
對象來自于兩種形式:聲明(字面)形式,和構造形式。
一個對象的字面語法看起來像這樣:
```source-js
var myObj = {
key: value
// ...
};
```
構造形式看起來像這樣:
```source-js
var myObj = new Object();
myObj.key = value;
```
構造形式和字面形式的結果是完全同種類的對象。唯一真正的區別在于你可以向字面聲明一次性添加一個或多個鍵/值對,而對于構造形式,你必須一個一個地添加屬性。
注意:?像剛才展示的那樣使用“構造形式”來創建對象是極其少見的。你很有可能總是想使用字面語法形式。這對大多數內建的對象也一樣(后述)。
## 類型
對象是大多數 JS 程序依賴的基本構建塊兒。它們是 JS 的六種主要類型(在語言規范中稱為“語言類型”)中的一種:
* `string`
* `number`
* `boolean`
* `null`
* `undefined`
* `object`
注意?*簡單基本類型*?(`string`、`number`、`boolean`、`null`、和?`undefined`)自身?不是?`object`。`null`?有時會被當成一個對象類型,但是這種誤解源自于一個語言中的 Bug,它使得?`typeof null`?錯誤地(而且令人困惑地)返回字符串?`"object"`。實際上,`null`?是它自己的基本類型。
一個常見的錯誤論斷是“JavaScript中的一切都是對象”。這明顯是不對的。
對比來看,存在幾種特殊的對象子類型,我們可以稱之為?*復雜基本類型*。
`function`?是對象的一種子類型(技術上講,叫做“可調用對象”)。函數在 JS 中被稱為“頭等(first class)”類型,是因為它們基本上就是普通的對象(附帶有可調用的行為語義),而且它們可以像其他普通的對象那樣被處理。
數組也是一種形式的對象,帶有特別的行為。數組在內容的組織上要稍稍比一般的對象更加結構化。
### 內建對象
有幾種其他的對象子類型,通常稱為內建對象。對于其中的一些來說,它們的名稱看起來暗示著它們和它們對應的基本類型有著直接的聯系,但事實上,它們的關系更復雜,我們一會兒就開始探索。
* `String`
* `Number`
* `Boolean`
* `Object`
* `Function`
* `Array`
* `Date`
* `RegExp`
* `Error`
如果你依照和其他語言的相似性來看的話,比如 Java 語言的?`String`?類,這些內建類型有著實際類型的外觀,甚至是類(class)的外觀,
但是在 JS 中,它們實際上僅僅是內建的函數。這些內建函數的每一個都可以被用作構造器(也就是一個可以通過?`new`?操作符調用的函數 —— 參照第二章),其結果是一個新?*構建*?的相應子類型的對象。例如:
```source-js
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]
```
我們會在本章稍后詳細地看到?`Object.prototype.toString...`?到底是如何工作的,但簡單地說,我們可以通過借用基本的默認?`toString()`?方法來考察內部子類型,而且你可以看到它揭示了?`strObject`?實際上是一個由?`String`?構造器創建的對象。
基本類型值?`"I am a string"`?不是一個對象,它是一個不可變的基本字面值。為了對它進行操作,比如檢查它的長度,訪問它的各個獨立字符內容等等,都需要一個?`String`?對象。
幸運的是,在必要的時候語言會自動地將?`"string"`?基本類型強制轉換為?`String`?對象類型,這意味著你幾乎從不需要明確地創建對象。JS 社區的絕大部分人都?強烈推薦?盡可能地使用字面形式的值,而非使用構造的對象形式。
考慮下面的代碼:
```source-js
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
```
在這兩個例子中,我們在字符串的基本類型上調用屬性和方法,引擎會自動地將它強制轉換為?`String`?對象,所以這些屬性/方法的訪問可以工作。
當使用如?`42.359.toFixed(2)`?這樣的方法時,同樣的強制轉換也發生在數字基本字面量?`42`?和包裝對象?`new Nubmer(42)`?之間。同樣的還有?`Boolean`?對象和?`"boolean"`?基本類型。
`null`?和?`undefined`?沒有對象包裝的形式,僅有它們的基本類型值。相比之下,`Date`?的值?*僅可以*?由它們的構造對象形式創建,因為它們沒有對應的字面形式。
無論使用字面還是構造形式,`Object`、`Array`、`Function`、和?`RegExp`(正則表達式)都是對象。在某些情況下,構造形式確實會比對應的字面形式提供更多的創建選項。因為對象可以被任意一種方式創建,更簡單的字面形式幾乎是所有人的首選。僅僅在你需要使用額外的選項時使用構建形式。
`Error`?對象很少在代碼中明示地被創建,它們通常在拋出異常時自動地被創建。它們可以由?`new Error(..)`?構造形式創建,但通常是不必要的。
## 內容
正如剛才提到的,對象的內容由存儲在特定命名的?*位置*?上的(任意類型的)值組成,我們稱這些值為屬性。
有一個重要的事情需要注意:當我們說“內容”時,似乎暗示著這些值?*實際上*?存儲在對象內部,但那只不過是表面現象。引擎會根據自己的實現來存儲這些值,而且通常都不是把它們存儲在容器對象?*內部*。在容器內存儲的是這些屬性的名稱,它們像指針(技術上講,叫?*引用(reference)*)一樣指向值存儲的地方。
考慮下面的代碼:
```source-js
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
```
為了訪問?`myObject`?在?*位置*?`a`?的值,我們需要使用?`.`?或?`[ ]`?操作符。`.a`?語法通常稱為“屬性(property)”訪問,而?`["a"]`?語法通常稱為“鍵(key)”訪問。在現實中,它們倆都訪問相同的?*位置*,而且會拿出相同的值,`2`,所以這些術語可以互換使用。從現在起,我們將使用最常見的術語 —— “屬性訪問”。
兩種語法的主要區別在于,`.`?操作符后面需要一個?`標識符(Identifier)`?兼容的屬性名,而?`[".."]`?語法基本可以接收任何兼容 UTF-8/unicode 的字符串作為屬性名。舉個例子,為了引用一個名為“Super-Fun!”的屬性,你不得不使用?`["Super-Fun!"]`語法訪問,因為?`Super-Fun!`?不是一個合法的?`Identifier`?屬性名。
而且,由于?`[".."]`?語法使用字符串的?值?來指定位置,這意味著程序可以動態地組建字符串的值。比如:
```source-js
var wantA = true;
var myObject = {
a: 2
};
var idx;
if (wantA) {
idx = "a";
}
// 稍后
console.log( myObject[idx] ); // 2
```
在對象中,屬性名?總是?字符串。如果你使用?`string`?以外的(基本)類型值,它會首先被轉換為字符串。這甚至包括在數組中常用于索引的數字,所以要小心不要將對象和數組使用的數字搞混了。
```source-js
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
```
### 計算型屬性名
如果你需要將一個計算表達式?*作為*?一個鍵名稱,那么我們剛剛描述的?`myObject[..]`?屬性訪問語法是十分有用的,比如?`myObject[prefix + name]`。但是當使用字面對象語法聲明對象時則沒有什么幫助。
ES6 加入了?*計算型屬性名*,在一個字面對象聲明的鍵名稱位置,你可以指定一個表達式,用?`[ ]`?括起來:
```source-js
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
```
*計算型屬性名*?的最常見用法,可能是用于 ES6 的?`Symbol`,我們將不會在本書中涵蓋關于它的細節。簡單地說,它們是新的基本數據類型,擁有一個不透明不可知的值(技術上講是一個?`string`?值)。你將會被強烈地不鼓勵使用一個?`Symbol`?的?*實際值*(這個值理論上會因 JS 引擎的不同而不同),所以?`Symbol`?的名稱,比如?`Symbol.Something`(這是個瞎編的名稱!),才是你會使用的:
```source-js
var myObject = {
[Symbol.Something]: "hello world"
};
```
### 屬性(Property) vs. 方法(Method)
有些開發者喜歡在討論對一個對象的屬性訪問時做一個區別,如果這個被訪問的值恰好是一個函數的話。因為這誘使人們認為函數?*屬于*?這個對象,而且在其他語言中,屬于對象(也就是“類”)的函數被稱作“方法”,所以相對于“屬性訪問”,我們常能聽到“方法訪問”。
有趣的是,語言規范也做出了同樣的區別。
從技術上講,函數絕不會“屬于”對象,所以,說一個偶然在對象的引用上被訪問的函數就自動地成為了一個“方法”,看起來有些像是牽強附會。
有些函數內部確實擁有?`this`?引用,而且?*有時*?這些?`this`?引用指向調用點的對象引用。但這個用法確實沒有使這個函數比其他函數更像“方法”,因為?`this`?是在運行時在調用點動態綁定的,這使得它與這個對象的關系至多是間接的。
每次你訪問一個對象的屬性都是一個?屬性訪問,無論你得到什么類型的值。如果你?*恰好*?從屬性訪問中得到一個函數,它也沒有魔法般地在那時成為一個“方法”。一個從屬性訪問得來的函數沒有任何特殊性(隱含的?`this`?綁定的情況在剛才已經解釋過了)。
舉個例子:
```source-js
function foo() {
console.log( "foo" );
}
var someFoo = foo; // 對 `foo` 的變量引用
var myObject = {
someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}
```
`someFoo`?和?`myObject.someFoo`?只不過是同一個函數的兩個分離的引用,它們中的任何一個都不意味著這個函數很特別或被其他對象所“擁有”。如果上面的?`foo()`?定義里面擁有一個?`this`?引用,那么?`myObject.someFoo`?的?*隱含綁定*?將會是這個兩個引用間?唯一?可以觀察到的不同。它們中的任何一個都沒有稱為“方法”的道理。
也許有人會爭辯,函數?*變成了方法*,不是在定義期間,而是在調用的執行期間,根據它是如何在調用點被調用的(是否帶有一個環境對象引用 —— 細節見第二章)。即便是這種解讀也有些牽強。
可能最安全的結論是,在 JavaScript 中,“函數”和“方法”是可以互換使用的。
注意:?ES6 加入了?`super`?引用,它通常是和?`class`(見附錄A)一起使用的。`super`?的行為方式(靜態綁定,而非像?`this`一樣延遲綁定),給了這種說法更多的權重:一個被?`super`?綁定到某處的函數比起“函數”更像一個“方法”。但是同樣地,這僅僅是微妙的語義上的(和機制上的)細微區別。
就算你聲明一個函數表達式作為字面對象的一部分,那個函數都不會魔法般地?*屬于*?這個對象 —— 仍然僅僅是同一個函數對象的多個引用罷了。
```source-js
var myObject = {
foo: function foo() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
```
注意:?在第六章中,我們會為字面對象的?`foo: function foo(){ .. }`?聲明語法介紹一種ES6的簡化語法。
### 數組
數組也使用?`[ ]`?訪問形式,但正如上面提到的,在存儲值的方式和位置上它們的組織更加結構化(雖然仍然在存儲值的?*類型*上沒有限制)。數組采用?*數字索引*,這意味著值被存儲的位置,通常稱為?*下標*,是一個非負整數,比如?`0`?和?`42`。
```source-js
var myArray = [ "foo", 42, "bar" ];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
```
數組也是對象,所以雖然每個索引都是正整數,你還可以在數組上添加屬性:
```source-js
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
```
注意,添加命名屬性(不論是使用?`.`?還是?`[ ]`?操作符語法)不會改變數組的?`length`?所報告的值。
你?*可以*?把一個數組當做普通的鍵/值對象使用,并且從不添加任何數字下標,但這不是一個好主意,因為數組對它本來的用途有著特定的行為和優化方式,普通對象也一樣。使用對象來存儲鍵/值對,而用數組在數字下標上存儲值。
小心:?如果你試圖在一個數組上添加屬性,但是屬性名?*看起來*?像一個數字,那么最終它會成為一個數字索引(也就是改變了數組的內容):
```source-js
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
```
### 復制對象
當開發者們初次拿起 Javascript 語言時,最常需要的特性就是如何復制一個對象。看起來應該有一個內建的?`copy()`?方法,對吧?但是事情實際上比這復雜一些,因為在默認情況下,復制的算法應當是什么,并不十分明確。
例如,考慮這個對象:
```source-js
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是拷貝!
c: anotherArray, // 又一個引用!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );
```
一個`myObject`的?*拷貝*?究竟應該怎么表現?
首先,我們應該回答它是一個?*淺(shallow)*?還是一個?*深(deep)*?拷貝?一個?*淺拷貝(shallow copy)*?會得到一個新對象,它的?`a`?是值?`2`?的拷貝,但?`b`、`c`?和?`d`?屬性僅僅是引用,它們指向被拷貝對象中引用的相同位置。一個?*深拷貝(deep copy)*?將不僅復制?`myObject`,還會復制?`anotherObject`?和?`anotherArray`。但之后我們讓?`anotherArray`?擁有?`anotherObject`?和?`myObject`?的引用,所以?*那些*?也應當被復制而不是僅保留引用。現在由于循環引用,我們得到了一個無限循環復制的問題。
我們應當檢測循環引用并打破循環遍歷嗎(不管位于深處的,沒有完全復制的元素)?我們應當報錯退出嗎?或者介于兩者之間?
另外,“復制”一個函數意味著什么,也不是很清楚。有一些技巧,比如提取一個函數源代碼的?`toString()`?序列化表達(這個源代碼會因實現不同而不同,而且根據被考察的函數的類型,其結果甚至在所有引擎上都不可靠)。
那么我們如何解決所有這些刁鉆的問題?不同的 JS 框架都各自挑選自己的解釋并且做出自己的選擇。但是哪一種(如果有的話)才是 JS 應當作為標準采用的呢?長久以來,沒有明確答案。
一個解決方案是,JSON 安全的對象(也就是,可以被序列化為一個 JSON 字符串,之后還可以被重新解析為擁有相同的結構和值的對象)可以簡單地這樣?*復制*:
```source-js
var newObj = JSON.parse( JSON.stringify( someObj ) );
```
當然,這要求你保證你的對象是 JSON 安全的。對于某些情況,這沒什么大不了的。而對另一些情況,這還不夠。
同時,淺拷貝相當易懂,而且沒有那么多問題,所以 ES6 為此任務已經定義了?`Object.assign(..)`。`Object.assign(..)`?接收?*目標*?對象作為第一個參數,然后是一個或多個?*源*?對象作為后續參數。它會在?*源*?對象上迭代所有的?*可枚舉(enumerable)*,*owned keys*(直接擁有的鍵),并把它們拷貝到?*目標*?對象上(僅通過?`=`?賦值)。它還會很方便地返回?*目標*?對象,正如下面你可以看到的:
```source-js
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(..)`?中發生的復制是單純的?`=`?式賦值,所以任何在源對象屬性的特殊性質(比如?`writable`)在目標對象上?都不會保留?。
### 屬性描述符(Property Descriptors)
在 ES5 之前,JavaScript 語言沒有給出直接的方法,讓你的代碼可以考察或描述屬性性質間的區別,比如屬性是否為只讀。
在 ES5 中,所有的屬性都用?屬性描述符(Property Descriptors)?來描述。
考慮這段代碼:
```source-js
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
```
正如你所見,我們普通的對象屬性?`a`?的屬性描述符(稱為“數據描述符”,因為它僅持有一個數據值)的內容要比?`value`?為?`2`多得多。它還包含另外三個性質:`writable`、`enumerable`、和?`configurable`。
當我們創建一個普通屬性時,可以看到屬性描述符的各種性質的默認值,同時我們可以用?`Object.defineProperty(..)`?來添加新屬性,或使用期望的性質來修改既存的屬性(如果它是?`configurable`?的!)。
舉例來說:
```source-js
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
```
使用?`defineProperty(..)`,我們手動、明確地在?`myObject`?上添加了一個直白的,普通的?`a`?屬性。然而,你通常不會使用這種手動方法,除非你想要把描述符的某個性質修改為不同的值。
#### 可寫性(Writable)
`writable`?控制著你改變屬性值的能力。
考慮這段代碼:
```source-js
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
```
如你所見,我們對?`value`?的修改悄無聲息地失敗了。如果我們在?`strict mode`?下進行嘗試,會得到一個錯誤:
```source-js
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
```
這個?`TypeError`?告訴我們,我們不能改變一個不可寫屬性。
注意:?我們一會兒就會討論 getters/setters,但是簡單地說,你可以觀察到?`writable:false`?意味著值不可改變,和你定義一個空的 setter 是有些等價的。實際上,你的空 setter 在被調用時需要扔出一個?`TypeError`,來和?`writable:false`?保持一致。
#### 可配置性(Configurable)
只要屬性當前是可配置的,我們就可以使用相同的?`defineProperty(..)`?工具,修改它的描述符定義。
```source-js
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(..)`?調用導致了一個 TypeError,這與?`strict mode`?無關,如果你試圖改變一個不可配置屬性的描述符定義,就會發生 TypeError。要小心:如你所看到的,將?`configurable`?設置為?`false`?是?一個單向操作,不可撤銷!
注意:?這里有一個需要注意的微小例外:即便屬性已經是?`configurable:false`,`writable`?總是可以沒有錯誤地從?`true`?改變為?`false`,但如果已經是?`false`?的話不能變回?`true`。
`configurable:false`?阻止的另外一個事情是使用?`delete`?操作符移除既存屬性的能力。
```source-js
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`?調用(無聲地)失敗了,因為我們將?`a`?屬性設置成了不可配置。
`delete`?僅用于直接從目標對象移除該對象的(可以被移除的)屬性。如果一個對象的屬性是某個其他對象/函數的最后一個現存的引用,而你?`delete`?了它,那么這就移除了這個引用,于是現在那個沒有被任何地方所引用的對象/函數就可以被作為垃圾回收。但是,將?`delete`?當做一個像其他語言(如 C/C++)中那樣的釋放內存工具是?不?恰當的。`delete`?僅僅是一個對象屬性移除操作 —— 沒有更多別的含義。
#### 可枚舉性(Enumerable)
我們將要在這里提到的最后一個描述符性質是?`enumerable`(還有另外兩個,我們將在一會兒討論 getter/setters 時談到)。
它的名稱可能已經使它的功能很明顯了,這個性質控制著一個屬性是否能在特定的對象-屬性枚舉操作中出現,比如?`for..in`循環。設置為?`false`?將會阻止它出現在這樣的枚舉中,即使它依然完全是可以訪問的。設置為?`true`?會使它出現。
所有普通的用戶定義屬性都默認是可?`enumerable`?的,正如你通常希望的那樣。但如果你有一個特殊的屬性,你想讓它對枚舉隱藏,就將它設置為?`enumerable:false`。
我們一會兒就更加詳細地演示可枚舉性,所以在大腦中給這個話題上打一個書簽。
### 不可變性(Immutability)
有時我們希望將屬性或對象(有意或無意地)設置為不可改變的。ES5 用幾種不同的微妙方式,加入了對此功能的支持。
一個重要的注意點是:所有?這些方法創建的都是淺不可變性。也就是,它們僅影響對象和它的直屬屬性的性質。如果對象擁有對其他對象(數組、對象、函數等)的引用,那個對象的?*內容*?不會受影響,任然保持可變。
```source-js
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
```
在這段代碼中,我們假設?`myImmutableObject`?已經被創建,而且被保護為不可變。但是,為了保護?`myImmutableObject.foo`?的內容(也是一個對象 —— 數組),你將需要使用下面的一個或多個方法將?`foo`?設置為不可變。
注意:?在 JS 程序中創建完全不可動搖的對象是不那么常見的。有些特殊情況當然需要,但作為一個普通的設計模式,如果你發現自己想要?*封印(seal)*?或?*凍結(freeze)*?你所有的對象,那么你可能想要退一步來重新考慮你的程序設計,讓它對對象值的潛在變化更加健壯。
#### 對象常量(Object Constant)
通過將?`writable:false`?與?`configurable:false`?組合,你可以實質上創建了一個作為對象屬性的?*常量*(不能被改變,重定義或刪除),比如:
```source-js
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
```
#### 防止擴展(Prevent Extensions)
如果你想防止一個對象被添加新的屬性,但另一方面保留其他既存的對象屬性,可以調用?`Object.preventExtensions(..)`:
```source-js
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
```
在非?`strict mode`?模式下,`b`?的創建會無聲地失敗。在?`strict mode`?下,它會拋出?`TypeError`。
#### 封印(Seal)
`Object.seal(..)`?創建一個“封印”的對象,這意味著它實質上在當前的對象上調用?`Object.preventExtensions(..)`,同時也將它所有的既存屬性標記為?`configurable:false`。
所以,你既不能添加更多的屬性,也不能重新配置或刪除既存屬性(雖然你依然?*可以*?修改它們的值)。
#### 凍結(Freeze)
`Object.freeze(..)`?創建一個凍結的對象,這意味著它實質上在當前的對象上調用?`Object.seal(..)`,同時也將它所有的“數據訪問”屬性設置為?`writable:false`,所以它們的值不可改變。
這種方法是你可以從對象自身獲得的最高級別的不可變性,因為它阻止任何對對象或對象直屬屬性的改變(雖然,就像上面提到的,任何被引用的對象的內容不受影響)。
你可以“深度凍結”一個對象:在這個對象上調用?`Object.freeze(..)`,然后遞歸地迭代所有它引用的(目前還沒有受過影響的)對象,然后也在它們上面調用?`Object.freeze(..)`。但是要小心,這可能會影響其他你并不打算影響的(共享的)對象。
### `[[Get]]`
關于屬性訪問如何工作有一個重要的細節。
考慮下面的代碼:
```source-js
var myObject = {
a: 2
};
myObject.a; // 2
```
`myObject.a`?是一個屬性訪問,但是它并不是看起來那樣,僅僅在?`myObject`?中尋找一個名為?`a`?的屬性。
根據語言規范,上面的代碼實際上在?`myObject`?上執行了一個?`[[Get]]`?操作(有些像?`[[Get]]()`?函數調用)。對一個對象進行默認的內建?`[[Get]]`?操作,會?*首先*?檢查對象,尋找一個擁有被請求的名稱的屬性,如果找到,就返回相應的值。
然而,如果按照被請求的名稱?*沒能*?找到屬性,`[[Get]]`?的算法定義了另一個重要的行為。我們會在第五章來解釋?*接下來*?會發生什么(遍歷?`[[Prototype]]`?鏈,如果有的話)。
但?`[[Get]]`?操作的一個重要結果是,如果它通過任何方法都不能找到被請求的屬性的值,那么它會返回?`undefined`。
```source-js
var myObject = {
a: 2
};
myObject.b; // undefined
```
這個行為和你通過標識符名稱來引用?*變量*?不同。如果你引用了一個在可用的詞法作用域內無法解析的變量,其結果不是像對象屬性那樣返回?`undefined`,而是拋出一個?`ReferenceError`。
```source-js
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
```
從?*值*?的角度來說,這兩個引用沒有區別 —— 它們的結果都是?`undefined`。然而,在?`[[Get]]`?操作的底層,雖然不明顯,但是比起處理引用?`myObject.a`,處理?`myObject.b`?的操作要多做一些潛在的“工作”。
如果僅僅考察結果的值,你無法分辨一個屬性是存在并持有一個?`undefined`?值,還是因為屬性根本?*不*?存在所以?`[[Get]]`?無法返回某個具體值而返回默認的?`undefined`。但是,你很快就能看到你其實?*可以*?分辨這兩種場景。
### `[[Put]]`
既然為了從一個屬性中取得值而存在一個內部定義的?`[[Get]]`?操作,那么很明顯應該也存在一個默認的?`[[Put]]`?操作。
這很容易讓人認為,給一個對象的屬性賦值,將會在這個對象上調用?`[[Put]]`?來設置或創建這個屬性。但是實際情況卻有一些微妙的不同。
調用?`[[Put]]`?時,它根據幾個因素表現不同的行為,包括(影響最大的)屬性是否已經在對象中存在了。
如果屬性存在,`[[Put]]`?算法將會大致檢查:
1. 這個屬性是訪問器描述符嗎(見下一節"Getters 與 Setters")?如果是,而且是 setter,就調用 setter。
2. 這個屬性是?`writable`?為?`false`?數據描述符嗎?如果是,在非?`strict mode`?下無聲地失敗,或者在?`strict mode`?下拋出?`TypeError`。
3. 否則,像平常一樣設置既存屬性的值。
如果屬性在當前的對象中還不存在,`[[Put]]`?操作會變得更微妙和復雜。我們將在第五章討論?`[[Prototype]]`?時再次回到這個場景,更清楚地解釋它。
### Getters 與 Setters
對象默認的?`[[Put]]`?和?`[[Get]]`?操作分別完全控制著如何設置既存或新屬性的值,和如何取得既存屬性。
注意:?使用較先進的語言特性,覆蓋整個對象(不僅是每個屬性)的默認?`[[Put]]`?和?`[[Get]]`?操作是可能的。這超出了我們要在這本書中討論的范圍,但我們會在后面的“你不懂 JS”系列中涵蓋此內容。
ES5 引入了一個方法來覆蓋這些默認操作的一部分,但不是在對象級別而是針對每個屬性,就是通過 getters 和 setters。Getter 是實際上調用一個隱藏函數來取得值的屬性。Setter 是實際上調用一個隱藏函數來設置值的屬性。
當你將一個屬性定義為擁有 getter 或 setter 或兩者兼備,那么它的定義就成為了“訪問器描述符”(與“數據描述符”相對)。對于訪問器描述符,它的?`value`?和?`writable`?性質因沒有意義而被忽略,取而代之的是 JS 將會考慮屬性的?`set`?和?`get`?性質(還有?`configurable`?和?`enumerable`)。
考慮下面的代碼:
```source-js
var myObject = {
// 為 `a` 定義一個 getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目標對象
"b", // 屬性名
{ // 描述符
// 為 `b` 定義 getter
get: function(){ return this.a * 2 },
// 確保 `b` 作為對象屬性出現
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
```
不管是通過在字面對象語法中使用?`get a() { .. }`,還是通過使用?`defineProperty(..)`?明確定義,我們都在對象上創建了一個沒有實際持有值的屬性,訪問它們將會自動地對 getter 函數進行隱藏的函數調用,其返回的任何值就是屬性訪問的結果。
```source-js
var myObject = {
// 為 `a` 定義 getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
```
因為我們僅為?`a`?定義了一個 getter,如果之后我們試著設置?`a`?的值,賦值操作并不會拋出錯誤而是無聲地將賦值廢棄。就算這里有一個合法的 setter,我們的自定義 getter 將返回值硬編碼為僅返回?`2`,所以賦值操作是沒有意義的。
為了使這個場景更合理,正如你可能期望的那樣,每個屬性還應當被定義一個覆蓋默認?`[[Put]]`?操作(也就是賦值)的 setter。幾乎可確定,你將總是想要同時聲明 getter 和 setter(僅有它們中的一個經常會導致意外的行為):
```source-js
var myObject = {
// 為 `a` 定義 getter
get a() {
return this._a_;
},
// 為 `a` 定義 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
```
注意:?在這個例子中,我們實際上將賦值操作(`[[Put]]`?操作)指定的值?`2`?存儲到了另一個變量?`_a_`?中。`_a_`?這個名稱只是用在這個例子中的單純慣例,并不意味著它的行為有什么特別之處 —— 它和其他普通屬性沒有區別。
### 存在性(Existence)
我們早先看到,像?`myObject.a`?這樣的屬性訪問可能會得到一個?`undefined`?值,無論是它明確存儲著?`undefined`?還是屬性?`a`根本就不存在。那么,如果這兩種情況的值相同,我們還怎么區別它們呢?
我們可以查詢一個對象是否擁有特定的屬性,而?*不必*?取得那個屬性的值:
```source-js
var myObject = {
a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
```
`in`?操作符會檢查屬性是否存在于對象?*中*,或者是否存在于?`[[Prototype]]`?鏈對象遍歷的更高層中(詳見第五章)。相比之下,`hasOwnProperty(..)`?*僅僅*?檢查?`myObject`?是否擁有屬性,但?*不會*?查詢?`[[Prototype]]`?鏈。我們會在第五章詳細講解?`[[Prototype]]`?時,回來討論這個兩個操作重要的不同。
通過委托到?`Object.prototype`,所有的普通對象都可以訪問?`hasOwnProperty(..)`(詳見第五章)。但是創建一個不鏈接到?`Object.prototype`?的對象也是可能的(通過?`Object.create(null)`?—— 詳見第五章)。這種情況下,像?`myObject.hasOwnProperty(..)`?這樣的方法調用將會失敗。
在這種場景下,一個進行這種檢查的更健壯的方式是?`Object.prototype.hasOwnProperty.call(myObject,"a")`,它借用基本的?`hasOwnProperty(..)`?方法而且使用?*明確的?`this`?綁定*(詳見第二章)來對我們的?`myObject`?實施這個方法。
注意:?`in`?操作符看起來像是要檢查一個值在容器中的存在性,但是它實際上檢查的是屬性名的存在性。在使用數組時注意這個區別十分重要,因為我們會有很強的沖動來進行?`4 in [2, 4, 6]`?這樣的檢查,但是這總是不像我們想象的那樣工作。
#### 枚舉(Enumeration)
先前,在學習?`enumerable`?屬性描述符性質時,我們簡單地解釋了"可枚舉性(enumerability)"的含義。現在,讓我們來更加詳細地重新講解它。
```source-js
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
```
你會注意到,`myObject.b`?實際上?存在,而且擁有可以訪問的值,但是它不出現在?`for..in`?循環中(然而令人詫異的是,它的?`in`?操作符的存在性檢查通過了)。這是因為 “enumerable” 基本上意味著“如果對象的屬性被迭代時會被包含在內”。
注意:?將?`for..in`?循環實施在數組上可能會給出意外的結果,因為枚舉一個數組將不僅包含所有的數字下標,還包含所有的可枚舉屬性。所以一個好主意是:將?`for..in`?循環?*僅*?用于對象,而為存儲在數組中的值使用傳統的?`for`?循環并用數字索引迭代。
另一個可以區分可枚舉和不可枚舉屬性的方法是:
```source-js
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(..)`?測試一個給定的屬性名是否直?*接存*?在于對象上,并且是?`enumerable:true`。
`Object.keys(..)`?返回一個所有可枚舉屬性的數組,而?`Object.getOwnPropertyNames(..)`?返回一個?*所有*?屬性的數組,不論能不能枚舉。
`in`?和?`hasOwnProperty(..)`?區別于它們是否查詢?`[[Prototype]]`?鏈,而?`Object.keys(..)`?和?`Object.getOwnPropertyNames(..)`?都?*只*?考察直接給定的對象。
(當下)沒有與?`in`?操作符的查詢方式(在整個?`[[Prototype]]`?鏈上遍歷所有的屬性,如我們在第五章解釋的)等價的、內建的方法可以得到一個?所有屬性?的列表。你可以近似地模擬一個這樣的工具:遞歸地遍歷一個對象的?`[[Prototype]]`?鏈,在每一層都從?`Object.keys(..)`?中取得一個列表——僅包含可枚舉屬性。
## 迭代(Iteration)
`for..in`?循環迭代一個對象上(包括它的?`[[Prototype]]`?鏈)所有的可迭代屬性。但如果你想要迭代值呢?
在數字索引的數組中,典型的迭代所有的值的辦法是使用標準的?`for`?循環,比如:
```source-js
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 1 2 3
```
但是這并沒有迭代所有的值,而是迭代了所有的下標,然后由你使用索引來引用值,比如?`myArray[i]`。
ES5 還為數組加入了幾個迭代幫助方法,包括?`forEach(..)`、`every(..)`、和?`some(..)`。這些幫助方法的每一個都接收一個回調函數,這個函數將施用于數組中的每一個元素,僅在如何響應回調的返回值上有所不同。
`forEach(..)`?將會迭代數組中所有的值,并且忽略回調的返回值。`every(..)`?會一直迭代到最后,*或者*?當回調返回一個?`false`(或“falsy”)值,而?`some(..)`?會一直迭代到最后,*或者*?當回調返回一個?`true`(或“truthy”)值。
這些在?`every(..)`?和?`some(..)`?內部的特殊返回值有些像普通?`for`?循環中的?`break`?語句,它們可以在迭代執行到末尾之前將它結束掉。
如果你使用?`for..in`?循環在一個對象上進行迭代,你也只能間接地得到值,因為它實際上僅僅迭代對象的所有可枚舉屬性,讓你自己手動地去訪問屬性來得到值。
注意:?與以有序數字的方式(`for`?循環或其他迭代器)迭代數組的下標比較起來,迭代對象屬性的順序是?不確定?的,而且可能會因 JS 引擎的不同而不同。對于需要跨平臺環境保持一致的問題,不要依賴?觀察到的順序,因為這個順序是不可靠的。
但是如果你想直接迭代值,而不是數組下標(或對象屬性)呢?ES6 加入了一個有用的?`for..of`?循環語法,用來迭代數組(和對象,如果這個對象有定義的迭代器):
```source-js
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
```
`for..of`?循環要求被迭代的?*東西*?提供一個迭代器對象(從一個在語言規范中叫做?`@@iterator`?的默認內部函數那里得到),每次循環都調用一次這個迭代器對象的?`next()`?方法,循環迭代的內容就是這些連續的返回值。
數組擁有內建的?`@@iterator`,所以正如展示的那樣,`for..of`?對于它們很容易使用。但是讓我們使用內建的?`@@iterator`?來手動迭代一個數組,來看看它是怎么工作的:
```source-js
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 }
```
注意:?我們使用一個 ES6 的?`Symbol`:`Symbol.iterator`?來取得一個對象的?`@@iterator`?*內部屬性*。我們在本章中簡單地提到過?`Symbol`?的語義(見“計算型屬性名”),同樣的原理也適用于這里。你總是希望通過?`Symbol`?名稱,而不是它可能持有的特殊的值,來引用這樣特殊的屬性。另外,盡管這個名稱有這樣的暗示,但?`@@iterator`?本身?不是迭代器對象, 而是一個返回迭代器對象的?方法?—— 一個重要的細節!
正如上面的代碼段揭示的,迭代器的?`next()`?調用的返回值是一個?`{ value: .. , done: .. }`?形式的對象,其中?`value`?是當前迭代的值,而?`done`?是一個?`boolean`,表示是否還有更多內容可以迭代。
注意值?`3`?和?`done:false`?一起返回,猛地一看會有些奇怪。你不得不第四次調用?`next()`(在前一個代碼段的?`for..of`?循環會自動這樣做)來得到?`done:true`,以使自己知道迭代已經完成。這個怪異之處的原因超出了我們要在這里討論的范圍,但是它源自于 ES6 生成器(generator)函數的語義。
雖然數組可以在?`for..of`?循環中自動迭代,但普通的對象?沒有內建的?`@@iterator`。這種故意省略的原因要比我們將在這里解釋的更復雜,但一般來說,為了未來的對象類型,最好不要加入那些可能最終被證明是麻煩的實現。
但是?*可以*?為你想要迭代的對象定義你自己的默認?`@@iterator`。比如:
```source-js
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)
};
}
};
}
} );
// 手動迭代 `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`(很大程度上是因為我們可以將它指定為不可枚舉的),但是通過將?`Symbol`?作為一個?*計算型屬性名*(在本章前面的部分討論過),我們也可以直接聲明它,比如?`var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }`。
每次?`for..of`?循環在?`myObject`?的迭代器對象上調用?`next()`?時,迭代器內部的指針將會向前移動并返回對象屬性列表的下一個值(關于對象屬性/值迭代順序,參照前面的注意事項)。
我們剛剛演示的迭代,是一個簡單的一個值一個值的迭代,當然你可以為你的自定義數據結構定義任意復雜的迭代方法,只要你覺得合適。對于操作用戶自定義對象來說,自定義迭代器與 ES6 的?`for..of`?循環相組合,是一個新的強大的語法工具。
舉個例子,一個?`Pixel(像素)`?對象列表(擁有?`x`?和?`y`?的坐標值)可以根據距離原點?`(0,0)`?的直線距離決定它的迭代順序,或者過濾掉那些“太遠”的點,等等。只要你的迭代器從?`next()`?調用返回期望的?`{ value: .. }`?返回值,并在迭代結束后返回一個?`{ done: true }`?值,ES6 的?`for..of`?循環就可以迭代它。
其實,你甚至可以生成一個永遠不會“結束”,并且總會返回一個新值(比如隨機數,遞增值,唯一的識別符等等)的“無窮”迭代器,雖然你可能不會將這樣的迭代器用于一個沒有邊界的?`for..of`?循環,因為它永遠不會結束,而且會阻塞你的程序。
```source-js
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;
}
```
這個迭代器會“永遠”生成隨機數,所以我們小心地僅從中取出 100 個值,以使我們的程序不被阻塞。
## 復習
JS 中的對象擁有字面形式(比如?`var a = { .. }`)和構造形式(比如?`var a = new Array(..)`)。字面形式幾乎總是首選,但在某些情況下,構造形式提供更多的構建選項。
許多人聲稱“Javascript 中的一切都是對象”,這是不對的。對象是六種(或七中,看你從哪個方面說)基本類型之一。對象有子類型,包括?`function`,還可以被行為特化,比如?`[object Array]`?作為內部的標簽表示子類型數組。
對象是鍵/值對的集合。通過?`.propName`?或?`["propName"]`?語法,值可以作為屬性訪問。不管屬性什么時候被訪問,引擎實際上會調用內部默認的?`[[Get]]`?操作(在設置值時調用?`[[Put]]`?操作),它不僅直接在對象上查找屬性,在沒有找到時還會遍歷?`[[Prototype]]`?鏈(見第五章)。
屬性有一些可以通過屬性描述符控制的特定性質,比如?`writable`?和?`configurable`。另外,對象擁有它的不可變性(它們的屬性也有),可以通過使用?`Object.preventExtensions(..)`、`Object.seal(..)`、和?`Object.freeze(..)`?來控制幾種不同等級的不可變性。
屬性不必非要包含值 —— 它們也可以是帶有 getter/setter 的“訪問器屬性”。它們也可以是可枚舉或不可枚舉的,這控制它們是否會在?`for..in`?這樣的循環迭代中出現。
你也可以使用 ES6 的?`for..of`?語法,在數據結構(數組,對象等)中迭代?值,它尋找一個內建或自定義的?`@@iterator`?對象,這個對象由一個?`next()`?方法組成,通過這個?`next()`?方法每次迭代一個數據。