JavaScript Lazy evaluation:可迭代對象與迭代器
本文已經過原作者 MelkorNemesis 授權翻譯。
Lazy evaluation
Lazy evaluation常被譯為“延遲計算”或“惰性計算”,指的是僅僅在真正需要執行的時候才計算表達式的值。
與惰性求值相反的是及早求值(eager evaluation)及早求值,也被稱為貪婪求值(greedy evaluation)或嚴格求值,是多數傳統編程語言的求值策略。
充分利用惰性求值的特性帶來的好處主要體現在以下兩個方面:
- 避免不必要的計算,帶來性能上的提升。
- 節省空間,使得無限循環的數據結構成為可能。
迭代器
ES6 中的迭代器使惰性求值和創建用戶定義的數據序列成為可能。迭代是一種遍歷數據的機制。迭代器是用于遍歷數據結構元素(稱為Iterable)的指針,用于產生值序列的指針。
迭代器是一個可以被迭代的對象。它抽象了數據容器,使其行為類似于可迭代對象。
迭代器在實例化時不計算每個項目的值,僅在請求時才生成下一個值。這非常有用,特別是對于大型數據集或無限個元素的序列。
可迭代對象
可迭代對象是希望其元素可被公眾訪問的數據結構。JS 中的很多對象都是可迭代的,它們可能不是很好的察覺,但是如果仔細檢查,就會發現迭代的特征:
- new Map([iterable])
- new WeakMap([iterable])
- new Set([iterable])
- new WeakSet([iterable])
- Promise.all([iterable])
- Promise.race([iterable])
- Array.from([iterable])
還有需要一個可迭代的對象,否則,它將拋出一個類型錯誤,例如:
- for ... of
- ... (展開操作符)const [a, b, ..] = iterable (解構賦值)
- yield* (生成器)
JavaScript中已有許多內置的可迭代項:
String,Array,TypedArray,Map,Set。
迭代協議
迭代器和可迭對象遵循迭代協議。
協議是一組接口,并規定了如何使用它們。
迭代器遵循迭代器協議,可迭代遵循可迭代協議。
可迭代的協議
要使對象變得可迭代,它必須實現一個通過Symbol.iterator的迭代器方法,這個方法是迭代器的工廠。
使用 TypeScript,可迭代協議如下所示:
- interface Iterable {
- [Symbol.iterator]() : Iterator;
- }
Symbol.iterator]()是無參數函數。在可迭代對象上調用它,這意味著我們可以通過this來訪問可迭代對象,它可以是常規函數或生成器函數。
迭代器協議
迭代器協議定義了產生值序列的標準方法。
為了使對象成為迭代器,它必須實現next()方法。迭代器可以實現return()方法,我們將在本文后面討論這個問題。
使用 TypeScript,迭代器協議如下所示:
- interface Iterator {
- next() : IteratorResult;
- return?(value?: any): IteratorResult;
- }
IteratorResult 的定義如下:
- interface IteratorResult {
- value?: any;
- done: boolean;
- }
- done通知消費者迭代器是否已經被使用,false表示仍有值需要生成,true表示迭代器已經結束。
- value 可以是任何 JS 值,它是向消費者展示的值。
當done為true時,可以省略value。
組合
迭代器和可以可迭代對象可以用下面這張圖來表示:
事例
基礎知識介紹完了,接著,我們來配合一些事例來加深我們的映像。
范圍迭代器
我們先從一個非常基本的迭代器開始,createRangeIterator迭代器。
我們手動調用it.next()以獲得下一個IteratorResult。最后一次調用返回{done:true},這意味著迭代器現在已被使用,不再產生任何值。
- function createRangeIterator(from, to) {
- let i = from;
- return {
- next() {
- if (i <= to) {
- return { value: i++, done: false };
- } else {
- return { done: true };
- }
- }
- }
- }
- const it = createRangeIterator(1, 3);
- console.log(it.next());
- console.log(it.next());
- console.log(it.next());
- console.log(it.next());
可迭代范圍迭代器
在本文的前面,我已經提到 JS 中的某些語句需要一個可迭代的對象。因此,我們前面的示例在與for ... of循環一起使用時將不起作用。
但是創建符合迭代器和可迭代協議的對象非常容易。
- function createRangeIterator (from, to) {
- let i = from
- return {
- [Symbol.iterator] () {
- return this
- },
- next() {
- if (i <= to) {
- return { value: i++, done: false }
- } else {
- return { done: true }
- }
- }
- }
- }
- const it = createRangeIterator(1, 3)
- for (const i of it) {
- console.log(i)
- }
無限序列迭代器
迭代器可以表示無限制大小的序列,因為它們僅在需要時才計算值。
注意不要在無限迭代器上使用擴展運算符(...),JS 將嘗試消費迭代器,由于迭代器是無限的,因此它將永遠不會結束。所以你的應用程序將崩潰,因為內存已被耗盡 ??
同樣,for ... of 循環也是一樣的情況,所以要確保能退出循環:
- function createEvenNumbersIterator () {
- let value = 0
- return {
- [Symbol.iterator] () {
- return this
- },
- next () {
- value += 2
- return { value, done: false}
- }
- }
- }
- const it = createEvenNumbersIterator()
- const [a, b, c] = it
- console.log({a, b, c})
- const [x, y, z] = it
- console.log({ x, y, z })
- for (const even of it) {
- console.log(even)
- if (even > 20) {
- break
- }
- }
關閉迭代器
前面我們提到過,迭代器可以有選擇地使用return()方法。當迭代器直到最后都沒有迭代時使用此方法,并讓迭代器進行清理。
for ... of循環可以通過以下方式更早地終止迭代:
- break
- continue
- throw
- return
- function createCloseableIterator () {
- let idx = 0
- const data = ['a', 'b', 'c', 'd', 'e']
- function cleanup() {
- console.log('Performing cleanup')
- }
- return {
- [Symbol.iterator]() { return this },
- next () {
- if (idx <= data.length - 1) {
- return { value: data[idx++], done: false }
- } else {
- cleanup()
- return { done: true }
- }
- },
- return () {
- cleanup()
- return { done: true }
- }
- }
- }
- const it = createCloseableIterator()
- for (const value of it) {
- console.log(value)
- if (value === 'c') {
- break
- }
- }
- console.log('\n----------\n')
- const _it = createCloseableIterator();
- for (const value of _it) {
- console.log(value);
- }
- 如果知道迭代器已經結束,則手動調用cleanup()函數。
- 如果突然完成,則return()起作用并為我們進行清理。
額外的內容
如果你已經做到了這一點,我們來看看一些額外的內容。
組合器
組合器是將現有可迭代對象組合在一起以創建新可迭代對象的函數。
因此,我們能夠創建許多實用函數。那map或者filter呢?看看下面的代碼,花一分鐘時間來理解它。
- function createEvenNumbersIterator() {
- let value = 0;
- return {
- [Symbol.iterator]() {
- return this;
- },
- next() {
- value += 2;
- return { value, done: false };
- }
- }
- }
- function map(fn, iterable) {
- const iter = iterable[Symbol.iterator]();
- return {
- [Symbol.iterator]() {
- return this;
- },
- next() {
- const n = iter.next();
- if (!n.done) {
- return { value: fn(n.value), done: false };
- } else {
- return { done: true };
- }
- }
- }
- }
- function filter(fn, iterable) {
- const iter = iterable[Symbol.iterator]();
- return {
- [Symbol.iterator]() {
- return this;
- },
- next() {
- const n = iter.next();
- if (!n.done) {
- if (fn(n.value)) {
- return { value: n.value, done: false };
- } else {
- return this.next();
- }
- } else {
- return { done: true };
- }
- }
- }
- }
- function take(n, iterable) {
- const iter = iterable[Symbol.iterator]();
- return {
- [Symbol.iterator]() {
- return this;
- },
- next() {
- if (n > 0) {
- n--;
- return iter.next();
- } else {
- return { done: true };
- }
- }
- }
- }
- function cycle(iterable) {
- const iter = iterable[Symbol.iterator]();
- const saved = [];
- let idx = 0;
- return {
- [Symbol.iterator]() {
- return this;
- },
- next() {
- const n = iter.next();
- if (!n.done) {
- saved[idx++] = n.value;
- return { value: n.value, done: false };
- } else {
- return { value: saved[idx++ % saved.length], done: false };
- }
- }
- }
- }
- function collect(iterable) {
- // consumes the iterator
- return Array.from(iterable);
- }
- const evenNumbersIterator = createEvenNumbersIterator();
- const result = collect( // 7. and collect the result
- filter( // ⬆️ 6. keep only values higher than 1
- val => val > 1, map( // ⬆️ 5. divide obtained values by 2
- val => val / 2, take( // ⬆️ 4. take only six of them
- 6, cycle( // ⬆️ 3. make an infinite cycling sequence of them
- take( // ⬆️ 2. take just three of them
- 3, evenNumbersIterator // ⬆️ 1. infinite sequence of even numbers
- )
- )
- )
- )
- )
- );
- console.log(result);
這是一大堆代碼,很快我們將看到如何使用生成器和函數式編程概念來重構所有這些內容。保持關注,并注意我的后續文章,我們仍然有很多內容要講。
作者:MelkorNemesis 譯者:前端小智 來源:medium
原文:https://medium.com/@MelrNemesis/javascript-lazy-evaluation-iterables-iterators-e0770a5de96f
本文轉載自微信公眾號「 大遷世界」,可以通過以下二維碼關注。轉載本文請聯系 大遷世界公眾號。