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等) 返回属性值

参考

JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别的更多相关文章

  1. [转] JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别

    ECMAScript将对象的属性分为两种:数据属性和访问器属性.每一种属性内部都有一些特性,这里我们只关注对象属性的[[Enumerable]]特征,它表示是否通过 for-in 循环返回属性,也可以 ...

  2. JavaScript中“&&”和“||”操作符的意义,深入理解和使用场景

     一.概念 与其他语言不同,在js中,逻辑运算符可以返回任何类型的数据,不仅仅是true和false. &&和||的返回值是两个操作数的其中一个.即a&&b或者a||b ...

  3. 全面解析JavaScript中“&&”和“||”操作符(总结篇)

    1.||(逻辑或), 从字面上来说,只有前后都是false的时候才返回false,否则返回true. ? 1 2 3 4 alert(true||false); // true alert(false ...

  4. JavaScript中typeof,instanceof,hasOwnProperty,in的用法和区别

    一. typeof操作符 typeof操作符用于返回正在使用值的类型. // 使用原始值 let mNull = null; let mUndefined = undefined; let mStri ...

  5. 小tips:JS之for in、Object.keys()和Object.getOwnPropertyNames()的区别

    for..in循环 使用for..in循环时,返回的是所有能够通过对象访问的.可枚举的属性,既包括存在于实例中的属性,也包括存在于原型中的实例.这里需要注意的是使用for-in返回的属性因各个浏览器厂 ...

  6. js 遍历对象属性(for in、Object.keys、Object.getOwnProperty) 以及高效地输出 js 数组

    js中几种遍历对象的方法,包括for in.Object.keys.Object.getOwnProperty,它们在使用场景方面各有不同. for in 主要用于遍历对象的可枚举属性,包括自有属性. ...

  7. 遍历对象属性(for in、Object.keys、Object.getOwnProperty)

    js中几种遍历对象的方法,包括for in.Object.keys.Object.getOwnProperty,它们在使用场景方面各有不同. for in 主要用于遍历对象的可枚举属性,包括自有属性. ...

  8. JavaScript中的global对象,window对象以及document对象的区别和联系

    JavaScript中的global对象,window对象以及document对象的区别和联系 一.概念区分:JavaScript中的global对象,window对象以及document对象 1.g ...

  9. 关于Object.keys()和for in的区别

    今天见到一道面试题让说一说Object.keys()和for in的区别,顿时有些发懵“What's Object.keys?”我立马上网搜了一下,大致作用也是做遍历,参数是一个对象,返回值是一个数组 ...

随机推荐

  1. 《CLR via C#》读书笔记--基元类型、引用类型和值类型

    编程语言的基元类型 编译器直接支持的数据类型称为基元类型.基元类型直接映射到Framework类库中存在的类型.例如:C#中的int直接映射到System.Int32类型.下表给出了C#基元类型与对应 ...

  2. Java 使用 JRegistry-1.8.1 读取和设置 windows 注册表

    在一个监控相关的Java项目中,需要读取windows系统的注册表,搜索到使用 JRegistery 可以解决.代码如下: /** * @author digdeep@126.com */ publi ...

  3. android开发环境搭建日记和嵌入式Android开发环境初探

    非常感谢博客园的各位,按照你们的博文,还有利用百度和谷歌逐渐建立了android的开发环境,只是给自己备份参考查看,看过的人可以忽略这篇文章. 本文章大部分参考了:http://www.cnblogs ...

  4. Xamarin Android中引用Jar包的方法

    新建一个Java Bingdings Library 将Jar包复制,或使用添加已存在的文件,到Jars文件夹中 确认属性中的“生成操作” 如果有类型转换不正确,请修改Transforms文件夹中的相 ...

  5. MySQL Database on Azure 参数设置

    在使用MySQL过程中,经常会根据需要对MySQL的参数进行一些设置和调整.作为PaaS版本的MySQL,MySQL Database on Azure在参数设置方面有一些限制,客户不能像使用on-p ...

  6. html5语法

    html5语法沿用html语法,但更简单,更人性化. 一.DOCTYPE及字符编码 DOCTYPE:<!doctype html>或者<!DOCTYPE html>因为html ...

  7. POJ 2653 Pick-up sticks【线段相交】

    题意:n根木棍随意摆放在一个平面上,问放在最上面的木棍是哪些. 思路:线段相交,因为题目说最多有1000根在最上面.所以从后往前处理,直到木棍没了或者最上面的木棍的总数大于1000. #include ...

  8. 由于启动用户实例的进程时出错,导致无法生成 SQL Server 的用户实例。该连接将关闭。

    错误:由于启动用户实例的进程时出错,导致无法生成SQL Server的用户实例. 原因:添加安装SQLEXPRESS时,估计装在了不同的目录下: 解决方法:关闭Sqlserver及相关的程序,删除目录 ...

  9. android 下载文件

    import com.example.android.R; import android.app.Activity;import android.os.Bundle;import android.os ...

  10. 开发之UI篇

    首先这里介绍一个软件一个插件,它们的主要功能是方便开发者看UI(如尺寸,颜色,大小等),两个配合使用 一. Sketch软件 1.Sketch  看ui图,还可以切图 2.Sketch 如何切图: 1 ...