关于前端:两千字助你理解for-of原理扩展for-of完美解决遍历对象问题

5次阅读

共计 4971 个字符,预计需要花费 13 分钟才能阅读完成。

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,直到大于 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 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
}

结语

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

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

正文完
 0