Iterator(遍历器) 和 for…of 循环
遍历器(Iterator)就是这样一种机制。它是一种接口,为各类不一样的数据结构提供统一的访问机制
任何数据结构只要部署 Iterator 接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)
html
1、迭代器和 for…of 浅谈
1.1 传统 for 循环
先来看一段标准的 for 循环的代码:前端
var arr = [1,2,3]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // 1 2 3
注意,咱们拿到了里面的元素,但却多作了不少事:git
- 咱们声明了 i 标索引;
- 肯定了边界,一旦多层嵌套;
function unique(array) { var res = []; for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { for (var j = 0, resLen = res.length; j < resLen; j++) { if (array[i] === res[j]) { break; } } if (j === resLen) { // 把首次出现的加入到新数组中 res.push(array[i]); } } return res; }
为了消除这种复杂度以及减小循环中的错误(好比错误使用其余循环中的变量),ES6 提供了迭代器和 for of 循环共同解决这个问题。es6
1.2 terator(迭代器)
迭代器的描述:github
- 是为各类数据结构,提供一个统一的、简便的访问接口,是用于遍历数据结构元素的指针
- 二是使得数据结构的成员可以按某种次序排列;
- 三是
ES6
创造的一种遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费。
迭代的过程以下:web
- 经过 Symbol.iterator 建立一个迭代器,指向当前数据结构的起始位置
- 随后经过 next 方法进行向下迭代指向下一个位置:
- next 方法会返回当前位置的对象,对象包含了
value
和done
两个属性; - value 是当前属性的值;
- done 用于判断是否遍历结束,done 为 true 时则遍历结束;
- next 方法会返回当前位置的对象,对象包含了
迭代的内部逻辑应该是:数组
var it = makeIterator(["a", "b"]); it.next(); // { value: "a", done: false } it.next(); // { value: "b", done: false } it.next(); // { value: undefined, done: true } function makeIterator(array) { let index = 0; const iterator = { }; iterator.next = function() { if (index < array.length) return { value: array[index++], done: false }; return { value: undefined, done: true }; }; return iterator; }
1.3 什么是 for…of?
注意这里咱们仅说起了 forof 与迭代器的关系。数据结构
for…of 的描述:函数
- for…of 语句在可迭代对象上建立一个迭代循环,调用自定义迭代钩子,并为每一个不一样属性的值执行语句——MDN
- 一个数据结构只要部署了
Symbol.iterator
属性,就被视为具备iterator
接口,就能够用for...of
循环遍历它的成员。
看到这里你会发现for...of
和迭代器老是在一块儿, for...of
循环内部调用的是数据结构的Symbol.iterator
方法。学习
举个例子:
const obj = { value: 1, }; for (value of obj) { console.log(value); } // TypeError: iterator is not iterable
咱们直接 for of 遍历一个对象,会报错,然而若是咱们给该对象添加 Symbol.iterator 属性:
const obj = { value: 1, }; obj[Symbol.iterator] = function() { return createIterator([1, 2, 3]); }; for (value of obj) { console.log(value); } // 1 // 2 // 3
由此,咱们也能够发现 for...of
遍历的实际上是对象的 Symbol.iterator
属性。
JavaScript 原有的 for…in 循环,只能得到对象的键名,不能直接获取键值。ES6 提供 for…of 循环,容许遍历得到键值。
var arr = ["a", "b", "c", "d"]; for (let a in arr) { console.log(a); // 0 1 2 3 } for (let a of arr) { console.log(a); // a b c d }
上面代码代表:
- for…in 循环读取键名
- for…of 循环读取键值
for…of 循环调用遍历器接口,数组的遍历器接口只返回具备数字索引的属性。这一点跟 for…in 循环也不同。
2、默认的 Iterator 接口
Iterator
接口的目的,就是为全部数据结构,提供了一种统一的访问机制。当使用 for…of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
原生具有 Iterator 接口的数据结构以下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
拿数组举例:
const item = [1, 2, 3][Symbol.iterator](); item.next(); item.next(); item.next(); // {value: 1, done: false} // {value: 2, done: false} // {value: 3, done: false} // {value: undefined, done: true}
对于原生部署Iterator
接口的数据结构,不用本身写遍历器生成函数,for...of
循环会自动遍历它们。除此以外,都须要本身在 Symbol.iterator 属性上面部署。
本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。
对象(Object)之因此没有默认部署 Iterator 接口,也是由于对象无法统一进行线性转换
一个对象若是要具有可被 for…of 循环调用的 Iterator 接口,就必须在 Symbol.iterator 的属性上部署遍历器生成方法(原型链上的对象具备该方法也可)。
class newiterator { constructor(start, stop) { this.value = start; this.stop = stop; } // Iterator接口 返回自己 [Symbol.iterator]() { return this; } next() { if (this.value < this.stop) { return { value: this.value++, done: false }; } return { value: undefined, done: true }; } } const iterator = new newiterator(0, 3); for (let key of iterator) { console.log(key); } // 0 1 2
上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator 属性对应一个函数,执行后返回当前对象的遍历器对象。
对于相似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // 或者 NodeList.prototype[Symbol.iterator] = [][Symbol.iterator]; [...document.querySelectorAll("div")]; // 能够执行了
NodeList 对象是相似数组的对象,原本就具备遍历接口,能够直接遍历。上面代码中,咱们将它的遍历接口改为数组的 Symbol.iterator 属性,能够看到没有任何影响。
注意,普通对象部署数组的 Symbol.iterator 方法,并没有效果。
let iterable = { a: "a", b: "b", c: "c", length: 3, [Symbol.iterator]: Array.prototype[Symbol.iterator], }; for (let item of iterable) { console.log(item); // undefined, undefined, undefined }
若是 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。
3、模拟实现的 for…of
其实模拟实现 for of
也比较简单,就是利用它与 Symbol.iterator 的关系。
function forOf(obj, cb) { let iterable, result; if (typeof obj[Symbol.iterator] !== "function") throw new TypeError(result + " is not iterable"); if (typeof cb !== "function") throw new TypeError("cb must be callable"); iterable = obj[Symbol.iterator](); result = iterable.next(); while (!result.done) { cb(result.value); result = iterable.next(); } }
4、使用 Iterator 接口的场景
有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法),除了 for…of 循环,还有几个别的场合。
4.1 解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator 方法。
let set = new Set() .add("a") .add("b") .add("c"); let [x, y] = set; // x='a'; y='b' let [first, ...rest] = set; // first='a'; rest=['b','c'];
4.2 扩展运算符
扩展运算符(…)也会调用默认的 Iterator 接口。
// 例一 var str = "hello"; [...str]; // ['h','e','l','l','o'] // 例二 let arr = ["b", "c"]; ["a", ...arr, "d"]; // ['a', 'b', 'c', 'd']
上面代码的扩展运算符内部就调用 Iterator 接口。
实际上,这提供了一种简便机制,能够将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就能够对它使用扩展运算符,将其转为数组。
4.3 yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function*() { yield 1; yield* [2, 3, 4]; yield 5; }; var iterator = generator(); iterator.next(); // { value: 1, done: false } iterator.next(); // { value: 2, done: false } iterator.next(); // { value: 3, done: false } iterator.next(); // { value: 4, done: false } iterator.next(); // { value: 5, done: false } iterator.next(); // { value: undefined, done: true }
4.4 其余场合
因为数组的遍历会调用遍历器接口,因此任何接受数组做为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(好比 new Map([[‘a’,1],[‘b’,2]]))
- Promise.all()
- Promise.race()
5、Iterator 接口与 Generator 函数
Symbol.iterator()
方法的最简单实现,仍是使用 ES6 新提出的 Generator 函数。
let myIterable = { [Symbol.iterator]: function* () { yield 1; yield 2; yield 3; } }; [...myIterable] // [1, 2, 3] // 或者采用下面的简洁写法 let obj = { [Symbol.iterator]() { yield 'hello'; yield 'world'; } }; for (let x of obj) { console.log(x); } // "hello" // "world"
上面代码中,Symbol.iterator()方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值便可。
6、遍历器对象的 return(),throw()
遍历器对象除了具备 next()方法,还能够具备 return()方法和 throw()方法。若是你本身写遍历器对象生成函数,那么 next()方法是必须部署的,return()方法和 throw()方法是否部署是可选的。
return()方法的使用场合是,若是 for…of 循环提早退出(一般是由于出错,或者有 break 语句),就会调用 return()方法。若是一个对象在完成遍历前,须要清理或释放资源,就能够部署 return()方法。
function readLinesSync(file) { return { [Symbol.iterator]() { return { next() { return { done: false }; }, return() { file.close(); return { done: true }; }, }; }, }; }
上面代码中,函数 readLinesSync 接受一个文件对象做为参数,返回一个遍历器对象,其中除了 next()方法,还部署了 return()方法。下面的两种状况,都会触发执行 return()方法。
// 状况一 for (let line of readLinesSync(fileName)) { console.log(line); break; } // 状况二 for (let line of readLinesSync(fileName)) { console.log(line); throw new Error(); }
上面代码中:
- 状况一输出文件的第一行之后,就会执行 return()方法,关闭这个文件;
- 状况二会在执行 return()方法关闭文件以后,再抛出错误。
参考
写在最后
JavaScript 系列:
关于我
- 花名:余光(沉迷 JS,虚心学习中)
- WX:j565017805
其余沉淀
这是文章所在 GitHub 仓库的传送门,您点的 star
,就是对我最大的鼓励 ~