本文深入解析 ES6 中的 for...of 循环,对比传统的 for 和 for...in 循环,详细阐述可迭代对象、迭代器协议及 Symbol.iterator 方法的工作原理,并通过代码示例展示如何创建自定义可迭代对象及使用生成器简化实现。
本文重点探讨 ES6 标准中引入的 for...of 循环机制。
传统遍历方式回顾
在 ES6 之前,JavaScript 主要依赖两种遍历方式。
第一种是经典的 for 循环,适用于遍历数组或具有 length 属性的索引对象。
for(i=0;i<things.length;i++) {
var thing = things[i]
/* ... */
}
第二种是 for...in 循环,主要用于遍历对象的键值对。
for(key in things) {
if(!thing.hasOwnProperty(key)) { continue; }
var thing = things[key]
/* ... */
}
for...in 循环通常被认为存在副作用,因为它会遍历对象所有可枚举属性[1],包括原型链上的属性以及被赋值的方法。这意味着它可能会遍历到开发者未预期的属性。因此,使用 for...in 时通常需要在循环体内添加保护子句以过滤不需要的属性。
早期 JavaScript 社区通过第三方库(如 Prototype.js、jQuery、lodash 等)提供了 each 或 foreach 等工具方法,以简化对象和数组的遍历,避免直接使用 for 或 for...in 循环。
ES6 引入 for...of 循环,旨在无需依赖第三方库的情况下解决上述遍历问题。
for...of 语法详解
for...of 循环用于遍历可迭代(iterable)对象。
for(const thing of things) {
/* ... */
}
可迭代对象是指定义了 @@iterator 方法的对象,该方法返回一个实现迭代器协议的对象,或者该方法本身是一个生成器函数。
理解这句话需要掌握多个概念:
以下是需要澄清的关键点:
- 什么是可迭代对象?
- @@iterator 方法及其含义
- 迭代器协议的内容
- 可迭代(iterable)与迭代器(iterator)的区别
- 生成器函数的作用
接下来将逐一解答上述疑问。
内置可迭代对象
JavaScript 中部分内置对象天然支持迭代,最典型的是数组。可以在 for...of 循环中直接使用数组,如以下代码所示:
const foo = [
'apples','oranges','pears'
]
for(const thing of foo) {
console.log(thing)
}
执行结果将输出数组中的所有元素。
输出示例: apples oranges pears
此外,数组的 entries 方法返回一个可迭代对象,每次循环返回键和值。示例代码如下:
const foo = [
'apples','oranges','pears'
]
for(const thing of foo.entries()) {
console.log(thing)
}
执行后将输出以下内容:
输出示例: [ 0, 'apples' ] [ 1, 'oranges' ] [ 2, 'pears' ]
当使用以下语法时,entries 方法更加实用:
const foo = [
'apples','oranges','pears'
]
for(const [key, value] of foo.entries()) {
console.log(key,':',value)
}
在 for 循环中声明两个变量,分别用于接收数组的索引(键)和对应的值。
普通 JavaScript 对象默认不可迭代。若执行以下代码:
// 无法正常执行
const foo = {
'apples':'oranges',
'pears':'prunes'
}
for(const [key, value] of foo) {
console.log(key,':',value)
}
将抛出错误:
错误信息: $ node test.js /path/to/test.js:6 for(const [key, value] of foo) { TypeError: foo is not iterable
然而,全局 Object 对象的静态 entries 方法接受普通对象作为参数,并返回一个可迭代对象。示例程序如下:
const foo = {
'apples':'oranges',
'pears':'prunes'
}
for(const [key, value] of Object.entries(foo)) {
console.log(key,':',value)
}
执行后可得到预期输出:
输出示例: $ node test.js apples : oranges pears : prunes
自定义可迭代对象
若需创建自定义可迭代对象,过程相对复杂。回顾前文所述:
可迭代对象需定义 @@iterator 方法,该方法返回实现迭代器协议的对象或为生成器函数。
理解这一概念的最佳方式是逐步构建可迭代对象。首先,需要定义 @@iterator 方法。注意,@@ 符号具有误导性,实际应使用预定义的 Symbol.iterator 符号来定义该方法。
若仅定义迭代器方法并尝试遍历:
const foo = {
[Symbol.iterator]: function() {
}
}
for(const [key, value] of foo) {
console.log(key, value)
}
将收到新错误:
错误信息: for(const [key, value] of foo) { ^ TypeError: Result of the Symbol.iterator method is not an object
该错误表明 JavaScript 尝试调用 Symbol.iterator 方法,但返回值不是对象。
为解决此错误,迭代器方法需返回实现迭代器协议的对象。这意味着该方法必须返回一个包含 next 键的对象,且 next 键对应一个函数。
const foo = {
[Symbol.iterator]: function() {
return {
next: function() {
}
}
}
}
for(const [key, value] of foo) {
console.log(key, value)
}
若运行上述代码,将遇到新错误。
错误信息: for(const [key, value] of foo) { ^ TypeError: Iterator result undefined is not an object
此错误表明 JavaScript 尝试调用 Symbol.iterator 方法,虽然返回值是对象且实现了 next 方法,但 next 的返回值不符合预期。
next 函数需返回特定格式的对象,包含 value 和 done 两个键。
next: function() {
//...
return {
done: false,
value: 'next value'
}
}
done 键为可选。若值为 true,表示迭代已完成。
若 done 为 false 或不存在,则需包含 value 键,该键表示循环中应返回的值。
因此,在代码中加入以下程序,创建一个返回前十个偶数的简单迭代器:
class First20Evens {
constructor() {
this.currentValue = 0
}
[Symbol.iterator]( "Symbol.iterator") {
return {
next: (function() {
this.currentValue+=2
if(this.currentValue > 20) {
return {done:true}
}
return {
value:this.currentValue
}
}).bind(this)
}
}
}
const foo = new First20Evens;
for(const value of foo) {
console.log(value)
}
生成器应用
手动构建实现迭代器协议的对象并非唯一选择。由生成器函数返回的生成器对象同样实现了迭代器协议。上述示例若使用生成器构建,代码如下:
class First20Evens {
constructor() {
this.currentValue = 0
}
[Symbol.iterator]( "Symbol.iterator") {
return function*() {
for(let i=1;i<=10;i++) {
if(i % 2 === 0) {
yield i
}
}
}()
}
}
const foo = new First20Evens;
for(const item of foo) {
console.log(item)
}
本文不深入探讨生成器细节,如需入门可参考相关文档。关键结论是,可使 Symbol.iterator 方法返回生成器对象,从而在 for...of 循环中“正常工作”。所谓“正常工作”,指循环能持续调用 next,直到生成器停止 yield 值。
输出示例: $ node sample-program.js 2 4 6 8 10
参考资料
对象的每个可枚举属性: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
总结
本文介绍了 ES6 的 for...of 循环与可迭代对象,更多相关内容可搜索本站往期文章或继续浏览后续文章,感谢支持!