箭號函式


在 ES6 之前,使用 function 建立匿名函式是很熟悉的一件事:

> var arr = [1, 2, 5, 2, 5, 6, 9];
undefined
> arr.filter(function(elem) {
...     return elem % 2 === 0;
... }).map(function(elem) {
...     return elem * 10;
... });
[ 20, 20, 60 ]
>

可讀性很差,在 ES6 中,可以寫成:

> let arr = [1, 2, 5, 2, 5, 6, 9];
undefined
> arr.filter(elem => elem % 2 == 0).map(elem => elem * 10);
[ 20, 20, 60 ]
>

elem => elem % 2 這個運算式,在 ES6 中稱為箭號函式(Arrow Function),是類似其他語言中被稱為 Lambda 運算式的東西,不過,Lambda 運算式這名詞其實沒有嚴謹的定義,在 ES6 中就用個箭號函式名詞來區別吧!

如果有兩個以上的參數,箭號函式的參數列必須使用 () 包含起來,沒有參數的話,就只要寫 () 就可以了,在只有一行運算式的情況下,=> 右邊的運算式傳回值會自動 return 為箭號函式的傳回值。如果要換行的話,可以使用 {},最後要傳回值時可以使用 return

> let plus = (a, b) => a + b;
undefined
> plus(1, 2);
3
> let minus = (a, b) => {
...     return a - b;
... }
undefined
> minus(10, 5)
5
>

箭號函式的目的之一,是讓那些運算本體很簡單的函式,可以用簡潔的方式撰寫,因此,雖然可以使用 {},然而,若本體很長的話,並不建議使用箭號函式。

另一方面,箭號函式也不單純只是 function 匿名函式的簡寫形式,它在 this 方面的解析方式,並不是依照 function 的既有方式,例如。底下是 function 的形式:

this.x = 1;

var o = {
    x: 10,
    foo : function() {
        console.log(this.x);
    }
};

o.foo();

你應該知道執行結果會顯示 10 吧!換成箭號函式的話會如何呢?

this.x = 1;

let o = {
    x: 10,
    foo : () => console.log(this.x)
};

o.foo();

執行結果會顯示 1,也就是說,箭號函式中的 this,並不是依當時呼叫者來綁定的,而是依當時的語彙(Lexical)環境來綁定,白話的話,就是哪個環境包含箭號函式,而當時環境中的 this,就是箭號函式的 this,一旦綁定了 this 的對象,就不會再改變,就算使用 o.foo.call(o) 也不會改變。

在上面的例子中,包含箭號函式的環境是全域,在全域中寫 this 是什麼物件,就是上例中箭號函式中 this 的物件,此時就像是:

let who = this;

this.x = 1;

let o = {
    x: 10,
    foo : function() {
        console.log(who.x);
    }
};

o.foo();

再來看一個例子:

var o = {
    x   : 10,
    foo : function() {
        return {
            x : 20,
            doFoo : function() {
                console.log(this.x);
            }
        }
    }
};

o.foo().doFoo();

這個比較難一些,也是為什麼 function 中的 this 難以掌握的例子之一,呼叫 o.foo 會傳回一個物件,該物件有個 doFoo 方法,實際上是呼叫 o.foo 傳回的物件在呼叫 doFoo 方法,因此 doFoo 方法中的 this 會是呼叫 o.foo 傳回的物件,該物件上的 x 特性是 20,也就是最後的顯示結果。

換成箭號函式的話:

let o = {
    x   : 10,
    foo : function() {
        return {
            x : 20,
            doFoo : () => console.log(this.x)
        }
    }
};

o.foo().doFoo();

哪個環境包含了箭號函式呢?foo 特性參考的函式!在該函式中寫 this 的話,代表著 o 參考的物件,因此箭號函式中的 this 代表的就是 o 參考的物件,因此會顯示 10,就像以底下的作用:

let o = {
    x   : 10,
    foo : function() {
        var who = this;
        return {
            x : 20,
            doFoo : function() {
                console.log(who.x);
            }
        }
    }
};

o.foo().doFoo();

如果想更進一步探索的話,箭號函式中若出現 super,也是依當時語彙環境決定的,super 可以出現在〈物件實字簡化與增強〉談過的方法簡便定義形式,代表著物件的原型(__proto__),因此底下的程式最後會顯示 10:

var o = {
    foo() {

        var o2 = {
            foo2() {
                console.log(super.x);
            }
        };

        o2.__proto__ = {x: 10};     

        o2.foo2();
    }
};

o.__proto__ = {x : 1};

o.foo();

__proto__ 算是被列入標準了(儘管不建議使用,ES6 提供了標準化的 Object.setPrototypeOf,之後有機會再介紹),在上例中,分別設定了兩個物件的 __proto__,而 super 是依哪個物件呼叫了方法而綁定的。

如果改成箭號函式的話:

var o = {
    foo() {

        var o2 = {
            foo2 : () => console.log(super.x)
        };

        o2.__proto__ = {x: 10};     

        o2.foo2();
    }
};

o.__proto__ = {x : 1};

o.foo();

其實如果單看 o2 的定義,是不能有 super 出現的,然而由於有 foo 方法的包覆,而 super 來自於 foo 環境,因此上面的程式得以執行成功,並且顯示 1 的結果。