遍历得到数组 or Iterator 遍历器?

avatar
公众号「 微医大前端技术 」

56.gif

左琳,微医前端技术部前端开发工程师。超~能吃,喜欢游泳健身和跳舞,热爱生活与技术。

一、背景

故事的开头是这样的... ​

在遍历数组与对象属性时,对使用 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() 方法有两种: ​

  1. ES5-ES2017 相继引入 Object.keys 、Object.values 和 Object.entries 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名/键值/键值对,可以用 for...of 循环进行遍历;
  2. ES6 提供 entries(),keys() 和 values() -- 可用于遍历数组/Map/Set 等类数组数据结构实例,返回一个(Iterator)遍历器对象,可以用 for...of 循环进行遍历。 ​

注意这里又有两点区别:

  1. 两者调用语法不同,显而易见;
  2. 前者返回的是一个可迭代的对象,而后者返回的是一个真正的数组。 ​

有没有被绕晕?那我们先来看第一个问题吧 -- 调用语法的不同

Q1: Object.keys 、Object.values 和 Object.entries 方法

为了区分这两种调用语法,我们必须得来回顾下原型链的相关知识。

因为这里的 entries(),keys()和 values() 方法正是是调用原型对象构造函数上的方法。如下图可以看到,对于一个普通对象,这三个方法在 Object 对象的[[prototype]]下的 constructor 中: image.png

而对于一个数组结构来说,这三个方法可以在数组原型链中和原型链上层对象原型的 constructor 中同时找到: image.png image.png

即 Object.keys(arr)调用的是数组原型链顶层原型对象 constructor 的方法,而数组本身也支持的 arr.keys()方法,则是调用数组原型链上的方法。 ​

即对象只支持前种调用方式,而数组同时支持这两种调用: image.png

同时我们知道在 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) 方法。 image.png

从上面的方法不难看出,我们在对象中通过添加 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 一章,已经十分详细清楚: image.png

因此,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() 遍历方法出发对遍历器产生的几点思考,如有不足之处,欢迎指正~~~

前往微医互联网医院在线诊疗平台,快速问诊,3分钟为你找到三甲医生。

副本_副本_未命名_自定义px_2021-11-04-0.gif