第 26 章:元碼風格指南
目錄
購買書籍
(廣告,請勿封鎖。)

第 26 章:元碼風格指南

JavaScript 有許多很棒的風格指南。因此,沒有必要再寫一本。相反地,本章描述元風格規則,並調查現有的風格指南和既定的最佳實務。我也會提到一些我喜歡的,但較有爭議的實務。這個想法是補充現有的風格指南,而不是取代它們。

現有的風格指南

這些是我喜歡的風格指南

此外,還有兩個元風格指南

一般提示

本節將涵蓋一些一般的程式碼撰寫提示。

程式碼應保持一致

撰寫一致程式碼有兩個重要的規則。第一個規則是,如果你開始一個新專案,你應該想出一個風格,記錄它,並在所有地方遵循它。團隊越大,就越需要透過 JSHint 等工具自動檢查是否遵守風格。在風格方面,有許多決定需要做出。大多數都有普遍同意的答案。其他則必須針對每個專案定義。例如:

  • 空白的數量(在括號後、陳述之間等)
  • 縮排(例如,每個縮排層級的空格數)
  • 如何以及在哪裡撰寫var陳述

第二個規則是,如果你加入一個現有的專案,你應該嚴格遵循它的規則(即使你不同意它們)。

程式碼應易於理解

對於大多數程式碼,用於閱讀的時間遠大於用於撰寫的時間。因此,讓前者盡可能容易非常重要。以下是這樣做的幾個準則

簡潔並不總是比較好
有時候寫更多內容表示實際上可以更快閱讀。我們來考慮兩個範例。首先,熟悉的事物比較容易理解。這表示使用熟悉的、稍微冗長的結構可能比較好。其次,人類閱讀的是符號,不是字元。因此,redBalloonrdBlln 容易閱讀。
好的程式碼就像教科書

大多數程式碼庫都充滿了新的想法和概念。這表示如果你想要使用程式碼庫,你需要學習這些想法和概念。與教科書不同的是,程式碼的額外挑戰在於人們不會線性閱讀它。他們會隨意跳進任何地方,而且應該能夠大致理解正在發生什麼事。程式碼庫的三個部分有助於

不要耍小聰明;不要讓我思考
有很多聰明的程式碼使用語言的深入知識來達到令人印象深刻的簡潔性。這種程式碼通常就像謎題,而且難以理解。你會遇到這樣的意見:如果人們不理解這樣的程式碼,也許他們應該先真正學習 JavaScript。但這不是重點。無論你有多聰明,進入其他人的心靈世界總是具有挑戰性。因此,簡單的程式碼並非愚蠢,而是將大部分精力花在讓一切都易於理解的程式碼。請注意,「其他人」包括你未來的自己。我常常發現,我過去的聰明想法對現在的我來說沒有意義。
避免針對速度或程式碼大小進行最佳化

許多聰明才智都用於這些最佳化。但是,你通常不需要它們。一方面,JavaScript 引擎變得越來越聰明,並自動最佳化遵循既定模式的程式碼速度。另一方面,縮小工具(第 32 章)會改寫你的程式碼,使其盡可能小。在兩種情況下,工具對你來說都很聰明,因此你不需要這樣。

有時候你別無選擇,只能最佳化程式碼的效能。如果你這樣做,請務必測量並最佳化正確的部分。在瀏覽器中,問題通常與 DOM 和 HTML 有關,而不是語言本身。

公認的最佳實務

大多數的 JavaScript 程式設計師同意下列的最佳實務:

大括號樣式

大括號用來界定程式碼區塊的語言中,大括號樣式決定您放置這些大括號的位置。在類 C 語言(例如 Java 和 JavaScript)中最常見的兩種大括號樣式是 Allman 樣式和 1TBS。

Allman 樣式

如果一個陳述式包含一個區塊,該區塊會被視為與陳述式的標頭有點分離:它的開頭大括號在自己的行中,與標頭具有相同的縮排層級。例如:

// Allman brace style
function foo(x, y, z)
{
    if (x)
    {
        a();
    }
    else
    {
        b();
        c();
    }
}

1TBS(唯一正確的大括號樣式)

在此,一個區塊與其陳述式的標頭更為緊密關聯;它在標頭之後,在同一行中開始。控制流程陳述式的本體總是放在大括號中,即使只有一個陳述式。例如:

// One True Brace Style
function foo(x, y, z) {
    if (x) {
        a();
    } else {
        b();
        c();
    }
}

1TBS 是(較舊的)K&R(Kernighan 和 Ritchie)樣式的變體。[21] 在 K&R 樣式中,函式是以 Allman 樣式撰寫,並且在大括號不必要時省略大括號,例如在單一陳述式的 then 狀況

// K&R brace style
function foo(x, y, z)
{
    if (x)
        a();
    else {
        b();
        c();
    }
}

JavaScript

JavaScript 世界中的實際標準是 1TBS。它繼承自 Java,而且大多數的風格指南都建議使用它。其中一個原因是客觀的。如果你傳回一個物件文字,你必須將開頭的大括號放在與關鍵字 return 相同的行,像這樣(否則,自動分號插入會在 return 之後插入一個分號,表示沒有傳回任何東西;請參閱 陷阱:ASI 會意外地中斷陳述式

return {
    name: 'Jane'
};

顯然地,物件文字不是一個程式區塊,但如果兩者都使用相同的方式格式化,看起來會更一致,而且你不太可能會犯錯。

我個人的風格和偏好是

  • 1TBS(表示你盡可能使用大括號)。
  • 作為一個例外,如果一個陳述式可以用單行撰寫,我會省略大括號。例如

    if (x) return x;

偏好使用文字而非建構函式

多個建構函式會產生物件,這些物件也可以透過文字建立。後者通常是較好的選擇:

var obj = new Object(); // no
var obj = {}; // yes

var arr = new Array(); // no
var arr = []; // yes

var regex = new RegExp('abc'); // avoid if possible
var regex = /abc/; // yes

永遠不要使用建構函式 Array建立一個包含給定元素的陣列。 使用元素初始化陣列(避免!) 會說明原因

var arr = new Array('a', 'b', 'c'); // never ever
var arr = [ 'a', 'b', 'c' ]; // yes

不要耍小聰明

本節收集了 不建議的耍小聰明範例。

條件運算子

不要巢狀 條件運算子:

// Don’t:
return x === 0 ? 'red' : x === 1 ? 'green' : 'blue';

// Better:
if (x === 0) {
    return 'red';
} else if (x === 1) {
    return 'green';
} else {
    return 'blue';
}

// Best:
switch (x) {
    case 0:
        return 'red';
    case 1:
        return 'green';
    default:
        return 'blue';
}

縮寫 if 陳述式

不要透過邏輯運算子來縮寫 if 陳述式

foo && bar(); // no
if (foo) bar(); // yes

foo || bar(); // no
if (!foo) bar(); // yes

遞增運算子

如果可能的話,將遞增運算子 (++) 和遞減運算子 (--) 用作陳述式;不要將它們用作運算式。在後者的情況下,它們會傳回一個值,而且雖然有一個助記符,你仍然需要思考才能找出正在發生什麼事:

// Unsure: what is happening?
return ++foo;

// Easy to understand
++foo;
return foo;

檢查未定義

if (x === void 0) x = 0; // not necessary in ES5
if (x === undefined) x = 0; // preferable

從 ECMAScript 5 開始,第二種檢查方式較佳。變更未定義說明原因。

將數字轉換為整數

return x >> 0; // no
return Math.round(x); // yes

位移運算子可用於將數字轉換為整數。不過,通常建議使用較明確的替代方案,例如 Math.round()轉換為整數提供所有轉換為整數方式的概觀。

可接受的巧妙手法

有時您可以在 JavaScript 中使用巧妙手法,前提是該巧妙手法已成為既定模式。

預設值

使用 Or (||) 運算子提供預設值是一種常見模式,例如,對於參數:

function f(x) {
    x = x || 0;
    ...
}

有關詳細資料和更多範例,請參閱模式:提供預設值

通用方法

如果您使用通用方法,您可以Object.prototype 縮寫為 {}。下列兩個表達式等效:

Object.prototype.hasOwnProperty.call(obj, propKey)
{}.hasOwnProperty.call(obj, propKey)

而且 Array.prototype 可以縮寫為 []

Array.prototype.slice.call(arguments)
[].slice.call(arguments)

我對這個有點猶豫。這是一種技巧(您透過執行個體存取原型屬性)。但它可以減少雜亂,而且我預期引擎最終會最佳化此模式。

ECMAScript 5:尾隨逗號

物件文字中的尾隨逗號在 ECMAScript 5 中是合法的:

var obj = {
    first: 'Jane',
    last: 'Doe', // legal: trailing comma
};

ECMAScript 5:保留字

ECMAScript 5 也允許您將保留字(例如 new)用作屬性金鑰:

> var obj = { new: 'abc' };
> obj.new
'abc'

有爭議的規則

讓我們來看看一些我喜歡的慣例,這些慣例有點有爭議。

語法

我們將從語法慣例開始

緊湊空白

我喜歡相對緊湊的空白。範例是書面英文:開括號後和閉括號前沒有空白。逗號後有空白:

var result = foo('a', 'b');
var arr = [ 1, 2, 3 ];
if (flag) {
    ...
}

對於匿名函式,我遵循 Douglas Crockford 的規則,在關鍵字 function 後加上空白。其理由是,如果您移除名稱,這就是命名函式表達式的樣子

function foo(arg) { ... }  // named function expression
function (arg) { ... }     // anonymous function expression
每個縮排層級四個空格
我看到的大部分程式碼都使用空格進行縮排,因為不同應用程式和作業系統顯示的標籤差異很大。我比較喜歡每個縮排層級使用四個空格,因為這樣可以讓縮排更明顯。
將條件運算子放在括號中

這樣有助於閱讀,因為可以更輕鬆地找出運算子的範圍:

return result ? result : theDefault;  // no
return (result ? result : theDefault);  // yes

變數

接下來,我將說明變數的慣例

每行一個變數宣告

我不會使用單一宣告宣告多個變數:

// no
var foo = 3,
    bar = 2,
    baz;

// yes
var foo = 3;
var bar = 2;
var baz;

這種方法的優點在於刪除、插入和重新排列行比較簡單,而且行會自動正確縮排。

讓變數宣告保持在區域
如果您的函式不太長(無論如何,它不應該太長),那麼您可以不用太小心處理提升,並假裝var宣告是區塊範圍。換句話說,您可以在使用變數的內容中宣告變數(在迴圈內、在then區塊或else區塊內,等等)。這種區域封裝讓程式碼片段在孤立狀態下更容易理解。移除程式碼片段或將它移到其他地方也比較容易。
如果您在區塊內,請待在那個區塊內

作為前一條規則的附錄:不要在兩個不同的區塊中宣告同一個變數兩次。例如

// Don’t do this
if (v) {
    var x = v;
} else {
    var x = 10;
}
doSomethingWith(x);

前述程式碼的效果和用意與下列程式碼相同,因此應該以這種方式撰寫

var x;
if (v) {
    x = v;
} else {
    x = 10;
}
doSomethingWith(x);

物件導向

現在我們將說明與物件導向相關的慣例。

優先使用建構函式,而非其他執行個體建立模式

我建議您

  • 始終使用建構函式。
  • 建立執行個體時,始終使用new

這樣做的主要優點是

  • 您的程式碼更符合 JavaScript 主流,而且比較有可能在架構之間移植。
  • 在現代引擎中,使用建構函式的執行個體非常快速(例如,透過隱藏類別)。
  • 類別,即即將推出的 ECMAScript 6 中的預設繼承建構,將會以建構函式為基礎。

對於建構函式,使用嚴格模式非常重要,因為它可以防止您忘記在實例化時使用new運算子。而且您應該知道,您可以在建構函式中傳回任何物件。在實作建構函式的秘訣中提到了更多使用建構函式的秘訣。

避免使用封閉函式處理私人資料
如果您希望物件的私有資料完全安全,您必須使用閉包。否則,您可以使用一般屬性。一個常見的做法是使用底線為私有屬性名稱加上前綴。閉包的問題是程式碼會變得更複雜(除非您將所有方法都放入實例中,這是不慣用的且緩慢的)且更慢(目前在閉包中存取資料比存取屬性還慢)。保持資料私密會更詳細地說明這個主題。
如果建構函式沒有引數,請寫入括號

我發現這樣的建構函式呼叫使用括號看起來更簡潔

var foo = new Foo;  // no
var foo = new Foo();  // yes
小心運算子優先順序

使用括號,讓兩個運算子不會互相競爭,結果不一定會是您預期的:

> false && true || true
true
> false && (true || true)
false
> (false && true) || true
true

instanceof 特別棘手

> ! {} instanceof Array
false
> (!{}) instanceof Array
false
> !({} instanceof Array)
true

不過,我發現建構函式後的函式呼叫沒有問題

new Foo().bar().baz();  // ok
(new Foo()).bar().baz();  // not necessary

其他

此區段收集各種提示

強制轉換

透過 BooleanNumberString()Object()(用作函式,切勿將這些函式用作建構函式)強制轉換值成某個類型。原因是此慣例更具描述性:

> +'123'  // no
123
> Number('123')  // yes
123

> ''+true  // no
'true'
> String(true)  // yes
'true'
避免將 this 當成隱含參數

this僅指目前函式呼叫的接收者;不應將其濫用為隱含參數。原因是這些函式較容易呼叫和理解。我也喜歡將物件導向和函式機制分開:

// Avoid:
function handler() {
    this.logError(...);
}

// Prefer:
function handler(context) {
    context.logError(...);
}
透過 inhasOwnProperty 檢查屬性是否存在(請參閱屬性的反覆運算和偵測)

這比與 undefined 比較或檢查真值更能說明問題且更安全

// All properties:
if (obj.foo)  // no
if (obj.foo !== undefined)  // no
if ('foo' in obj) ... // yes

// Own properties:
if (obj.hasOwnProperty('foo')) ... // risky for arbitrary objects
if (Object.prototype.hasOwnProperty.call(obj, 'foo')) ... // safe
快速失敗
如果您可以,最好快速失敗,不要靜默失敗。JavaScript 只能這麼寬容(例如,除以零),因為 ECMAScript 的第一個版本沒有例外。例如,不要強制轉換值;請擲回例外。不過,當您的程式碼在執行階段時,您必須找到從失敗中優雅復原的方法。

結論

每當您考慮樣式問題時,請自問:什麼讓我的程式碼更容易理解?抵擋住耍小聰明的誘惑,並將大部分的機械式小聰明留給 JavaScript 引擎和縮小程式(請參閱第 32 章)。



[21] 甚至有人說它們是同義詞,1TBS 是開玩笑地稱呼 K&R 的一種方式。

下一頁:27. 除錯語言機制