注意: JavaScript 中數組不是 關聯數組。 JavaScript 中只有對象 來管理鍵值的對應關系。但是關聯數組是保持順序的,而對象不是。
由於 for in 循環會枚舉原型鏈上的所有屬性,唯一過濾這些屬性的方式是使用 `hasOwnProperty` 函數,因此會比普通的 for 循環慢上好多倍。
遍歷(Iteration)
為了達到遍歷數組的最佳性能,推薦使用經典的 for 循環。
復制代碼 代碼如下:
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
上面代碼有一個處理,就是通過 l = list.length 來緩存數組的長度。
雖然 length 是數組的一個屬性,但是在每次循環中訪問它還是有性能開銷。 可能最新的 JavaScript 引擎在這點上做了優化,但是我們沒法保證自己的代碼是否運行在這些最近的引擎之上。
實際上,不使用緩存數組長度的方式比緩存版本要慢很多。
`length` 屬性(The `length` property)
length 屬性的 getter 方式會簡單的返回數組的長度,而 setter 方式會截斷數組。
復制代碼 代碼如下:
var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]
foo.length = 6;
foo; // [1, 2, 3]
譯者注:在 Firebug 中查看此時 foo 的值是: [1, 2, 3, undefined, undefined, undefined] 但是這個結果並不准確,如果你在 Chrome 的控制台查看 foo 的結果,你會發現是這樣的: [1, 2, 3] 因為在 JavaScript 中 undefined 是一個變量,注意是變量不是關鍵字,因此上面兩個結果的意義是完全不相同的。
// 譯者注:為了驗證,我們來執行下面代碼,看序號 5 是否存在於 foo 中。
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 true
為 length 設置一個更小的值會截斷數組,但是增大 length 屬性值不會對數組產生影響。
結論(In conclusion)
為了更好的性能,推薦使用普通的 for 循環並緩存數組的 length 屬性。使用 for in 遍歷數組被認為是不好的代碼習慣並傾向於產生錯誤和導致性能問題。
Array 構造函數
由於 Array 的構造函數在如何處理參數時有點模稜兩可,因此總是推薦使用數組的字面語法 - [] - 來創建數組。
[1, 2, 3]; // 結果: [1, 2, 3]
new Array(1, 2, 3); // 結果: [1, 2, 3]
[3]; // 結果: [3]
new Array(3); // 結果: []
new Array('3') // 結果: ['3']
譯者注:這裡的模稜兩可指的是數組的兩種構造函數語法 var arr1 = new Array(arrayLength); var arr2 = new Array(element0, element1, ..., elementN);
// 譯者注:因此下面的代碼將會使人很迷惑
new Array(3, 4, 5); // 結果: [3, 4, 5]
new Array(3) // 結果: [],此數組長度為 3
由於只有一個參數傳遞到構造函數中(譯者注:指的是 new Array(3); 這種調用方式),並且這個參數是數字,構造函數會返回一個 length 屬性被設置為此參數的空數組。需要特別注意的是,此時只有 length 屬性被設置,真正的數組並沒有生成。譯者注:在 Firebug 中,你會看到 [undefined, undefined, undefined],這其實是不對的。在上一節有詳細的分析。
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 數組還沒有生成
這種優先於設置數組長度屬性的做法只在少數幾種情況下有用,比如需要循環字符串,可以避免 for 循環的麻煩。
new Array(count + 1).join(stringToRepeat);
// 譯者注:new Array(3).join('#') 將會返回 "##"
結論(In conclusion)
應該盡量避免使用數組構造函數創建新數組。推薦使用數組的字面語法。它們更加短小和簡潔,因此增加了代碼的可讀性。
for in 循環
和 in 操作符一樣,for in 循環同樣在查找對象屬性時遍歷原型鏈上的所有屬性。
注意: for in 循環不會遍歷那些 enumerable 設置為 false 的屬性;比如數組的 length 屬性。
復制代碼 代碼如下:
// 修改 Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // 輸出兩個屬性:bar 和 moo
}
由於不可能改變 for in 自身的行為,因此有必要過濾出那些不希望出現在循環體中的屬性,這可以通過 Object.prototype 原型上的 `hasOwnProperty` 函數來完成。
注意: 由於 for in 總是要遍歷整個原型鏈,因此如果一個對象的繼承層次太深的話會影響性能。
使用 `hasOwnProperty` 過濾(Using `hasOwnProperty` for filtering)
復制代碼 代碼如下:
// foo 變量是上例中的
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
這個版本的代碼是唯一正確的寫法。由於我們使用了 hasOwnProperty,所以這次只輸出 moo。如果不使用 hasOwnProperty,則這段代碼在原生對象原型(比如 Object.prototype)被擴展時可能會出錯。
一個廣泛使用的類庫 Prototype 就擴展了原生的 JavaScript 對象。因此,但這個類庫被包含在頁面中時,不使用 hasOwnProperty 過濾的 for in 循環難免會出問題。
最佳實踐(Best practices)
推薦總是使用 hasOwnProperty。不要對代碼運行的環境做任何假設,不要假設原生對象是否已經被擴展了。
typeof 操作符
typeof 操作符(和 `instanceof` 一起)或許是 JavaScript 中最大的設計缺陷,因為幾乎不可能從它們那裡得到想要的結果。
盡管 instanceof 還有一些極少數的應用場景,typeof 只有一個實際的應用(譯者注:這個實際應用是用來檢測一個對象是否已經定義或者是否已經賦值),而這個應用卻不是用來檢查對象的類型。
注意: 由於 typeof 也可以像函數的語法被調用,比如 typeof(obj),但這並是一個函數調用。那兩個小括號只是用來計算一個表達式的值,這個返回值會作為 typeof 操作符的一個操作數。實際上不存在名為 typeof 的函數。
JavaScript 類型表格(The JavaScript type table) Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
上面表格中,Type 一列表示 typeof 操作符的運算結果。可以看到,這個值在大多數情況下都返回 "object"。
Class 一列表示對象的內部屬性 [[Class]] 的值。
JavaScript 標准文檔中定義: [[Class]] 的值只可能是下面字符串中的一個: Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String.
為了獲取對象的 [[Class]],我們需要使用定義在 Object.prototype 上的方法 toString。
對象的類定義(The Class of an object)
JavaScript 標准文檔只給出了一種獲取 [[Class]] 值的方法,那就是使用 Object.prototype.toString。
復制代碼 代碼如下:
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
is('String', 'test'); // true
is('String', new String('test')); // true
上面例子中,Object.prototype.toString 方法被調用,this 被設置為了需要獲取 [[Class]] 值的對象。
譯者注:Object.prototype.toString 返回一種標准格式字符串,所以上例可以通過 slice 截取指定位置的字符串,如下所示:
復制代碼 代碼如下:
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(2) // "[object Number]"
ES5 提示: 在 ECMAScript 5 中,為了方便,對 null 和 undefined 調用 Object.prototype.toString 方法,其返回值由 Object 變成了 Null 和 Undefined。
譯者注:這種變化可以從 IE8 和 Firefox 4 中看出區別,如下所示:
復制代碼 代碼如下:
// IE8
Object.prototype.toString.call(null) // "[object Object]"
Object.prototype.toString.call(undefined) // "[object Object]"
// Firefox 4
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
測試為定義變量(Testing for undefined variables)
typeof foo !== 'undefined'
上面代碼會檢測 foo 是否已經定義;如果沒有定義而直接使用會導致 ReferenceError 的異常。這是 typeof 唯一有用的地方。
結論(In conclusion)
為了檢測一個對象的類型,強烈推薦使用 Object.prototype.toString 方法;因為這是唯一一個可依賴的方式。正如上面表格所示,typeof 的一些返回值在標准文檔中並未定義,因此不同的引擎實現可能不同。
除非為了檢測一個變量是否已經定義,我們應盡量避免使用 typeof 操作符。
instanceof 操作符
instanceof 操作符用來比較兩個操作數的構造函數。只有在比較自定義的對象時才有意義。如果用來比較內置類型,將會和 typeof 操作符 一樣用處不大。
比較自定義對象(Comparing custom objects)
復制代碼 代碼如下:
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// 如果僅僅設置 Bar.prototype 為函數 Foo 本省,而不是 Foo 構造函數的一個實例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
`instanceof` 比較內置類型(Using `instanceof` with native types)
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true
'foo' instanceof String; // false
'foo' instanceof Object; // false
有一點需要注意,instanceof 用來比較屬於不同 JavaScript 上下文的對象(比如,浏覽器中不同的文檔結構)時將會出錯,因為它們的構造函數不會是同一個對象。
結論(In conclusion)
instanceof 操作符應該僅僅用來比較來自同一個 JavaScript 上下文的自定義對象。正如 `typeof` 操作符一樣,任何其它的用法都應該是避免的。