ES6中的新特性:Iterables和iterators
- 2021 年 6 月 17 日
- 筆記
- ECMAScript6, ES6, NodeJs, 程式那些事
簡介
為了方便集合數據的遍歷,在ES6中引入了一個iteration的概念。為我們提供了更加方便的數據遍歷的手段。
一起來學習一下吧。
什麼是iteration
iteration也稱為遍歷,就是像資料庫的游標一樣,一步一步的遍歷集合或者對象的數據。
根據ES6的定義,iteration主要由三部分組成:
- Iterable
先看下Iterable的定義:
interface Iterable {
[Symbol.iterator]() : Iterator;
}
Iterable表示這個對象裡面有可遍歷的數據,並且需要實現一個可以生成Iterator的工廠方法。
- Iterator
interface Iterator {
next() : IteratorResult;
}
可以從Iterable中構建Iterator。Iterator是一個類似游標的概念,可以通過next訪問到IteratorResult。
- IteratorResult
IteratorResult是每次調用next方法得到的數據。
interface IteratorResult {
value: any;
done: boolean;
}
IteratorResult中除了有一個value值表示要獲取到的數據之外,還有一個done,表示是否遍歷完成。
Iterable是一個介面,通過這個介面,我們可以連接數據提供者和數據消費者。
Iterable對象叫做數據提供者。對於數據消費者來說,除了可以調用next方法來獲取數據之外,還可以使用for-of 或者 …擴展運算符來進行遍歷。
for-of的例子:
for (const x of ['a', 'b', 'c']) {
console.log(x);
}
…擴展運算符的例子:
const arr = [...new Set(['a', 'b', 'c'])];
Iterable對象
ES6中,可以被稱為Iterable對象的有下面幾種:
- Arrays
- Strings
- Maps
- Sets
- DOM
先看一個Arrays的情況,假如我們有一個Arrays,可以通過Symbol.iterator這個key來獲取到Iterator:
> const arr = ['a', 'b', 'c'];
> const iter = arr[Symbol.iterator]();
> iter.next()
{ value: 'a', done: false }
> iter.next()
{ value: 'b', done: false }
> iter.next()
{ value: 'c', done: false }
> iter.next()
{ value: undefined, done: true }
更加簡單的辦法就是使用for-of:
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'
看一個遍歷String的情況,String的遍歷是通過Unicode code points來區分的:
for (const x of 'a\uD83D\uDC0A') {
console.log(x);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)
上面的例子中,基礎類型的String在遍歷的時候,會自動轉換成為String對象。
Maps是通過遍歷entries來實現的:
const map = new Map().set('a', 1).set('b', 2);
for (const pair of map) {
console.log(pair);
}
// Output:
// ['a', 1]
// ['b', 2]
還記得之前提到的WeakMaps嗎?
WeakMap,WeakSet和Map於Set的區別在於,WeakMap的key只能是Object對象,不能是基本類型。
為什麼會有WeakMap呢?
對於JS中的Map來說,通常需要維護兩個數組,第一個數組中存儲key,第二個數組中存儲value。每次添加和刪除item的時候,都需要同時操作兩個數組。
這種實現有兩個缺點,第一個缺點是每次查找的時候都需要遍歷key的數組,然後找到對應的index,再通過index來從第二個數組中查找value。
第二個缺點就是key和value是強綁定的,即使key不再被使用了,也不會被垃圾回收。
所以引入了WeakMap的概念,在WeakMap中,key和value沒有這樣的強綁定關係,key如果不再被使用的話,可以被垃圾回收器回收。
因為引用關係是weak的,所以weakMap不支援key的遍歷,如果你想遍歷key的話,請使用Map。
看下Set的遍歷:
const set = new Set().add('a').add('b');
for (const x of set) {
console.log(x);
}
// Output:
// 'a'
// 'b'
我們還可以遍歷arguments對象:
function printArgs() {
for (const x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// Output:
// 'a'
// 'b'
對於大部分DOM來說,也是可以遍歷的:
for (const node of document.querySelectorAll('div')) {
···
}
普通對象不是可遍歷的
簡單對象就是通過字面量創建出來的對象,這些對象雖然也有key-value的內容,但是是不可遍歷的。
為什麼呢?
因為可遍歷對象比如Array,Map,Set也是普通對象的一種特例。如果普通對象可以遍歷了,那麼會導致可以遍歷對象的一些遍歷中的衝突。
for (const x of {}) { // TypeError
console.log(x);
}
雖然不能直接遍歷普通對象,但是我們可以通過使用objectEntries方法來遍歷普通對象。
先看下objectEntries的實現:
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]] };
}
};
}
我們通過Reflect.ownKeys()反射拿到對象中的iterator.然後通過這個iterator來進行普通對象的遍歷。
看下具體的使用:
const obj = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
自定義iterables
除了ES6中默認的iterables之外,我們還可以自定義iterables。
因為iterables是一個介面,我們只需要實現它就可以了。我們看一個iterables的例子:
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;
}
iterateOver方法會返回一個iterable對象。在這個對象中,我們實現了Symbol.iterator為key的方法。這個方法返回一個iterator,在iterator中,我們實現了next方法。
上面的方法使用起來是下面的效果:
// Using `iterateOver()`:
for (const x of iterateOver('fee', 'fi', 'fo', 'fum')) {
console.log(x);
}
// Output:
// fee
// fi
// fo
// fum
上面的例子中,如果Symbol.iterator返回的對象是iterable本身,那麼iterable也是一個iterator。
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;
}
這樣做的好處就是,我們可以使用for-of同時遍歷iterables和iterators,如下所示:
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
}
關閉iterators
如果我們需要遍歷的過程中,從iterators中返回該怎麼處理呢?
通過實現return方法,我們可以在程式中斷的時候(break,return,throw)調用iterators的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);
break;
}
// Output:
// a
// return() was called!
上面例子中,我們通過break來中斷遍歷,最終導致return方法的調用。
注意,return方法必須要返回一個對象,{ done: true, value: x }
總結
上面就是ES6中引入的Iterables和iterators的一些概念。
本文作者:flydean程式那些事
本文鏈接://www.flydean.com/es6-iterables-iterator/
本文來源:flydean的部落格
歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!