## 2.3 對象
### 2.3.1 語法
定義對象方法:
* 文字聲明形式(常用)
~~~
var myObj = {
key: value
// ...
};
~~~
* 構造形式
~~~
var myObj = new Object();
myObj.key = value;
~~~
構造形式和文字形式生成的對象是一樣的。唯一的區別是,在文字聲明中你可以添加多個鍵/ 值對,但是在構造形式中你必須逐個添加屬性。
### 2.3.2 類型
對象是JavaScript 的基礎。在JavaScript 中一共有六種主要類型(術語是“**語言類型**”):
~~~
? string
? number
? boolean
? null
? undefined
? object
~~~
注意,**簡單基本類型(string、boolean、number、null 和undefined)本身并不是對象。**null 有時會被當作一種對象類型,但是這其實只是語言本身的一個bug,即對null 執行typeof null 時會返回字符串"object"。實際上,null 本身是基本類型。
#### 內置對象
JavaScript 中還有一些對象子類型,通常被稱為**內置對象**。有些內置對象的名字看起來和簡單基礎類型一樣,不過實際上它們的關系更復雜。
~~~
? String
? Number
? Boolean
? Object
? Function
? Array
? Date
? RegExp
? Error
~~~
這些內置對象從表現形式來說很像其他語言中的類型(type)或者類(class),比如Java中的String 類。
但是在JavaScript 中,它們實際上只是一些內置函數。這些內置函數可以當作構造函數(由new 產生的函數調用來使用,從而可以構造一個對應子類型的新對象。
~~~
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
// 檢查sub-type 對象
Object.prototype.toString.call( strObject ); // [object String]
~~~
### 2.3.3 內容
對象的內容是由一些存儲在特定命名位置的(任意類型的)值組成的,稱之為**屬性**。
在引擎內部,屬性的值的存儲方式是多種多樣的,一般并不會存在對象容器內部。存儲在對象容器內部的是這些屬性的名稱,它們就像指針(從技術角度來說就是引用)一樣,指向這些值真正的存儲位置。
~~~
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
~~~
如果要訪問myObject 中a 位置上的值,我們需要使用` . `操作符或者` []` 操作符。`.a` 語法通常被稱為“**屬性訪問**”,`["a"]` 語法通常被稱為“**鍵訪問**”。
這兩種語法的主要區別在于`. `操作符要求屬性名滿足標識符的命名規范,而`[".."] `語法可以接受任意UTF-8/Unicode 字符串作為屬性名。舉例來說,如果要引用名稱為"Super-Fun!" 的屬性,那就必須使用["Super-Fun!"] 語法訪問,因為Super-Fun! 并不是一個有效的標識符屬性名。
此外,由于`[".."]` 語法使用字符串來訪問屬性,所以可以在程序中構造這個字符串。
~~~
var myObject = {
a:2
};
var idx;
if (wantA) {
idx = "a";
}
// 之后
console.log( myObject[idx] ); // 2
~~~
在對象中,屬性名永遠都是字符串。如果你使用string(字面量)以外的其他值作為屬性名,那它首先會被轉換為一個字符串。即使是數字也不例外,雖然在數組下標中使用的的確是數字,但是在對象屬性名中數字會被轉換成字符串,所以當心不要搞混對象和數組中數字的用法:
~~~
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
~~~
**1. 可計算屬性名**
ES6 增加了可計算屬性名,可以在文字形式中使用[] 包裹一個表達式來當作屬性名:
~~~
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
~~~
可計算屬性名最常用的場景可能是ES6 的符號(Symbol),它們是一種新的基礎數據類型,包含一個不透明且無法預測的值(從技術角度來說就是一個字符串)。
**2. 屬性與方法**
由于函數很容易被認為是屬于某個對象,在其他語言中,屬于對象(也被稱為“類”)的函數通常被稱為“方法”,因此把“屬性訪問”說成是“方法訪問”。
JavaScript 的語法規范也做出了同樣的區分。從技術角度來說,函數永遠不會“屬于”一個對象,所以把對象內部引用的函數稱為“方法”不妥。
無論返回值是什么類型,每次訪問對象的屬性就是**屬性訪問**。如果屬性訪問返回的是一個函數,那它也并不是一個“方法”。屬性訪問返回的函數和其他函數沒有任何區別(除了可能發生的隱式綁定this)。
~~~
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 中的this 會被隱式綁定到一個對象。無論哪種引用形式都不能稱之為“方法”。
即使你在對象的文字形式中聲明一個函數表達式,這個函數也不會“屬于”這個對象——它們只是對于相同函數對象的多個引用。
~~~
var myObject = {
foo: function() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
~~~
**3. 數組**
數組也支持[] 訪問形式,數組期望的是數值下標,也就是說值存儲的位置(通常被稱為索引)是整數。
~~~
var myArray = [ "foo", 42, "bar" ];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
~~~
數組也是對象,所以雖然每個下標都是整數,你仍然可以給數組添加屬性:
~~~
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
~~~
可以看到雖然添加了命名屬性(無論是通過. 語法還是[] 語法),數組的length 值并未發生變化。
注意:如果你試圖向數組添加一個屬性,但是屬性名“看起來”像一個數字,那它會變成一個數值下標(因此會修改數組的內容而不是添加一個屬性):
~~~
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
~~~
**4. 復制對象**
~~~
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是復本!
c: anotherArray, // 另一個引用!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );
~~~
**如何準確地表示myObject 的復制呢?**
首先,我們應該判斷它是淺復制還是深復制。對于淺拷貝來說,復制出的新對象中a 的值會復制舊對象中a 的值,也就是2,但是新對象中b、c、d 三個屬性其實只是三個引用,它們和舊對象中b、c、d 引用的對象是一樣的。對于深復制來說,除了復制`myObject` 以外還會復制`anotherObject` 和`anotherArray`。這時問題就來了,`anotherArray` 引用了`anotherObject` 和`myObject`,所以又需要復制`myObject`,這樣就會由于循環引用導致死循環。
我們是應該檢測循環引用并終止循環(不復制深層元素)?還是應當直接報錯或者是選擇其他方法?
對于JSON 安全(也就是說可以被序列化為一個JSON 字符串并且可以根據這個字符串解析出一個結構和值完全一樣的對象)的對象來說,有一種巧妙的復制方法:
~~~
var newObj = JSON.parse( JSON.stringify( someObj ) );
~~~
當然,這種方法需要保證對象是JSON 安全的,所以只適用于部分情況。
ES6 定義了`Object.assign(..) `方法來實現**淺復制**。`Object.assign(..) `方法的第一個參數是目標對象,之后還可以跟一個或多個源對象。它會遍歷一個或多個源對象的所有可枚舉(enumerable)的自有鍵(owned key)并把它們復制(使用 = 操作符賦值)到目標對象,最后返回目標對象,就像這樣:
~~~
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
~~~
注意:由于Object.assign(..) 就是使用= 操作符來賦值,所以源對象屬性的一些特性(比如writable)不會被復制到目標對象。
**5. 屬性描述符**
從ES5 開始,所有的屬性都具備了屬性描述符,用于檢測屬性特性。
~~~
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
~~~
這個普通的對象屬性對應的屬性描述符(也被稱為“數據描述符”,因為它只保存一個數據值)可不僅僅只是一個2。它還包含另外三個特性:`writable`(可寫)、`enumerable`(可枚舉)和`configurable`(可配置)。
在創建普通屬性時屬性描述符會使用默認值,也可以使用`Object.defineProperty(..)`來添加一個新屬性或者修改一個已有屬性(如果它是configurable)并對特性進行設置。
~~~
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
~~~
使用defineProperty(..) 給myObject 添加了一個普通的屬性并顯式指定了一些特性。然而,一般不會使用這種方式,除非你想修改屬性描述符。
* Writable : 決定是否可以修改屬性的值
~~~
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
~~~
對于屬性值的修改靜默失敗(silently failed)了。如果在嚴格模式下,這種方法會出錯:
~~~
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
~~~
TypeError 錯誤表示無法修改一個不可寫的屬性。
* Configurable : 只要屬性是可配置的,就可以使用`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(..) 會產生一個TypeError 錯誤,不管是不是處于嚴格模式,嘗試修改一個不可配置的屬性描述符都會出錯。注意:把configurable 修改成false 是**單向操作,無法撤銷**!即便屬性是`configurable:false`, 我們還是可以把writable 的狀態由true 改為false,但是無法由false 改為true。
除了無法修改,configurable:false 還會禁止刪除這個屬性:
~~~
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 語句(靜默)失敗了,因為屬性是不可配置的。
在本例中,delete 只用來直接刪除對象的(可刪除)屬性。如果對象的某個屬性是某個對象/ 函數的最后一個引用者,對這個屬性執行delete 操作之后,這個未引用的對象/ 函數就可以被垃圾回收。但是,不要把delete 看作一個釋放內存的工具(就像C/C++ 中那樣),它就是一個刪除對象屬性的操作,僅此而已。
* Enumerable : 控制屬性是否會出現在對象的屬性枚舉中
比如說for..in 循環。如果把enumerable 設置成false,這個屬性就不會出現在枚舉中,雖然仍然可以正常訪問它。相對地,設置成true 就會讓它出現在枚舉中。
用戶定義的所有的普通屬性默認都是enumerable,這通常就是你想要的。但是如果你不希望某些特殊屬性出現在枚舉中,那就把它設置成enumerable:false。
**6. 不變性**
有時候你會希望**屬性或者對象是不可改變**(無論有意還是無意)的,在ES5 中可以通過很多種方法來實現。
所有的方法創建的都是淺不變形,也就是說,它們只會影響目標對象和它的直接屬性。如果目標對象引用了其他對象(數組、對象、函數,等),其他對象的內容不受影響,仍然是可變的:
~~~
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
~~~
假設代碼中的myImmutableObject 已經被創建而且是不可變的,但是為了保護它的內容myImmutableObject.foo,你還需要使用下面的方法讓foo 也不可變。
* 對象常量
結合`writable:false` 和`configurable:false` 就可以創建一個真正的常量屬性(不可修改、重定義或者刪除):
~~~
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
~~~
* 禁止擴展
如果你想禁止一個對象添加新屬性并且保留已有屬性, 可以使用`Object.preventExtensions(..)`:
~~~
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
~~~
在非嚴格模式下,創建屬性b 會靜默失敗。在嚴格模式下,將會拋出TypeError 錯誤。
* 密封
`Object.seal(..) `會創建一個“密封”的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(..) 并把所有現有屬性標記為`configurable:false`。所以,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現有屬性(雖然可以修改屬性的值)。
* 凍結
`Object.freeze(..) `會創建一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal(..) 并把所有“數據訪問”屬性標記為`writable:false`,這樣就無法修改它們的值。
這個方法是你可以應用在對象上的級別最高的不可變性,它會禁止對于對象本身及其任意直接屬性的修改(但這個對象引用的其他對象是不受影響的)。
你可以“深度凍結”一個對象,具體方法為,首先在這個對象上調用Object.freeze(..),然后遍歷它引用的所有對象并在這些對象上調用Object.freeze(..)。但是一定要小心,因為這樣做有可能會在無意中凍結其他(共享)對象。
**7. [[get]]**
屬性訪問在實現時有一個微妙卻非常重要的細節,思考下面的代碼:
~~~
var myObject = {
a: 2
};
myObject.a; // 2
~~~
在語言規范中,myObject.a 在myObject 上實際上是實現了`[[Get]]` 操作(有點像函數調用:`[[Get]]()`)。對象默認的內置[[Get]] 操作首先在對象中查找是否有名稱相同的屬性,如果找到就會返回這個屬性的值。
然而,如果沒有找到名稱相同的屬性,按照[[Get]] 算法的定義會執行另外一種非常重要的行為。(其實就是遍歷可能存在的[[Prototype]] 鏈,也就是原型鏈)。
如果無論如何都沒有找到名稱相同的屬性,那[[Get]] 操作會返回值undefined:
~~~
var myObject = {
a:2
};
myObject.b; // undefined
~~~
注意,這種方法和訪問變量時是不一樣的。如果你引用了一個當前詞法作用域中不存在的變量,并不會像對象屬性一樣返回undefined,而是會拋出一個ReferenceError 異常:
~~~
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
~~~
從返回值的角度來說,這兩個引用沒有區別——它們都返回了undefined。然而,盡管乍看之下沒什么區別,實際上底層的[[Get]] 操作對`myObject.b` 進行了更復雜的處理。
由于僅根據返回值無法判斷出到底變量的值為undefined 還是變量不存在,所以[[Get]]操作返回了undefined。
**8. [[put]]**
[[Put]] 被觸發時,實際的行為取決于許多因素,包括對象中是否已經存在這個屬性(這是最重要的因素)。
如果已經存在這個屬性,[[Put]] 算法大致會檢查下面這些內容。
* 屬性是否是訪問描述符(下面第9點)?如果是并且存在setter 就調用setter。
* 屬性的數據描述符中writable 是否是false ?如果是,在非嚴格模式下靜默失敗,在嚴格模式下拋出TypeError 異常。
* 如果都不是,將該值設置為屬性的值。
**9. Getter和Setter**
對象默認的[[Put]] 和[[Get]] 操作分別可以控制屬性值的設置和獲取。
在ES5 中可以使用getter 和setter 部分改寫默認操作,但是只能應用在單個屬性上,無法應用在整個對象上。getter 是一個隱藏函數,會在獲取屬性值時調用。setter 也是一個隱藏函數,會在設置屬性值時調用。
當你給一個屬性定義getter、setter 或者兩者都有時,這個屬性會被定義為“**訪問描述符**”(和“數據描述符”相對)。對于訪問描述符來說,JavaScript 會忽略它們的value 和writable 特性,取而代之的是關心set 和get(還有configurable 和enumerable)特性。
~~~
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(..) 中的顯式定義,二者都會在對象中創建一個不包含值的屬性,對于這個屬性的訪問會自動調用一個隱藏函數,它的返回值會被當作屬性訪問的返回值:
~~~
var myObject = {
// 給 a 定義一個getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
~~~
由于只定義了a 的getter,所以對a 的值進行設置時set 操作會忽略賦值操作,不會拋出錯誤。而且即便有合法的setter,由于自定義的getter 只會返回2,所以set 操作是沒有意義的。
為了讓屬性更合理,還應當定義setter,setter 會覆蓋單個屬性默認的[[Put]](也被稱為賦值)操作。通常來說getter 和setter 是成對出現的(只定義一個的話通常會產生意料之外的行為):
~~~
var myObject = {
// 給 a 定義一個getter
get a() {
return this._a_;
},
// 給 a 定義一個setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
~~~
**10. 存在性**
前面說過,如myObject.a 的屬性訪問返回值可能是undefined,但是這個值有可能是屬性中存儲的undefined,也可能是因為屬性不存在所以返回undefined。那么如何區分這兩種情況呢?
~~~
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]] 鏈。
所有的普通對象都可以通過對于Object.prototype 的委托 來訪問hasOwnProperty(..), 但是有的對象可能沒有連接到Object.prototype( 通過Object.create(null) 來創建)。在這種情況下,形如myObejct.hasOwnProperty(..)就會失敗。
這時可以使用一種更加強硬的方法來進行判斷:Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基礎的hasOwnProperty(..) 方法并把它顯式綁定到myObject 上。
* 枚舉
~~~
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 操作符來判斷是否存在)。原因是“可枚舉”就相當于“可以出現在對象屬性的遍歷中”。
~~~
在數組上應用for..in 循環有時會產生出人意料的結果,因為這種枚舉不僅會包含所有數值索引,還會包含所有可枚舉屬性。
最好只在對象上應用for..in 循環,如果要遍歷數組就使用傳統的for 循環來遍歷數值索引。
~~~
也可以通過另一種方式來區分屬性是否可枚舉:
~~~
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(..) 得到的屬性列表——只包含可枚舉屬性。
### 2.3.4 遍歷
對于數值索引的數組來說,可以使用標準的for 循環來遍歷值:
~~~
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(或者“假”值),`some(..) `會一直運行直到回調函數返回true(或者“真”值)。
every(..) 和some(..) 中特殊的返回值和普通for 循環中的break 語句類似,它們會提前終止遍歷。
使用for..in 遍歷對象是無法直接獲取屬性值的,因為它實際上遍歷的是對象中的所有可枚舉屬性,你需要手動獲取屬性值。
那么如何直接遍歷值而不是數組下標(或者對象屬性)呢?ES6 增加了一種用來遍歷數組的`for..of `循環語法(如果對象本身定義了迭代器的話也可以遍歷對象):
~~~
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
~~~
`for..of` 循環首先會向被訪問對象請求一個迭代器對象,然后通過調用迭代器對象的next() 方法來遍歷所有返回值。
`for..of`會尋找內置或者自定義的@@iterator 對象并調用它的next() 方法來遍歷數據值。
我們使用內置的@@iterator 來手動遍歷數組,看看它是怎么工作的:
~~~
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.iterator` 來獲取對象的`@@iterator` 內部屬性。`@@iterator` 本身并不是一個迭代器對象,而是一個返回迭代器對象的函數。
和數組不同,普通的對象沒有內置的`@@iterator`,所以無法自動完成for..of 遍歷。
當然,你可以給任何想遍歷的對象定義`@@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)
};
}
};
}
} );
// 手動遍歷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
~~~
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數