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)物件,當中包含了 valuedonevalue 是當次迭代的結果,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 的傳回物件在 valueundefined,可以省略不寫(你應該知道為什麼吧!)。

物件必須有 [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,除此之外,迭代器可以選擇性地提供 returnthrow 方法。

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)時,可以看到產生器本質上也是個迭代器,產生器會實作 nextreturnthrow 三個方法。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter