iterator (迭代器/遍历器)
今天的主角是iterator。他的汉语意思是(迭代器,遍历器) 遍历器 ? 那岂不是和for循环, forEach, map 差不多,都是遍历,循环数据? 今天我们就来窥见历史,一起来聊聊js中的 遍历器,迭代。看看他们是怎么从for 循环到ForEach,到iterator,再到for of 的
从for 说起
for 循环是一个老前辈,在js出来的时候,就已经存在了。我们似乎也在乐此不疲的使用的。
比如我们有5个学生的信息,需要依次输出
let student = [
{
name: '张一',
age: 10,
},
{
name: '张二',
age: 11,
},
{
name: '张三',
age: 12,
},
];
数据结构如上,代码如下
for (let i = 0; i < student.length; i++) {
console.log(student[i].name);
}
这样实现并没有什么问题。但是从写法上,我们看看有没有什么优化的点,那我们先来简单分析一下这个for循环
-
需要引入一个变量(i)做计数器,来做终止条件的判断 i < student.length
-
需要靠下标去取值 student[i].name
-
很多数据结构不是数组,无法使用 Map, Set ...
-
每次遍历,都要student.length, 时间复杂度是O(n) 。 当然,可以用变量把student.length,接收,这样时间复杂度就是O(1)了。
那聪明的javascript, 面对上面这种情况,有什么好的妙招呢?
forEach
为了解决上面所说的问题,数组提供了内置的forEach
方法。(但是并没有解决for循环的所有缺点,而且还只是针对数组)
我们先来看一段js代码
student.forEach((item, index, arr) => {
console.log(item.name);
});
从写法上来看,确实有不少优化,forEach内部做了封装。所以我们不用引入新的变量,也不用判断终止条件,就可以轻松遍历数据。但是当我们要中在forEach中,中途退出时
let arr = [1,2,3,4,5]
arr.forEach((item, index, arr) => {
if (item == 3) return;
console.log(item); // 1,2,4,5
});
forEach为什么不能中断 zhuanlan.zhihu.com/p/385521894
设置了终止条件,但是并不会终止,而是当item == 3 的时候,只是跳出了本次循环,后面的循环依旧会执行
除了抛出异常以外,没有办法中止或跳出 forEach() 循环。如果你需要中止或跳出循环,forEach() 方法不是应当使用的工具,可以使用try cache 来强抛出错误,接收错误(为了终止条件,强行报错,是不是合理)...
难道循环就止步不前了吗? 随着javascript的升级,来到了es6,es6 又引入了一些新的复杂数据类型(Map, Set),老的问题还没有优化完,新的数据复杂数据又出来了,这些新增进来的数据结构,又该用什么样的方式遍历,之前的那一摊子烂泥,,该怎么一起糊上墙呢 😂
很庆幸啊,真的很庆幸, 机智的javascript真的是太机智了,引出了Iterator(遍历器) 。 这个主角,真的是千呼万唤使出来啊
Iterator:
以前我们只说遍历,现在出来了个遍历器。自如其名,器: 就是工具,容器,可以处理很多数据结构,只要部署了 Iterator 接口
,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Q: 怎么是```部署了 Iterator 接口```?
A: 就是数据结构的原型上有Symbol.iterator这个方法
例如:
Array.prototype
通过数据类型的原型查找,看是否部署了 Symbol.iterator
当然,你不用一个一个的试,我已经给你准备好了
- Array
- Map
- Set
- String
- 函数的 arguments 对象
- NodeList 对象
ES6 的这些些数据结构原生具备 Iterator 接口,即不用任何处理,就可以被for...of
循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator
属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator
属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象(iterable)。
[Symbol.iterator]的迭代过程
摘录至阮一峰大佬的文章
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
精简描述[Symbol.iterator]的迭代过程
- 迭代器的返回对象,需要有next方法,用于下一下遍历数据结构的成员
- 每一次遍历,都返回一个对象{done: false, value: xxx}
- done 记录是否遍历完成
- value 当前遍历的结果
我们怎么样去实现一个迭代器?
- 首先要知道当前的迭代数据的长度
- 迭代器返回一个对象 return {}
- 需要有next方法 next(){}
- 每一次迭代,都返回一个对象 {done: false, value: xxx}
- 根据终止条件,改写返回对象的值 {done: true, value: undefinde}
先来看一个残缺版,容易理解
let student = ['a', 'b'];
function makeIterator(arr) {
let index = 0;
return {
// 迭代器返回一个对象
next() {
// 有next方法,next 会返回一个对象,{done:false, value: xxx}
return {
done: false,
value: arr[index++],
};
},
};
}
残缺版是不是很easy😂,为什么说残缺呢,因为还没有加上终止条件。
let student = ['a', 'b'];
function makeIterator(arr) {
let index = 0;
return {
next() {
if (index < arr.length) {
return {
done: false,
value: arr[index++],
};
} else {
return {
done: true,
value: undefined,
};
}
},
};
}
let it = makeIterator(student); //调用迭代器(iterator),返回的是迭代器对象(iterable)。此时并不能执行,需要调用next方法
console.log('it', it); // 迭代器对象
console.log(it.next()); // 调用next,会返回一个对象。 {done: false, value: xxx} done: 迭代结束标识,value, 每次迭代的值
console.log(it.next());
console.log(it.next()); // 如果迭代完毕, {done: true, value: undefined}
上面其实还可以优化
--- else {}
index < arr.length
? {
done: false,
value: arr[index++],
}
: {
done: true,
value: undefined,
};
其实每次我们取值,迭代结束之前,只关心value,迭代结束,只关心done
index < arr.length
? {
value: arr[index++],
}
: {
done: true,
};
使用for of 迭代 迭代对象
对象迭代
let student = {
a: 1,
b: 2,
[Symbol.iterator]() {
let index = 0,
keys = Reflect.ownKeys(this),
len = keys.length;
return {
next() {
return index < len
? { done: false, value: keys[index++] }
: { done: true, value: undefined };
},
};
},
};
for (let key of student) {
console.log('key', key);
}
[Symbol.iterator]的迭代的核心
- 循环逻辑: 不停的调用next,进行下一次循环
- 终止条件: 每一次遍历,都返回一个对象{done: false, value: xxx},done: true, 标识终止。value: xxx每一次迭代的值
- 固有属性: [Symbol.iterator]
扩展
概念梳理
- 什么是循环:循环算是最基础的概念, 凡是重复执行一段代码, 都可以称之为循环. 大部分的递归, 遍历, 迭代, 都是循环。所以说,我认为迭代可以算作是循环的子集。
- 什么是迭代:百度百科给出的迭代的定义 迭代是指让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值
所以说: 这个过程当中, 旧值和新的值可以不连续, 可以不是顺序的,只要满足以下下三个条件的都算是迭代
-
有个初始的值
-
有一套算法,对这个初始值操作, 并计算出新的值
-
新的值还可以再次调用刚才的算法,再产生新的值,这个过程是可控的
-
可迭代对象(iterable):能够被迭代的对象,成为可迭代对象(iterable),例如 Map, Set...。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)
-
迭代器(Iterator):它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
-
迭代器协议( Iterator protocol ): 所谓协议,就是一系列规定的实现的一个东西。就是我们说的迭代器的实现原理
为什么对象没有默认部署
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了 Map。
判断是否可迭代
只需要判断对象的Symbol.iterator 是不是一个函数
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
类似数组的对象
ECMA-262 的定义是
- 它必须是一个对象
- 它有 length 属性
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let bb = {
v: 1,
b: 2,
c: 34,
length:3
}
// 所以这两个都是类数组对象
定义里还有两条注释
1, 通常,类似数组的对象也有一些具有整数索引名的属性。然而,这不是这个定义的要求。
2, 数组对象和字符串对象是类数组对象的实例。
所以 NodeList 对象和 arguments 对象和 类似数组的对象 都可以叫类数组,只一种表现为数组形式,一种表现为对象形式。但是只有整数索引的属性才会部署Iterator 接口
字符串的 Iterator 接口
字符串为什么还会部署Iterator 接口?
字符串是一个类似数组的对象,有length,有整数索引名的属性。 也原生具有 Iterator 接口。
当我们有一个类似数组的对象,并且整数索引名的属性。但是没有Iterator 接口,该怎么取部署一个Iterator 接口?
- 从0 开始,手写一个Iterator 接口
- 奇淫技巧
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
既然部署了Iterator 接口,那么 数组的结构,扩展运算符,for of, 都适用于字符串,换句话说,只要部署了Iterator 接口,上面说的这些都能用!!!
结构赋值
扩展运算符
Set
遍历器对象的 return(),throw()
遍历器对象除了具有next()
方法,还可以具有return()
方法和throw()
方法。如果你自己写遍历器对象生成函数,那么next()
方法是必须部署的,return()
方法和throw()
方法是否部署是可选的。
return()
方法的使用场合是,如果for...of
循环提前退出(通常是因为出错,或者有break
语句),就会调用return()
方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()
方法。 这里不讲他的具体实现,使用起来和for循环一致
yield* 关键字
yield*后面跟的是一个可遍历的结构,执行时也会调用迭代器函数。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
涉及到了生成器:Generator 函数 ,下一篇讲解
回顾
从for到forEach到迭代器,for of。js在不断的进步,for循环虽然简单,可以return,break,continue,但是需要自己实现终止条件,需要根据索引获取数据。颇为麻烦。
ES5 出来了forEach,内部做了封装,不需要我们去关心终止条件,也不用索引去获取当前项,缺点是不能终止代码执行,如果利用 Error,进行try catch 强行中断,又不太优雅。
面对不断增加的数据类型,我们该怎么统一去遍历这些数据类型呢?比如ES6新增了Map,Set
。出现了一种新的遍历方式,for of。 for of
机制兼顾了 for
和forEach
for of的底层实现,是用一个叫做iterator的接口实现的,我们讲了iterator的接口的核心逻辑
- 循环逻辑: 不停的调用next,进行下一次循环
- 终止条件: 每一次遍历,都返回一个对象{done: false, value: xxx},done: true, 标识终止。value: xxx每一次迭代的值
- 固有属性: [Symbol.iterator]
讲到此处,蓦然回首,从for 循环的简单纯朴,到现在新的名词,新的封装出现,我们似乎少写很多代码,殊不知,早已在底层封装完备
真是感慨啊,哪有什么虽有静好,只不过有人在替我们负罪前行。(哭唧唧o(╥﹏╥)o)