for/of 與迭代器
August 7, 2022不管你過去是如何循序走訪字串內容,在 ES6 看到 for/of
語法可以走訪字串,應該會覺得很方便:
> let name = 'Justin';
undefined
> for(let ch of name) {
... console.log(ch);
... }
J
u
s
t
i
n
undefined
>
使用 for/of
如果你想要走訪陣列,也可以使用 for/of
:
> for(let v of [10, 20, 30]) {
... console.log(v);
... }
10
20
30
undefined
>
實際上,for/of
會嘗試從字串或陣列上取得迭代器(Iterator),一個可以傳回迭代器的函式,是使用 Symbol.iterator
作為特性儲存著,試著取得該函式…
> let arr = [10, 20, 30];
undefined
> let it = arr[Symbol.iterator]();
undefined
> it.next();
{ value: 10, done: false }
> it.next();
{ value: 20, done: false }
> it.next();
{ value: 30, done: false }
> it.next();
{ value: undefined, done: true }
>
可以看到傳回的迭代器具有 next
方法,每次呼叫都會傳回一個迭代器結果(IteratorResult)物件,當中包含了 value
與 done
,value
是當次迭代的結果,done
表示迭代是否結束,當迭代結束時,value
會是 undefined
。
Iterable/Iterator
只要物件上具有可傳回迭代器的函式,該物件就會是可迭代的(Iterable),也就可以使用 for/of
來進行迭代。例如,來建立一個 Python 風格的 range
函式:
function range(start, end) {
let i = start;
return {
[Symbol.iterator]() {
return this;
},
next() {
return i < end ?
{value: i++, done: false} :
{value: undefined, done: true}
}
};
}
for(let n of range(3, 8)) {
console.log(n);
}
執行結果會顯示 3 到 7 的數字,range
函式傳回的物件,具有 [Symbol.iterator]
特性,執行後可傳回迭代器,由於該物件本身也實作了迭代器的 next
特性,因此直接傳回自己(return this
)就可以了。
next
的傳回物件在 value
是 undefined
,可以省略不寫(你應該知道為什麼吧!)。
物件必須有 [Symbol.iterator]
特性,執行後可傳回迭代器,才能使用 for/of
,因此單純的物件實字,別寄望能使用 for/of
來迭代特性名稱或特性值(沒有迭代器,怎麼知道你要迭代什麼東西),這會引發 TypeError
:
> var o = { x : 10 };
undefined
> for(let what of o) {
... console.log(what);
... }
TypeError: o is not iterable
at repl:1:14
at ContextifyScript.Script.runInThisContext (vm.js:50:33)
at REPLServer.defaultEval (repl.js:240:29)
at bound (domain.js:301:14)
at REPLServer.runBound [as eval] (domain.js:314:12)
at REPLServer.onLine (repl.js:441:10)
at emitOne (events.js:121:20)
at REPLServer.emit (events.js:211:7)
at REPLServer.Interface._onLine (readline.js:282:10)
at REPLServer.Interface._line (readline.js:631:8)
>
ES6 中大多數內建的迭代器僅實作了 next
,除此之外,迭代器可以選擇性地提供 return
或 throw
方法。
return
是提供迭代器的客戶端呼叫,告知迭代器不需要再迭代了,因此迭代器此時可在 return
中實作回收資源之類的動作,然而傳回 {value: undefined, done: true}
,value
的實際值,也可能是迭代器的客戶端指定之值,在此之後,呼叫迭代器的 next
不應該有進一步的迭代結果。
例如,for/of
會在被 break
或拋出一個錯誤時呼叫 return
,不會傳入任何值。
function range(start, end) {
let i = start;
return {
[Symbol.iterator]() {
return this;
},
next() {
return i < end ?
{value: i++, done: false} :
{done: true}
},
return() {
console.log('return');
return {done : true};
}
};
}
let r = range(3, 8);
for(let n of r) {
console.log(n);
throw new Error('shit');
}
執行的結果中可以看到,迭代器的 return
被呼叫了:
3
return
C:\workspace\helloworld.js:19
throw new Error('shit');
^
Error: shit
at Object.<anonymous> (C:\workspace\helloworld.js:19:8)
at Module._compile (module.js:635:30)
at Object.Module._extensions..js (module.js:646:10)
at Module.load (module.js:554:32)
at tryModuleLoad (module.js:497:12)
at Function.Module._load (module.js:489:3)
at Function.Module.runMain (module.js:676:10)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:608:3
迭代器的客戶端可以透過迭代器 throw
方法,將指定的例外送入迭代器,這時就要看迭代器怎麼處理了,可以是單純結束迭代,然而忽略錯誤,例如,底下只會顯示 3:
function range(start, end) {
let i = start;
return {
[Symbol.iterator]() {
return this;
},
next() {
return i < end ?
{value: i++, done: false} :
{done: true}
},
return() {
console.log('return');
return {done : true};
},
throw(e) {
i = end;
return {done: true};
}
};
}
let r = range(3, 8);
for(let n of r) {
console.log(n);
r.throw(new Error());
}
由於 i
被設為 end
了,在下一次呼叫 next
時,會傳回 {done: true}
而中止迭代,照規範,throw
要傳回迭代器結果物件。當然,也許你的迭代器只是跳過下個迭代值也可以,例如,底下顯示 3、5、7:
function range(start, end) {
let i = start;
return {
[Symbol.iterator]() {
return this;
},
next() {
return i < end ?
{value: i++, done: false} :
{done: true}
},
return() {
console.log('return');
return {done : true};
},
throw(e) {
return {value: ++i, done: false};
}
};
}
let r = range(3, 8);
for(let n of r) {
console.log(n);
r.throw(new Error());
}
又或者直接拋出例外:
function range(start, end) {
let i = start;
return {
[Symbol.iterator]() {
return this;
},
next() {
return i < end ?
{value: i++, done: false} :
{done: true}
},
return() {
console.log('return');
return {done : true};
},
throw(e) {
throw e;
}
};
}
let r = range(3, 8);
for(let n of r) {
console.log(n);
r.throw(new Error());
}
底下是執行結果:
3
return
C:\workspace\helloworld.js:15
throw e;
^
Error
at Object.<anonymous> (C:\workspace\helloworld.js:22:10)
at Module._compile (module.js:635:30)
at Object.Module._extensions..js (module.js:646:10)
at Module.load (module.js:554:32)
at tryModuleLoad (module.js:497:12)
at Function.Module._load (module.js:489:3)
at Function.Module.runMain (module.js:676:10)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:608:3
可以看到,當你拋出例外時,此例外又傳播至 for/of
,因此就觸發了 return
方法的執行。
之後在看到產生器(Generator)時,可以看到產生器本質上也是個迭代器,產生器會實作 next
、return
與 throw
三個方法。