浅析JavaScript中in操作符(for in/for of)、Object.keys()和Object.getOwnPropertyNames()的区别
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输出数组中包括不存在的值在内的所有值