1 前言
JavaScript在百度一直有着廣泛的應用,特別是在瀏覽器端的行為管理。本文檔的目標是使JavaScript代碼風格保持一致,容易被理解和被維護。
雖然本文檔是針對JavaScript設計的,但是在使用各種JavaScript的預編譯語言時(如TypeScript等)時,適用的部分也應盡量遵循本文檔的約定。
2 代碼風格
1. 結構
-
[強制] 使用 4 個空格做為一個縮進層級,不允許使用 2 個空格 或 tab 字符。 -
[強制] switch 下的 case 和 default 必須增加一個縮進層級。
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 關鍵字后,必須有一個空格。
示例:
// 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 等語句中,() 和 [] 內緊貼括號部分不允許有空格。
// 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};
[強制] 行尾不得有多余的空格。
3. 換行
[強制] 每個獨立語句結束后必須換行。
[強制] 每行不得超過 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語句等場景中,不允許在 , 或 ; 前換行。
示例:
// 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 時,根據邏輯條件合理縮進。
示例:
// 較復雜的邏輯條件組合,將每個條件獨立一行,邏輯運算符放置在行首進行分隔,或將部分邏輯按邏輯組合進行分隔。
// 建議最終將右括號 ) 與左大括號 { 放在獨立一行,保證與 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 (condition) {
// some statements;
}
else {
// some statements;
}
try {
// some statements;
}
catch (ex) {
// some statements;
}
4. 語句
[強制] 不得省略語句結束的分號。
[強制] 在 if / else / for / do / while 語句中,即使只有一行,也不得省略塊 {...}。
示例:
// good
if (condition) {
callFunc();
}
// bad
if (condition) callFunc();
if (condition)
callFunc();
[強制] 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 () {
});
3 語言特性
1. 結構
[強制] 變量在使用前必須通過 var 定義。
解釋:
不通過 var 定義變量將導致變量污染全局環境。
示例:
// good
var name = 'MyName';
// bad
name = 'MyName';
[強制] 每個 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;
}
2. 條件
- 對象 被計算為 true
- Undefined 被計算為 false
- Null 被計算為 false
- 布爾值 被計算為 布爾的值
- 數字 如果是 +0、-0 或 NaN 被計算為 false,否則為 true
- 字符串 如果是空字符串 '' 被計算為 false,否則為 true
[強制] 在 Equality Expression 中使用類型嚴格的 ===。僅當判斷 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。
示例:
// 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。
示例:
// good
function getName() {
if (name) {
return name;
}
return 'unnamed';
}
// bad
function getName() {
if (name) {
return name;
}
else {
return 'unnamed';
}
}
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 能有效提高程序性能。
示例:
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
// ......
}
[建議] 對有序集合進行順序無關的遍歷時,使用逆序遍歷。
解釋:
逆序遍歷可以節省變量,代碼比較優化。
示例:
var len = elements.length;
while (len--) {
var element = elements[len];
// ......
}
4.塊
- 使用大括號包裹所有的多行代碼塊。
/bad
if (test)
return false;
// good
if (test) return false;
// good
if (test) {
return false;
}
// bad
function () { return false; }
// good
function () {
return false;
}
- 如果通過 if 和 else 使用多行代碼塊,把 else 放在 if 代碼塊關閉括號的同一行。
// bad
if (test) {
thing1();
thing2();
}
else {
thing3();
}
// good
if (test) {
thing1();
thing2();
} else {
thing3();
}
4. 類型
4.1 類型檢測
[建議] 類型檢測優先使用 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'
4.2 類型轉換
[建議] 轉換成 string 時,使用 + ''。
示例:
// good
num + '';
// bad
new String(num);
num.toString();
String(num);
[建議] 轉換成 number 時,通常使用 +。
示例:
// good
+str;
// bad
Number(str);
[建議] string 轉換成 number,要轉換的字符串結尾包含非數字並期望忽略時,使用 parseInt。
示例:
var width = '200px';
parseInt(width, 10);
[建議] 使用 parseInt 時,必須指定進制。
示例:
// good
parseInt(str, 10);
// bad
parseInt(str);
[建議] 轉換成 boolean 時,使用 !!。
示例:
var num = 3.14;
!!num;
[建議] number 去除小數點,使用 Math.floor / Math.round / Math.ceil,不使用 parseInt。
示例:
// good
var num = 3.14;
Math.ceil(num);
// bad
var num = 3.14;
parseInt(num, 10);
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>';
[建議] 復雜的數據到視圖字符串的轉換過程,選用一種模板引擎。
解釋:
使用模板引擎有如下好處:
- 在開發過程中專注於數據,將視圖生成的過程由另外一個層級維護,使程序邏輯結構更清晰。
- 優秀的模板引擎,通過模板編譯技術和高質量的編譯產物,能獲得比手工拼接字符串更高的性能。
- artTemplate: 體積較小,在所有環境下性能高,語法靈活。
- dot.js: 體積小,在現代瀏覽器下性能高,語法靈活。
- etpl: 體積較小,在所有環境下性能高,模板復用性高,語法靈活。
- handlebars: 體積大,在所有環境下性能高,擴展性高。
- hogon: 體積小,在現代瀏覽器下性能高。
- nunjucks: 體積較大,性能一般,模板復用性高。
6. 對象
[建議] 使用對象字面量 {} 創建新 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 過濾掉原型中的屬性。
示例:
var newInfo = {};
for (var key in info) {
if (info.hasOwnProperty(key)) {
newInfo[key] = info[key];
}
}
7. 數組
[強制] 使用數組字面量 [] 創建新數組,除非想要創建的是指定長度的數組。
示例:
// good
var arr = [];
// bad
var arr = new Array();
[強制] 遍歷數組不使用 for in。
解釋:
數組對象可能存在數字以外的屬性, 這種情況下 for in 不會得到正確結果.
示例:
var arr = ['a', 'b', 'c'];
arr.other = 'other things'; // 這里僅作演示, 實際中應使用Object類型
// 正確的遍歷方式
for (var i = 0, len = arr.length; i < len; i++) {
console.log(i);
}
// 錯誤的遍歷方式
for (i in arr) {
console.log(i);
}
[建議] 不因為性能的原因自己實現數組排序功能,盡量使用數組的 sort 方法。
解釋:
自己實現的常規排序算法,在性能上並不優於數組默認的 sort 方法。以下兩種場景可以自己實現排序:
需要穩定的排序算法,達到嚴格一致的排序結果。
數據特點鮮明,適合使用桶排。
[建議] 清空數組使用 .length = 0。
8. 函數
8.1 函數長度
[建議] 一個函數的長度控制在 50 行以內。
解釋:
將過多的邏輯單元混在一個大函數中,易導致難以維護。一個清晰易懂的函數應該完成單一的邏輯單元。復雜的操作應進一步抽取,通過函數的調用來體現流程。
特定算法等不可分割的邏輯允許例外。
示例:
function syncViewStateOnUserAction() {
if (x.checked) {
y.checked = true;
z.value = '';
}
else {
y.checked = false;
}
if (!a.value) {
warning.innerText = 'Please enter it';
submitButton.disabled = true;
}
else {
warning.innerText = '';
submitButton.disabled = false;
}
}
// 直接閱讀該函數會難以明確其主線邏輯,因此下方是一種更合理的表達方式:
function syncViewStateOnUserAction() {
syncXStateToView();
checkAAvailability();
}
function syncXStateToView() {
if (x.checked) {
y.checked = true;
z.value = '';
}
else {
y.checked = false;
}
}
function checkAAvailability() {
if (!a.value) {
displayWarningForAMissing();
}
else {
clearWarnignForA();
}
}
8.2 參數設計
[建議] 一個函數的參數控制在 6 個以內。
解釋:
除去不定長參數以外,函數具備不同邏輯意義的參數建議控制在 6 個以內,過多參數會導致維護難度增大。
某些情況下,如使用 AMD Loader 的 require 加載多個模塊時,其 callback 可能會存在較多參數,因此對函數參數的個數不做強制限制。
[建議] 通過 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 對象只需判斷屬性是否存在,實現得以簡化。
8.3 閉包
[建議] 在適當的時候將閉包內大對象置為 null。
解釋:
在 JavaScript 中,無需特別的關鍵詞就可以使用閉包,一個函數可以任意訪問在其定義的作用域外的變量。需要注意的是,函數的作用域是靜態的,即在定義時決定,與調用的時機和方式沒有任何關系。
閉包會阻止一些變量的垃圾回收,對於較老舊的JavaScript引擎,可能導致外部所有變量均無法回收。
首先一個較為明確的結論是,以下內容會影響到閉包內變量的回收:
- 嵌套的函數中是否有使用該變量。
- 嵌套的函數中是否有 直接調用eval。
- 是否使用了 with 表達式。
Chakra、V8 和 SpiderMonkey 將受以上因素的影響,表現出不盡相同又較為相似的回收策略,而JScript.dll和Carakan則完全沒有這方面的優化,會完整保留整個 LexicalEnvironment 中的所有變量綁定,造成一定的內存消耗。
由於對閉包內變量有回收優化策略的 Chakra、V8 和 SpiderMonkey 引擎的行為較為相似,因此可以總結如下,當返回一個函數 fn 時:
- 如果 fn 的 [[Scope]] 是ObjectEnvironment(with 表達式生成 ObjectEnvironment,函數和 catch 表達式生成 DeclarativeEnvironment),則:
- 如果是 V8 引擎,則退出全過程。
- 如果是 SpiderMonkey,則處理該 ObjectEnvironment 的外層 LexicalEnvironment。
- 獲取當前 LexicalEnvironment 下的所有類型為 Function 的對象,對於每一個 Function 對象,分析其 FunctionBody:
- 如果 FunctionBody 中含有 直接調用eval,則退出全過程。
- 否則得到所有的 Identifier。
- 對於每一個 Identifier,設其為 name,根據查找變量引用的規則,從 LexicalEnvironment 中找出名稱為 name 的綁定 binding。
- 對 binding 添加 notSwap 屬性,其值為 true。
- 獲取當前 LexicalEnvironment 下的所有類型為 Function 的對象,對於每一個 Function 對象,分析其 FunctionBody
- 如果是V8引擎,刪除該綁定。
- 如果是SpiderMonkey,將該綁定的值設為 undefined,將刪除 notSwap 屬性。
對於Chakra引擎,暫無法得知是按 V8 的模式還是按 SpiderMonkey 的模式進行。
如果有 非常龐大 的對象,且預計會在 老舊的引擎 中執行,則使用閉包時,注意將閉包不需要的對象置為空引用。
[建議] 使用 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]();
}
8.4 空函數
[建議] 空函數不使用 new Function() 的形式。
示例:
var emptyFunction = function () {};
[建議] 對於性能有高要求的場合,建議存在一個空函數的常量,供多處使用共享。
示例:
var EMPTY_FUNCTION = function () {};
function MyClass() {
}
MyClass.prototype.abstractMethod = EMPTY_FUNCTION;
MyClass.prototype.hooks.before = EMPTY_FUNCTION;
MyClass.prototype.hooks.after = EMPTY_FUNCTION;
9. 面向對象
[強制] 類的繼承方案,實現時需要修正 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 的正確性。
示例:
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
參數。如果事件需要傳遞較多信息,應仔細設計事件對象。
解釋:
一個事件對象的好處有:
- 順序無關,避免事件監聽者需要記憶參數順序。
- 每個事件信息都可以根據需要提供或者不提供,更自由。
- 擴展方便,未來添加事件信息時,無需考慮會破壞監聽器參數形式而無法向后兼容。
[建議] 設計自定義事件時,應考慮禁止默認行為。
解釋:
常見禁止默認行為的方式有兩種:
- 事件監聽函數中 return false。
- 事件對象中包含禁止默認行為的方法,如 preventDefault。
10. 動態特性
10.1 eval
[強制] 避免使用直接 eval 函數。
解釋:
直接 eval,指的是以函數方式調用 eval 的調用方法。直接 eval 調用執行代碼的作用域為本地作用域,應當避免。
如果有特殊情況需要使用直接 eval,需在代碼中用詳細的注釋說明為何必須使用直接 eval,不能使用其它動態執行代碼的方式,同時需要其他資深工程師進行 Code Review。
[建議] 盡量避免使用 eval 函數。
10.2 動態執行代碼
[建議] 使用 new Function 執行動態代碼。
解釋:
通過 new Function 生成的函數作用域是全局使用域,不會影響當當前的本地作用域。如果有動態代碼執行的需求,建議使用 new Function。
示例:
var handler = new Function('x', 'y', 'return x + y;');
var result = handler($('#x').val(), $('#y').val());
10.3 with
[建議] 盡量不要使用 with。
解釋:
使用 with 可能會增加代碼的復雜度,不利於閱讀和管理;也會對性能有影響。大多數使用 with 的場景都能使用其他方式較好的替代。所以,盡量不要使用 with。
10.4 delete
[建議] 減少 delete 的使用。
解釋:
如果沒有特別的需求,減少或避免使用delete。delete的使用會破壞部分 JavaScript 引擎的性能優化。
[建議] 處理 delete 可能產生的異常。
解釋:
對於有被遍歷需求,且值 null 被認為具有業務邏輯意義的值的對象,移除某個屬性必須使用 delete 操作。
在嚴格模式或IE下使用 delete 時,不能被刪除的屬性會拋出異常,因此在不確定屬性是否可以刪除的情況下,建議添加 try-catch 塊。
示例:
try {
delete o.x;
}
catch (deleteError) {
o.x = null;
}。
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 / 0 作為其值。對於標識性的屬性,如對代碼體積有嚴格要求,可以從一開始就設計為 number 類型且將 0 作為否定值。
- 從 DOM 中取出的值通常為 string 類型,如果有對象或函數的接收類型為 number 類型,提前作好轉換,而不是期望對象、函數可以處理多類型的值。
4.瀏覽器環境
4.1 模塊化
4.1.1 AMD
[強制] 使用 AMD 作為模塊定義。
解釋:
AMD 作為由社區認可的模塊定義形式,提供多種重載提供靈活的使用方式,並且絕大多數優秀的 Library 都支持 AMD,適合作為規范。
目前,比較成熟的 AMD Loader 有:
[強制] 模塊 id 必須符合標准。
解釋:
模塊 id 必須符合以下約束條件:
- 類型為 string,並且是由 / 分割的一系列 terms 來組成。例如:this/is/a/module。
- term 應該符合 [a-zA-Z0-9_-]+ 規則。
- 不應該有 .js 后綴。
- 跟文件的路徑保持一致。
4.1.2 define
[建議] 定義模塊時不要指明 id 和 dependencies。
解釋:
在 AMD 的設計思想里,模塊名稱是和所在路徑相關的,匿名的模塊更利於封包和遷移。模塊依賴應在模塊定義內部通過 local require 引用。
所以,推薦使用 define(factory) 的形式進行模塊定義。
示例:
define(
function (require) {
}
);
[建議] 使用 return 來返回模塊定義。
解釋:
使用 return 可以減少 factory 接收的參數(不需要接收 exports 和 module),在沒有 AMD Loader 的場景下也更容易進行簡單的處理來偽造一個 Loader。
示例:
define(
function (require) {
var exports = {};
// ...
return exports;
}
);
4.1.3 require
[強制] 全局運行環境中,require 必須以 async require 形式調用。
解釋:
模塊的加載過程是異步的,同步調用並無法保證得到正確的結果。
示例:
// good
require(['foo'], function (foo) {
});
// bad
var foo = require('foo');
[強制] 模塊定義中只允許使用 local require,不允許使用 global require。
解釋:
- 在模塊定義中使用 global require,對封裝性是一種破壞。
- 在 AMD 里,global require 是可以被重命名的。並且 Loader 甚至沒有全局的 require 變量,而是用 Loader 名稱做為 global require。模塊定義不應該依賴使用的 Loader。
[強制] Package在實現時,內部模塊的 require 必須使用 relative id。
解釋:
對於任何可能通過 發布-引入 的形式復用的第三方庫、框架、包,開發者所定義的名稱不代表使用者使用的名稱。因此不要基於任何名稱的假設。在實現源碼中,require 自身的其它模塊時使用 relative id。
示例:
define(
function (require) {
var util = require('./util');
}
);
[建議] 不會被調用的依賴模塊,在 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。
[建議] 對於多個元素的集合,盡可能使用 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');
document.body.insertBefore(p, div);
// 顯示為 P
alert(elements[0].tagName);
</script>
[建議] 獲取元素的直接子元素時使用 children。避免使用childNodes,除非預期是需要包含文本、注釋和屬性類型的節點。
4.2.2 樣式獲取
[建議] 獲取元素實際樣式信息時,應使用 getComputedStyle 或 currentStyle。
解釋:
通過 style 只能獲得內聯定義或通過 JavaScript 直接設置的樣式。通過 CSS class 設置的元素樣式無法直接通過 style 獲取。
4.2.3 樣式設置
[建議] 盡可能通過為元素添加預定義的 className 來改變元素樣式,避免直接操作 style 設置。
[強制] 通過 style 對象設置元素樣式時,對於帶單位非 0 值的屬性,不允許省略單位。
解釋:
除了 IE,標准瀏覽器會忽略不規范的屬性值,導致兼容性問題。
4.2.4 DOM 操作
[建議] 操作 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 操作有助於提高性能。舉一個簡單的例子,構建一個列表。我們可以用兩種方式:
- 在循環體中 createElement 並 append 到父元素中。
- 在循環體中拼接 HTML 字符串,循環結束后寫父元素的 innerHTML。
第一種方法看起來比較標准,但是每次循環都會對 DOM 進行操作,性能極低。在這里推薦使用第二種方法。
4.2.5 DOM 事件
[建議] 優先使用 addEventListener / attachEvent 綁定事件,避免直接在 HTML 屬性中或 DOM 的 expando 屬性綁定事件處理。
解釋:
expando 屬性綁定事件容易導致互相覆蓋。
[建議] 使用 addEventListener 時第三個參數使用 false。
解釋:
標准瀏覽器中的 addEventListener 可以通過第三個參數指定兩種時間觸發模型:冒泡和捕獲。而 IE 的 attachEvent 僅支持冒泡的事件觸發。所以為了保持一致性,通常 addEventListener 的第三個參數都為 false。
[建議] 在沒有事件自動管理的框架支持下,應持有監聽器函數的引用,在適當時候(元素釋放、頁面卸載等)移除添加的監聽器。
