【翻譯】CoffeeScript 選譯筆記

於明昊發表於2014-07-27

譯者按:最近工作中常用到 CoffeeScript,感覺比起原生的 JS,Coffee 在語法上的便捷確實令人讚歎。可惜的是目前 Coffee 的中文官網上並沒有全部譯完,因此在學習過程中著實產生了一些阻力,因此準備對其沒有譯完的部分做一下選譯。與官網翻譯組的術語不譯,難句意譯原則不同,本文會在翻譯過程中穿插譯者自己的理解和補充。

迴圈與推導式

在 CoffeeScript 裡,包含陣列、物件、範圍(range)在內,幾乎所有迴圈都可以寫作推導式(Comprehension)的形式 注1。推導式會被替換並編譯成 for 迴圈,其包含一個可選的子句 注2 以及當前陣列的索引值。與 for 迴圈不同的一點是,陣列推導式是一個表示式,可以被返回並進行賦值。

# 注重健康的一餐
foods = ['broccoli', 'spinach', 'chocolate']
# 這個例子很好的表現了推導式、子句、推導式賦值的情況
# 其中 when food isnt 'chocolate' 便是所謂的子句
eat food for food in foods when food isnt 'chocolate'

推導式應該能處理所有你用到迴圈或 each/forEach 語句,mapselect/filter 函式的地方。例如:

shortNames = (name for name in list when name.length < 5)

如果你知道迴圈的開始位置和結束位置,或是想要固定步長的增量迴圈,你可以用範圍來指定推導式的起始。

countdown = (num for num in [10..1])

在上面的例子中,請各位注意我們是如何將推導式的值賦給變數的—— CoffeeScript 收集每次迭代的值,將其返回為陣列格式。有些時候在函式的末尾寫一個迴圈僅僅是為了對陣列進行推導式操作 注3,在這種情況下就需要注意在函式的末尾加上有意義的返回值,如 true 或者 null 之類,而不要誤把推導式的值當做函式的返回值返回 注4

設定範圍推導式的固定步長大小,可以使用 by,如:evens = (x for x in [0..10] by 2)

推導式也可以用來遍歷物件中的鍵和值,使用 of 而不是 in 用來告訴推導式要遍歷的是一個物件的屬性而不是陣列的值。

yearsOld = max: 10, ida: 9, tim: 11

ages = for child, age of yearsOld
  "#{child} is #{age}"

如果你僅想遍歷物件自有屬性的值,也就是在 JS 中我們常用的使用 hasOwnProperty 來檢測避免那些從原型鏈上繼承得來的屬性,可以使用 own 關鍵字:for own key, value of object

CoffeeScript 中唯一提供的一種低層次迴圈是 while 迴圈。與原生 JS 的 while迴圈最大的不同是:Coffee 的 while 迴圈可以被用作表示式,返回一個包含每次迴圈遍歷結果的陣列。

為了可讀性考慮, until 關鍵字等效於 while notloop 關鍵字等效於 while true

當使用 JS 迴圈生成函式時,通常使用引入閉包來保證迴圈遍歷的正確關閉,並保證所有生成的函式不是僅引用了最後一個迴圈值 注5 。Coffee 提供了 do 關鍵詞,可以呼叫一個可傳遞任意引數的函式。

# CoffeeScript
for filename in list
  do (filename) ->
    fs.readFile filename, (err, contents) ->
      compile filename, contents.toString()

// JavaScript
var filename, _fn, _i, _len;

_fn = function(filename) {
  return fs.readFile(filename, function(err, contents) {
    return compile(filename, contents.toString());
  });
};
for (_i = 0, _len = list.length; _i < _len; _i++) {
  filename = list[_i];
  _fn(filename);
}

陣列切片

範圍也可用於提取陣列切片。使用兩個點時,如 [3..6] ,範圍包含3、4、5、6,當使用三個點時,如 [3...6] ,範圍不包含結尾,即3、4、5。切片可以採用預設值,當忽略第一個範圍索引時,預設值為0,忽略第二個範圍索引時,預設值為陣列長度。

我們可以用相同的語法和賦值來完成陣列拼接。

numbers[3..6] = [-3, -4, -5, -6]

這裡在拼接的時候譯者還是遇到了一點問題,即拼接時不能使用負數。這和 JS 原生的 splice 方法不支援負數個數有關。JS 原生的 slice(start, end) 方式支援的引數為切片的首尾索引值,而 splice(index, howmany, item1...itemx) 方法的引數為首位索引和長度,如下面這個例子:

num = [1..5]
num[2..-1]                # 3, 4, 5
num[2..-1] = [1, 2]       # 1, 2, 6, 7, 3, 4, 5

當使用負數的時候,splice方法認為其不進行長度切割,自動將其忽略。

還有一個注意的問題是 string 物件是不可變的,其無法被拼接(但是可以被切片)。

一切都是表示式

你可能已經注意到,我們的函式裡沒有新增 return 語句卻依然可以返回其最一行表示式的值(簡稱終值,final value)。coffee 的編輯器檢視將所有的語句均編譯為表示式。儘管我們的函式每次都能返回終值,當你確定函式體內有提前返回的值時,我們還是允許並鼓勵使用者寫出顯示的 return 語句。

因為變數的宣告語句被提升到了作用域的最頂部,賦值語句也可以使用內嵌的表示式,即使這些表示式裡含有尚未被定義過的變數。

six = (one = 1) + (two = 2) + (three = 3)

在 coffee中,如果想在表示式裡將 js 裡的語句轉化為表示式使用,可以在其外側包裹一個括號。你可以利用這個特性幹一些非常有用的事情,例如將推導式的結果賦給一個變數:

globals = (name for name of window)[0...10]

也可以幹一些非常不明智的事情,例如將 try/catch 語句當引數傳給一個函式。

在 JS 中,有少量的語句不能被轉化為有意義的表示式,如 breakcontinuereturn 。如果你在程式碼裡寫了這些語句,coffee 不會對其進行轉換。

註釋

  • 注1 推導式是一個程式語言的概念,指在一個已存在列表的基礎上產生一個新列表的語法結構,詳見:wiki - List Comprehension
  • 注2 Guard Course,直譯為防禦性從句,多為邏輯表示式,用於在迴圈中進行過濾,由於最初多用來進行空指標異常的避免而得此名。詳見:wiki - Guard
  • 注3 原文為 side-effects,即函式副作用,指函式在執行過程中除正常返回值外還修改了外部變數。在 JS 中因為函式作用域鏈的存在,這種情況十分常見。文中所說的副作用應該是指由於 JS 中的陣列物件全是以引用的形式儲存,因此如果本函式傳入了外部變數陣列 A,那麼函式對陣列 A 進行推導式運算將導致外部變數A的值也被直接修改,從而導致副作用的產生。但這裡的副作用最有可能指代列表推導式的迴圈功能。詳見:wiki - Side effect
  • 注4 在 CoffeScript 中,函式的最後一行表示式的值被當做函式返回值返回。
  • 注5 這裡原文中沒有點名,但應該指的是存在非同步的情況。 比如下面這個例子:

    for i in [0..9] 
      setTimeout () -> (console.log i), 5000
    

    這裡的本意是在5s之後輸出0..9,結果卻輸出了十個10,其原因就在於setTimeout是非同步的,每當函式執行迴圈體時,setTimeout中的函式體都會被丟擲當前執行環境,同時迴圈繼續執行。當5s之後迴圈體早已經執行完畢,i 的值增長到10,才開始依次執行 console.log,因此輸出的結果就都是10。為了避免出現這種情況,則需要構建一個閉包對變數進行暫存,如下所示:

    a = (i) ->
      setTimeout () -> (console.log i), 5000
    a i for i in [0..9]
    

    do 語句實際上就是幫我們封裝了這樣一個函式,省去了我們再封裝一次的麻煩。

相關文章