JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别
ECMAScript将对象的属性分为两种:数据属性和访问器属性。每一种属性内部都有一些特性,这里我们只关注对象属性的[[Enumerable]]特征,它表示是否通过 for-in 循环返回属性,也可以理解为:是否可枚举。
然后根据具体的上下文环境的不同,我们又可以将属性分为:原型属性和实例属性。原型属性是定义在对象的原型(prototype)中的属性,而实例属性一方面来自己构造函数中,然后就是构造函数实例化后添加的新属性。
本文主要介绍JavaScript中获取对象属性常用到的三种方法的区别和适用场景。
for..in循环
使用for..in循环时,返回的是所有能够通过对象访问的、可枚举的属性,既包括存在于实例中的属性,也包括存在于原型中的实例。这里需要注意的是使用for-in返回的属性因各个浏览器厂商遵循的标准不一致导致对象属性遍历的顺序有可能不是当初构建时的顺序。
遍历数组
虽然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
遍历对象
其实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"]
补充for..of
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可以输出包括数组中不存在的值在内的所有值。
其实除了使用for..of直接获取属性值外,我们也可以利用Array.prototype.forEach()
来达到同样的目的。
var colors = ['red', 'green', 'blue'];
colors.foo = 'hello';
Object.keys(colors).forEach(function(elem, index) {
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
})
总结
其实这几个方法之间的差异主要在属性是否可可枚举,是来自原型,还是实例。
方法 | 适用范围 | 描述 |
---|---|---|
for..in | 数组,对象 | 获取可枚举的实例和原型属性名 |
Object.keys() | 数组,对象 | 返回可枚举的实例属性名组成的数组 |
Object.getPropertyNames() | 数组,对象 | 返回除原型属性以外的所有属性(包括不可枚举的属性)名组成的数组 |
for..of | 可迭代对象(Array, Map, Set, arguments等) | 返回属性值 |
参考
- MDN: Object.getOwnPropertyDescriptor()
- MDN: Object.prototype.propertyIsEnumerable()
- MDN: Object.keys()
- MDN: Object.getOwnPropertyNames()
- MDN: for..in
- MDN: for..of
- Why is using “for…in” with array iteration such a bad idea?
- js中数组遍历for与for in区别(强烈建议不要使用for in遍历数组)
- Javascript 里的 in
- Object.keys(obj) Object.getOwnPropertyNames() for-in
JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别的更多相关文章
- [转] JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别
ECMAScript将对象的属性分为两种:数据属性和访问器属性.每一种属性内部都有一些特性,这里我们只关注对象属性的[[Enumerable]]特征,它表示是否通过 for-in 循环返回属性,也可以 ...
- JavaScript中“&&”和“||”操作符的意义,深入理解和使用场景
一.概念 与其他语言不同,在js中,逻辑运算符可以返回任何类型的数据,不仅仅是true和false. &&和||的返回值是两个操作数的其中一个.即a&&b或者a||b ...
- 全面解析JavaScript中“&&”和“||”操作符(总结篇)
1.||(逻辑或), 从字面上来说,只有前后都是false的时候才返回false,否则返回true. ? 1 2 3 4 alert(true||false); // true alert(false ...
- JavaScript中typeof,instanceof,hasOwnProperty,in的用法和区别
一. typeof操作符 typeof操作符用于返回正在使用值的类型. // 使用原始值 let mNull = null; let mUndefined = undefined; let mStri ...
- 小tips:JS之for in、Object.keys()和Object.getOwnPropertyNames()的区别
for..in循环 使用for..in循环时,返回的是所有能够通过对象访问的.可枚举的属性,既包括存在于实例中的属性,也包括存在于原型中的实例.这里需要注意的是使用for-in返回的属性因各个浏览器厂 ...
- js 遍历对象属性(for in、Object.keys、Object.getOwnProperty) 以及高效地输出 js 数组
js中几种遍历对象的方法,包括for in.Object.keys.Object.getOwnProperty,它们在使用场景方面各有不同. for in 主要用于遍历对象的可枚举属性,包括自有属性. ...
- 遍历对象属性(for in、Object.keys、Object.getOwnProperty)
js中几种遍历对象的方法,包括for in.Object.keys.Object.getOwnProperty,它们在使用场景方面各有不同. for in 主要用于遍历对象的可枚举属性,包括自有属性. ...
- JavaScript中的global对象,window对象以及document对象的区别和联系
JavaScript中的global对象,window对象以及document对象的区别和联系 一.概念区分:JavaScript中的global对象,window对象以及document对象 1.g ...
- 关于Object.keys()和for in的区别
今天见到一道面试题让说一说Object.keys()和for in的区别,顿时有些发懵“What's Object.keys?”我立马上网搜了一下,大致作用也是做遍历,参数是一个对象,返回值是一个数组 ...
随机推荐
- C#委托学习
标签(空格分隔): C# 看Markdown效果支持的不大好. 买来<CLR Via C#>这本书很久了,一直也没有对其进行总结,看的非常凌乱,趁此机会好好总结一下,也算对C#学习的一个总 ...
- Oracle12c安装出错
Database Configuration Assistant安装失败 向广大园友求助
- jstl 中<c:forEach />标签
<c:forEach>标签有如下属性: 属性 描述 是否必要 默认值 items 要被循环的信息 否 无 begin 开始的元素(0=第一个元素,1=第二个元素) 否 0 end 最后一个 ...
- x01.os.7: 傻子一样的等
傻子一样的等 昨日出差,办完事后,下午 2:30,准备进长途汽车站买票回家,被一人拦住,说可以带我进去,车马上就要开了,买票来不及.我以为是汽车司机,就跟了进去.进去后,他打了个电话,说 3:30 车 ...
- android 利用线程刷新UI方法
新建线程new Thread(new Runnable() 线程方法:public void run() private void setAddWidgetEnabled(boolean enable ...
- Linux设置环境变量小结:设置永久变量&临时变量 全局变量&局部变量
1.总结背景 在linux系统下,如果你下载并安装了应用程序,很有可能在键入它的名称时出现“command not found”的提示内容.如果每次都到安装目标文件夹内,找到可执行文件来进行操作就太繁 ...
- 构建 ARM Linux 4.7.3 嵌入式开发环境 —— BusyBox 构建 RootFS
上一篇我们已经成功将 ARM Linux 4.7.3 的内核利用 U-BOOT 引导了起来.但是细心的你会发现,引导到后面,系统无法启动,出现内核恐慌 (Kernel Panic). 原因是找不到文件 ...
- WPF捕获未处理的异常
WPF程序中,对于异常的捕获一般使用try/catch块.就像程序中的bug一样,很难保证程序中所有的异常都能够通过try/catch捕获.如果异常没有被捕获,轻则影响用户体验,严重时会导致数据丢失 ...
- [转]html5 js 访问 sqlite 数据库的操作类
本文转自:http://blog.csdn.net/tsxw24/article/details/7613815 webkit 核心的浏览器提供了 3个 api接口,用于访问本地sqlite数据,但使 ...
- 从WinCE到Linux
到新的公司已经快两个月了,新的工作主要方向是Linux驱动移植和Android系统定制.由于项目还在立项的阶段,并没有分配具体的工作任务,所以找来一个Linux的开发板先玩一玩.它采用的处理器NUC9 ...