2013年11月15日 星期五

Gjs Style Guide 中譯

譯註:這篇文章適合已經對 Gtk/GLib 程式設計及 JavaScript 語言有一定基礎的人閱讀。 原文在此


原始碼排版格式

本文的目標是讓所有 GNOME 專案中的 JavaScript 程式碼有一致的排版風格。在像 JavaScript 這樣的動態語言裡,嚴格地要求排版格式(及單元測試)是有其必要性的,否則你很快便會發現你的程式碼像攪成一團的義大利麵一樣難以理清頭緒。

分號(; Semicolons)

雖然 JavaScript 允許你省略行末的分號,但千萬不要這麼做。請務必在每個敘述結束時加上分號。

js2-mode

如果你習慣使用 Emacs,試看看 js2-mode。它有點像 "lint",會以不同的顏色為你強調漏寫的分號或其他語法單元。

引用模組

當你需要引用模組時,使用 CamelCase 來和普通的變數區別,如下所示: const Big = imports.big; const GLib = imports.gi.GLib;

變數宣告

務必使用 const、var 或是 let 其中一個來定義變數。當你需要定義一個只在某個程式區塊有效的區域變數時,使用 let就對了。一般來說,在 for 或 while 迴圈中幾乎都應該使用 let。

// 處理陣列中的每個值 for (let i = 0; i < 10; i++) { let foo = bar(i); } // 處理物件的每個屬性 for (let prop in someobj) { ... }

如果你不使用 let,那麼這個變數將會在整個函數內都有效,而不是只在迴圈中有效。詳細資料可以參考 JavaScript 1.7 有什麼新東西(英文網頁)

比較常見的例子是,當你在迴圈中使用 closure(譯註:沒有名稱的函數、無名函數) 的時候:

for (let i = 0; i < 10; i++) { mainloop.idle_add(function() { log('number is: ' + i); }); }

如果你改用 var,那你應該會在 log 裡看到好幾個 "10"。

就我們所知,在函數中都應該用 let 而不是 var。當你想在 with() 裡加點東西的時候 var 會很好用,通常你應該在定義模組變數的時候使用它,因為我們的模組系統在載入模組的時候也會使用 with(模組物件變數)。

closure 中的 "this"

在一個 closure 中,"this" 的定義是不固定的,由 closure 被呼叫時的狀況來決定,與定義 closure 時的狀況沒有關係。這是因為 this 是一個關鍵字而非變數,它會在執行的時候才代換成當時的物件,所以不能像一般的變數那樣把值鎖在 closure 中。

要解決這個問題,可以使用 Lang.bind。例如:

const Lang = imports.lang; let closure = Lang.bind(this, function() { this._fnorbate(); }); 把 prototype 裡的某個方法與信號連結是實作上常見的例子: const Lang = imports.lang; MyPrototype = { _init: function() { fnorb.connect('fronate', Lang.bind(this, this._onFnorbFrobate)); }, _onFnorbFrobate: function(fnorb) { this._updateFnorb(); }, };

定義物件的語法

在 JavaScript 裡,下列兩行程式是相同的

foo = { 'bar': 42 }; foor = { bar: 42 };

下面兩行程式也是相同的

var b = foo['bar']; var b = foo.bar;

如果你把物件當成正常的物件來使用,那就代表了你是在定義一個物件的成員。請使用沒有引號的語法(上方)和沒有中括號的語法(下方),也就是 { bar: 42 } 和 foo.bar。

如果你把物件當成 hash table 來使用,請使用沒有引號的語法(上方),以及有中括號的語法(下方),也就是 { bar: 42 } 和 foo['bar']。

變數命名

  • 請使用 java 式的命名風格 (CamelCase),型別或類別的開頭要大寫(如 CamelCase),變數或函數的開頭則使用小寫(如 lowerCamelCase),然而,當你使用 introspection 引入的 C 函式時(譯註:如 some_function_written_in_c()),為了簡潔方便起見,只要維持它原來的樣子就好。
  • 私有的變數,不論是物件成員還是模組領域的變數,請以底線開頭。
  • 請盡所能地避免使用「真正的」全域變數(在全域中定義,或是 window 物件的成員)。 如果真的有必要,請在變數前面加上 namespace(命名空間),如 BigFoo。
  • 使用別名來引用模組的時候,使用 "const TitleCase" 這樣的風格,如 "const Bar = imports.foo.bar;"。
  • 如果在命名變數的時候怕與現有的 namespace 混淆,可以在最後加上底線(注意:是最後,不要加到前面讓別人誤認成私有變數了)。
  • 在定義 GObject 的建構子時,務必使用 lowerCamelCase 的格式來為屬性命名。

空白字元的使用

  • 每一層的縮排是4個空白。
  • 行末不可以有空白。
  • 請勿使用 tab 字元來進行縮排。
  • 你可以啟用預先定義好的 git pre-commit hook (chmod +x .git/hooks/pre-commit) 來檢查行末是否有空白,但它無法檢查 tab 縮排,請自行把編輯器的 tab 縮排功能關閉。

JavaScript 中的「類別」

在 JavaScript 中並沒有像 C++ 或 Java 中那樣的「類別」,這點請牢記在心;這代表了你無法在內建型別(像是 Object, Array, RegExp, String 一類的)之外創建你自行撰寫的型別。不過,你還是可以透過 prototype 的機制來讓不同的物件之間有共通的屬性,包括物件的方法在內。

每個 JavaScript 的物件裡都有個特殊的成員:__proto__;當你寫 obj.foo,而 obj 這個物件裡沒有 foo 這個成員的時候,JavaScript 便會自動在 __proto__ 裡找看看有沒有 foo。所以若是幾個不同的物件都有相同的 __proto__ 的時候,那麼它們就可以分享一些共通的方法或是狀態。

建構子是一種特別的函式,你可以用它來創造物件,比如:

function Foo() {} let f = new Foo();

當執行到 "new Foo()" 的時候,JavaScript 會產生一個全新的空物件,然後用這個物件當做 "this" 來執行 Foo()。如此 Foo() 這個函式便能設定物件的初始值。

"new Foo()" 同時也會把這個新物件的 __proto__ 設定成 Foo.prototype。也就是說建構子的 prototype 屬性是用來設定用這個建構子產生出來的物件的 __proto__ 的。所以如果你必須把建構子的 prototype 設定好,這樣你產生出來的物件才會有正確的 __proto__。

你可以想像 "f = new Foo()" 所做的事情如下:

let f = {}; // 產生一個新的空白物件 f.__proto__ = Foo.prototype; // 好孩子千萬不要模仿,這只是為了讓你想像而寫的 Foo.call(f); // 用 f 當做 this 來執行 Foo()

我們寫物件定義的模式是這樣

function Foo(arg1, arg2) { this._init(arg1, arg2); } Foo.prototype = { _init: function(arg1, arg2) { this._myPrivateInstanceVariable = arg1; }, myMethod: function() { }, myClassVariable: 42, myOtherClassVariable: 'Hello' }

這個模式的意義在於明確的指定初始化物件時所需要的資訊:f 是新的物件,f.__proto__ 會指向 Foo.prototype,然後 Foo.prototype._init 會執行,完成物件的初始化。

  • 注意:再次提醒,對於 JavaScript 來說,Foo 並不是 C++ 或 Java 所謂的「類別」;它只是一個建構子,用來配合 "new Foo()" 這種語法產生一個特定格式的物件。從 JavaScript 的角度來說,這個新的物件他是內建的 Object 型別而不是 Foo 型別,雖然我們用起來的感覺比較像是真的有一個 Foo 型別。JavaScript 並沒有(未來也不會有)把基於建構子來做型別檢查的機制(譯註:也就是 typeof new Foo() == typeof new Bar() == "Object")。型別只有固定的那幾種,所有的型別(也就是內建的型別,如Object, String, Error, RegExp 以及 Array),才會有型別檢查機制。
  • 注意:如果建構子有回傳值(return),這個回傳值就會取代預設的 this 做為 new Foo() 的值。如果建構子傳回的是 undefined,那 new Foo() 的值就是都設的 this。一般來說應該避免使用這項功能,也就是說建構子不應該傳回任何東西。不過某些特殊情形你可能會用得上這個功能,比如你希望這個新的物件是 Object 以外的型別。所以,若你的建構子傳了某個值回去,那麼 this 就會被傳回去的值取代,這種情況下在建構子裡使用 this 就沒有意義了。

JavaScript的「繼承」

你有無數種方式可以在 JavaScript 裡模擬出「繼承」的效果。一般來說,應該要避免在 JavaScript 裡使用「繼承」,不過有時候「繼承」是最好的選擇。

我們比較偏好的方式是使用某個 Spidermonkey(譯註:Mozilla 的 JavaScript 解譯器,Gjs 就是從 Spidermonkey 改來的) 獨有的擴充語法,並且直接將子物件的 prototype.__proto__ 指向父物件的 prototype。如此一來,JavaScript 尋找屬性時會先先從子物件本身找起,然後是從 __proto__ 裡面找,再來是 __proto__.__proto__ 如此一直遞迴下去。

const Lang = imports.lang; function Base(foo) { this._init(foo); } Base.prototype = { _init: function(foo) { this._foo = foo; }, frobate: function() { } }; function Sub(foo, bar) { this._init(foo, bar); } Sub.prototype = { __proto__: Base.prototype, _init: function(foo, bar) { // here is an example of how to "chain up" Base.prototype._init.call(this, foo); this._bar = bar; } // add a newMethod property in Sub.prototype newMethod: function() { } }
  • 注意:這個方法不能用來繼承內建的型別(如 String),因為內建型別的方法要求它的物件比需有正確的型別(比如 replace 要求 String 型別),而這個方法產生的物件會是 Object 型別。
  • 注意:這個方法也不能用來繼承 GObject。如果要繼承 GObject,你必須用 C 寫一個類別繼承 GType,覆載(override)必要的方法,然後才能在 JavaScript 裡使用它。這可是件麻煩事。目前而言,GObject 的繼承一定要用 C 才行。

在一些具有可攜性(譯註:不限於 Spodermonkey 使用)的 JavaScript 程式碼中,你也許看過有人用這樣的方式來模擬繼承:

function Base(foo) ... Base.prototype = ... function Sub(foo, bar) ... // 叔叔有練過,好孩子千萬不要學 Sub.prototype = new Base();

這種方式的問題是你的程式會受到 Base() 這個建構子的邊際效應影響。假設 Base 是一個對話視窗一類的東西,那麼 Base() 這個建構子裡應該會有一些產生視窗的程式碼。如果你用這種模式來繼承,你的 __proto__ 會是一個真正的 Base 物件,當然包括了建構子所產生出來的多餘視窗。

另一個問題是這種寫法會對物件和 prototype 造成混淆。

JavaScript 的 getter 與 setter

當你在存取屬性值的時候,如果需要做一些額外處理(譯註:檢查範圍一類的)的時候,不要使用 getter/setter 語法。也就是說下列的程式碼

foo.bar = 10;

代表的就只是把數字 10 存入 foo 物件的 bar 屬性裡,否則就容易讓人混淆。如果你在設定屬性時需要做一些額外的動作,那麼把它定義成方法會比較好。

實作上,這意味著 getter/setter 的唯一用途就是設定唯讀的屬性:

get bar() { return this._bar; }

如果一個屬性需要使用 setter,或者取得屬性值的時候需要一些額外處理,寫成相應的方法會比較清楚明白。