arguments
for-of
迴圈Array.from()
...
)yield*
return()
和 throw()
ES6 引進一個新的機制來遍歷資料:迭代。兩個概念是迭代的核心
Symbol.iterator
。那個方法是迭代器的工廠。以 TypeScript 符號表示的介面,這些角色看起來像這樣
interface
Iterable
{
[
Symbol
.
iterator
]()
:
Iterator
;
}
interface
Iterator
{
next
()
:
IteratorResult
;
}
interface
IteratorResult
{
value
:
any
;
done
:
boolean
;
}
下列值可迭代
一般物件不可迭代(原因說明於 專門章節)。
透過迭代存取資料的語言建構
const
[
a
,
b
]
=
new
Set
([
'a'
,
'b'
,
'c'
]);
for-of
迴圈
for
(
const
x
of
[
'a'
,
'b'
,
'c'
])
{
console
.
log
(
x
);
}
Array.from()
:
const
arr
=
Array
.
from
(
new
Set
([
'a'
,
'b'
,
'c'
]));
...
)
const
arr
=
[...
new
Set
([
'a'
,
'b'
,
'c'
])];
const
map
=
new
Map
([[
false
,
'no'
],
[
true
,
'yes'
]]);
const
set
=
new
Set
([
'a'
,
'b'
,
'c'
]);
Promise.all()
、Promise.race()
Promise
.
all
(
iterableOverPromises
).
then
(
···
);
Promise
.
race
(
iterableOverPromises
).
then
(
···
);
yield*
:
yield
*
anIterable
;
可迭代性的概念如下。
for-of
迴圈會迴圈處理值,而展開運算子(...
)會將值插入陣列或函式呼叫中。讓每個消費者都支援所有來源並不實際,特別是因為應該可以建立新的來源(例如透過函式庫)。因此,ES6 引入了 Iterable
介面。資料消費者使用它,資料來源實作它
由於 JavaScript 沒有介面,Iterable
比較像慣例
Symbol.iterator
的方法,且該方法會傳回所謂的迭代器,則該值會被視為可迭代。迭代器是一個物件,它會透過其方法 next()
傳回值。我們說:它會迴圈處理可迭代項目的項目(內容),每個方法呼叫一次。讓我們看看陣列 arr
的消耗情況。首先,您透過其金鑰為 Symbol.iterator
的方法建立一個迭代器
> const arr = ['a', 'b', 'c'];
> const iter = arr[Symbol.iterator]();
然後,您重複呼叫迭代器的 next()
方法,以擷取陣列「內部」的項目
> iter.next()
{ value: 'a', done: false }
> iter.next()
{ value: 'b', done: false }
> iter.next()
{ value: 'c', done: false }
> iter.next()
{ value: undefined, done: true }
如您所見,next()
會傳回每個項目包裝在一個物件中,作為 value
屬性的值。布林屬性 done
表示項目順序的結尾何時到達。
Iterable
和迭代器是所謂協定(介面加上使用它們的規則)的一部分,用於迭代。此協定的主要特性是它具有順序性:迭代器一次傳回一個值。這表示如果可迭代資料結構是非線性的(例如樹),則迭代會將其線性化。
我將使用 for-of
迴圈(請參閱章節「for-of
迴圈」)來反覆運算各種可迭代資料。
陣列(和型別化陣列)可反覆運算其元素
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// 'a'
// 'b'
字串可迭代,但它們會反覆運算 Unicode 編碼點,每個編碼點可能包含一個或兩個 JavaScript 字元
for
(
const
x
of
'a\uD83D\uDC0A'
)
{
console
.
log
(
x
);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)
映射可反覆運算其條目。每個條目編碼為 [金鑰、值] 對,一個具有兩個元素的陣列。條目總是會以確定性的方式反覆運算,順序與其加入映射的順序相同。
const
map
=
new
Map
().
set
(
'a'
,
1
).
set
(
'b'
,
2
);
for
(
const
pair
of
map
)
{
console
.
log
(
pair
);
}
// Output:
// ['a', 1]
// ['b', 2]
請注意,弱映射不可迭代。
集合可反覆運算其元素(反覆運算的順序與其加入集合的順序相同)。
const
set
=
new
Set
().
add
(
'a'
).
add
(
'b'
);
for
(
const
x
of
set
)
{
console
.
log
(
x
);
}
// Output:
// 'a'
// 'b'
請注意,弱集合不可迭代。
arguments
儘管特殊變數 arguments
在 ECMAScript 6 中或多或少已過時(由於 rest 參數),但它仍可迭代
function
printArgs
()
{
for
(
const
x
of
arguments
)
{
console
.
log
(
x
);
}
}
printArgs
(
'a'
,
'b'
);
// Output:
// 'a'
// 'b'
大多數 DOM 資料結構最終都會是可迭代的
for
(
const
node
of
document
.
querySelectorAll
(
'div'
))
{
···
}
請注意,實作此功能的工作仍在進行中。但這相對容易做到,因為符號 Symbol.iterator
無法與現有屬性金鑰衝突。
並非所有可迭代內容都必須來自資料結構,它也可以即時計算。例如,所有主要的 ES6 資料結構(陣列、型別化陣列、映射、集合)都有三種方法會傳回可迭代物件
entries()
回傳一個可迭代的物件,編碼為 [key, value] 陣列。對於陣列,值是陣列元素,而鍵是其索引。對於集合,每個鍵和值都是相同的 - 集合元素。keys()
回傳一個可迭代的物件,包含條目的鍵。values()
回傳一個可迭代的物件,包含條目的值。讓我們看看它看起來像什麼。entries()
提供了一個很好的方法來取得陣列元素及其索引
const
arr
=
[
'a'
,
'b'
,
'c'
];
for
(
const
pair
of
arr
.
entries
())
{
console
.
log
(
pair
);
}
// Output:
// [0, 'a']
// [1, 'b']
// [2, 'c']
純粹物件(由物件字面值建立)不可迭代
for
(
const
x
of
{})
{
// TypeError
console
.
log
(
x
);
}
為什麼物件預設不可迭代屬性?原因如下。在 JavaScript 中,你可以迭代兩個層級
預設讓迭代屬性會混淆這些層級,這會有兩個缺點
如果引擎要透過方法 Object.prototype[Symbol.iterator]()
實作可迭代性,那麼會有一個額外的警告:透過 Object.create(null)
建立的物件將不可迭代,因為 Object.prototype
不在它們的原型鏈中。
重要的是要記住,迭代物件的屬性主要是有趣的,如果你使用物件作為 Maps1。但我們只在 ES5 中這樣做,因為我們沒有更好的替代方案。在 ECMAScript 6 中,我們有內建的資料結構 Map
。
迭代屬性的正確(且安全的)方法是透過工具函式。例如,透過 objectEntries()
,其實作稍後顯示(未來的 ECMAScript 版本可能會內建類似功能)
const
obj
=
{
first
:
'Jane'
,
last
:
'Doe'
};
for
(
const
[
key
,
value
]
of
objectEntries
(
obj
))
{
console
.
log
(
`
${
key
}
:
${
value
}
`
);
}
// Output:
// first: Jane
// last: Doe
以下 ES6 語言建構使用迭代協定
for-of
迴圈Array.from()
...
)Promise.all()
、Promise.race()
yield*
以下各節詳細說明每一個建構。
透過陣列範本解構適用於任何可迭代物件
const
set
=
new
Set
().
add
(
'a'
).
add
(
'b'
).
add
(
'c'
);
const
[
x
,
y
]
=
set
;
// x='a'; y='b'
const
[
first
,
...
rest
]
=
set
;
// first='a'; rest=['b','c'];
for-of
迴圈 for-of
是 ECMAScript 6 中的新迴圈。其基本形式如下所示
for
(
const
x
of
iterable
)
{
···
}
如需更多資訊,請參閱章節「for-of
迴圈」。
請注意,需要 iterable
的可迭代性,否則 for-of
無法對值進行迴圈。這表示必須將不可迭代值轉換為可迭代值。例如,透過 Array.from()
。
Array.from()
Array.from()
將可迭代值和類似陣列的值轉換為陣列。它也可用於類型化陣列。
> Array.from(new Map().set(false, 'no').set(true, 'yes'))
[[false,'no'], [true,'yes']]
> Array.from({ length: 2, 0: 'hello', 1: 'world' })
['hello', 'world']
如需有關 Array.from()
的更多資訊,請參閱陣列章節。
...
) 散佈運算子將可迭代值插入陣列
>
const
arr
=
[
'b'
,
'c'
];
>
[
'a'
,
...
arr
,
'd'
]
[
'a'
,
'b'
,
'c'
,
'd'
]
這表示它提供一種簡潔的方式將任何可迭代值轉換為陣列
const
arr
=
[...
iterable
];
散佈運算子也會將可迭代值轉換為函式、方法或建構式呼叫的引數
>
Math
.
max
(...[
-
1
,
8
,
3
])
8
Map 的建構式將可迭代的 [key, value] 成對轉換為 Map
> const map = new Map([['uno', 'one'], ['dos', 'two']]);
> map.get('uno')
'one'
> map.get('dos')
'two'
Set 的建構式將可迭代的元素轉換為 Set
>
const
set
=
new
Set
([
'red'
,
'green'
,
'blue'
]);
>
set
.
has
(
'red'
)
true
>
set
.
has
(
'yellow'
)
false
WeakMap
和 WeakSet
的建構式以類似的方式運作。此外,Map 和 Set 本身是可迭代的(WeakMap 和 WeakSet 則否),這表示您可以使用其建構式複製它們。
Promise.all()
和 Promise.race()
接受可迭代的 Promise
Promise
.
all
(
iterableOverPromises
).
then
(
···
);
Promise
.
race
(
iterableOverPromises
).
then
(
···
);
yield*
yield*
是一個僅在產生器內可用的運算子。它會產生可迭代物件迭代的所有項目。
function
*
yieldAllValuesOf
(
iterable
)
{
yield
*
iterable
;
}
yield*
最重要的使用案例是遞迴呼叫產生器(產生可迭代物件)。
在本節中,我將詳細說明如何實作可迭代物件。請注意,ES6 產生器 通常比「手動」執行此任務方便得多。
迭代協定如下所示。
如果物件有一個金鑰為 Symbol.iterator
的方法(自有或繼承),則該物件會變成可迭代(「實作」Iterable
介面)。該方法必須傳回一個迭代器,一個透過其方法 next()
迭代可迭代物件「內部」項目 的物件。
在 TypeScript 表示法中,可迭代物件和迭代器的介面如下所示2。
interface
Iterable
{
[
Symbol
.
iterator
]()
:
Iterator
;
}
interface
Iterator
{
next
()
:
IteratorResult
;
return
?
(
value
?
:
any
)
:
IteratorResult
;
}
interface
IteratorResult
{
value
:
any
;
done
:
boolean
;
}
return()
是我們稍後會用到的選用方法3。讓我們先實作一個虛擬可迭代物件來了解迭代運作的方式。
const
iterable
=
{
[
Symbol
.
iterator
]()
{
let
step
=
0
;
const
iterator
=
{
next
()
{
if
(
step
<=
2
)
{
step
++
;
}
switch
(
step
)
{
case
1
:
return
{
value
:
'hello'
,
done
:
false
};
case
2
:
return
{
value
:
'world'
,
done
:
false
};
default
:
return
{
value
:
undefined
,
done
:
true
};
}
}
};
return
iterator
;
}
};
讓我們檢查 iterable
是否真的是可迭代的
for
(
const
x
of
iterable
)
{
console
.
log
(
x
);
}
// Output:
// hello
// world
這段程式碼執行三個步驟,其中計數器 step
確保所有事情都按順序發生。首先,我們傳回值 'hello'
,然後傳回值 'world'
,最後我們指出已到達迭代的結尾。每個項目都包覆在一個具有下列屬性的物件中
value
,它包含實際項目,以及done
,它是一個布林旗標,指出是否尚未到達結尾。如果 done
為 false
,則可以省略 done
;如果 value
為 undefined
,則可以省略 value
。也就是說,switch
陳述式可以寫成如下所示。
switch
(
step
)
{
case
1
:
return
{
value
:
'hello'
};
case
2
:
return
{
value
:
'world'
};
default
:
return
{
done
:
true
};
}
如產生器章節中所說明,有些情況下,你會希望即使是最後一個項目 done: true
也有 value
。否則,next()
可以更簡單,並直接傳回項目(不將它們包覆在物件中)。然後,將透過特殊值(例如符號)指出迭代的結尾。
我們來看一個可迭代物件的另一個實作。函式 iterateOver()
傳回一個可迭代物件,迭代傳遞給它的引數
function
iterateOver
(...
args
)
{
let
index
=
0
;
const
iterable
=
{
[
Symbol
.
iterator
]()
{
const
iterator
=
{
next
()
{
if
(
index
<
args
.
length
)
{
return
{
value
:
args
[
index
++
]
};
}
else
{
return
{
done
:
true
};
}
}
};
return
iterator
;
}
}
return
iterable
;
}
// Using `iterateOver()`:
for
(
const
x
of
iterateOver
(
'fee'
,
'fi'
,
'fo'
,
'fum'
))
{
console
.
log
(
x
);
}
// Output:
// fee
// fi
// fo
// fum
如果可迭代物件和迭代器是同一個物件,則前一個函式可以簡化
function
iterateOver
(...
args
)
{
let
index
=
0
;
const
iterable
=
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
index
<
args
.
length
)
{
return
{
value
:
args
[
index
++
]
};
}
else
{
return
{
done
:
true
};
}
},
};
return
iterable
;
}
即使原始的可迭代物件和迭代器不是同一個物件,如果迭代器有下列方法(這也讓它成為可迭代物件),偶爾還是很有用的
[
Symbol
.
iterator
]()
{
return
this
;
}
所有內建的 ES6 迭代器都遵循此模式(透過一個共用原型,請參閱生成器的章節)。例如,陣列的預設迭代器
> const arr = [];
> const iterator = arr[Symbol.iterator]();
> iterator[Symbol.iterator]() === iterator
true
為什麼迭代器同時也是可迭代物件是有用的?for-of
僅適用於可迭代物件,不適用於迭代器。由於陣列迭代器是可迭代的,因此您可以在另一個迴圈中繼續反覆運算
const
arr
=
[
'a'
,
'b'
];
const
iterator
=
arr
[
Symbol
.
iterator
]();
for
(
const
x
of
iterator
)
{
console
.
log
(
x
);
// a
break
;
}
// Continue with same iterator:
for
(
const
x
of
iterator
)
{
console
.
log
(
x
);
// b
}
繼續反覆運算的一個用例是,您可以在透過 for-of
處理實際內容之前移除初始項目(例如標頭)。
return()
和 throw()
兩個迭代器方法是可選的
return()
讓迭代器在反覆運算提早結束時有機會清理。throw()
關於將方法呼叫轉發給透過 yield*
進行反覆運算的產生器。它在生成器的章節中說明。return()
關閉迭代器 如前所述,可選的迭代器方法 return()
是關於讓迭代器在未反覆運算到結尾時進行清理。它會關閉迭代器。在 for-of
迴圈中,提早(或在規格語言中為突然)終止可能是由下列原因造成的
中斷
continue
(如果您繼續外部迴圈,continue
會像 break
一樣作用)throw
return
在這些情況的每一個情況中,for-of
會讓迭代器知道迴圈不會完成。我們來看一個範例,一個函式 readLinesSync
,它會傳回檔案中文字行的可迭代物件,並希望不論發生什麼事都關閉該檔案
function
readLinesSync
(
fileName
)
{
const
file
=
···
;
return
{
···
next
()
{
if
(
file
.
isAtEndOfFile
())
{
file
.
close
();
return
{
done
:
true
};
}
···
},
return
()
{
file
.
close
();
return
{
done
:
true
};
},
};
}
由於 return()
,檔案會在下列迴圈中適當地關閉
// Only print first line
for
(
const
line
of
readLinesSync
(
fileName
))
{
console
.
log
(
x
);
break
;
}
return()
方法必須傳回一個物件。這是因為產生器如何處理 return
陳述式,並會在生成器的章節中說明。
下列結構會關閉未完全「耗盡」的迭代器
for-of
yield*
Array.from()
Map()
、Set()
、WeakMap()
、WeakSet()
Promise.all()
、Promise.race()
在後面的章節有更多關於關閉迭代器的資訊。
在本章節中,我們將探討更多可迭代物件的範例。這些可迭代物件大多數都可以透過產生器更輕鬆地實作。關於產生器的章節將會說明如何實作。
傳回可迭代物件的工具函式和方法與可迭代資料結構一樣重要。以下是用於遍歷物件自有屬性的工具函式。
function
objectEntries
(
obj
)
{
let
index
=
0
;
// In ES6, you can use strings or symbols as property keys,
// Reflect.ownKeys() retrieves both
const
propKeys
=
Reflect
.
ownKeys
(
obj
);
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
index
<
propKeys
.
length
)
{
const
key
=
propKeys
[
index
];
index
++
;
return
{
value
:
[
key
,
obj
[
key
]]
};
}
else
{
return
{
done
:
true
};
}
}
};
}
const
obj
=
{
first
:
'Jane'
,
last
:
'Doe'
};
for
(
const
[
key
,
value
]
of
objectEntries
(
obj
))
{
console
.
log
(
`
${
key
}
:
${
value
}
`
);
}
// Output:
// first: Jane
// last: Doe
另一個選項是使用迭代器而不是索引來遍歷具有屬性金鑰的陣列
function
objectEntries
(
obj
)
{
let
iter
=
Reflect
.
ownKeys
(
obj
)[
Symbol
.
iterator
]();
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
let
{
done
,
value
:
key
}
=
iter
.
next
();
if
(
done
)
{
return
{
done
:
true
};
}
return
{
value
:
[
key
,
obj
[
key
]]
};
}
};
}
組合器4是將現有的可迭代物件組合起來以建立新的可迭代物件的函式。
take(n, iterable)
讓我們從組合器函式 take(n, iterable)
開始,它會傳回 iterable
前 n
個項目的可迭代物件。
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
};
}
}
};
}
const
arr
=
[
'a'
,
'b'
,
'c'
,
'd'
];
for
(
const
x
of
take
(
2
,
arr
))
{
console
.
log
(
x
);
}
// Output:
// a
// b
zip(...iterables)
zip
將 n 個可迭代物件轉換成 n 個元組的可迭代物件(編碼為長度為 n 的陣列)。
function
zip
(...
iterables
)
{
const
iterators
=
iterables
.
map
(
i
=>
i
[
Symbol
.
iterator
]());
let
done
=
false
;
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
!
done
)
{
const
items
=
iterators
.
map
(
i
=>
i
.
next
());
done
=
items
.
some
(
item
=>
item
.
done
);
if
(
!
done
)
{
return
{
value
:
items
.
map
(
i
=>
i
.
value
)
};
}
// Done for the first time: close all iterators
for
(
const
iterator
of
iterators
)
{
if
(
typeof
iterator
.
return
===
'function'
)
{
iterator
.
return
();
}
}
}
// We are done
return
{
done
:
true
};
}
}
}
正如你所見,最短的可迭代物件會決定結果的長度
const
zipped
=
zip
([
'a'
,
'b'
,
'c'
],
[
'd'
,
'e'
,
'f'
,
'g'
]);
for
(
const
x
of
zipped
)
{
console
.
log
(
x
);
}
// Output:
// ['a', 'd']
// ['b', 'e']
// ['c', 'f']
有些可迭代物件可能永遠不會 done
。
function
naturalNumbers
()
{
let
n
=
0
;
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
return
{
value
:
n
++
};
}
}
}
對於無限可迭代物件,你不能遍歷它的「所有」項目。例如,透過中斷 for-of
迴圈
for
(
const
x
of
naturalNumbers
())
{
if
(
x
>
2
)
break
;
console
.
log
(
x
);
}
或只存取無限可迭代物件的開頭
const
[
a
,
b
,
c
]
=
naturalNumbers
();
// a=0; b=1; c=2;
或使用組合器。take()
是一個選項
for
(
const
x
of
take
(
3
,
naturalNumbers
()))
{
console
.
log
(
x
);
}
// Output:
// 0
// 1
// 2
zip()
傳回的可迭代物件的「長度」是由其最短的輸入可迭代物件決定的。這表示 zip()
和 naturalNumbers()
可以讓你對任意(有限)長度的可迭代物件進行編號
const
zipped
=
zip
([
'a'
,
'b'
,
'c'
],
naturalNumbers
());
for
(
const
x
of
zipped
)
{
console
.
log
(
x
);
}
// Output:
// ['a', 0]
// ['b', 1]
// ['c', 2]
你可能會擔心迭代協定很慢,因為每次呼叫 next()
都會建立一個新的物件。然而,現代引擎中小型物件的記憶體管理很快,而且從長遠來看,引擎可以最佳化迭代,這樣就不需要配置中間物件。在es-discuss 上的討論串有更多資訊。
原則上,沒有任何因素會阻止迭代器重複使用相同的迭代結果物件好幾次,我預期大多數情況都能順利運作。不過,如果客戶端快取迭代結果,就會出現問題
const
iterationResults
=
[];
const
iterator
=
iterable
[
Symbol
.
iterator
]();
let
iterationResult
;
while
(
!
(
iterationResult
=
iterator
.
next
()).
done
)
{
iterationResults
.
push
(
iterationResult
);
}
如果迭代器重複使用其迭代結果物件,iterationResults
通常會包含同一個物件好幾次。
您可能會好奇為什麼 ECMAScript 6 沒有可迭代組合器,也就是用於處理可迭代物件或建立可迭代物件的工具。這是因為計畫分兩步驟進行
最後,會將其中一個函式庫或來自多個函式庫的程式碼片段新增到 JavaScript 標準函式庫。
如果您想了解這種函式庫可能長什麼樣子,請查看標準 Python 模組 itertools
。
是的,可迭代物件很難實作,如果您手動實作的話。 下一章將介紹產生器,它有助於執行這項任務(以及其他事情)。
迭代協定包含下列介面(我已從 Iterator
遺漏 throw()
,它僅受 yield*
支援,而且在其中是選用的)
interface
Iterable
{
[
Symbol
.
iterator
]()
:
Iterator
;
}
interface
Iterator
{
next
()
:
IteratorResult
;
return
?
(
value
?
:
any
)
:
IteratorResult
;
}
interface
IteratorResult
{
value
:
any
;
done
:
boolean
;
}
next()
的規則
x
可產生,next()
就會傳回物件 { value: x, done: false }
。next()
應始終傳回一個其 done
屬性為 true
的物件。IteratorResult
迭代結果的 done
屬性不一定要是 true
或 false
,真值或假值就夠了。所有內建語言機制都讓您可以省略 done: false
。
有些可迭代物件每次被要求時都會產生一個新的反覆器。例如,陣列
function
getIterator
(
iterable
)
{
return
iterable
[
Symbol
.
iterator
]();
}
const
iterable
=
[
'a'
,
'b'
];
console
.
log
(
getIterator
(
iterable
)
===
getIterator
(
iterable
));
// false
其他可迭代物件每次都會傳回同一個反覆器。例如,產生器物件
function
*
elements
()
{
yield
'a'
;
yield
'b'
;
}
const
iterable
=
elements
();
console
.
log
(
getIterator
(
iterable
)
===
getIterator
(
iterable
));
// true
當您多次反覆運算同一個可迭代物件時,可迭代物件是否產生新的反覆器很重要。例如,透過下列函數
function
iterateTwice
(
iterable
)
{
for
(
const
x
of
iterable
)
{
console
.
log
(
x
);
}
for
(
const
x
of
iterable
)
{
console
.
log
(
x
);
}
}
使用新的反覆器,您可以多次反覆運算同一個可迭代物件
iterateTwice
([
'a'
,
'b'
]);
// Output:
// a
// b
// a
// b
如果每次都傳回同一個反覆器,您就無法這麼做
iterateTwice
(
elements
());
// Output:
// a
// b
請注意,標準函式庫中的每個反覆器也是一個可迭代物件。它的方法 [Symbol.iterator]()
傳回 this
,表示它總是傳回同一個反覆器(本身)。
反覆運算協定區分結束反覆器的兩種方式
next()
直到它傳回一個屬性 done
為 true
的物件。return()
,表示您不再打算呼叫 next()
。呼叫 return()
的規則
return()
是個選用方法,並非所有反覆器都有。有這個方法的反覆器稱為可關閉。return()
。例如,當 for-of
突然離開(在結束前)時,會呼叫 return()
。下列操作會導致突然離開:break
、continue
(帶有外部區塊的標籤)、return
、throw
。實作 return()
的規則
return(x)
通常會產生物件 { done: true, value: x }
,但如果結果不是物件,語言機制只會擲回錯誤(規格中的來源)。return()
後,next()
傳回的物件也應為 done
。以下程式碼說明,如果在收到 done
迭代器結果之前中斷 for-of
迴圈,則會呼叫 return()
。也就是說,即使在收到最後一個值後中斷,也會呼叫 return()
。這很微妙,當您手動迭代或實作迭代器時,必須小心處理才能正確無誤。
function
createIterable
()
{
let
done
=
false
;
const
iterable
=
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
!
done
)
{
done
=
true
;
return
{
done
:
false
,
value
:
'a'
};
}
else
{
return
{
done
:
true
,
value
:
undefined
};
}
},
return
()
{
console
.
log
(
'return() was called!'
);
},
};
return
iterable
;
}
for
(
const
x
of
createIterable
())
{
console
.
log
(
x
);
// There is only one value in the iterable and
// we abort the loop after receiving it
break
;
}
// Output:
// a
// return() was called!
如果迭代器具有 return()
方法,則表示該迭代器為可關閉的。並非所有迭代器都可關閉。例如,陣列迭代器即不可關閉
> let iterable = ['a', 'b', 'c'];
> const iterator = iterable[Symbol.iterator]();
> 'return' in iterator
false
預設情況下,產生器物件可關閉。例如,以下產生器函式傳回的物件
function
*
elements
()
{
yield
'a'
;
yield
'b'
;
yield
'c'
;
}
如果您在 elements()
的結果上呼叫 return()
,則迭代會結束
> const iterator = elements();
> iterator.next()
{ value: 'a', done: false }
> iterator.return()
{ value: undefined, done: true }
> iterator.next()
{ value: undefined, done: true }
如果迭代器不可關閉,則在從 for-of
迴圈異常結束(例如 A 行中的結束)後,您仍可繼續迭代
function
twoLoops
(
iterator
)
{
for
(
const
x
of
iterator
)
{
console
.
log
(
x
);
break
;
// (A)
}
for
(
const
x
of
iterator
)
{
console
.
log
(
x
);
}
}
function
getIterator
(
iterable
)
{
return
iterable
[
Symbol
.
iterator
]();
}
twoLoops
(
getIterator
([
'a'
,
'b'
,
'c'
]));
// Output:
// a
// b
// c
相反地,elements()
傳回可關閉的迭代器,而 twoLoops()
內部的第二個迴圈沒有任何內容可供迭代
twoLoops
(
elements
());
// Output:
// a
以下類別是防止關閉迭代器的通用解決方案。它透過包裝迭代器並轉送 return()
以外的所有方法呼叫來執行此操作。
class
PreventReturn
{
constructor
(
iterator
)
{
this
.
iterator
=
iterator
;
}
/** Must also be iterable, so that for-of works */
[
Symbol
.
iterator
]()
{
return
this
;
}
next
()
{
return
this
.
iterator
.
next
();
}
return
(
value
=
undefined
)
{
return
{
done
:
false
,
value
};
}
// Not relevant for iterators: `throw()`
}
如果我們使用 PreventReturn
,則在 twoLoops()
的第一個迴圈中異常結束後,產生器 elements()
的結果不會關閉。
function
*
elements
()
{
yield
'a'
;
yield
'b'
;
yield
'c'
;
}
function
twoLoops
(
iterator
)
{
for
(
const
x
of
iterator
)
{
console
.
log
(
x
);
break
;
// abrupt exit
}
for
(
const
x
of
iterator
)
{
console
.
log
(
x
);
}
}
twoLoops
(
elements
());
// Output:
// a
twoLoops
(
new
PreventReturn
(
elements
()));
// Output:
// a
// b
// c
還有另一種方法可以讓產生器無法關閉:產生器函式 elements()
產生的所有產生器物件都具有原型物件 elements.prototype
。您可以透過 elements.prototype
隱藏 return()
的預設實作(位於 elements.prototype
的原型中),如下所示
// Make generator object unclosable
// Warning: may not work in transpilers
elements
.
prototype
.
return
=
undefined
;
twoLoops
(
elements
());
// Output:
// a
// b
// c
try-finally
處理產生器中的清除 有些產生器需要在完成迭代後清除(釋放已配置的資源、關閉開啟的檔案等)。天真地說,我們會這樣實作
function
*
genFunc
()
{
yield
'a'
;
yield
'b'
;
console
.
log
(
'Performing cleanup'
);
}
在正常的 for-of
迴圈中,一切都很好
for
(
const
x
of
genFunc
())
{
console
.
log
(
x
);
}
// Output:
// a
// b
// Performing cleanup
但是,如果您在第一次 yield
後結束迴圈,執行看似永遠停在那裡,永遠不會到達清除步驟
for
(
const
x
of
genFunc
())
{
console
.
log
(
x
);
break
;
}
// Output:
// a
實際上發生的是,每當有人提早離開 for-of
迴圈時,for-of
就會將 return()
傳送至目前的迭代器。這表示不會到達清除步驟,因為產生器函式會在之前傳回。
感謝的是,這很容易透過在 finally
子句中執行清除來修正
function
*
genFunc
()
{
try
{
yield
'a'
;
yield
'b'
;
}
finally
{
console
.
log
(
'Performing cleanup'
);
}
}
現在一切都按照預期運作
for
(
const
x
of
genFunc
())
{
console
.
log
(
x
);
break
;
}
// Output:
// a
// Performing cleanup
因此,使用需要以某種方式關閉或清除的資源的通用模式如下
function
*
funcThatUsesResource
()
{
const
resource
=
allocateResource
();
try
{
···
}
finally
{
resource
.
deallocate
();
}
}
const
iterable
=
{
[
Symbol
.
iterator
]()
{
function
hasNextValue
()
{
···
}
function
getNextValue
()
{
···
}
function
cleanUp
()
{
···
}
let
returnedDoneResult
=
false
;
return
{
next
()
{
if
(
hasNextValue
())
{
const
value
=
getNextValue
();
return
{
done
:
false
,
value
:
value
};
}
else
{
if
(
!
returnedDoneResult
)
{
// Client receives first `done` iterator result
// => won’t call `return()`
cleanUp
();
returnedDoneResult
=
true
;
}
return
{
done
:
true
,
value
:
undefined
};
}
},
return
()
{
cleanUp
();
}
};
}
}
請注意,當你第一次要傳回 done
迭代器結果時,你必須呼叫 cleanUp()
。你不能提早執行,因為這樣 return()
仍然可能會被呼叫。這可能會很棘手。
如果你使用迭代器,你應該適當地關閉它們。在產生器中,你可以讓 for-of
為你完成所有工作
/**
* Converts a (potentially infinite) sequence of
* iterated values into a sequence of length `n`
*/
function
*
take
(
n
,
iterable
)
{
for
(
const
x
of
iterable
)
{
if
(
n
<=
0
)
{
break
;
// closes iterable
}
n
--
;
yield
x
;
}
}
如果你手動管理事物,則需要更多工作
function
*
take
(
n
,
iterable
)
{
const
iterator
=
iterable
[
Symbol
.
iterator
]();
while
(
true
)
{
const
{
value
,
done
}
=
iterator
.
next
();
if
(
done
)
break
;
// exhausted
if
(
n
<=
0
)
{
// Abrupt exit
maybeCloseIterator
(
iterator
);
break
;
}
yield
value
;
n
--
;
}
}
function
maybeCloseIterator
(
iterator
)
{
if
(
typeof
iterator
.
return
===
'function'
)
{
iterator
.
return
();
}
}
如果你不使用產生器,則需要更多工作
function
take
(
n
,
iterable
)
{
const
iter
=
iterable
[
Symbol
.
iterator
]();
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
n
>
0
)
{
n
--
;
return
iter
.
next
();
}
else
{
maybeCloseIterator
(
iter
);
return
{
done
:
true
};
}
},
return
()
{
n
=
0
;
maybeCloseIterator
(
iter
);
}
};
}
return()
,則必須執行清理活動。
try-finally
讓你可以在單一位置處理這兩者。return()
關閉一個迭代器後,它不應該透過 next()
產生任何更多迭代器結果。for-of
等方式)
return
關閉迭代器,如果(且僅在)你沒有用盡它。要正確執行這一點可能會很棘手。