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,直到大于endfunction 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,直到大于endfunction* 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}
结语
如果文中有谬误或不谨严的中央,请务必给予斧正,非常感激。
内容整顿不易,如果喜爱或者有所启发,心愿能点赞关注,激励一下作者。