ES6 推出的 for of
语句非常强大,远超曾经的所有遍历方式。
for of
可以很轻松地遍历数组、集合、映射,写法也十分简洁。
在我的项目中,除了需要获取特定返回值的时候还采用数组的 map
filter
reduce
方法,其余遍历都由 for of
代劳。
本文我将逐层深入地介绍 for of
语句的用法与注意事项,并刨析其原理——迭代器和生成器,最后在对象与数字类型上扩展 for of
的功能。
语法与优势
for of
语句的语法如下:
for (let variable of iterable) {
//statements
}
iterable
是要被遍历的目标,一般是数组、字符串、集合、映射,或者是其他实现迭代器接口的类数组对象,比如函数的参数列表arguments
、DOM 的节点列表NodeList
variable
是自己定义的一个变量,用来存储每次迭代中迭代器的返回值。
可以看看下面的示例,能更好的理解怎么使用。
迭代数组,变量存储的是数组的值。
let iterable = [10, 20, 30];
for (let value of iterable) {
console.log(value);
}
// 10
// 20
// 30
迭代字符串,变量存储的是单个字符。
let iterable = "boo";
for (let value of iterable) {
console.log(value);
}
// "b"
// "o"
// "o"
迭代集合,变量存储的是集合的值。
let iterable = new Set([1, 1, 2, 2, 3, 3]);
for (let value of iterable) {
console.log(value);
}
// 1
// 2
// 3
迭代映射,变量存储的是一个键值对的数组,一般会通过解构赋值来使用。
let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let entry of iterable) {
console.log(entry);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]
for (let [key, value] of iterable) {
console.log(key, value);
}
// a 1
// b 2
// c 3
相较于传统的循环语句,for of
语句更简洁,传统的循环是无法遍历集合与映射的,因为它们不具有索引。
在 for of
中可以使用 break
continue
操作符结束或终止迭代,这使其超越 forEach
方法。
注意事项
for of
虽然好用,但要注意下面的几个问题:
- 不能直接遍历对象。对象没有实现迭代器接口,直接遍历会抛出异常。如果想遍历对象的属性,可以先通过
Object.keys()
方法获取对象的属性列表,然后再遍历。 - 不能实现数组的赋值。
for of
遍历数组时并没有提供索引,无法直接修改数组。如果打算改变数组,建议使用其他遍历方法。 - 不要提前修改未迭代的项目。如果你在遍历途中修改后面项的值,在之后的迭代中获取的是新的值。
- 不要在遍历途中增删项目。如果你在遍历途中删除了未迭代的项目,会导致迭代次数的减少;如果你在遍历途中添加了新项,它们也将会被迭代。
前两个问题在 扩展 for of 一节中将得到完美解决,另外两个问题我们在遍历时理应遵守的规范,如果不遵守将导致代码逻辑的混乱。
预备知识
想要弄清楚 for of
语句的原理,要先认识3个概念:迭代协议、迭代器、生成器。
迭代协议
MDN 对迭代协议的介绍比较复杂,我简单概括一下,详情可点击连接。
一个对象要想能够被迭代,需要实现一个迭代接口,其值应该是一个符合规定的迭代器。
在 JS 中,对象的迭代接口通过属性 Symbol.iterator
暴露给了开发者。
迭代器
迭代器是一个对象,它具有一个 next
方法,该方法会返回一个对象,包含 value
和 done
两个属性。value
表示这次迭代的值; done
表示是否已经迭代到序列中的最后一个。
迭代器对象可以重复调用 next()
方法,该过程称迭代一个迭代器,又称消耗了这个迭代器,因为它通常只能迭代一次。 在产生终止值之后,对 next()
的额外调用只会返回 {value: 最终值, done:true}
。
迭代器的写法较为复杂,在这里只展示一个实例,该函数用于生成迭代器对象。
不需要对其过多研究,因为我们不用手写迭代器,JS 向我们提供了便捷的生成器用来生成迭代器对象。
// 数字从start开始,每次迭代增加step,直到大于end
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start
const rangeIterator = {
next: function () {
let result
if (nextIndex <= end) {z
result = { value: nextIndex, done: false }
nextIndex += step
} else {
result = { value: undefined, done: true }
}
return result
},
}
return rangeIterator
}
生成器
生成器是一个函数,我将从语法和调用两个方面详细说明生成器函数。
语法:
生成器函数的语法有一定规则,该函数要使用 function*
语法编写,在其内部可以使用 yield
关键字指定每一次迭代产出的值,也可以使用 return
关键字作为迭代器的终值。
调用:调用生成器函数只会返回一个迭代器对象,不会执行函数体中的代码。通过调用生成器的 next()
方法,才会执行函数体中的内容,直到遇到 yield
关键字或执行完毕。
只看文字不好理解,看个例子吧
function* generator() {
console.log('第一次调用')
yield 'a'
console.log('第二次调用')
yield 'b'
console.log('第三次调用')
return 'c'
}
let iterator = generator()
console.log('创建迭代器')
console.log('next1:', iterator.next())
console.log('next2:', iterator.next())
console.log('next3:', iterator.next())
console.log('next4:', iterator.next())
控制台的打印如下:
生成器函数的内容是分步调用的,每次迭代只运行到下一个 yield
的位置,将 yield
关键字后的表达式作为本次迭代的值产出。当遇到 return
或执行完函数后,返回对象的 done
属性会被设置为 true
,表示这个迭代器被完全消耗了。
将之前的例子改用生成器的写法,代码十分简洁:
// 数字从start开始,每次迭代增加step,直到大于end
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
for (let i = start; i <= end; i += step) {
yield i
}
}
next
方法是可以传参的,参数将以 yield
关键字的返回值的形式被使用,第一次 next
调用传递的参数将被忽略。
看看下面这个无限累加器,传递0会将其重置:
function* generator(start) {
let cur = start
while (true) {
let num = yield cur
if (num == 0) {
console.log('迭代器被重置')
cur = start
} else {
cur += num
}
}
}
let iterator = generator(10)
console.log('创建迭代器')
console.log('next1:', iterator.next().value)
console.log('next2:', iterator.next(2).value)
console.log('next3:', iterator.next(4).value)
console.log('next4:', iterator.next(5).value)
console.log('next5:', iterator.next(0).value)
console.log('next6:', iterator.next(5).value)
console.log('next7:', iterator.next(10).value)
控制台输出如下
原理与实现
for of
的原理,就是调用目标的迭代接口(生成器函数)获取一个迭代器,然后不断迭代这个迭代器,将返回对象的 value
属性赋值给变量,直到返回对象的 done
属性为 true
。
通过函数的方式简单实现一下:
/**
* @description: for of 方法实现
* @param {object} iteratorObj 可迭代对象
* @param {Function} fn 回调函数
* @return {void}
*/
function myForOf(iteratorObj, fn) {
// 如果传入的对象不具备迭代接口,抛出异常
if (typeof iteratorObj[Symbol.iterator] != 'function') {
throw new TypeError(`${iteratorObj} is not iterable`)
}
// 获取迭代器
let iterator = iteratorObj[Symbol.iterator]()
// 遍历迭代器
let i
while (!(i = iterator.next()).done) {
fn(i.value)
}
}
const arr = [10, 20, 30]
myForOf(arr, (item) => {
console.log(item)
})
let map = new Map([
['a', 1],
['b', 2],
['c', 3],
])
myForOf(map, ([key, value]) => {
console.log(key, value)
})
控制台输入如下:
for of
与forEach
方法原理一致,上述代码稍作修改就能实现forEach
方法
扩展 for of
我们知道,for of
美中不足的一点是没法直接遍历对象的属性
我们只要实现 Object
原型对象上的迭代接口,将其定义为一个返回包含对象所有属性的生成器
实现如下:
Object.prototype[Symbol.iterator] = function* () {
const keys = Object.keys(this)
for (let i = 0; i < keys.length; i++) {
yield keys[i]
}
}
const obj = { a: 1, b: 2, c: 3 }
for (const key of obj) {
console.log(key, obj[key])
}
// a 1
// b 2
// c 3
我们在生成器中提前获取了对象的属性数组,在迭代器中不断产生就好了
令人惊喜的是,这个行为不会影响到 ...
操作符的对象浅拷贝功能,百利无一害。
console.log({ ...obj }) // {a: 1, b: 2, c: 3}
console.log([...obj]) // ['a', 'b', 'c']
使用...
浅拷贝对象,实际上调用的Object.assign
方法
for of
另一点不足就是取不到索引,没法修改数组,可以通过实现 Number
原型对象上的迭代接口解决
代码如下:
Number.prototype[Symbol.iterator] = function* () {
const num = this.valueOf()
for (let i = 0; i < num; i++) {
yield i
}
}
const arr = [...5]
console.log(arr) // [0, 1, 2, 3, 4]
for (const index of arr.length) {
arr[index] *= 2
}
console.log(arr) // [0, 2, 4, 6, 8]
如果是在 ts 中扩展 for of
后,使用时会提示错误,加入下列接口声明就好了
declare interface Object {
[Symbol.iterator]: any
}
declare interface Number {
[Symbol.iterator]: any
}
结语
如果文中有错误或不严谨的地方,请务必给予指正,十分感谢。
内容整理不易,如果喜欢或者有所启发,希望能点赞关注,鼓励一下作者。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。