前言
在 Javascript
中,对象遍历是再普遍不过的操作,常用的方法有以下5种:
for...in
Object.keys()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
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原型链的知识。 再然后,依次给 target
和 proto
设置属性。 关于属性名,我做了语义化的命名, 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))
总结
最后用一张表来总结,这张表将上述的各种遍历方式的能力和局限都展示了出来,觉得有帮助的小伙伴可以点个👍。