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 办法,该办法会返回一个对象,蕴含 valuedone 两个属性。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 offorEach 办法原理统一,上述代码稍作批改就能实现 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}

结语

如果文中有谬误或不谨严的中央,请务必给予斧正,非常感激。

内容整顿不易,如果喜爱或者有所启发,心愿能点赞关注,激励一下作者。