(for..in)、Object.keys() 和 Object.getOwnPropertyNames(),for...o

1,749 阅读13分钟
原文链接: www.talkingcoder.com

Object.keys(obj),返回一个数组,数组里是该obj可被枚举的所有属性。请看示例:

示例一:

   function Pasta(grain, width, shape) {
            this.grain = grain;
            this.width = width;
            this.shape = shape;
            this.toString = function () {
                    return (this.grain + ", " + this.width + ", " + this.shape);
            }
    }
    console.log(Object.keys(Pasta)); //console: []
    var spaghetti = new Pasta("wheat", 0.2, "circle");
    console.log(Object.keys(spaghetti)); //console: ["grain", "width", "shape", "toString"]

示例二:

  var arr = ["a", "b", "c"];
    console.log(Object.keys(arr)); // console: ["0", "1", "2"]
    var obj = { 0 : "a", 1 : "b", 2 : "c"};
    console.log(Object.keys(obj)); // console: ["0", "1", "2"]
    var an_obj = { 100: "a", 2: "b", 7: "c"};
    console.log(Object.keys(an_obj)); // console: ["2", "7", "100"]
    var my_obj = Object.create({}, { getFoo : { value : function () { return this.foo } } });
    my_obj.foo = 1;
    console.log(Object.keys(my_obj)); // console: ["foo"]



js中几种遍历对象的方法,包括for in、Object.keys、Object.getOwnProperty,它们在使用场景方面各有不同。

for in

主要用于遍历对象的可枚举属性,包括自有属性、继承自原型的属性

var obj = {"name":"Poly", "career":"it"}
Object.defineProperty(obj, "age", {value:"forever 18", enumerable:false});
Object.prototype.protoPer1 = function(){console.log("proto");};
Object.prototype.protoPer2 = 2;
console.log("For In : ");
for(var a in obj) console.log(a);





Object.keys

返回一个数组,元素均为对象自有的可枚举属性

var obj = {"name":"Poly", "career":"it"}
Object.defineProperty(obj, "age", {value:"forever 18", enumerable:false});
Object.prototype.protoPer1 = function(){console.log("proto");};
Object.prototype.protoPer2 = 2;
console.log("Object.keys:")
console.log(Object.keys(obj));

 






Object.getOwnProperty

用于返回对象的自有属性,包括可枚举和不可枚举的

var obj = {"name":"Poly", "career":"it"}
Object.defineProperty(obj, "age", {value:"forever 18", enumerable:false});
Object.prototype.protoPer1 = function(){console.log("proto");};
Object.prototype.protoPer2 = 2;
console.log("Object.getOwnPropertyNames: ");
console.log(Object.getOwnPropertyNames(obj));






for..of

var obj = {"name":"Poly", "career":"it"}
Object.defineProperty(obj, "age", {value:"forever 18", enumerable:false});
Object.prototype.protoPer1 = function(){console.log("proto");};
Object.prototype.protoPer2 = 2;
console.log("For of : ");
for(var a of Object.entries(obj)) console.log(a);












1 Object.keys()

ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键名。


     var obj = { foo: "bar", baz: 42 };  

  1. Object.keys(obj)  
  2. // ["foo", "baz"]  

目前, ES7 有一个提案,引入了跟Object.keys配套的Object.values和Object.entries。


  1. let {keys, values, entries} = Object;  
  2. let obj = { a: 1, b: 2, c: 3 };  
  3. for (let key of keys(obj)) {  
  4.     console.log(key); // 'a', 'b', 'c'  
  5. }  
  6. for (let value of values(obj)) {  
  7.     console.log(value); // 1, 2, 3  
  8. }  
  9. for (let [key, value] of entries(obj)) {  
  10. console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]  
  11. }  

2 Object.values()


Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键值。



  1. var obj = { foo: "bar", baz: 42 };  
  2. Object.values(obj)  
  3. // ["bar", 42]  

返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。


  1. var obj = { 100: 'a', 2: 'b', 7: 'c' };  
  2. Object.values(obj)  
  3. // ["b", "c", "a"]  

上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a。

Object.values只返回对象自身的可遍历属性。



  1. var obj = Object.create({}, {p: {value: 42}});  
  2. Object.values(obj) // []  

上面代码中,Object.create方法的第二个参数添加的对象属性(属性p),如果不显式声明,默认是不可遍历的。Object.values不会返回这个属性。



  1. Object.values会过滤属性名为 Symbol 值的属性。  
  2. Object.values({ [Symbol()]: 123, foo: 'abc' });  
  3. // ['abc']  

如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。



  1. Object.values('foo')  
  2. // ['f', 'o', 'o']  

上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组。

如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values会返回空数组。



  1. Object.values(42) // []  
  2. Object.values(true) // []  

3 Object.entries

Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键值对数组。



  1. var obj = { foo: 'bar', baz: 42 };  
  2. Object.entries(obj)  
  3. // [ ["foo", "bar"], ["baz", 42] ]  

除了返回值不一样,该方法的行为与Object.values基本一致。

如果原对象的属性名是一个 Symbol 值,该属性会被省略。



  1. Object.entries({ [Symbol()]: 123, foo: 'abc' });  
  2. // [ [ 'foo', 'abc' ] ]  

上面代码中,原对象有两个属性,Object.entries只输出属性名非 Symbol 值的属性。将来可能会有Reflect.ownEntries()方法,返回对象自身的所有属性。

Object.entries的基本用途是遍历对象的属性。



  1. for (let [k, v] of Object.entries(obj)) {  
  2.     console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`);  
  3. }  
  4. // "one": 1  
  5. // "two": 2  

Object.entries方法的一个用处是,将对象转为真正的Map结构。



  1. var obj = { foo: 'bar', baz: 42 };  
  2. var map = new Map(Object.entries(obj));  
  3. map // Map { foo: "bar", baz: 42 }  

自己实现Object.entries方法,非常简单。




  1. function* entries(obj) {  
  2.     for (let key of Object.keys(obj)) {  
  3.         yield [key, obj[key]];  
  4.     }  
  5. }  
  6. //  非 Generator 函数的版本  
  7. function entries(obj) {  
  8.     let arr = [];  
  9.     for (let key of Object.keys(obj)) {  
  10.         arr.push([key, obj[key]]);  
  11.     }  
  12.     return arr;  
  13. }  




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
})

总结

其实这几个方法之间的差异主要在属性是否可可枚举,是来自原型,还是实例



著作权归作者专业去除马赛克所有 文章发表于2017年03月27日