一、背景
故事的开头是这样的...
在遍历数组与对象属性时,对使用 obj.keys()、obj.values()和 obj.entries() 还是 Object.keys(obj)、Object.values(obj)、Object.entries(obj)方法产生了一些困惑。话不多说,先放问题:
需求:想要遍历一个对象,并获取遍历对象的属性值 实现:Object.keys()、Object.values() 和 Object.entries() 方法 问题:一不小心同数组的 entries(),keys()和 values() 方法混淆了~QAQ
二、keys()、values()、entries()遍历方法
熟悉 ES 语法数据结构的朋友一定很清楚,原生对象数据结构并不支持 obj.keys()、obj.values()和 obj.entries() 方法,数组与 map、set 等数据结构才支持。但仍可以通过 Object.keys(obj)、Object.values(obj)、Object.entries(obj)获取原生对象中可遍历的属性组成数组类型数据结构。
也就是说,keys()、values()和 entries() 方法有两种:
ES5-ES2017 相继引入 Object.keys 、Object.values 和 Object.entries 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名/键值/键值对,可以用 for...of 循环进行遍历;
ES6 提供 entries(),keys() 和 values() -- 可用于遍历数组/Map/Set 等类数组数据结构实例,返回一个(Iterator)遍历器对象,可以用 for...of 循环进行遍历。
注意这里又有两点区别:
两者调用语法不同,显而易见;
前者返回的是一个可迭代的对象,而后者返回的是一个真正的数组。
有没有被绕晕?那我们先来看第一个问题吧 -- 调用语法的不同
Q1: Object.keys 、Object.values 和 Object.entries 方法
为了区分这两种调用语法,我们必须得来回顾下原型链的相关知识。
因为这里的 entries(),keys()和 values() 方法正是是调用原型对象构造函数上的方法。如下图可以看到,对于一个普通对象,这三个方法在 Object 对象的[[prototype]]下的 constructor 中:
而对于一个数组结构来说,这三个方法可以在数组原型链中和原型链上层对象原型的 constructor 中同时找到:
即 Object.keys(arr)调用的是数组原型链顶层原型对象 constructor 的方法,而数组本身也支持的 arr.keys()方法,则是调用数组原型链上的方法。
即对象只支持前种调用方式,而数组同时支持这两种调用:
同时我们知道在 JavaScript 中,对象是所有复杂结构的基础。也正对应了其他复杂结构原型链的顶端是对象原型结构。现在应该能够知道为何普通对象不支持 obj.keys()、obj.values()和 obj.entries() 方法了,但到这里就不得不提出另一个疑问了:
Q2: 如何让一个对象支持 obj.keys()、obj.values()和 obj.entries() 方法呢?
理论上,我们是可以为一个对象构造任意方法,那么如何实现和数组一样的遍历方法呢?本质上这个方法是能够生成一个遍历器。
- let objE = {
- data: [ 'hello', 'world' ],
- keys: function() {
- const self = this;
- return {
- [Symbol.iterator]() {
- let index = 0;
- return {
- next() {
- if (index < self.data.length) {
- return {
- value: self.data[index++],
- done: false
- };
- }
- return { value: undefined, done: true };
- }
- };
- }
- }
- }
- };
上述,我们自己创建了一个 data 对象,并实现了它自己的 data.values() 方法。同时,我们依然可以对它调用 Object.values(data) 方法。
从上面的方法不难看出,我们在对象中通过添加 Symbol.iterator 手动构造了一个输出遍历器函数,关于遍历器的讨论我们在下一节讨论,现在先来讨论调用返回结果的区别。
Q3: 两种调用方法返回结果:遍历器与数组
1)第一种调用方法,根据定义可知:返回一个数组,数组成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名/键值/键值对。
敲重点!!!这三个方法只返回对象自身的可遍历属性,即属性描述对象的 enumerable 为 true。
我们可以通过 for ... in 循环来实现相同的遍历效果。
2)而第二种方法,返回一个遍历器:顾名思义,遍历器也可以满足循环遍历的需求。
本质上,遍历器的定义是一种接口,为各种不同的数据结构提供统一的访问机制。接下来就来了解下适用于不同数据结构的遍历器。
三、Iterator 遍历器
首先我们知道,目前主要有四种表示“集合”的数据结构:数组(Array)、对象(Object)、Map 和 Set,这里表示"集合"的对象例如 NodeList 集合类数组对象,而遍历器可以使我们遍历访问这些集合。
实际上,原生具备 Iterator 接口的数据结构包括 Array、Map、Set、String、TypedArray、函数的 arguments 对象和 NodeList 对象。
具体遍历器的概念可参考阮一峰老师 ES6 入门 Iterator 一章,已经十分详细清楚:
因此,Iterator 遍历器本质上为所有数据结构,提供了一种统一的访问机制,即 for...of 循环。
关于遍历,我们前面已经讲到了遍历对象属性,这里再提一嘴:
1. 遍历类数组对象/Array/Map/Set 等数组数据结构实例
当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,换句话说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可迭代/遍历的”(iterable)。
2. 获取对象可遍历属性
Object.keys 、Object.values 和 Object.entries 方法只返回对象自身的可遍历属性,通过属性描述对象的 enumerable 标识改对象属性是否可以遍历。同时因为普通对象 not iterable,即普通对象不具有 Symbol.iterator 属性,所以无法通过 for...of 循环直接遍历,否则会报错 Uncaught TypeError: obj is not iterable。
可见,数组及类数组的遍历(迭代)与普通对象中的提到的遍历是不同的,这分别取决于各自的 iterable 和 enumerable 属性。
3. for ... of
ES6 中引入 for...of 循环,很多时候用以替代 for...in 和 forEach() ,并支持新的迭代协议。for...of 语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
那么终极问题:如何实现 Symbol.iterator 方法,使普通对象可被 for of 迭代?其实在 Q2 部分已经实现了。
尝试给普通对象实现一个 Symbol.iterator 接口:
- // 普通对象
- const obj = {
- foo: 'value1',
- bar: 'value2',
- [Symbol.iterator]() {
- // 这里 Object.keys 不会获取到 Symbol.iterator 属性
- const keys = Object.keys(obj); // 得到一个数组
- let index = 0;
- return {
- next: () => {
- if (index < keys.length) {
- // 迭代结果 未结束
- return {
- value: this[keys[index++]],
- done: false
- };
- } else {
- // 迭代结果 结束
- return { value: undefined, done: true };
- }
- }
- };
- }
- }
- for (const value of obj) {
- console.log(value); // value1 value2
- };
for...of 循环内部调用的是数据结构的 Symbol.iterator 方法,for...of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
for...of 循环作为 ES6 新引入的一种循环,具有以下明显优势(按需使用):
有着同 for...in 一样的简洁语法,但是没有 for...in 那些缺点(无序,不适用于遍历数组)。
不同于 forEach 方法,它可以与 break、continue 和 return 配合使用。
提供了遍历所有数据结构的统一操作接口。
以上是我从 keys()、values()、entries() 遍历方法出发对遍历器产生的几点思考,如有不足之处,欢迎指正~~~