细数遍历对象的几种方式

929 阅读4分钟

前言

Javascript 中,对象遍历是再普遍不过的操作,常用的方法有以下5种:

  1. for...in
  2. Object.keys()
  3. Object.getOwnPropertyNames()
  4. Object.getOwnPropertySymbols()
  5. Reflect.ownKeys()

您知道上面几种遍历方法的区别吗 ? 如果您已经对他们的区别了然于胸,那就别浪费自己的时间了,如果您对其还有一些疑惑,那么请您花几分钟时间看看接下来的文章。

准备工作

关于对象的属性,我们需要了解以下几个知识点:

  • 属性名可以是 string 也可以是一个 Symbol
  • 属性可以是可枚举也可以是不可枚举的;
  • 属性可能是来自于自身,也可能来自于原型链;

我们用下面的代码来创造一个对象,它自身和原型都有相应的 可枚举/不可枚举 字符串/Symbol 属性.

/**
 * 定义对象的属性
 * @param {*} target      对象
 * @param {*} key         键名
 * @param {*} value       值
 * @param {*} enumerable  是否可以枚举
 */
function defineProperty (target,key,value,enumerable) {
  Object.defineProperty(target,key,{
    enumerable:!!enumerable,
    value
  });
}

// 将proto设置为target的原型
const target = {};
const proto = {};
target.__proto__ = proto;

defineProperty(target,'self_str_enum','self_str_enum',true);
defineProperty(target,'self_str_not_enum','self_str_not_enum',false);
defineProperty(target,Symbol.for('self_symbol_enum'),Symbol.for('self_symbol_enum'),true);
defineProperty(target,Symbol.for('self_symbol_not_enum'),Symbol.for('self_symbol_not_enum'),false);

defineProperty(proto,'proto_str_enum','proto_str_enum',true);
defineProperty(proto,'proto_str_not_enum','proto_str_not_enum',false);
defineProperty(proto,Symbol.for('proto_symbol_enum'),Symbol.for('proto_symbol_enum'),true);
defineProperty(proto,Symbol.for('proto_symbol_not_enum'),Symbol.for('proto_symbol_not_enum'),false);

/**
 * 打印对象遍历出来的属性
 */
const logKeys = (key,value,target) => {
  console.log(key);
}

首先,定义了一个 defineProperty 工具函数,它通过调用Object.defineProperty函数来为对象添加属性,重点在于 enumerable 这个参数,为 true 代表该属性是一个可枚举属性, 为 false 代表该属性是一个不可枚举属性。 接下来,创建了一个target对象和一个proto对象,并将target.__proto__属性设置为proto,让 proto 成为target 的原型。对这一部分有疑惑的小伙伴,可以先看看js原型链的知识。 再然后,依次给 targetproto 设置属性。 关于属性名,我做了语义化的命名, self_str_enum代表了 是自身可枚举字符串属性,而 proto_symbol_not_enum 则代表了 原型链上的不可枚举符号属性,其它的可以以此类推。 最后,我们定义了一个工具函数 logKeys来打印遍历的结果,在遍历的时候用。到这里我们可以得到这样一个对象,它包含了自身/原型 的 字符串/Symbol 可枚举/不可枚举 属性。

我们的准备工作就做好了,接下来依次用这几个方法来遍历它:

for...in

作用: 遍历对象自身和原型可枚举字符串属性

function use_ForIn (target,cb) {
  for (const key in target) {
    cb(key,target[key],target);
  }
}
use_ForIn(target,logKeys);

// 输出结果:
// self_str_enum
// proto_str_enum

从输出结果可以看出, for...in 的方式可以遍历出自身和原型上的可枚举字符串属性, 而 Symbol 属性被略过了。

Object.keys()

作用: 接受一个对象,返回对象的自身的可枚举字符串属性名组成的一个数组

function use_Object_keys (target,cb) {
  Object.keys(target).forEach(key => cb(key,target[key],target));
}
use_Object_keys(target,logKeys);

// 输出结果:
// self_str_enum

从输出结果可以看出, Object.keys 的方式只能遍历出自身的可枚举字符串属性, 而 Symbol 属性和原型属性都被略过了。这个api就是限制自身版本的 for...in

Object.getOwnPropertyNames()

作用: 接受一个对象,返回对象的自身的可枚举和不可枚举字符串属性名组成的一个数组

function use_Object_getOwnPropertyNames (target,cb) {
  Object.getOwnPropertyNames(target).forEach(key => cb(key,target[key],target));
}
use_Object_getOwnPropertyNames(target,logKeys);

// 输出结果:
// self_str_enum
// self_str_not_enum

从输出结果可以看出, Object.getOwnPropertyNames 的方式可以遍历出自身的字符串属性,不管它是否是可枚举的,而Symbol 属性和原型属性都被略过了。

Object.getOwnPropertySymbols()

作用: 接受一个对象,返回对象自身可枚举和不可枚举Symbol属性名组成的一个数组

function use_Object_getOwnPropertySymbols (target,cb) {
  Object.getOwnPropertySymbols(target).forEach(key => cb(key,target[key],target));
}
use_Object_getOwnPropertySymbols(target,logKeys);

// 输出结果:
// Symbol(self_symbol_enum)
// Symbol(self_symbol_not_enum)

从输出结果可以看出, Object.getOwnPropertySymbols 的方式可以遍历出自身的Symbol属性,不管它是否是可枚举的,而string 属性和原型属性都被略过了。这个 api 更像是对 Object.getOwnPropertyNames的补充。

Reflect.ownKeys()

作用: 接受一个对象,返回对象自身的字符串和Symbol属性名组成的一个数组,包括不可枚举的属性

function use_Reflect_ownKeys (target,cb) {
  Reflect.ownKeys(target).forEach(key => cb(key,target[key],target));
}
use_Reflect_ownKeys(target,logKeys);

// 输出结果:
// self_str_enum
// self_str_not_enum
// Symbol(self_symbol_enum)
// Symbol(self_symbol_not_enum)

从输出结果可以看出, Reflect.ownKeys 的方式可以遍历出自身的字符串和Symbol属性,不管它是否是可枚举的。 它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

总结

最后用一张表来总结,这张表将上述的各种遍历方式的能力和局限都展示了出来,觉得有帮助的小伙伴可以点个👍。