JavaScript編碼規範
-
- 1 前言
- 2 代碼風格
-
- 2.1 檔案
-
-
- [建議] JavaScript 檔案使用無 `BOM` 的 `UTF-8` 編碼。
- [建議] 在檔案結尾處,保留一個空行。
-
- 2.2 結構
-
- 2.2.1 縮進
-
- [強制] 使用 `4` 個空格做為一個縮進層級,不允許使用 `2` 個空格 或 `tab` 字元。
- [強制] `switch` 下的 `case` 和 `default` 必須增加一個縮進層級。
- 2.2.2 空格
-
- [強制] 二進制運算符兩側必須有一個空格,一進制運算符與操作對象之間不允許有空格。
- [強制] 用作代碼塊起始的左花括号 `{` 前必須有一個空格。
- [強制] `if / else / for / while / function / switch / do / try / catch / finally` 關鍵字後,必須有一個空格。
- [強制] 在對象建立時,屬性中的 `:` 之後必須有空格,`:` 之前不允許有空格。
- [強制] 函數聲明、具名函數表達式、函數調用中,函數名和 `(` 之間不允許有空格。
- [強制] `,` 和 `;` 前不允許有空格。如果不位于行尾,`,` 和 `;` 後必須跟一個空格。
- [強制] 在函數調用、函數聲明、括号表達式、屬性通路、`if / for / while / switch / catch` 等語句中,`()` 和 `[]` 内緊貼括号部分不允許有空格。
- [強制] 單行聲明的數組與對象,如果包含元素,`{}` 和 `[]` 内緊貼括号部分不允許包含空格。
- [強制] 行尾不得有多餘的空格。
- 2.2.3 換行
-
- [強制] 每個獨立語句結束後必須換行。
- [強制] 每行不得超過 `120` 個字元。
- [強制] 運算符處換行時,運算符必須在新行的行首。
- [強制] 在函數聲明、函數表達式、函數調用、對象建立、數組建立、`for` 語句等場景中,不允許在 `,` 或 `;` 前換行。
- [建議] 不同行為或邏輯的語句集,使用空行隔開,更易閱讀。
- [建議] 在語句的行長度超過 `120` 時,根據邏輯條件合理縮進。
- [建議] 對于 `if...else...`、`try...catch...finally` 等語句,推薦使用在 `}` 号後添加一個換行 的風格,使代碼層次結構更清晰,閱讀性更好。
- 2.2.4 語句
-
- [強制] 不得省略語句結束的分号。
- [強制] 在 `if / else / for / do / while` 語句中,即使隻有一行,也不得省略塊 `{...}`。
- [強制] 函數定義結束不允許添加分号。
- [強制] `IIFE` 必須在函數表達式外添加 `(`,非 `IIFE` 不得在函數表達式外添加 `(`。
- 2.3 命名
-
-
- [強制] `變量` 使用 `Camel命名法`。
- [強制] `常量` 使用 `全部字母大寫,單詞間下劃線分隔` 的命名方式。
- [強制] `函數` 使用 `Camel命名法`。
- [強制] 函數的 `參數` 使用 `Camel命名法`。
- [強制] `類` 使用 `Pascal命名法`。
- [強制] 類的 `方法` / `屬性` 使用 `Camel命名法`。
- [強制] `枚舉變量` 使用 `Pascal命名法`,`枚舉的屬性` 使用 `全部字母大寫,單詞間下劃線分隔` 的命名方式。
- [強制] `命名空間` 使用 `Camel命名法`。
- [強制] 由多個單詞組成的縮寫詞,在命名中,根據目前命名法和出現的位置,所有字母的大小寫與首字母的大小寫保持一緻。
- [強制] `類名` 使用 `名詞`。
- [建議] `函數名` 使用 `動賓短語`。
- [建議] `boolean` 類型的變量使用 `is` 或 `has` 開頭。
- [建議] `Promise對象` 用 `動賓短語的進行時` 表達。
-
- 2.4 注釋
-
- 2.4.1 單行注釋
-
- [強制] 必須獨占一行。`//` 後跟一個空格,縮進與下一行被注釋說明的代碼一緻。
- 2.4.2 多行注釋
-
- [建議] 避免使用 `` 這樣的多行注釋。有多行注釋内容時,使用多個單行注釋。
- 2.4.3 文檔化注釋
-
- [強制] 為了便于代碼閱讀和自文檔化,以下内容必須包含以 `` 形式的塊注釋中。
- [強制] 文檔注釋前必須空一行。
- [建議] 自文檔化的文檔說明 what,而不是 how。
- 2.4.4 類型定義
-
- [強制] 類型定義都是以 `{` 開始, 以 `}` 結束。
- [強制] 對于基本類型 {string}, {number}, {boolean},首字母必須小寫。
- 2.4.5 檔案注釋
-
- [強制] 檔案頂部必須包含檔案注釋,用 `@file` 辨別檔案說明。
- [建議] 檔案注釋中可以用 `@author` 辨別開發者資訊。
- 2.4.6 命名空間注釋
-
- [建議] 命名空間使用 `@namespace` 辨別。
- 2.4.7 類注釋
-
- [建議] 使用 `@class` 标記類或構造函數。
- [建議] 使用 `@extends` 标記類的繼承資訊。
- [強制] 使用包裝方式擴充類成員時, 必須通過 `@lends` 進行重新指向。
- [強制] 類的屬性或方法等成員資訊不是 `public` 的,應使用 `@protected` 或 `@private` 辨別可通路性。
- 2.4.8 函數/方法注釋
-
- [強制] 函數/方法注釋必須包含函數說明,有參數和傳回值時必須使用注釋辨別。
- [強制] 參數和傳回值注釋必須包含類型資訊,且不允許省略參數的說明。
- [建議] 當函數是内部函數,外部不可通路時,可以使用 `@inner` 辨別。
- [強制] 對 Object 中各項的描述, 必須使用 `@param` 辨別。
- [建議] 重寫父類方法時, 應當添加 `@override` 辨別。如果重寫的形參個數、類型、順序和傳回值類型均未發生變化,可省略 `@param`、`@return`,僅用 `@override` 辨別,否則仍應作完整注釋。
- 2.4.9 事件注釋
-
- [強制] 必須使用 `@event` 辨別事件,事件參數的辨別與方法描述的參數辨別相同。
- [強制] 在會廣播事件的函數前使用 `@fires` 辨別廣播的事件,在廣播事件代碼前使用 `@event` 辨別事件。
- [建議] 對于事件對象的注釋,使用 `@param` 辨別,生成文檔時可讀性更好。
- 2.4.10 常量注釋
-
- [強制] 常量必須使用 `@const` 标記,并包含說明和類型資訊。
- 2.4.11 複雜類型注釋
-
- [建議] 對于類型未定義的複雜結構的注釋,可以使用 `@typedef` 辨別來定義。
- 2.4.12 AMD 子產品注釋
-
- [強制] AMD 子產品使用 `@module` 或 `@exports` 辨別。
- [強制] 對于已使用 `@module` 辨別為 AMD子產品 的引用,在 `namepaths` 中必須增加 `module:` 作字首。
- [建議] 對于類定義的子產品,可以使用 `@alias` 辨別建構函數。
- [建議] 多子產品定義時,可以使用 `@exports` 辨別各個子產品。
- [建議] 對于 exports 為 Object 的子產品,可以使用`@namespace`辨別。
- [建議] 對于 exports 為類名的子產品,使用 `@class` 和 `@exports` 辨別。
- 2.4.13 細節注釋
- [建議] 細節注釋遵循單行注釋的格式。說明必須換行時,每行是一個單行注釋的起始。
-
- [強制] 有時我們會使用一些特殊标記進行說明。特殊标記必須使用單行注釋的形式。下面列舉了一些常用标記:
- 3 語言特性
-
- 3.1 變量
-
-
- [強制] 變量、函數在使用前必須先定義。
- [強制] 每個 `var` 隻能聲明一個變量。
- [強制] 變量必須 `即用即聲明`,不得在函數或其它形式的代碼塊起始位置統一聲明所有變量。
-
- 3.2 條件
-
-
- [強制] 在 Equality Expression 中使用類型嚴格的 `===`。僅當判斷 `null` 或 `undefined` 時,允許使用 `== null`。
- [建議] 盡可能使用簡潔的表達式。
- [建議] 按執行頻率排列分支的順序。
- [建議] 對于相同變量或表達式的多值條件,用 `switch` 代替 `if`。
- [建議] 如果函數或全局中的 `else` 塊後沒有任何語句,可以删除 `else`。
-
- 3.3 循環
-
-
- [建議] 不要在循環體中包含函數表達式,事先将函數提取到循環體外。
- [建議] 對循環内多次使用的不變值,在循環外用變量緩存。
- [建議] 對有序集合進行周遊時,緩存 `length`。
- [建議] 對有序集合進行順序無關的周遊時,使用逆序周遊。
-
- 3.4 類型
-
- 3.4.1 類型檢測
-
- [建議] 類型檢測優先使用 `typeof`。對象類型檢測使用 `instanceof`。`null` 或 `undefined` 的檢測使用 `== null`。
- 3.4.2 類型轉換
-
- [建議] 轉換成 `string` 時,使用 `+ ''`。
- [建議] 轉換成 `number` 時,通常使用 `+`。
- [建議] `string` 轉換成 `number`,要轉換的字元串結尾包含非數字并期望忽略時,使用 `parseInt`。
- [強制] 使用 `parseInt` 時,必須指定進制。
- [建議] 轉換成 `boolean` 時,使用 `!!`。
- [建議] `number` 去除小數點,使用 `Math.floor` / `Math.round` / `Math.ceil`,不使用 `parseInt`。
- 3.5 字元串
-
-
- [強制] 字元串開頭和結束使用單引号 `'`。
- [建議] 使用 `數組` 或 `+` 拼接字元串。
- [建議] 使用字元串拼接的方式生成HTML,需要根據語境進行合理的轉義。
- [建議] 複雜的資料到視圖字元串的轉換過程,選用一種模闆引擎。
-
- 3.6 對象
-
-
- [強制] 使用對象字面量 `{}` 建立新 `Object`。
- [建議] 對象建立時,如果一個對象的所有 `屬性` 均可以不添加引号,建議所有 `屬性` 不添加引号。
- [建議] 對象建立時,如果任何一個 `屬性` 需要添加引号,則所有 `屬性` 建議添加 `'`。
- [強制] 不允許修改和擴充任何原生對象和宿主對象的原型。
- [建議] 屬性通路時,盡量使用 `.`。
- [建議] `for in` 周遊對象時, 使用 `hasOwnProperty` 過濾掉原型中的屬性。
-
- 3.7 數組
-
-
- [強制] 使用數組字面量 `[]` 建立新數組,除非想要建立的是指定長度的數組。
- [強制] 周遊數組不使用 `for in`。
- [建議] 不因為性能的原因自己實作數組排序功能,盡量使用數組的 `sort` 方法。
- [建議] 清空數組使用 `.length = 0`。
-
- 3.8 函數
-
- 3.8.1 函數長度
-
- [建議] 一個函數的長度控制在 `50` 行以内。
- 3.8.2 參數設計
-
- [建議] 一個函數的參數控制在 `6` 個以内。
- [建議] 通過 `options` 參數傳遞非資料輸入型參數。
- 3.8.3 閉包
-
- [建議] 在适當的時候将閉包内大對象置為 `null`。
- [建議] 使用 `IIFE` 避免 `Lift 效應`。
- 3.8.4 空函數
-
- [建議] 空函數不使用 `new Function()` 的形式。
- [建議] 對于性能有高要求的場合,建議存在一個空函數的常量,供多處使用共享。
- 3.9 面向對象
-
-
- [強制] 類的繼承方案,實作時需要修正 `constructor`。
- [建議] 聲明類時,保證 `constructor` 的正确性。
- [建議] 屬性在構造函數中聲明,方法在原型中聲明。
- [強制] 自定義事件的 `事件名` 必須全小寫。
- [強制] 自定義事件隻能有一個 `event` 參數。如果事件需要傳遞較多資訊,應仔細設計事件對象。
- [建議] 設計自定義事件時,應考慮禁止預設行為。
-
- 3.10 動态特性
-
- 3.10.1 eval
-
- [強制] 避免使用直接 `eval` 函數。
- [建議] 盡量避免使用 `eval` 函數。
- 3.10.2 動态執行代碼
-
- [建議] 使用 `new Function` 執行動态代碼。
- 3.10.3 with
-
- [建議] 盡量不要使用 `with`。
- 3.10.4 delete
-
- [建議] 減少 `delete` 的使用。
- [建議] 處理 `delete` 可能産生的異常。
- 3.10.5 對象屬性
-
- [建議] 避免修改外部傳入的對象。
- [建議] 具備強類型的設計。
- 4 浏覽器環境
-
- 4.1 子產品化
-
- 4.1.1 AMD
-
- [強制] 使用 `AMD` 作為子產品定義。
- [強制] 子產品 `id` 必須符合标準。
- 4.1.2 define
-
- [建議] 定義子產品時不要指明 `id` 和 `dependencies`。
- [建議] 使用 `return` 來傳回子產品定義。
- 4.1.3 require
-
- [強制] 全局運作環境中,`require` 必須以 `async require` 形式調用。
- [強制] 子產品定義中隻允許使用 `local require`,不允許使用 `global require`。
- [強制] Package 在實作時,内部子產品的 `require` 必須使用 `relative id`。
- [建議] 不會被調用的依賴子產品,在 `factory` 開始處統一 `require`。
- 4.2 DOM
-
- 4.2.1 元素擷取
-
- [建議] 對于單個元素,盡可能使用 `document.getElementById` 擷取,避免使用`document.all`。
- [建議] 對于多個元素的集合,盡可能使用 `context.getElementsByTagName` 擷取。其中 `context` 可以為 `document` 或其他元素。指定 `tagName` 參數為 `*` 可以獲得所有子元素。
- [建議] 周遊元素集合時,盡量緩存集合長度。如需多次操作同一集合,則應将集合轉為數組。
- [建議] 擷取元素的直接子元素時使用 `children`。避免使用`childNodes`,除非預期是需要包含文本、注釋和屬性類型的節點。
- 4.2.2 樣式擷取
-
- [建議] 擷取元素實際樣式資訊時,應使用 `getComputedStyle` 或 `currentStyle`。
- 4.2.3 樣式設定
-
- [建議] 盡可能通過為元素添加預定義的 className 來改變元素樣式,避免直接操作 style 設定。
- [強制] 通過 style 對象設定元素樣式時,對于帶機關非 0 值的屬性,不允許省略機關。
- 4.2.4 DOM 操作
-
- [建議] 操作 `DOM` 時,盡量減少頁面 `reflow`。
- [建議] 盡量減少 `DOM` 操作。
- 4.2.5 DOM 事件
-
- [建議] 優先使用 `addEventListener / attachEvent` 綁定事件,避免直接在 HTML 屬性中或 DOM 的 `expando` 屬性綁定事件處理。
- [建議] 使用 `addEventListener` 時第三個參數使用 `false`。
- [建議] 在沒有事件自動管理的架構支援下,應持有監聽器函數的引用,在适當時候(元素釋放、頁面解除安裝等)移除添加的監聽器。
1 前言
JavaScript 在百度一直有着廣泛的應用,特别是在浏覽器端的行為管理。本文檔的目标是使 JavaScript 代碼風格保持一緻,容易被了解和被維護。
雖然本文檔是針對 JavaScript 設計的,但是在使用各種 JavaScript 的預編譯語言時(如 TypeScript 等)時,适用的部分也應盡量遵循本文檔的約定。
任何問題或建議,歡迎跟我們讨論: [email protected]
2 代碼風格
2.1 檔案
[建議] JavaScript 檔案使用無 BOM
的 UTF-8
編碼。
BOM
UTF-8
解釋:
UTF-8 編碼具有更廣泛的适應性。BOM 在使用程式或工具處理檔案時可能造成不必要的幹擾。
[建議] 在檔案結尾處,保留一個空行。
2.2 結構
2.2.1 縮進
[強制] 使用 4
個空格做為一個縮進層級,不允許使用 2
個空格 或 tab
字元。
4
2
tab
[強制] switch
下的 case
和 default
必須增加一個縮進層級。
switch
case
default
示例:
// good
switch (variable) {
case '1':
// do...
break;
case '2':
// do...
break;
default:
// do...
}
// bad
switch (variable) {
case '1':
// do...
break;
case '2':
// do...
break;
default:
// do...
}
2.2.2 空格
[強制] 二進制運算符兩側必須有一個空格,一進制運算符與操作對象之間不允許有空格。
示例:
var a = !arr.length;
a++;
a = b + c;
[強制] 用作代碼塊起始的左花括号 {
前必須有一個空格。
{
示例:
// good
if (condition) {
}
while (condition) {
}
function funcName() {
}
// bad
if (condition){
}
while (condition){
}
function funcName(){
}
[強制] if / else / for / while / function / switch / do / try / catch / finally
關鍵字後,必須有一個空格。
if / else / for / while / function / switch / do / try / catch / finally
示例:
// good
if (condition) {
}
while (condition) {
}
(function () {
})();
// bad
if(condition) {
}
while(condition) {
}
(function() {
})();
[強制] 在對象建立時,屬性中的 :
之後必須有空格, :
之前不允許有空格。
:
:
示例:
// good
var obj = {
a: 1,
b: 2,
c: 3
};
// bad
var obj = {
a : 1,
b:2,
c :3
};
[強制] 函數聲明、具名函數表達式、函數調用中,函數名和 (
之間不允許有空格。
(
示例:
// good
function funcName() {
}
var funcName = function funcName() {
};
funcName();
// bad
function funcName () {
}
var funcName = function funcName () {
};
funcName ();
[強制] ,
和 ;
前不允許有空格。如果不位于行尾, ,
和 ;
後必須跟一個空格。
,
;
,
;
示例:
// good
callFunc(a, b);
// bad
callFunc(a , b) ;
[強制] 在函數調用、函數聲明、括号表達式、屬性通路、 if / for / while / switch / catch
等語句中, ()
和 []
内緊貼括号部分不允許有空格。
if / for / while / switch / catch
()
[]
示例:
// good
callFunc(param1, param2, param3);
save(this.list[this.indexes[i]]);
needIncream && (variable += increament);
if (num > list.length) {
}
while (len--) {
}
// bad
callFunc( param1, param2, param3 );
save( this.list[ this.indexes[ i ] ] );
needIncreament && ( variable += increament );
if ( num > list.length ) {
}
while ( len-- ) {
}
[強制] 單行聲明的數組與對象,如果包含元素, {}
和 []
内緊貼括号部分不允許包含空格。
{}
[]
解釋:
聲明包含元素的數組與對象,隻有當内部元素的形式較為簡單時,才允許寫在一行。元素複雜的情況,還是應該換行書寫。
示例:
// good
var arr1 = [];
var arr2 = [1, 2, 3];
var obj1 = {};
var obj2 = {name: 'obj'};
var obj3 = {
name: 'obj',
age: 20,
sex: 1
};
// bad
var arr1 = [ ];
var arr2 = [ 1, 2, 3 ];
var obj1 = { };
var obj2 = { name: 'obj' };
var obj3 = {name: 'obj', age: 20, sex: 1};
[強制] 行尾不得有多餘的空格。
2.2.3 換行
[強制] 每個獨立語句結束後必須換行。
[強制] 每行不得超過 120
個字元。
120
解釋:
超長的不可分割的代碼允許例外,比如複雜的正規表達式。長字元串不在例外之列。
[強制] 運算符處換行時,運算符必須在新行的行首。
示例:
// good
if (user.isAuthenticated()
&& user.isInRole('admin')
&& user.hasAuthority('add-admin')
|| user.hasAuthority('delete-admin')
) {
// Code
}
var result = number1 + number2 + number3
+ number4 + number5;
// bad
if (user.isAuthenticated() &&
user.isInRole('admin') &&
user.hasAuthority('add-admin') ||
user.hasAuthority('delete-admin')) {
// Code
}
var result = number1 + number2 + number3 +
number4 + number5;
[強制] 在函數聲明、函數表達式、函數調用、對象建立、數組建立、 for
語句等場景中,不允許在 ,
或 ;
前換行。
for
,
;
示例:
// good
var obj = {
a: 1,
b: 2,
c: 3
};
foo(
aVeryVeryLongArgument,
anotherVeryLongArgument,
callback
);
// bad
var obj = {
a: 1
, b: 2
, c: 3
};
foo(
aVeryVeryLongArgument
, anotherVeryLongArgument
, callback
);
[建議] 不同行為或邏輯的語句集,使用空行隔開,更易閱讀。
示例:
// 僅為按邏輯換行的示例,不代表setStyle的最優實作
function setStyle(element, property, value) {
if (element == null) {
return;
}
element.style[property] = value;
}
[建議] 在語句的行長度超過 120
時,根據邏輯條件合理縮進。
120
示例:
// 較複雜的邏輯條件組合,将每個條件獨立一行,邏輯運算符放置在行首進行分隔,或将部分邏輯按邏輯組合進行分隔。
// 建議最終将右括号 ) 與左大括号 { 放在獨立一行,保證與 `if` 内語句塊能容易視覺辨識。
if (user.isAuthenticated()
&& user.isInRole('admin')
&& user.hasAuthority('add-admin')
|| user.hasAuthority('delete-admin')
) {
// Code
}
// 按一定長度截斷字元串,并使用 + 運算符進行連接配接。
// 分隔字元串盡量按語義進行,如不要在一個完整的名詞中間斷開。
// 特别的,對于 HTML 片段的拼接,通過縮進,保持和 HTML 相同的結構。
var html = '' // 此處用一個空字元串,以便整個 HTML 片段都在新行嚴格對齊
+ '<article>'
+ '<h1>Title here</h1>'
+ '<p>This is a paragraph</p>'
+ '<footer>Complete</footer>'
+ '</article>';
// 也可使用數組來進行拼接,相對 `+` 更容易調整縮進。
var html = [
'<article>',
'<h1>Title here</h1>',
'<p>This is a paragraph</p>',
'<footer>Complete</footer>',
'</article>'
];
html = html.join('');
// 當參數過多時,将每個參數獨立寫在一行上,并将結束的右括号 ) 獨立一行。
// 所有參數必須增加一個縮進。
foo(
aVeryVeryLongArgument,
anotherVeryLongArgument,
callback
);
// 也可以按邏輯對參數進行組合。
// 最經典的是 baidu.format 函數,調用時将參數分為“模闆”和“資料”兩塊
baidu.format(
dateFormatTemplate,
year, month, date, hour, minute, second
);
// 當函數調用時,如果有一個或以上參數跨越多行,應當每一個參數獨立一行。
// 這通常出現在匿名函數或者對象初始化等作為參數時,如 `setTimeout` 函數等。
setTimeout(
function () {
alert('hello');
},
200
);
order.data.read(
'id=' + me.model.id,
function (data) {
me.attchToModel(data.result);
callback();
},
300
);
// 鍊式調用較長時采用縮進進行調整。
$('#items')
.find('.selected')
.highlight()
.end();
// 三元運算符由3部分組成,是以其換行應當根據每個部分的長度不同,形成不同的情況。
var result = thisIsAVeryVeryLongCondition
? resultA : resultB;
var result = condition
? thisIsAVeryVeryLongResult
: resultB;
// 數組和對象初始化的混用,嚴格按照每個對象的 `{` 和結束 `}` 在獨立一行的風格書寫。
var array = [
{
// ...
},
{
// ...
}
];
[建議] 對于 if...else...
、 try...catch...finally
等語句,推薦使用在 }
号後添加一個換行 的風格,使代碼層次結構更清晰,閱讀性更好。
if...else...
try...catch...finally
}
示例:
if (condition) {
// some statements;
}
else {
// some statements;
}
try {
// some statements;
}
catch (ex) {
// some statements;
}
2.2.4 語句
[強制] 不得省略語句結束的分号。
[強制] 在 if / else / for / do / while
語句中,即使隻有一行,也不得省略塊 {...}
。
if / else / for / do / while
{...}
示例:
// good
if (condition) {
callFunc();
}
// bad
if (condition) callFunc();
if (condition)
callFunc();
[強制] 函數定義結束不允許添加分号。
示例:
// good
function funcName() {
}
// bad
function funcName() {
};
// 如果是函數表達式,分号是不允許省略的。
var funcName = function () {
};
[強制] IIFE
必須在函數表達式外添加 (
,非 IIFE
不得在函數表達式外添加 (
。
IIFE
(
IIFE
(
解釋:
IIFE = Immediately-Invoked Function Expression.
額外的 ( 能夠讓代碼在閱讀的一開始就能判斷函數是否立即被調用,進而明白接下來代碼的用途。而不是一直拖到底部才恍然大悟。
示例:
// good
var task = (function () {
// Code
return result;
})();
var func = function () {
};
// bad
var task = function () {
// Code
return result;
}();
var func = (function () {
});
2.3 命名
[強制] 變量
使用 Camel命名法
。
變量
Camel命名法
示例:
[強制] 常量
使用 全部字母大寫,單詞間下劃線分隔
的命名方式。
常量
全部字母大寫,單詞間下劃線分隔
示例:
[強制] 函數
使用 Camel命名法
。
函數
Camel命名法
示例:
function stringFormat(source) {
}
[強制] 函數的 參數
使用 Camel命名法
。
參數
Camel命名法
示例:
function hear(theBells) {
}
[強制] 類
使用 Pascal命名法
。
類
Pascal命名法
示例:
function TextNode(options) {
}
[強制] 類的 方法
/ 屬性
使用 Camel命名法
。
方法
屬性
Camel命名法
示例:
function TextNode(value, engine) {
this.value = value;
this.engine = engine;
}
TextNode.prototype.clone = function () {
return this;
};
[強制] 枚舉變量
使用 Pascal命名法
, 枚舉的屬性
使用 全部字母大寫,單詞間下劃線分隔
的命名方式。
枚舉變量
Pascal命名法
枚舉的屬性
全部字母大寫,單詞間下劃線分隔
示例:
var TargetState = {
READING: 1,
READED: 2,
APPLIED: 3,
READY: 4
};
[強制] 命名空間
使用 Camel命名法
。
命名空間
Camel命名法
示例:
[強制] 由多個單詞組成的縮寫詞,在命名中,根據目前命名法和出現的位置,所有字母的大小寫與首字母的大小寫保持一緻。
示例:
function XMLParser() {
}
function insertHTML(element, html) {
}
var httpRequest = new HTTPRequest();
[強制] 類名
使用 名詞
。
類名
名詞
示例:
function Engine(options) {
}
[建議] 函數名
使用 動賓短語
。
函數名
動賓短語
示例:
function getStyle(element) {
}
[建議] boolean
類型的變量使用 is
或 has
開頭。
boolean
is
has
示例:
var isReady = false;
var hasMoreCommands = false;
[建議] Promise對象
用 動賓短語的進行時
表達。
Promise對象
動賓短語的進行時
示例:
var loadingData = ajax.get('url');
loadingData.then(callback);
2.4 注釋
2.4.1 單行注釋
[強制] 必須獨占一行。 //
後跟一個空格,縮進與下一行被注釋說明的代碼一緻。
//
2.4.2 多行注釋
[建議] 避免使用 這樣的多行注釋。有多行注釋内容時,使用多個單行注釋。
2.4.3 文檔化注釋
[強制] 為了便于代碼閱讀和自文檔化,以下内容必須包含以 形式的塊注釋中。
解釋:
- 檔案
- namespace
- 類
- 函數或方法
- 類屬性
- 事件
- 全局變量
- 常量
- AMD 子產品
[強制] 文檔注釋前必須空一行。
[建議] 自文檔化的文檔說明 what,而不是 how。
2.4.4 類型定義
[強制] 類型定義都是以 {
開始, 以 }
結束。
{
}
解釋:
常用類型如:{string}, {number}, {boolean}, {Object}, {Function}, {RegExp}, {Array}, {Date}。
類型不僅局限于内置的類型,也可以是自定義的類型。比如定義了一個類 Developer,就可以使用它來定義一個參數和傳回值的類型。
[強制] 對于基本類型 {string}, {number}, {boolean},首字母必須小寫。
類型定義 | 文法示例 | 解釋 |
---|---|---|
String | {string} | – |
Number | {number} | – |
Boolean | {boolean} | – |
Object | {Object} | – |
Function | {Function} | – |
RegExp | {RegExp} | – |
Array | {Array} | – |
Date | {Date} | – |
單一類型集合 | {Array.<string>} | string 類型的數組 |
多類型 | {(number|boolean)} | 可能是 number 類型, 也可能是 boolean 類型 |
允許為null | {?number} | 可能是 number, 也可能是 null |
不允許為null | {!Object} | Object 類型, 但不是 null |
Function類型 | {function(number, boolean)} | 函數, 形參類型 |
Function帶傳回值 | {function(number, boolean):string} | 函數, 形參, 傳回值類型 |
Promise | Promise.<resolveType, rejectType> | Promise,成功傳回的資料類型,失敗傳回的錯誤類型 |
參數可選 | @param {string=} name | 可選參數, =為類型字尾 |
可變參數 | @param {…number} args | 變長參數, …為類型字首 |
任意類型 | {*} | 任意類型 |
可選任意類型 | @param {*=} name | 可選參數,類型不限 |
可變任意類型 | @param {…*} args | 變長參數,類型不限 |
2.4.5 檔案注釋
[強制] 檔案頂部必須包含檔案注釋,用 @file
辨別檔案說明。
@file
示例:
/**
* @file Describe the file
*/
[建議] 檔案注釋中可以用 @author
辨別開發者資訊。
@author
解釋:
開發者資訊能夠展現開發人員對檔案的貢獻,并且能夠讓遇到問題或希望了解相關資訊的人找到維護人。通常情況檔案在被建立時辨別的是建立者。随着項目的進展,越來越多的人加入,參與這個檔案的開發,新的作者應該被加入
@author
辨別。
@author
辨別具有多人時,原則是按照
責任
進行排序。通常的說就是如果有問題,就是找第一個人應該比找第二個人有效。比如檔案的建立者由于各種原因,子產品移交給了其他人或其他團隊,後來因為新增需求,其他人在新增代碼時,添加
@author
辨別應該把自己的名字添加在建立人的前面。
@author
中的名字不允許被删除。任何勞動成果都應該被尊重。
業務項目中,一個檔案可能被多人頻繁修改,并且每個人的維護時間都可能不會很長,不建議為檔案增加
@author
辨別。通過版本控制系統追蹤變更,按業務邏輯單元确定子產品的維護責任人,通過文檔與wiki跟蹤和查詢,是更好的責任管理方式。
對于業務邏輯無關的技術型基礎項目,特别是開源的公共項目,應使用
@author
辨別。
示例:
/**
* @file Describe the file
* @author author-name([email protected])
* author-name2([email protected])
*/
2.4.6 命名空間注釋
[建議] 命名空間使用 @namespace
辨別。
@namespace
示例:
/**
* @namespace
*/
var util = {};
2.4.7 類注釋
[建議] 使用 @class
标記類或構造函數。
@class
解釋:
對于使用對象
constructor
屬性來定義的構造函數,可以使用
@constructor
來标記。
示例:
/**
* 描述
*
* @class
*/
function Developer() {
// constructor body
}
[建議] 使用 @extends
标記類的繼承資訊。
@extends
示例:
/**
* 描述
*
* @class
* @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.inherits(Fronteer, Developer);
[強制] 使用包裝方式擴充類成員時, 必須通過 @lends
進行重新指向。
@lends
解釋:
沒有
@lends
标記将無法為該類生成包含擴充類成員的文檔。
示例:
/**
* 類描述
*
* @class
* @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.extend(
Fronteer.prototype,
/** @lends Fronteer.prototype */{
getLevel: function () {
// TODO
}
}
);
[強制] 類的屬性或方法等成員資訊不是 public
的,應使用 @protected
或 @private
辨別可通路性。
public
@protected
@private
解釋:
生成的文檔中将有可通路性的标記,避免使用者直接使用非
public
的屬性或方法。
示例:
/**
* 類描述
*
* @class
* @extends Developer
*/
var Fronteer = function () {
Developer.call(this);
/**
* 屬性描述
*
* @type {string}
* @private
*/
this.level = 'T12';
// constructor body
};
util.inherits(Fronteer, Developer);
/**
* 方法描述
*
* @private
* @return {string} 傳回值描述
*/
Fronteer.prototype.getLevel = function () {
};
2.4.8 函數/方法注釋
[強制] 函數/方法注釋必須包含函數說明,有參數和傳回值時必須使用注釋辨別。
解釋:
當
return
關鍵字僅作退出函數/方法使用時,無須對傳回值作注釋辨別。
[強制] 參數和傳回值注釋必須包含類型資訊,且不允許省略參數的說明。
[建議] 當函數是内部函數,外部不可通路時,可以使用 @inner
辨別。
@inner
示例:
/**
* 函數描述
*
* @param {string} p1 參數1的說明
* @param {string} p2 參數2的說明,比較長
* 那就換行了.
* @param {number=} p3 參數3的說明(可選)
* @return {Object} 傳回值描述
*/
function foo(p1, p2, p3) {
var p3 = p3 || 10;
return {
p1: p1,
p2: p2,
p3: p3
};
}
[強制] 對 Object 中各項的描述, 必須使用 @param
辨別。
@param
示例:
/**
* 函數描述
*
* @param {Object} option 參數描述
* @param {string} option.url option項描述
* @param {string=} option.method option項描述,可選參數
*/
function foo(option) {
// TODO
}
[建議] 重寫父類方法時, 應當添加 @override
辨別。如果重寫的形參個數、類型、順序和傳回值類型均未發生變化,可省略 @param
、 @return
,僅用 @override
辨別,否則仍應作完整注釋。
@override
@param
@return
@override
解釋:
簡而言之,當子類重寫的方法能直接套用父類的方法注釋時可省略對參數與傳回值的注釋。
2.4.9 事件注釋
[強制] 必須使用 @event
辨別事件,事件參數的辨別與方法描述的參數辨別相同。
@event
示例:
/**
* 值變更時觸發
*
* @event Select#change
* @param {Object} e e描述
* @param {string} e.before before描述
* @param {string} e.after after描述
*/
this.fire(
'change',
{
before: 'foo',
after: 'bar'
}
);
[強制] 在會廣播事件的函數前使用 @fires
辨別廣播的事件,在廣播事件代碼前使用 @event
辨別事件。
@fires
@event
[建議] 對于事件對象的注釋,使用 @param
辨別,生成文檔時可讀性更好。
@param
示例:
/**
* 點選處理
*
* @fires Select#change
* @private
*/
Select.prototype.clickHandler = function () {
/**
* 值變更時觸發
*
* @event Select#change
* @param {Object} e e描述
* @param {string} e.before before描述
* @param {string} e.after after描述
*/
this.fire(
'change',
{
before: 'foo',
after: 'bar'
}
);
};
2.4.10 常量注釋
[強制] 常量必須使用 @const
标記,并包含說明和類型資訊。
@const
示例:
/**
* 常量說明
*
* @const
* @type {string}
*/
var REQUEST_URL = 'myurl.do';
2.4.11 複雜類型注釋
[建議] 對于類型未定義的複雜結構的注釋,可以使用 @typedef
辨別來定義。
@typedef
示例:
// `namespaceA~` 可以換成其它 namepaths 字首,目的是為了生成文檔中能顯示 `@typedef` 定義的類型和連結。
/**
* 伺服器
*
* @typedef {Object} namespaceA~Server
* @property {string} host 主機
* @property {number} port 端口
*/
/**
* 伺服器清單
*
* @type {Array.<namespaceA~Server>}
*/
var servers = [
{
host: '1.2.3.4',
port: 8080
},
{
host: '1.2.3.5',
port: 8081
}
];
2.4.12 AMD 子產品注釋
[強制] AMD 子產品使用 @module
或 @exports
辨別。
@module
@exports
解釋:
@exports 與 @module 都可以用來辨別子產品,差別在于 @module 可以省略子產品名稱。而隻使用 @exports 時在 namepaths 中可以省略 module: 字首。
示例:
define(
function (require) {
/**
* foo description
*
* @exports Foo
*/
var foo = {
// TODO
};
/**
* baz description
*
* @return {boolean} return description
*/
foo.baz = function () {
// TODO
};
return foo;
}
);
也可以在 exports 變量前使用 @module 辨別:
define(
function (require) {
/**
* module description.
*
* @module foo
*/
var exports = {};
/**
* bar description
*
*/
exports.bar = function () {
// TODO
};
return exports;
}
);
如果直接使用 factory 的 exports 參數,還可以:
/**
* module description.
*
* @module
*/
define(
function (require, exports) {
/**
* bar description
*
*/
exports.bar = function () {
// TODO
};
return exports;
}
);
[強制] 對于已使用 @module
辨別為 AMD子產品 的引用,在 namepaths
中必須增加 module:
作字首。
@module
namepaths
module:
解釋:
namepaths 沒有 module: 字首時,生成的文檔中将無法正确生成連結。
示例:
/**
* 點選處理
*
* @fires module:Select#change
* @private
*/
Select.prototype.clickHandler = function () {
/**
* 值變更時觸發
*
* @event module:Select#change
* @param {Object} e e描述
* @param {string} e.before before描述
* @param {string} e.after after描述
*/
this.fire(
'change',
{
before: 'foo',
after: 'bar'
}
);
};
[建議] 對于類定義的子產品,可以使用 @alias
辨別建構函數。
@alias
示例:
/**
* A module representing a jacket.
* @module jacket
*/
define(
function () {
/**
* @class
* @alias module:jacket
*/
var Jacket = function () {
};
return Jacket;
}
);
[建議] 多子產品定義時,可以使用 @exports
辨別各個子產品。
@exports
示例:
// one module
define('html/utils',
/**
* Utility functions to ease working with DOM elements.
* @exports html/utils
*/
function () {
var exports = {
};
return exports;
}
);
// another module
define('tag',
/** @exports tag */
function () {
var exports = {
};
return exports;
}
);
[建議] 對于 exports 為 Object 的子產品,可以使用 @namespace
辨別。
@namespace
解釋:
使用 @namespace 而不是 @module 或 @exports 時,對子產品的引用可以省略 module: 字首。
[建議] 對于 exports 為類名的子產品,使用 @class
和 @exports
辨別。
@class
@exports
示例:
// 隻使用 @class Bar 時,類方法和屬性都必須增加 @name Bar#methodName 來辨別,與 @exports 配合可以免除這一麻煩,并且在引用時可以省去 module: 字首。
// 另外需要注意類名需要使用 var 定義的方式。
/**
* Bar description
*
* @see foo
* @exports Bar
* @class
*/
var Bar = function () {
// TODO
};
/**
* baz description
*
* @return {(string|Array)} return description
*/
Bar.prototype.baz = function () {
// TODO
};
2.4.13 細節注釋
對于内部實作、不容易了解的邏輯說明、摘要資訊等,我們可能需要編寫細節注釋。
[建議] 細節注釋遵循單行注釋的格式。說明必須換行時,每行是一個單行注釋的起始。
示例:
function foo(p1, p2, opt_p3) {
// 這裡對具體内部邏輯進行說明
// 說明太長需要換行
for (...) {
....
}
}
[強制] 有時我們會使用一些特殊标記進行說明。特殊标記必須使用單行注釋的形式。下面列舉了一些常用标記:
解釋:
- TODO: 有功能待實作。此時需要對将要實作的功能進行簡單說明。
- FIXME: 該處代碼運作沒問題,但可能由于時間趕或者其他原因,需要修正。此時需要對如何修正進行簡單說明。
- HACK: 為修正某些問題而寫的不太好或者使用了某些詭異手段的代碼。此時需要對思路或詭異手段進行描述。
- XXX: 該處存在陷阱。此時需要對陷阱進行描述。
3 語言特性
3.1 變量
[強制] 變量、函數在使用前必須先定義。
解釋:
不通過 var 定義變量将導緻變量污染全局環境。
示例:
// good
var name = 'MyName';
// bad
name = 'MyName';
原則上不建議使用全局變量,對于已有的全局變量或第三方架構引入的全局變量,需要根據檢查工具的文法辨別。
示例:
/* globals jQuery */
var element = jQuery('#element-id');
[強制] 每個 var
隻能聲明一個變量。
var
解釋:
一個
var
聲明多個變量,容易導緻較長的行長度,并且在修改時容易造成逗号和分号的混淆。
示例:
// good
var hangModules = [];
var missModules = [];
var visited = {};
// bad
var hangModules = [],
missModules = [],
visited = {};
[強制] 變量必須 即用即聲明
,不得在函數或其它形式的代碼塊起始位置統一聲明所有變量。
即用即聲明
解釋:
變量聲明與使用的距離越遠,出現的跨度越大,代碼的閱讀與維護成本越高。雖然JavaScript的變量是函數作用域,還是應該根據程式設計中的意圖,縮小變量出現的距離空間。
示例:
// good
function kv2List(source) {
var list = [];
for (var key in source) {
if (source.hasOwnProperty(key)) {
var item = {
k: key,
v: source[key]
};
list.push(item);
}
}
return list;
}
// bad
function kv2List(source) {
var list = [];
var key;
var item;
for (key in source) {
if (source.hasOwnProperty(key)) {
item = {
k: key,
v: source[key]
};
list.push(item);
}
}
return list;
}
3.2 條件
[強制] 在 Equality Expression 中使用類型嚴格的 ===
。僅當判斷 null
或 undefined
時,允許使用 == null
。
===
null
undefined
== null
解釋:
使用
===
可以避免等于判斷中隐式的類型轉換。
示例:
// good
if (age === 30) {
// ......
}
// bad
if (age == 30) {
// ......
}
[建議] 盡可能使用簡潔的表達式。
示例:
// 字元串為空
// good
if (!name) {
// ......
}
// bad
if (name === '') {
// ......
}
// 字元串非空
// good
if (name) {
// ......
}
// bad
if (name !== '') {
// ......
}
// 數組非空
// good
if (collection.length) {
// ......
}
// bad
if (collection.length > 0) {
// ......
}
// 布爾不成立
// good
if (!notTrue) {
// ......
}
// bad
if (notTrue === false) {
// ......
}
// null 或 undefined
// good
if (noValue == null) {
// ......
}
// bad
if (noValue === null || typeof noValue === 'undefined') {
// ......
}
[建議] 按執行頻率排列分支的順序。
解釋:
按執行頻率排列分支的順序好處是:
- 閱讀的人容易找到最常見的情況,增加可讀性。
- 提高執行效率。
[建議] 對于相同變量或表達式的多值條件,用 switch
代替 if
。
switch
if
示例:
// good
switch (typeof variable) {
case 'object':
// ......
break;
case 'number':
case 'boolean':
case 'string':
// ......
break;
}
// bad
var type = typeof variable;
if (type === 'object') {
// ......
}
else if (type === 'number' || type === 'boolean' || type === 'string') {
// ......
}
[建議] 如果函數或全局中的 else
塊後沒有任何語句,可以删除 else
。
else
else
示例:
// good
function getName() {
if (name) {
return name;
}
return 'unnamed';
}
// bad
function getName() {
if (name) {
return name;
}
else {
return 'unnamed';
}
}
3.3 循環
[建議] 不要在循環體中包含函數表達式,事先将函數提取到循環體外。
解釋:
循環體中的函數表達式,運作過程中會生成循環次數個函數對象。
示例:
// good
function clicker() {
// ......
}
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
addListener(element, 'click', clicker);
}
// bad
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
addListener(element, 'click', function () {});
}
[建議] 對循環内多次使用的不變值,在循環外用變量緩存。
示例:
// good
var width = wrap.offsetWidth + 'px';
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
element.style.width = width;
// ......
}
// bad
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
element.style.width = wrap.offsetWidth + 'px';
// ......
}
[建議] 對有序集合進行周遊時,緩存 length
。
length
解釋:
雖然現代浏覽器都對數組長度進行了緩存,但對于一些宿主對象和老舊浏覽器的數組對象,在每次
length
通路時會動态計算元素個數,此時緩存
length
能有效提高程式性能。
示例:
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
// ......
}
[建議] 對有序集合進行順序無關的周遊時,使用逆序周遊。
解釋:
逆序周遊可以節省變量,代碼比較優化。
示例:
var len = elements.length;
while (len--) {
var element = elements[len];
// ......
}
3.4 類型
3.4.1 類型檢測
[建議] 類型檢測優先使用 typeof
。對象類型檢測使用 instanceof
。 null
或 undefined
的檢測使用 == null
。
typeof
instanceof
null
undefined
== null
示例:
// string
typeof variable === 'string'
// number
typeof variable === 'number'
// boolean
typeof variable === 'boolean'
// Function
typeof variable === 'function'
// Object
typeof variable === 'object'
// RegExp
variable instanceof RegExp
// Array
variable instanceof Array
// null
variable === null
// null or undefined
variable == null
// undefined
typeof variable === 'undefined'
3.4.2 類型轉換
[建議] 轉換成 string
時,使用 + ''
。
string
+ ''
示例:
// good
num + '';
// bad
new String(num);
num.toString();
String(num);
[建議] 轉換成 number
時,通常使用 +
。
number
+
示例:
// good
+str;
// bad
Number(str);
[建議] string
轉換成 number
,要轉換的字元串結尾包含非數字并期望忽略時,使用 parseInt
。
string
number
parseInt
示例:
var width = '200px';
parseInt(width, 10);
[強制] 使用 parseInt
時,必須指定進制。
parseInt
示例:
// good
parseInt(str, 10);
// bad
parseInt(str);
[建議] 轉換成 boolean
時,使用 !!
。
boolean
!!
示例:
var num = 3.14;
!!num;
[建議] number
去除小數點,使用 Math.floor
/ Math.round
/ Math.ceil
,不使用 parseInt
。
number
Math.floor
Math.round
Math.ceil
parseInt
示例:
// good
var num = 3.14;
Math.ceil(num);
// bad
var num = 3.14;
parseInt(num, 10);
3.5 字元串
[強制] 字元串開頭和結束使用單引号 '
。
'
解釋:
- 輸入單引号不需要按住
,友善輸入。shift
- 實際使用中,字元串經常用來拼接 HTML。為友善 HTML 中包含雙引号而不需要轉義寫法。
示例:
var str = '我是一個字元串';
var html = '<div class="cls">拼接HTML可以省去雙引号轉義</div>';
[建議] 使用 數組
或 +
拼接字元串。
數組
+
解釋:
- 使用
拼接字元串,如果拼接的全部是 StringLiteral,壓縮工具可以對其進行自動合并的優化。是以,靜态字元串建議使用+
拼接。+
- 在現代浏覽器下,使用
拼接字元串,性能較數組的方式要高。+
- 如需要兼顧老舊浏覽器,應盡量使用數組拼接字元串。
示例:
// 使用數組拼接字元串
var str = [
// 推薦換行開始并縮進開始第一個字元串, 對齊代碼, 友善閱讀.
'<ul>',
'<li>第一項</li>',
'<li>第二項</li>',
'</ul>'
].join('');
// 使用 `+` 拼接字元串
var str2 = '' // 建議第一個為空字元串, 第二個換行開始并縮進開始, 對齊代碼, 友善閱讀
+ '<ul>',
+ '<li>第一項</li>',
+ '<li>第二項</li>',
+ '</ul>';
[建議] 使用字元串拼接的方式生成HTML,需要根據語境進行合理的轉義。
解釋:
在
JavaScript
中拼接,并且最終将輸出到頁面中的字元串,需要進行合理轉義,以防止安全漏洞。下面的示例代碼為場景說明,不能直接運作。
示例:
// HTML 轉義
var str = '<p>' + htmlEncode(content) + '</p>';
// HTML 轉義
var str = '<input type="text" value="' + htmlEncode(value) + '">';
// URL 轉義
var str = '<a href="/?key=' + htmlEncode(urlEncode(value)) + '">link</a>';
// JavaScript字元串 轉義 + HTML 轉義
var str = '<button οnclick="check(\'' + htmlEncode(strLiteral(name)) + '\')">送出</button>';
[建議] 複雜的資料到視圖字元串的轉換過程,選用一種模闆引擎。
解釋:
使用模闆引擎有如下好處:
- 在開發過程中專注于資料,将視圖生成的過程由另外一個層級維護,使程式邏輯結構更清晰。
- 優秀的模闆引擎,通過模闆編譯技術和高品質的編譯産物,能獲得比手工拼接字元串更高的性能。
- 模闆引擎能友善的對動态資料進行相應的轉義,部分模闆引擎預設進行HTML轉義,安全性更好。
- artTemplate: 體積較小,在所有環境下性能高,文法靈活。
- dot.js: 體積小,在現代浏覽器下性能高,文法靈活。
- etpl: 體積較小,在所有環境下性能高,模闆複用性高,文法靈活。
- handlebars: 體積大,在所有環境下性能高,擴充性高。
- hogon: 體積小,在現代浏覽器下性能高。
- nunjucks: 體積較大,性能一般,模闆複用性高。
3.6 對象
[強制] 使用對象字面量 {}
建立新 Object
。
{}
Object
示例:
// good
var obj = {};
// bad
var obj = new Object();
[建議] 對象建立時,如果一個對象的所有 屬性
均可以不添加引号,建議所有 屬性
不添加引号。
屬性
屬性
示例:
var info = {
name: 'someone',
age: 28
};
[建議] 對象建立時,如果任何一個 屬性
需要添加引号,則所有 屬性
建議添加 '
。
屬性
屬性
'
解釋:
如果屬性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。
示例:
// good
var info = {
'name': 'someone',
'age': 28,
'more-info': '...'
};
// bad
var info = {
name: 'someone',
age: 28,
'more-info': '...'
};
[強制] 不允許修改和擴充任何原生對象和宿主對象的原型。
示例:
// 以下行為絕對禁止
String.prototype.trim = function () {
};
[建議] 屬性通路時,盡量使用 .
。
.
解釋:
屬性名符合 Identifier 的要求,就可以通過
.
來通路,否則就隻能通過
[expr]
方式通路。
通常在 JavaScript 中聲明的對象,屬性命名是使用 Camel 命名法,用
.
來通路更清晰簡潔。部分特殊的屬性(比如來自後端的 JSON ),可能采用不尋常的命名方式,可以通過
[expr]
方式通路。
示例:
info.age;
info['more-info'];
[建議] for in
周遊對象時, 使用 hasOwnProperty
過濾掉原型中的屬性。
for in
hasOwnProperty
示例:
var newInfo = {};
for (var key in info) {
if (info.hasOwnProperty(key)) {
newInfo[key] = info[key];
}
}
3.7 數組
[強制] 使用數組字面量 []
建立新數組,除非想要建立的是指定長度的數組。
[]
示例:
// good
var arr = [];
// bad
var arr = new Array();
[強制] 周遊數組不使用 for in
。
for in
解釋:
數組對象可能存在數字以外的屬性, 這種情況下
for in
不會得到正确結果。
示例:
var arr = ['a', 'b', 'c'];
// 這裡僅作示範, 實際中應使用 Object 類型
arr.other = 'other things';
// 正确的周遊方式
for (var i = 0, len = arr.length; i < len; i++) {
console.log(i);
}
// 錯誤的周遊方式
for (var i in arr) {
console.log(i);
}
[建議] 不因為性能的原因自己實作數組排序功能,盡量使用數組的 sort
方法。
sort
解釋:
自己實作的正常排序算法,在性能上并不優于數組預設的
sort
方法。以下兩種場景可以自己實作排序:
- 需要穩定的排序算法,達到嚴格一緻的排序結果。
- 資料特點鮮明,适合使用桶排。
[建議] 清空數組使用 .length = 0
。
.length = 0
3.8 函數
3.8.1 函數長度
[建議] 一個函數的長度控制在 50
行以内。
50
解釋:
将過多的邏輯單元混在一個大函數中,易導緻難以維護。一個清晰易懂的函數應該完成單一的邏輯單元。複雜的操作應進一步抽取,通過函數的調用來展現流程。
特定算法等不可分割的邏輯允許例外。
示例:
function syncViewStateOnUserAction() {
if (x.checked) {
y.checked = true;
z.value = '';
}
else {
y.checked = false;
}
if (a.value) {
warning.innerText = '';
submitButton.disabled = false;
}
else {
warning.innerText = 'Please enter it';
submitButton.disabled = true;
}
}
// 直接閱讀該函數會難以明确其主線邏輯,是以下方是一種更合理的表達方式:
function syncViewStateOnUserAction() {
syncXStateToView();
checkAAvailability();
}
function syncXStateToView() {
y.checked = x.checked;
if (x.checked) {
z.value = '';
}
}
function checkAAvailability() {
if (a.value) {
clearWarnignForA();
}
else {
displayWarningForAMissing();
}
}
3.8.2 參數設計
[建議] 一個函數的參數控制在 6
個以内。
6
解釋:
除去不定長參數以外,函數具備不同邏輯意義的參數建議控制在
6
個以内,過多參數會導緻維護難度增大。
某些情況下,如使用 AMD Loader 的
require
加載多個子產品時,其
callback
可能會存在較多參數,是以對函數參數的個數不做強制限制。
[建議] 通過 options
參數傳遞非資料輸入型參數。
options
解釋:
有些函數的參數并不是作為算法的輸入,而是對算法的某些分支條件判斷之用,此類參數建議通過一個
options
參數傳遞。
如下函數:
/**
* 移除某個元素
*
* @param {Node} element 需要移除的元素
* @param {boolean} removeEventListeners 是否同時将所有注冊在元素上的事件移除
*/
function removeElement(element, removeEventListeners) {
element.parent.removeChild(element);
if (removeEventListeners) {
element.clearEventListeners();
}
}
可以轉換為下面的簽名:
/**
* 移除某個元素
*
* @param {Node} element 需要移除的元素
* @param {Object} options 相關的邏輯配置
* @param {boolean} options.removeEventListeners 是否同時将所有注冊在元素上的事件移除
*/
function removeElement(element, options) {
element.parent.removeChild(element);
if (options.removeEventListeners) {
element.clearEventListeners();
}
}
這種模式有幾個顯著的優勢:
-
型的配置項具備名稱,從調用的代碼上更易了解其表達的邏輯意義。boolean
- 當配置項有增長時,無需無休止地增加參數個數,不會出現
這樣難以了解的調用代碼。removeElement(element, true, false, false, 3)
- 當部配置設定置參數可選時,多個參數的形式非常難處理重載邏輯,而使用一個 options 對象隻需判斷屬性是否存在,實作得以簡化。
3.8.3 閉包
[建議] 在适當的時候将閉包内大對象置為 null
。
null
解釋:
在 JavaScript 中,無需特别的關鍵詞就可以使用閉包,一個函數可以任意通路在其定義的作用域外的變量。需要注意的是,函數的作用域是靜态的,即在定義時決定,與調用的時機和方式沒有任何關系。
閉包會阻止一些變量的垃圾回收,對于較老舊的 JavaScript 引擎,可能導緻外部所有變量均無法回收。
首先一個較為明确的結論是,以下内容會影響到閉包内變量的回收:
- 嵌套的函數中是否有使用該變量。
- 嵌套的函數中是否有 直接調用eval。
- 是否使用了 with 表達式。
Chakra、V8 和 SpiderMonkey 将受以上因素的影響,表現出不盡相同又較為相似的回收政策,而 JScript.dll 和 Carakan 則完全沒有這方面的優化,會完整保留整個 LexicalEnvironment 中的所有變量綁定,造成一定的記憶體消耗。
由于對閉包内變量有回收優化政策的 Chakra、V8 和 SpiderMonkey 引擎的行為較為相似,是以可以總結如下,當傳回一個函數 fn 時:
- 如果 fn 的
是 ObjectEnvironment(with 表達式生成 ObjectEnvironment,函數和 catch 表達式生成 DeclarativeEnvironment),則:[[Scope]]
- 如果是 V8 引擎,則退出全過程。
- 如果是 SpiderMonkey,則處理該 ObjectEnvironment 的外層 LexicalEnvironment。
- 擷取目前 LexicalEnvironment 下的所有類型為 Function 的對象,對于每一個 Function 對象,分析其 FunctionBody:
- 如果 FunctionBody 中含有 直接調用 eval,則退出全過程。
- 否則得到所有的 Identifier。
- 對于每一個 Identifier,設其為 name,根據查找變量引用的規則,從 LexicalEnvironment 中找出名稱為 name 的綁定 binding。
- 對 binding 添加 notSwap 屬性,其值為
。true
- 檢查目前 LexicalEnvironment 中的每一個變量綁定,如果該綁定有 notSwap 屬性且值為
,則:true
- 如果是 V8 引擎,删除該綁定。
- 如果是 SpiderMonkey,将該綁定的值設為
,将删除 notSwap 屬性。undefined
對于 Chakra 引擎,暫無法得知是按 V8 的模式還是按 SpiderMonkey 的模式進行。
如果有 非常龐大 的對象,且預計會在 老舊的引擎 中執行,則使用閉包時,注意将閉包不需要的對象置為空引用。
[建議] 使用 IIFE
避免 Lift 效應
。
IIFE
Lift 效應
解釋:
在引用函數外部變量時,函數執行時外部變量的值由運作時決定而非定義時,最典型的場景如下:
var tasks = [];
for (var i = 0; i < 5; i++) {
tasks[tasks.length] = function () {
console.log('Current cursor is at ' + i);
};
}
var len = tasks.length;
while (len--) {
tasks[len]();
}
以上代碼對 tasks 中的函數的執行均會輸出
Current cursor is at 5
,往往不符合預期。
此現象稱為 Lift 效應 。解決的方式是通過額外加上一層閉包函數,将需要的外部變量作為參數傳遞來解除變量的綁定關系:
var tasks = [];
for (var i = 0; i < 5; i++) {
// 注意有一層額外的閉包
tasks[tasks.length] = (function (i) {
return function () {
console.log('Current cursor is at ' + i);
};
})(i);
}
var len = tasks.length;
while (len--) {
tasks[len]();
}
3.8.4 空函數
[建議] 空函數不使用 new Function()
的形式。
new Function()
示例:
[建議] 對于性能有高要求的場合,建議存在一個空函數的常量,供多處使用共享。
示例:
var EMPTY_FUNCTION = function () {};
function MyClass() {
}
MyClass.prototype.abstractMethod = EMPTY_FUNCTION;
MyClass.prototype.hooks.before = EMPTY_FUNCTION;
MyClass.prototype.hooks.after = EMPTY_FUNCTION;
3.9 面向對象
[強制] 類的繼承方案,實作時需要修正 constructor
。
constructor
解釋:
通常使用其他 library 的類繼承方案都會進行
constructor
修正。如果是自己實作的類繼承方案,需要進行
constructor
修正。
示例:
/**
* 建構類之間的繼承關系
*
* @param {Function} subClass 子類函數
* @param {Function} superClass 父類函數
*/
function inherits(subClass, superClass) {
var F = new Function();
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}
[建議] 聲明類時,保證 constructor
的正确性。
constructor
示例:
function Animal(name) {
this.name = name;
}
// 直接prototype等于對象時,需要修正constructor
Animal.prototype = {
constructor: Animal,
jump: function () {
alert('animal ' + this.name + ' jump');
}
};
// 這種方式擴充prototype則無需理會constructor
Animal.prototype.jump = function () {
alert('animal ' + this.name + ' jump');
};
[建議] 屬性在構造函數中聲明,方法在原型中聲明。
解釋:
原型對象的成員被所有執行個體共享,能節約記憶體占用。是以編碼時我們應該遵守這樣的原則:原型對象包含程式不會修改的成員,如方法函數或配置項。
function TextNode(value, engine) {
this.value = value;
this.engine = engine;
}
TextNode.prototype.clone = function () {
return this;
};
[強制] 自定義事件的 事件名
必須全小寫。
事件名
解釋:
在 JavaScript 廣泛應用的浏覽器環境,絕大多數 DOM 事件名稱都是全小寫的。為了遵循大多數 JavaScript 開發者的習慣,在設計自定義事件時,事件名也應該全小寫。
[強制] 自定義事件隻能有一個 event
參數。如果事件需要傳遞較多資訊,應仔細設計事件對象。
event
解釋:
一個事件對象的好處有:
- 順序無關,避免事件監聽者需要記憶參數順序。
- 每個事件資訊都可以根據需要提供或者不提供,更自由。
- 擴充友善,未來添加事件資訊時,無需考慮會破壞監聽器參數形式而無法向後相容。
[建議] 設計自定義事件時,應考慮禁止預設行為。
解釋:
常見禁止預設行為的方式有兩種:
- 事件監聽函數中
。return false
- 事件對象中包含禁止預設行為的方法,如
。preventDefault
3.10 動态特性
3.10.1 eval
[強制] 避免使用直接 eval
函數。
eval
解釋:
直接
eval
,指的是以函數方式調用
eval
的調用方法。直接
eval
調用執行代碼的作用域為本地作用域,應當避免。
如果有特殊情況需要使用直接
eval
,需在代碼中用詳細的注釋說明為何必須使用直接
eval
,不能使用其它動态執行代碼的方式,同時需要其他資深工程師進行 Code Review。
[建議] 盡量避免使用 eval
函數。
eval
3.10.2 動态執行代碼
[建議] 使用 new Function
執行動态代碼。
new Function
解釋:
通過
new Function
生成的函數作用域是全局使用域,不會影響當目前的本地作用域。如果有動态代碼執行的需求,建議使用
new Function
。
示例:
var handler = new Function('x', 'y', 'return x + y;');
var result = handler($('#x').val(), $('#y').val());
3.10.3 with
[建議] 盡量不要使用 with
。
with
解釋:
使用
with
可能會增加代碼的複雜度,不利于閱讀和管理;也會對性能有影響。大多數使用
with
的場景都能使用其他方式較好的替代。是以,盡量不要使用
with
。
3.10.4 delete
[建議] 減少 delete
的使用。
delete
解釋:
如果沒有特别的需求,減少或避免使用
delete
。
delete
的使用會破壞部分 JavaScript 引擎的性能優化。
[建議] 處理 delete
可能産生的異常。
delete
解釋:
對于有被周遊需求,且值
null
被認為具有業務邏輯意義的值的對象,移除某個屬性必須使用
delete
操作。
在嚴格模式或 IE 下使用
delete
時,不能被删除的屬性會抛出異常,是以在不确定屬性是否可以删除的情況下,建議添加
try-catch
塊。
示例:
try {
delete o.x;
}
catch (deleteError) {
o.x = null;
}
3.10.5 對象屬性
[建議] 避免修改外部傳入的對象。
解釋:
JavaScript 因其腳本語言的動态特性,當一個對象未被 seal 或 freeze 時,可以任意添加、删除、修改屬性值。
但是随意地對 非自身控制的對象 進行修改,很容易造成代碼在不可預知的情況下出現問題。是以,設計良好的元件、函數應該避免對外部傳入的對象的修改。
下面代碼的 selectNode 方法修改了由外部傳入的 datasource 對象。如果 datasource 用在其它場合(如另一個 Tree 執行個體)下,會造成狀态的混亂。
function Tree(datasource) {
this.datasource = datasource;
}
Tree.prototype.selectNode = function (id) {
// 從datasource中找出節點對象
var node = this.findNode(id);
if (node) {
node.selected = true;
this.flushView();
}
};
對于此類場景,需要使用額外的對象來維護,使用由自身控制,不與外部産生任何互動的 selectedNodeIndex 對象來維護節點的選中狀态,不對 datasource 作任何修改。
function Tree(datasource) {
this.datasource = datasource;
this.selectedNodeIndex = {};
}
Tree.prototype.selectNode = function (id) {
// 從datasource中找出節點對象
var node = this.findNode(id);
if (node) {
this.selectedNodeIndex[id] = true;
this.flushView();
}
};
除此之外,也可以通過 deepClone 等手段将自身維護的對象與外部傳入的分離,保證不會互相影響。
[建議] 具備強類型的設計。
解釋:
- 如果一個屬性被設計為
類型,則不要使用boolean
或 作為其值。對于辨別性的屬性,如對代碼體積有嚴格要求,可以從一開始就設計為1
類型且将 作為否定值。number
- 從 DOM 中取出的值通常為
類型,如果有對象或函數的接收類型為string
類型,提前作好轉換,而不是期望對象、函數可以處理多類型的值。number
4 浏覽器環境
4.1 子產品化
4.1.1 AMD
[強制] 使用 AMD
作為子產品定義。
AMD
解釋:
AMD 作為由社群認可的子產品定義形式,提供多種重載提供靈活的使用方式,并且絕大多數優秀的 Library 都支援 AMD,适合作為規範。
目前,比較成熟的 AMD Loader 有:
- 官方實作的 requirejs
- 百度自己實作的 esl
[強制] 子產品 id
必須符合标準。
id
解釋:
子產品 id 必須符合以下限制條件:
- 類型為 string,并且是由
分割的一系列 terms 來組成。例如:/
。this/is/a/module
- term 應該符合 [a-zA-Z0-9_-:]+ 規則。
- 不應該有 .js 字尾。
- 跟檔案的路徑保持一緻。
4.1.2 define
[建議] 定義子產品時不要指明 id
和 dependencies
。
id
dependencies
解釋:
在 AMD 的設計思想裡,子產品名稱是和所在路徑相關的,匿名的子產品更利于封包和遷移。子產品依賴應在子產品定義内部通過
local require
引用。
是以,推薦使用
define(factory)
的形式進行子產品定義。
示例:
define(
function (require) {
}
);
[建議] 使用 return
來傳回子產品定義。
return
解釋:
使用 return 可以減少 factory 接收的參數(不需要接收 exports 和 module),在沒有 AMD Loader 的場景下也更容易進行簡單的處理來僞造一個 Loader。
示例:
define(
function (require) {
var exports = {};
// ...
return exports;
}
);
4.1.3 require
[強制] 全局運作環境中, require
必須以 async require
形式調用。
require
async require
解釋:
子產品的加載過程是異步的,同步調用并無法保證得到正确的結果。
示例:
// good
require(['foo'], function (foo) {
});
// bad
var foo = require('foo');
[強制] 子產品定義中隻允許使用 local require
,不允許使用 global require
。
local require
global require
解釋:
- 在子產品定義中使用
,對封裝性是一種破壞。global require
- 在 AMD 裡,
是可以被重命名的。并且 Loader 甚至沒有全局的global require
變量,而是用 Loader 名稱做為require
。子產品定義不應該依賴使用的 Loader。global require
[強制] Package 在實作時,内部子產品的 require
必須使用 relative id
。
require
relative id
解釋:
對于任何可能通過 釋出-引入 的形式複用的第三方庫、架構、包,開發者所定義的名稱不代表使用者使用的名稱。是以不要基于任何名稱的假設。在實作源碼中,
require
自身的其它子產品時使用
relative id
。
示例:
define(
function (require) {
var util = require('./util');
}
);
[建議] 不會被調用的依賴子產品,在 factory
開始處統一 require
。
factory
require
解釋:
有些子產品是依賴的子產品,但不會在子產品實作中被直接調用,最為典型的是
css
/
js
/
tpl
等 Plugin 所引入的外部内容。此類内容建議放在子產品定義最開始處統一引用。
示例:
define(
function (require) {
require('css!foo.css');
require('tpl!bar.tpl.html');
// ...
}
);
4.2 DOM
4.2.1 元素擷取
[建議] 對于單個元素,盡可能使用 document.getElementById
擷取,避免使用 document.all
。
document.getElementById
document.all
[建議] 對于多個元素的集合,盡可能使用 context.getElementsByTagName
擷取。其中 context
可以為 document
或其他元素。指定 tagName
參數為 *
可以獲得所有子元素。
context.getElementsByTagName
context
document
tagName
*
[建議] 周遊元素集合時,盡量緩存集合長度。如需多次操作同一集合,則應将集合轉為數組。
解釋:
原生擷取元素集合的結果并不直接引用 DOM 元素,而是對索引進行讀取,是以 DOM 結構的改變會實時反映到結果中。
示例:
<div></div>
<span></span>
<script>
var elements = document.getElementsByTagName('*');
// 顯示為 DIV
alert(elements[0].tagName);
var div = elements[0];
var p = document.createElement('p');
docpment.body.insertBefore(p, div);
// 顯示為 P
alert(elements[0].tagName);
</script>
[建議] 擷取元素的直接子元素時使用 children
。避免使用 childNodes
,除非預期是需要包含文本、注釋和屬性類型的節點。
children
childNodes
4.2.2 樣式擷取
[建議] 擷取元素實際樣式資訊時,應使用 getComputedStyle
或 currentStyle
。
getComputedStyle
currentStyle
解釋:
通過 style 隻能獲得内聯定義或通過 JavaScript 直接設定的樣式。通過 CSS class 設定的元素樣式無法直接通過 style 擷取。
4.2.3 樣式設定
[建議] 盡可能通過為元素添加預定義的 className 來改變元素樣式,避免直接操作 style 設定。
[強制] 通過 style 對象設定元素樣式時,對于帶機關非 0 值的屬性,不允許省略機關。
解釋:
除了 IE,标準浏覽器會忽略不規範的屬性值,導緻相容性問題。
4.2.4 DOM 操作
[建議] 操作 DOM
時,盡量減少頁面 reflow
。
DOM
reflow
解釋:
頁面 reflow 是非常耗時的行為,非常容易導緻性能瓶頸。下面一些場景會觸發浏覽器的reflow:
- DOM元素的添加、修改(内容)、删除。
- 應用新的樣式或者修改任何影響元素布局的屬性。
- Resize浏覽器視窗、滾動頁面。
- 讀取元素的某些屬性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 。
[建議] 盡量減少 DOM
操作。
DOM
解釋:
DOM 操作也是非常耗時的一種操作,減少 DOM 操作有助于提高性能。舉一個簡單的例子,建構一個清單。我們可以用兩種方式:
- 在循環體中 createElement 并 append 到父元素中。
- 在循環體中拼接 HTML 字元串,循環結束後寫父元素的 innerHTML。
第一種方法看起來比較标準,但是每次循環都會對 DOM 進行操作,性能極低。在這裡推薦使用第二種方法。
4.2.5 DOM 事件
[建議] 優先使用 addEventListener / attachEvent
綁定事件,避免直接在 HTML 屬性中或 DOM 的 expando
屬性綁定事件處理。
addEventListener / attachEvent
expando
解釋:
expando 屬性綁定事件容易導緻互相覆寫。
[建議] 使用 addEventListener
時第三個參數使用 false
。
addEventListener
false
解釋:
标準浏覽器中的 addEventListener 可以通過第三個參數指定兩種時間觸發模型:冒泡和捕獲。而 IE 的 attachEvent 僅支援冒泡的事件觸發。是以為了保持一緻性,通常 addEventListener 的第三個參數都為 false。