函式的增強


ES6 中對函式的增強,在之前的文件中多少都有看過了,例如〈增強的數值與字串〉看過的,函式中可以使用 ... 運算子,用來將多於參數的引數收集在一個陣列中:

> function some(a, b, ...others) {
...     console.log(a);
...     console.log(b);
...     console.log(others);
... }
undefined
> some(10);
10
undefined
[]
undefined
> some(10, 20);
10
20
[]
undefined
> some(10, 20, 30);
10
20
[ 30 ]
undefined
> some(10, 20, 30, 40);
10
20
[ 30, 40 ]
undefined
>

這可以用來取代函式中的 arguments,在其他語言中,這個特性可能被稱為不定長度引數,規則也類似,... 只能用在最後一個參數,不能有兩個以上的 ...

反過來,如果一個物件是可迭代的,在它前面可以放置 ...,這時稱其為 Spread 運算子,與函式結合使用的時候,可以將可迭代物件的元素,逐個分配給對應的參數,例如:

> some(...[1, 2, 3, 4]);
1
2
[ 3, 4 ]
undefined
>

而在〈增強的數值與字串〉中也看過,當函式使用標記模版時,會是一個函式的特殊呼叫形式,詳請看參考該文件,這邊不再說明了。

在〈Destructuring、Rest 與 Spread 運算〉中看過解構語法,也看過函式中,在參數設置上也可以使用解構語法,那時還玩了個函數式風格的範例:

function sum([head, ...tail]) {
    return head ? head + sum(tail) : 0;
}

console.log(sum([1, 2, 3, 4, 5])); // 15

話說,透過 Rest 與 Spread,也可以寫出底下有趣的函式呢!不過不鼓勵這麼寫啦!

function sum(head, ...tail) {
    return head ? head + sum(...tail) : 0;
}

console.log(sum(...[1, 2, 3, 4, 5])); // 15

在 ES6 中,每個函式實例都會有個 name 特性,用來指出函式的名稱,其實這個特性在 ES6 之中已經被廣泛使用,只不過在 ES6 中才標準化。

> function f() {}
undefined
> f.name;
'f'
> (function() {
... }).name;
''
> let f2 = function() {};
undefined
> f2.name;
'f2'
> let f3 = f2;
undefined
> f3.name;
'f2'
>

在 ES6 之前,函式的參數無法設置預設值,若想要有預設值的效果,通常會透過偵測參數是否為 undefined 來達成,在 ES6 中,函式的參數可以指定預設值了:

> function doSome(a, b, c = 10) {
...     console.log(a, b, c);
... }
undefined
> doSome(1, 2);
1 2 10
undefined
> doSome(1, 2, 3);
1 2 3
undefined
>

參數的預設值,每次都會重新運算,這可以避免其他語言中有預設值,然而預設值持續被持有的問題(像是 Python):

> function f(a, b = []) {
...     b.push(a);
...     console.log(a, b);
... }
undefined
> f(1)
1 [ 1 ]
undefined
> f(1)
1 [ 1 ]
undefined
> f(1, [1, 2, 3])
1 [ 1, 2, 3, 1 ]
undefined
>

參數的預設值,也可以指定運算式(個人覺得應該避免使用,除非有很好的理由):

> function f(y = x + 1) {
...     console.log(y);
... }
undefined
> let x = 10;
undefined
> f();
11
undefined
> x = 20;
20
> f();
21
undefined
>
> f2(1);
1 2
undefined
> f2(10);
10 11
undefined
>

理論上,你只能把有預設值的參數寫在參數列的後面,不過,要玩也是可以(也是不鼓勵的寫法)…

> function f(a = 10, b) {
...     console.log(a, b);
... }
undefined
> f(0, 2);
0 2
undefined
> f(undefined, 20);
10 20
undefined
>

函式實例有個 length 特性,可以用來取得定義函式時參數的個數,不過,在使用了 ... Rest 運算,或者是指定了預設值的參數,是不會計入 length 的:

> function f(a, b) {}
undefined
> f.length;
2
> function f2(a, b = 10) {}
undefined
> f2.length;
1
> function f3(a, ...b) {}
undefined
> f3.length;
1
>

ES6 若是在嚴格模式下,支援 Tail Call Optimization,只要在呼叫下個函式前,當前的函式不需要保留任何狀態,也就是所謂的 Proper Tail Call,ES6 規定就要進行最佳化,不需要使用一個新的 Stack frame,只需要重複使用目前的 Stack frame 就可以了。

如果你不知道什麼是 Proper Tail Call,或者沒聽過 Tail Call Optimization,那表示你可能沒遇過相對應的問題,或者很少寫遞迴的東西,許多語言也不支援 Tail Call Optimization,就暫時記得這邊曾經提過這個東西就可以了。

如果真的想知道的話,可以看一下〈Tail call〉或者參考〈遞迴的美麗與哀愁〉。