高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

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,例如:

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

console.log(skinny "eddie")

編譯出來的結果是:

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

執行結果是:

hello, I'm skinny eddie

=> 也是會產生Anonymous function:

1
2
3
4
fatty = (name) =>
  "hello, I'm fatty #{name}"

console.log(fatty "eddie")

執行結果是:

hello, I'm fatty eddie

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

1
2
3
4
5
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()

直接來看段程式碼:

1
2
3
4
5
6
var who = "Eddie";
function doctor(){
  alert(who);
}

doctor();

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

1
2
3
4
5
6
var who = "Eddie";
function doctor(){
  alert(this.who);
}

doctor();

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

1
2
3
4
5
6
7
8
9
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()也可以改寫成:

1
doctor.call(this);

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

1
2
3
4
5
6
7
8
9
10
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()的參數則是一個一個傳進去,並用逗號分開:

1
2
3
4
5
6
7
8
9
10
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編譯出來的程式碼:

1
2
3
4
5
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的時候會用得上,來看個範例:

1
2
3
4
5
6
7
8
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,大概就可以知道->=>各別做什麼不同的事。

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

Comments