ECMAScript將對象的屬性分為兩種:數據屬性和訪問器屬性。每一種屬性內部都有一些特性,這里我們只關注對象屬性的[[Enumerable]]特征,它表示是否通過 for-in 循環返回屬性,也可以理解為:是否可枚舉。然后根據具體的上下文環境的不同,我們又可以將屬性分為:原型屬性和實例屬性。原型屬性是定義在對象的原型(prototype)中的屬性,而實例屬性一方面來自己構造函數中,然后就是構造函數實例化后添加的新屬性。
本文主要介紹JavaScript中獲取對象屬性常用到的三種方法的區別和適用場景。
一、for..in循環
使用for..in循環時,返回的是所有能夠通過對象訪問的、可枚舉的屬性,既包括存在於實例中的屬性,也包括存在於原型中的實例。這里需要注意的是使用for-in返回的屬性因各個瀏覽器廠商遵循的標准不一致導致對象屬性遍歷的順序有可能不是當初構建時的順序。(實例+原型中的可枚舉屬性)
1、遍歷數組
雖然for..in主要用於遍歷對象的屬性,但同樣也可以用來遍歷數組元素。
var arr = ['a', 'b', 'c', 'd']; // 使用for..in
for (var i in arr) { console.log('索引:' + i + ',值:' + arr[i]); } // 使用for循環
for (var j = 0; j < arr.length; j++) { console.log('索引:' + j + ',值:' + arr[j]); } /* 兩種方式都輸出: * ---------------- * 索引:0,值:a * 索引:1,值:b * 索引:2,值:c * 索引:3,值:d * ---------------- */
上面這個簡單例子相信大家對輸出沒有任何質疑吧。然而,我在網上看到一些關於for和for..in遍歷數組的文章,比如js中數組遍歷for與for in區別(強烈建議不要使用for in遍歷數組)、[原]js數組遍歷 千萬不要使用for...in...,同時也看了stackoverflow關於Why is using “for…in” with array iteration such a bad idea?的討論。看完后還是雲里霧里的,於是尋根問底,打算自己來研究一下。for..in在數組遍歷方面就那么差強人意嗎?
關於for..in和for遍歷數組的的爭論總結起來主要在三個點。
第一個問題:如果擴展了原生的Array,那么擴展的屬性為什么會被for..in輸出?
這個問題也是上面我提到的兩篇文章關注的重點。其實,這個問題如果我們將關注點放在for..in方法的定義上就不難看出端倪,定義中強調了一點它所遍歷的是可枚舉的屬性。我們在擴展Array原型的時候有去對比自己添加的屬性與Array原生的屬性有什么不一樣的地方嗎?這里我強調的不一致的地方在於屬性其中的一個特性[[enumberable]],在文章開頭也有特意介紹了一下。如何查看一個屬性的特性可以使用propertyIsEnumberable()
和Object.getOwnPropertyDescriptor()
這兩個方法。
var colors = ['red', 'green', 'blue']; // 擴展Array.prototype
Array.prototype.demo = function () {}; for (var i in colors) { console.log(i); // 輸出: 0 1 2 demo
} // 查看原生的方法[[enumberable]]特征,這里以splice為例
Array.prototype.propertyIsEnumerable('splice'); // false
Object.getOwnPropertyDescriptor(Array.prototype, 'splice'); // {writable: true, enumerable: false, configurable: true} // 查看 demo 屬性的特性
Array.prototype.propertyIsEnumerable('demo'); // true
Object.getOwnPropertyDescriptor(Array.prototype, 'demo'); // {writable: true, enumerable: true, configurable: true}
從上面的示例代碼中可以看出,我們添加的demo
方法,默認是可以被for..in枚舉出來的。如果想讓其不被枚舉,那么可以使用ES5的Object.defineProperty()
來定義屬性,此外如果瀏覽器版本不支持ES5的話,我們可以使用hasOwnProperty()
方法在for..in代碼塊內將可枚舉的屬性過濾掉。
var colors = ['red', 'green', 'blue']; Object.defineProperty(Array.prototype, 'demo', { enumerable: false, value: function() {} }); Array.prototype.propertyIsEnumerable('demo'); // false
Object.getOwnPropertyDescriptor(Array.prototype, 'demo'); // {writable: false, enumerable: false, configurable: false}
for (var i in colors) { console.log(i); // 輸出:0 1 2
} // 或者使用 hasOwnProperty
var colors = ['red', 'green', 'blue']; Array.prototype.demo = function() {}; // 安全使用hasOwnProperty方法
var hasOwn = Object.prototype.hasOwnProperty; for (var i in colors) { if (hasOwn.call(colors, i)) { console.log(i); // 輸出:0 1 2
} }
第二問題:for..in和for遍歷數組時下標類型不一樣
這里指的是for (var i in colors) {}
與for (var i = 0; i < colors.length; i++) {}
中的i
,示例如下:
var colors = ['red', 'green', 'blue']; for (var i in colors) { typeof i; // string
} for (var j = 0; j < colors.length; j++) { typoef i; // number
}
至於為什么for..in在遍歷數組時i
為字符串?我的理解是如果我們從對象的視角來看待數組的話,實際上它是一個key為下標,value為數組元素值的對象,比如colors
數組可以寫成下面對象的形式:
var colors = { 0: 'red', 1: 'green', 2: 'blue' }
然后,我們需要訪問colors對象中的屬性,colors.0
這樣顯然會報語法錯識,那么只能使用colors['0']
這種形式了。這可能就是為什么i
的值為字符串,而不是數字的原因。
第三個問題:對於不存在的數組項的處理差異
最后一個問題在於數組中不存在元素的處理。對於數組來講,我們知道如果將其length
屬性設置為大於數組項數的值,則新增的每一項都會取得undefined
值。
var colors = ['red', 'green', 'blue']; // 將數組長度變為10
colors.length = 10; // 再添加一個元素的數組末尾
colors.push('yellow'); for (var i in colors) { console.log(i); // 0 1 2 10
} for (var j = 0; j < colors.length; j++) { console.log(j); // 0 1 2 3 4 5 6 7 8 9 10
}
示例中colors
數組位置3到位置10項實際上都是不存在的。仔細觀察使用for..in遍歷數組的結果,我們發現對於不存在的項是不會被枚舉出來的。通過chrome調式並監聽colors
變量,我們可以看到它的內部結構如下:
|----------------------|
| colors |
|----------------------|
| 0 | 'red' |
|----------------------|
| 1 | 'green' |
|----------------------|
| 2 | 'blue' |
|----------------------|
| 10 | 'yellow' |
|----------------------|
| length | 11 |
|----------------------|
| __proto__ | Array[0] |
|----------------------|
也就是說使用for..in遍歷數組的結果實際上是和它在調試工具中看到的結構是一致的。雖然不存在的元素沒有在調試工具中顯示出來,但是它在內存中是存在的,我們仍然可以刪除這些元素。
var colors = ['red', 'green', 'blue']; colors.length = 10; colors.push('yellow'); // 刪除第4至第10項元素
colors.splice(3, 6); for (var i in colors) { console.log(i); // 輸出:0 1 2 4
}
雖然使用for..in遍歷數組它自動過濾掉了不存在的元素,但是對於存在的元素且值為undefined
或者'null'仍然會有效輸出。此外我們也可以使用in
操作符來判斷某個key值(數組中的索引)是否存在對應的元素。
var colors = ['red', 'green', 'blue']; 1 in colors; // true // 或者
'1' in colors; // true // colors[3]沒有對應的元素
'3' in colors; // false
2、遍歷對象
其實for..in操作的主要目的就是遍歷對象的屬性,如果只需要獲取對象的實例屬性,可以使用hasOwnProperty()
進行過濾。
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function() { return this.name; } // 實例化
var jenemy = new Person('jenemy', 25); for (var prop in Person) { console.log(prop); // name age getName
} var hasOwn = Object.prototype.hasOwnProperty; for (var prop2 in jenemy) { if (hasOwn.call(jenemy, prop2)) { console.log(prop2); // name age
} }
二、Object.keys()
Object.keys()
用於獲取對象自身所有的可枚舉的屬性值,但不包括原型中的屬性,然后返回一個由屬性名組成的數組。注意它同for..in一樣不能保證屬性按對象原來的順序輸出。
// 遍歷數組
var colors = ['red', 'green', 'blue']; colors.length = 10; colors.push('yellow'); Array.prototype.demo = function () {}; Object.keys(colors); // ["0", "1", "2", "10"] // 遍歷對象
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.demo = function() {}; var jenemy = new Person('jenemy', 25); Object.keys(jenemy); // ["name", "age"]
注意在 ES5 環境,如果傳入的參數不是一個對象,而是一個字符串,那么它會報 TypeError。在 ES6 環境,如果傳入的是一個非對象參數,內部會對參數作一次強制對象轉換,如果轉換不成功會拋出 TypeError。
// 在 ES5 環境
Object.keys('foo'); // TypeError: "foo" is not an object // 在 ES6 環境
Object.keys('foo'); // ["0", "1", "2"] // 傳入 null 對象
Object.keys(null); // Uncaught TypeError: Cannot convert undefined or null to object // 傳入 undefined
Object.keys(undefined); // Uncaught TypeError: Cannot convert undefined or null to object
由於Object.keys()
為ES5上的方法,因此對於ES5以下的環境需要進行polyfill
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) { Object.keys = (function() { 'use strict'; var hasOwn = Object.prototype.hasOwnProperty, hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), dontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], dontEnumsLength = dontEnums.length; return function(obj) { if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { throw new TypeError('Object.keys called on non-object'); } var result = [], prop, i; for (prop in obj) { if (hasOwn.call(obj, prop)) { result.push(prop); } } if (hasDontEnumBug) { for (i = 0; i < dontEnumsLength; i++) { if (hasOwn.call(obj, dontEnums[i])) { result.push(dontEnums[i]); } } } return result; } }) (); }
三、Object.getOwnPropertyNames()
Object.getOwnPropertyNames()
方法返回對象的所有自身屬性的屬性名(包括不可枚舉的屬性)組成的數組,但不會獲取原型鏈上的屬性。
function A(a,aa) { this.a = a; this.aa = aa; this.getA = function() { return this.a; } } // 原型方法
A.prototype.aaa = function () {}; var B = new A('b', 'bb'); B.myMethodA = function() {}; // 不可枚舉方法
Object.defineProperty(B, 'myMethodB', { enumerable: false, value: function() {} }); Object.getOwnPropertyNames(B); // ["a", "aa", "getA", "myMethodA", "myMethodB"]
只獲取不可枚舉的屬性:下面的例子使用了 Array.prototype.filter()
方法,從所有的屬性名數組(使用 Object.getOwnPropertyNames() 方法獲得
)中去除可枚舉的屬性(使用 Object.keys()
方法獲得
),剩余的屬性便是不可枚舉的屬性了:
var target = myObject; var enum_and_nonenum = Object.getOwnPropertyNames(target); var enum_only = Object.keys(target); var nonenum_only = enum_and_nonenum.filter(function(key) { var indexInEnum = enum_only.indexOf(key); if (indexInEnum == -1) { // not found in enum_only keys mean the key is non-enumerable, // so return true so we keep this in the filter
return true; } else { return false; } }); console.log(nonenum_only);
在 ES5 中,如果參數不是一個對象類型,將拋出一個 TypeError
異常。在 ES2015 中, non-object 參數被強制轉換為 object 。
Object.getOwnPropertyNames('foo'); // TypeError: "foo" is not an object (ES5 code)
Object.getOwnPropertyNames('foo'); // ['length', '0', '1', '2'] (ES2015 code)
四、補充for..of
1、for..of為ES6新增的方法,主要來遍歷可迭代的對象(包括Array, Map, Set, arguments等),它主要用來獲取對象的屬性值,而for..in主要獲取對象的屬性名。
var colors = ['red', 'green', 'blue']; colors.length = 5; colors.push('yellow'); for (var i in colors) { console.log(colors[i]); // red green blue yellow
} for (var j of colors) { console.log(j); // red green blue undefined undefined yellow
}
可以看到使用for..of可以輸出包括數組中不存在的值在內的所有值。
2、其實除了使用for..of直接獲取屬性值外,我們也可以利用Array.prototype.forEach()
來達到同樣的目的。
var colors = ['red', 'green', 'blue']; colors.foo = 'hello'; console.log(Object.keys(colors));//["0", "1", "2", "foo"]
Object.keys(colors).forEach(function(elem, index) { console.log(elem);//0 1 2 foo
console.log(colors[elem]); // red green blue hello
console.log(colors[index]); // red green blue undefined
}); colors.forEach(function(elem, index) { console.log(elem); // red green blue
console.log(index); // 0 1 2
})
五、總結
其實這幾個方法之間的差異主要在屬性是否可可枚舉,是來自原型,還是實例。
一、1、for in循環。遍歷實例+原型中可枚舉的屬性
2、for in不適合遍歷數組
3、擴展Array原型自己添加的屬性和原生屬性的區別(是否可枚舉)
4、propertyIsEnumberable()和Object.getOwnPropertyDescriptor()這兩個方法
5、Object.defineProperty定義屬性的方法:
Object.defineProperty(Array.prototype,"demo",{
enumerable:false,
value:function(){}
})
6、安全使用hasOwnProperty方法
7、for in遍歷數組的下標類型不一樣 :for in為String,for為Number
8、對不存在項的處理:for in不存在的項不被枚舉出來;for全部枚舉出
9、index in array特性可以判斷某個索引是否存在對應元素
10、遍歷對象可通過hasOwnProperty()過濾掉原型里面的屬性
二、Object.keys(),遍歷實例可枚舉屬性,返回屬性名組成的數組。
三、Object.getOwnPropertyNames(),遍歷實例屬性(包括不可枚舉),返回屬性名組成的數組
四、for of輸出數組中包括不存在的值在內的所有值