前言
最近看到一道面试题觉得很有意思,如何才能使下面的代码打印出结果
const obj = {
a: 1,
b: 2,
c: 3
}
for (let v of obj) {
console.log(v)
}
猛地看上去挺简单,但是里面其实蕴含了很多东西,想让其打印出结果就必须要了解一个概念就是迭代器Iterator,这也正是本文所要讲的,先说下答案吧。
const obj = {
a: 1,
b: 2,
c: 3,
[Symbol.iterator]: function() {
const values = Object.values(this);
let index = 0;
return {
next: () => {
return{
value: values[index++],
done: index > values.length? true : false
}
}
};
}
};
只需要在for of之前给对象添加Symbol.iterator属性就可以完美打印出1,2,3,这个属性是一个函数,这里巧妙借用了数组可迭代这一点完成对对象的遍历,首先对象本身会默认调用Symbol.iterator方法。看不懂?没关系,接下来才是正文部分,那就先从概念部分说起吧。
Iterator是什么

相信经验比较丰富的一些同学可能会看到过这个,在一些数组或者Map、Set中经常会看到这个属性,但在对象(Object)中就没有,下面就来一起彻底弄明白这个东西。
Iterator名为遍历器,ES6中规定遍历器是一种机制,也是一种接口,为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
原生具备Iterator的数据结构有
- Array
- Map
- Set
- String
- arguments对象
- NodeList对象
为什么需要Iterator
在Javascript中表示集合的数据结构有数组、对象、Map和Set数据结构,它们都有各自的特点,但同时也有一个共同的需求:遍历它们的内容。然而不同的数据结构都有对应的遍历方法,比如,数组有forEach、map、filter等方法,对象有for in/Object.keys()方法,Map和Set也都有各自的遍历方法,这就导致一个问题,如果要处理一个不同类型的集合,那么就要写很多的判断条件来根据集合的类型选择对应的遍历方法,这显然是不方便的。
还有一点就是迭代注重的是过程,而遍历指的是在一个固定的长度中把每个元素依次取出来,比如说数组结构必须有长度才可以进行遍历,而迭代不需要关注有多少元素只负责把元素依次取出来。
于是ES6中为了解决这个问题引入了Iterator这种统一的访问机制,即for of循环,使用for of循环时会自动去寻找该数据结构是否部署了Iterator接口。这就是为什么会出现Iterator的原因,总结一下:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列(自定义遍历行为);
- 供
for of使用;
Iterator的实现
Iterator其实就是一个对象,对象中有一个next方法(固定格式),第一次调用对象的next方法可以返回当前数据结构中的第一个成员,第二次调用对象的next方法可以返回当前数据结构中的第二个成员,直到数据结构中的结束位置结束。下面来模拟实现一个遍历器生成函数:
const arr = [1,2,3];
function makeIterator(arr){
let nextIndex = 0;
return {
next(){
return nextIndex < arr.length?
{value:arr[nextIndex++],done:false}:
{value:undefined,done:true}
}
}
}
const iterator = makeIterator(arr);
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
可以看出调用遍历器对象的next方法,就可以遍历事先给定的数据结构。在makeIterator函数中首先声明了一个表示从头开始的下标,每调用一次next方法就会以对象的形式返回数据结构中对应的元素,其中value表示数据结构中当前位置的值,done表示遍历是否结束。
上面提到的Symbol.iterator属性,它本身是一个函数,是当前数据结构默认的遍历器生成函数,这个函数的实现类似于makeItrerator函数,它默认部署在可遍历的数据结构中。
只要数据结构具有Symbol.iterator属性就可以使用for of遍历,for...of循环内部调用的其实就是数据结构的Symbol.iterator方法。
const arr = [1,2,3,4];
for (let v of arr){
console.log(v) // 1 2 3 4
}
Object对象上的没有默认的Iterator接口,是因为对象是无序的,里面的哪个属性先遍历哪个属性后遍历是不确定的,需要开发者手动确定。要使其可以进行遍历,添加一个遍历器生成函数即可。
const obj = {
name:"xxx",
age:18,
}
// 如果不加此函数,将会报错 obj is not iterable
Object.prototype[Symbol.iterator] = function(){
let nextIndex = 0
return {
next:()=>{ // 确定this指向
return nextIndex < Object.values(this).length?{value:Object.values(this)[nextIndex++],done:false}:{value:undefined,done:true}
}
}
}
// 相当于下面代码的语法糖
for (let v of obj){
console.log("v",v) // xxx 18
}
// for of 实现自动迭代的原理
const iterator = obj[Symbol.iterator]();
let result = iterator.next();
while(!result.done){
const item = result.value;
console.log(item);
result = iterator.next();
}
此外其他默认调用Iterator接口的场景还有解构赋值、扩展运算符。再补充一下for in 和for of是两种不同的数据结构,分别用于遍历两种不同的数据结构,for in 枚举的对象的键,而不是值,for of直接遍历的是对象的值,而不是键。
for of不是只能获取到对象的值,因为它是基于迭代器的所以遍历的形式可以是各种各样的,我们可以根据需求自行定义迭代器:
const obj = {
name: "zhangsan",
age: 18,
[Symbol.iterator]() {
let i = 0;
const keys = Object.keys(this);
return {
next: () => {
const propName = keys[i]
const propValue = this[propName];
const result = {
value: { propName, propValue },
done: i >= keys.length ? true : false
}
i++;
return result
}
}
}
}
for (let v of obj) {
console.log(v) // {propName:'name',propValue:'zhangsan'} {propName:'age',propValue:18}
}
生成器
语法
生成器(generator 函数)是为了更方便的使用迭代器,生成器内函数内部是为了给生成器的每次迭代提供数据。
function* test(){
console.log('test...')
}
const iterator = test()
代码中的 test 的函数虽然被调用但并不会立马执行里面的语句,而是会返回一个迭代器对象,调用迭代器对象中的next方法才会开始执行。
每次调用生成器的 next 方法时,会导致生成器函数会执行到下一个 yield 语句,yield 是一个关键字,该关键字只能在函数内部使用,表达“产生”了一个迭代数据,并返回一个对象,对象中包含 value 和 done 属性,下面用代码来解释这句话。
function* test(){
console.log('第一次执行')
yield 1;
console.log('第二次执行')
yield 2;
console.log('第三次执行')
}
const generator=test()
generator.next() // 第一次执行 {value:1,done:false}
generator.next() // 第二次执行 {value:2,done:false}
generator.next() // 第三次执行 {value:undefined,done:true}
当执行第一次 next 方法时会输出'第一次执行'和 1,然后会暂停下面代码的执行,直到遇到第二个 next 方法的调用,当输出{value:undefined,done:;true}时则表示迭代完成。
返回值
生成器函数可以有返回值,返回值会出现在第一次done 为 true 的value 属性中。
function* test(){
console.log('第一次执行')
yield 1;
console.log('第二次执行')
yield 2;
console.log('第三次执行')
return 3;
}
const generator=test()
generator.next()
generator.next()
generator.next() // 第三次执行 {value:3,done:true}
还是以上面的代码为例,当完成最后一次迭代时,存在返回值的话 value 属性的值将不再是undefined 而是我们返回的值。
参数
调用生成器 next 方法时,可以传递参数,传递的参数会交给 yield表达式的返回值。
function* test(){
let info=yield 1;
yield 2+info;
}
const generator=test()
generator.next(); //{value:1,done:false}
generator.next(1); // {value:3,done:false}
总结
这篇文章主要讲解了JavaScript中一个可迭代对象是如何进行迭代的,详细说明了Iterator遍历器的概念、出现的原因以及如何实现,属于原理级别的知识,虽然在工作中不会经常用到,但是本着知其然知其所以然的道理了解它的原理,内部是如何运行的才能知道一些报错信息出现的原因以及解决方案,大家加油吧!