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?”我立马上网搜了一下,大致作用也是做遍历,参数是一个对象,返回值是一个数组 ...
随机推荐
- Hadoop生态系统如何选择搭建
Apache Hadoop项目的目前版本(2.0版)含有以下模块: Hadoop通用模块:支持其他Hadoop模块的通用工具集. Hadoop分布式文件系统(HDFS):支持对应用数据高吞吐量访问的分 ...
- Windows环境下载与安装JBOSS服务器的详细图文教程
一.JDK的安装 首先安装JDK,配置环境变量(PATH,CLASSPATH,JAVA_HOME). 可以参照:Windows环境下JDK安装与环境变量配置 二.Jboss的介绍 JBOSS是EJB的 ...
- Linux磁盘管理之日志文件系统和非日志文件系统08
略. 查看linux支持的文件系统命令: ls /lib/module/`uname -r`/x86/fs blkid查看文件系统的类型 mkfs.ext2 == mkfs –t ext2
- Java报表工具FineReport常见的数据集报错错误代码和解释
在使用finereport制作报表,若预览发生错误,很多朋友便手忙脚乱不知所措了,其实没什么,只要看懂报错代码和含义,可以很快的排除错误,这里我就分享一下finereport的数据集报错错误代码和解释 ...
- 学习OpenStack之 (4): Linux 磁盘、分区、挂载、逻辑卷管理 (Logical Volume Manager)
0. 背景: inux用户安装Linux操作系统时遇到的一个常见的难以决定的问题就是如何正确地评估各分区大小,以分配合适的硬盘空间.普通的磁盘分区管理方式在逻辑分区划分好之后就无法改变其大小,当一个逻 ...
- java设计模式之装饰模式
发现设计模式的学习越来越让自己学习的东西太少了,应该多接触一些东西,多出去走一走. 装饰模式概念: 动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活(大话设计模式) 在不 ...
- AC日记——求10000以内n的阶乘 openjudge 1.6 14
14:求10000以内n的阶乘 总时间限制: 5000ms 内存限制: 655360kB 描述 求10000以内n的阶乘. 输入 只有一行输入,整数n(0<=n<=10000). 输出 ...
- 代码覆盖率工具 EMMA
使用 EMMA 获得功能测试覆盖率 测试覆盖率是评价测试完整性的重要的度量标准之一. EMMA 是一个面向 Java 代码的测试覆盖率收集工具.在测试过程中,使用 EMMA 能使收集和报告测试覆盖率的 ...
- ArrayList的线程安全测试
public class TestThread implements Runnable{ private List list; CountDownLatch cdl; public TestThrea ...
- java 28 - 4 JDK5的新特性 之 枚举的概述和自定义枚举类
枚举 枚举概述 是指将变量的值一一列出来,变量的值只限于列举出来的值的范围内.举例:一周只有7天,一年只有12个月等. 回想单例设计模式:单例类是一个类只有一个实例 那么多例类就是一个类有多个实例,但 ...