CoffeeScript -> 與 => 的差別

不知道各位在用 CoffeeScript 的 -> (dash rocket)在寫 function 的時候,有沒有發現有另一個長得跟它有點像,但比較胖一點的 => (fat arrow),在 CoffeeScript 的 source code 裡有一段這樣的簡短說明:

CoffeeScript has two different symbols for functions. -> is for ordinary functions, and => is for functions bound to the current value of this.

在 CoffeeScript 裡,用 -> 會產生一個Anonymous function,例如:

skinny = (name) ->
  "hello, I'm skinny #{name}"

console.log(skinny "eddie")

編譯出來的結果是:

var skinny;
skinny = function(name) {
  return "hello, I'm skinny " + name;
};
console.log(skinny("eddie"));

執行結果是:

hello, I'm skinny eddie

=> 也是會產生 Anonymous function:

fatty = (name) =>
  "hello, I'm fatty #{name}"

console.log(fatty "eddie")

執行結果是:

hello, I'm fatty eddie

就以程式的執行結果來看,用 -> 跟用 => 似乎沒什麼兩樣,但看一下用 => 編譯出來的 Javascript 程式碼:

var fatty;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
fatty = __bind(function(name) {
  return "hello, I'm fatty " + name;
}, this);

編出了看起來有點複雜的東東西,這串程式碼裡面看起來比較複雜的大概就是那個 fn.apply(),我們先來看看那是做什麼的。

function.call() v.s function.apply()

直接來看段程式碼:

var who = "Eddie";
function doctor(){
  alert(who);
}

doctor();

這光用我們的人腦執行就猜得到結果是 Eddie,如果我們再改一下:

var who = "Eddie";
function doctor(){
  alert(this.who);
}

doctor();

多了個 this,但執行的結果還是一樣。這裡的 this.who 其實指的就是整個 global 的那個 who,所以印出來結果也一樣。再來加一點變化:

var who = "Eddie";
var time_lord = { who: "the real doctor" }
function doctor(){
  alert(this);
  alert(this.who);
}

doctor();
doctor.call(time_lord);

其實 this 指的對像會隨著不同的情境而有所變化。例如我們平常可能會說:「媽! 我在這裡」,如果你是在公司說這句話,「這裡」表示的是公司;如果你是在家裡說這句話,「這裡」表示的是家裡。

再回來看程式碼,在上面程式碼的第 8 行的呼叫指的 this 是整個 global,在這邊就是 window,而第 9 行指的 this 則是透過 call() 方法傳進去的那個物件,程式碼所在的情境變了,所以當第5行要印出 this.who 的時候,會印出 the real doctor

其實通常沒特別指定的變數或function呼叫,前面的 this 是可以省略的,所以上面的第 8 行的 doctor() 也可以改寫成:

doctor.call(this);

執行結果是一樣的。再來繼續再加點變化,call() 還可以傳更多的參數進去:

var who = "Eddie";
var time_lord = { who: "the real doctor" }

function doctor(real_name){
  alert(this.who);
  alert("His real name is : " + real_name);
}

doctor("Aquarianboy");
doctor.call(time_lord, "No one knows!");

另外,在 JavaScript 裡,還有一個跟 call() 有點像的 function 叫做 apply(),這兩個的功能差不多,最大的差別是 apply() 傳的第二個參數是陣列,而 call() 的參數則是一個一個傳進去,並用逗號分開:

var who = "Eddie";
var time_lord = { who: "the real doctor" }

function doctor(real_name){
  alert(this.who);
  alert("His real name is : " + real_name);
}

doctor("Aquarianboy");
doctor.apply(time_lord, ["No one knows!"]);

大概知道在 JavaScript 裡 call()apply() 的意思之後,再回來看我們剛剛那段 CoffeeScript 編譯出來的程式碼:

var fatty;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
fatty = __bind(function(name) {
  return "hello, I'm fatty " + name;
}, this);

function 一層包一層,return 再 return,不過大意就是把 this 當做參數傳進 fatty 這個 function 裡的意思。

什麼時候會用到

有點離題了..再回來看看 CoffeeScript,原來,=> 在定義 function 的同時,還會把 this 也同時給綁到這個 function,雖然 this 會隨著所在的情境而有所改變,但 =>this 給綁進來之後,可以確保在指向 this 的時候不會指錯人。

為什麼要這樣做? 什麼時候會用到它? 大多是被拿來當做 event callback 的時候會用得上,來看個範例:

class Student
  constructor: (@username) ->
  say_hello: =>
    alert "Hello, my name is #{@username}"

$ ->
  eddie = new Student("Eddie")
  $("#student_1").click(eddie.say_hello)

我把 eddie.say_hello 傳給 click 做為 callback,意思就是當頁面上某個 id 叫做 student_1 的元素被點擊之後,就會去執行它,而執行結果是:

Hello, my name is Eddie

但是如果你把第3行程式碼的 => 換成 -> 的話,執行結果會變成:

Hello, my name is undefined

為什麼結果是 undefined?前面提到,this 會隨著出現在不同的地方而會有不同的意思,如果你是用 -> 的話,它的 this 是指向你剛剛點擊的那顆按鈕,而當然那顆按鈕上面不會有 username 這個屬性,所以印出 undefined ;如果是用 => 的話,它會透過 fn.apply()this 給包進來,所以 say_hello 裡的 this,指的就是它自己這個物件,也就是 Student 類別產生出來的 instance,印出來的結果就是你要的了。

細節可以再看看它編譯出來的 JavaScript code,大概就可以知道 ->=> 各別做什麼不同的事。

以上,供大家參考,如果有哪邊寫錯再請跟我說 :)