JavaScript編碼規(guī)范
[2 代碼風(fēng)格]
[2.1 文件]
[2.2 結(jié)構(gòu)]
[2.2.1 縮進(jìn)]
1 前言
JavaScript 在百度一直有著廣泛的應(yīng)用,特別是在瀏覽器端的行為管理。本文檔的目標(biāo)是使 JavaScript 代碼風(fēng)格保持一致,容易被理解和被維護(hù)。
雖然本文檔是針對(duì) JavaScript 設(shè)計(jì)的,但是在使用各種 JavaScript 的預(yù)編譯語言時(shí)(如 TypeScript 等)時(shí),適用的部分也應(yīng)盡量遵循本文檔的約定。
2 代碼風(fēng)格
2.1 文件
[建議] JavaScript 文件使用無 BOM
的 UTF-8
編碼。
解釋:
UTF-8 編碼具有更廣泛的適應(yīng)性。BOM 在使用程序或工具處理文件時(shí)可能造成不必要的干擾。
[建議] 在文件結(jié)尾處,保留一個(gè)空行。
2.2 結(jié)構(gòu)
2.2.1 縮進(jìn)
[強(qiáng)制] 使用 4
個(gè)空格做為一個(gè)縮進(jìn)層級(jí),不允許使用 2
個(gè)空格 或 tab
字符。
[強(qiáng)制] switch
下的 case
和 default
必須增加一個(gè)縮進(jìn)層級(jí)。
示例:
// 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 空格
[強(qiáng)制] 二元運(yùn)算符兩側(cè)必須有一個(gè)空格,一元運(yùn)算符與操作對(duì)象之間不允許有空格。
示例:
var a = !arr.length;
a++;
a = b + c;
[強(qiáng)制] 用作代碼塊起始的左花括號(hào) {
前必須有一個(gè)空格。
示例:
// good
if (condition) {
}
while (condition) {
}
function funcName() {
}
// bad
if (condition){
}
while (condition){
}
function funcName(){
}
[強(qiáng)制] if / else / for / while / function / switch / do / try / catch / finally
關(guān)鍵字后,必須有一個(gè)空格。
示例:
// good
if (condition) {
}
while (condition) {
}
(function () {
})();
// bad
if(condition) {
}
while(condition) {
}
(function() {
})();
[強(qiáng)制] 在對(duì)象創(chuàng)建時(shí),屬性中的 :
之后必須有空格,:
之前不允許有空格。
示例:
// good
var obj = {
a: 1,
b: 2,
c: 3
};
// bad
var obj = {
a : 1,
b:2,
c :3
};
[強(qiáng)制] 函數(shù)聲明、具名函數(shù)表達(dá)式、函數(shù)調(diào)用中,函數(shù)名和 (
之間不允許有空格。
示例:
// good
function funcName() {
}
var funcName = function funcName() {
};
funcName();
// bad
function funcName () {
}
var funcName = function funcName () {
};
funcName ();
[強(qiáng)制] ,
和 ;
前不允許有空格。如果不位于行尾,,
和 ;
后必須跟一個(gè)空格。
示例:
// good
callFunc(a, b);
// bad
callFunc(a , b) ;
[強(qiáng)制] 在函數(shù)調(diào)用、函數(shù)聲明、括號(hào)表達(dá)式、屬性訪問、if / for / while / switch / catch
等語句中,()
和 []
內(nèi)緊貼括號(hào)部分不允許有空格。
示例:
// 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-- ) {
}
[強(qiáng)制] 單行聲明的數(shù)組與對(duì)象,如果包含元素,{}
和 []
內(nèi)緊貼括號(hào)部分不允許包含空格。
解釋:
聲明包含元素的數(shù)組與對(duì)象,只有當(dāng)內(nèi)部元素的形式較為簡(jiǎn)單時(shí),才允許寫在一行。元素復(fù)雜的情況,還是應(yīng)該換行書寫。
示例:
// 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};
[強(qiáng)制] 行尾不得有多余的空格。
2.2.3 換行
[強(qiáng)制] 每個(gè)獨(dú)立語句結(jié)束后必須換行。
[強(qiáng)制] 每行不得超過 120
個(gè)字符。
解釋:
超長(zhǎng)的不可分割的代碼允許例外,比如復(fù)雜的正則表達(dá)式。長(zhǎng)字符串不在例外之列。
[強(qiáng)制] 運(yùn)算符處換行時(shí),運(yùn)算符必須在新行的行首。
示例:
// 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;
[強(qiáng)制] 在函數(shù)聲明、函數(shù)表達(dá)式、函數(shù)調(diào)用、對(duì)象創(chuàng)建、數(shù)組創(chuàng)建、for
語句等場(chǎng)景中,不允許在 ,
或 ;
前換行。
示例:
// 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的最優(yōu)實(shí)現(xiàn)
function setStyle(element, property, value) {
if (element == null) {
return;
}
element.style[property] = value;
}
[建議] 在語句的行長(zhǎng)度超過 120
時(shí),根據(jù)邏輯條件合理縮進(jìn)。
示例:
// 較復(fù)雜的邏輯條件組合,將每個(gè)條件獨(dú)立一行,邏輯運(yùn)算符放置在行首進(jìn)行分隔,或?qū)⒉糠诌壿嫲催壿嫿M合進(jìn)行分隔。
// 建議最終將右括號(hào) ) 與左大括號(hào) { 放在獨(dú)立一行,保證與 `if` 內(nèi)語句塊能容易視覺辨識(shí)。
if (user.isAuthenticated()
&& user.isInRole('admin')
&& user.hasAuthority('add-admin')
|| user.hasAuthority('delete-admin')
) {
// Code
}
// 按一定長(zhǎng)度截?cái)嘧址⑹褂?+ 運(yùn)算符進(jìn)行連接。
// 分隔字符串盡量按語義進(jìn)行,如不要在一個(gè)完整的名詞中間斷開。
// 特別的,對(duì)于 HTML 片段的拼接,通過縮進(jìn),保持和 HTML 相同的結(jié)構(gòu)。
var html = '' // 此處用一個(gè)空字符串,以便整個(gè) HTML 片段都在新行嚴(yán)格對(duì)齊
+ '<article>'
+ '<h1>Title here</h1>'
+ '<p>This is a paragraph</p>'
+ '<footer>Complete</footer>'
+ '</article>';
// 也可使用數(shù)組來進(jìn)行拼接,相對(duì) `+` 更容易調(diào)整縮進(jìn)。
var html = [
'<article>',
'<h1>Title here</h1>',
'<p>This is a paragraph</p>',
'<footer>Complete</footer>',
'</article>'
];
html = html.join('');
// 當(dāng)參數(shù)過多時(shí),將每個(gè)參數(shù)獨(dú)立寫在一行上,并將結(jié)束的右括號(hào) ) 獨(dú)立一行。
// 所有參數(shù)必須增加一個(gè)縮進(jìn)。
foo(
aVeryVeryLongArgument,
anotherVeryLongArgument,
callback
);
// 也可以按邏輯對(duì)參數(shù)進(jìn)行組合。
// 最經(jīng)典的是 baidu.format 函數(shù),調(diào)用時(shí)將參數(shù)分為“模板”和“數(shù)據(jù)”兩塊
baidu.format(
dateFormatTemplate,
year, month, date, hour, minute, second
);
// 當(dāng)函數(shù)調(diào)用時(shí),如果有一個(gè)或以上參數(shù)跨越多行,應(yīng)當(dāng)每一個(gè)參數(shù)獨(dú)立一行。
// 這通常出現(xiàn)在匿名函數(shù)或者對(duì)象初始化等作為參數(shù)時(shí),如 `setTimeout` 函數(shù)等。
setTimeout(
function () {
alert('hello');
},
200
);
order.data.read(
'id=' + me.model.id,
function (data) {
me.attchToModel(data.result);
callback();
},
300
);
// 鏈?zhǔn)秸{(diào)用較長(zhǎng)時(shí)采用縮進(jìn)進(jìn)行調(diào)整。
$('#items')
.find('.selected')
.highlight()
.end();
// 三元運(yùn)算符由3部分組成,因此其換行應(yīng)當(dāng)根據(jù)每個(gè)部分的長(zhǎng)度不同,形成不同的情況。
var result = thisIsAVeryVeryLongCondition
? resultA : resultB;
var result = condition
? thisIsAVeryVeryLongResult
: resultB;
// 數(shù)組和對(duì)象初始化的混用,嚴(yán)格按照每個(gè)對(duì)象的 `{` 和結(jié)束 `}` 在獨(dú)立一行的風(fēng)格書寫。
var array = [
{
// ...
},
{
// ...
}
];
[建議] 對(duì)于 if...else...
、try...catch...finally
等語句,推薦使用在 }
號(hào)后添加一個(gè)換行 的風(fēng)格,使代碼層次結(jié)構(gòu)更清晰,閱讀性更好。
示例:
if (condition) {
// some statements;
}
else {
// some statements;
}
try {
// some statements;
}
catch (ex) {
// some statements;
}
2.2.4 語句
[強(qiáng)制] 不得省略語句結(jié)束的分號(hào)。
[強(qiáng)制] 在 if / else / for / do / while
語句中,即使只有一行,也不得省略塊 {...}
。
示例:
// good
if (condition) {
callFunc();
}
// bad
if (condition) callFunc();
if (condition)
callFunc();
[強(qiáng)制] 函數(shù)定義結(jié)束不允許添加分號(hào)。
示例:
// good
function funcName() {
}
// bad
function funcName() {
};
// 如果是函數(shù)表達(dá)式,分號(hào)是不允許省略的。
var funcName = function () {
};
[強(qiáng)制] IIFE
必須在函數(shù)表達(dá)式外添加 (
,非 IIFE
不得在函數(shù)表達(dá)式外添加 (
。
解釋:
IIFE = Immediately-Invoked Function Expression.
額外的 ( 能夠讓代碼在閱讀的一開始就能判斷函數(shù)是否立即被調(diào)用,進(jìn)而明白接下來代碼的用途。而不是一直拖到底部才恍然大悟。
示例:
// good
var task = (function () {
// Code
return result;
})();
var func = function () {
};
// bad
var task = function () {
// Code
return result;
}();
var func = (function () {
});
2.3 命名
[強(qiáng)制] 變量
使用 Camel命名法
。
示例:
var loadingModules = {};
[強(qiáng)制] 常量
使用 全部字母大寫,單詞間下劃線分隔
的命名方式。
示例:
var HTML_ENTITY = {};
[強(qiáng)制] 函數(shù)
使用 Camel命名法
。
示例:
function stringFormat(source) {
}
[強(qiáng)制] 函數(shù)的 參數(shù)
使用 Camel命名法
。
示例:
function hear(theBells) {
}
[強(qiáng)制] 類
使用 Pascal命名法
。
示例:
function TextNode(options) {
}
[強(qiáng)制] 類的 方法
/ 屬性
使用 Camel命名法
。
示例:
function TextNode(value, engine) {
this.value = value;
this.engine = engine;
}
TextNode.prototype.clone = function () {
return this;
};
[強(qiáng)制] 枚舉變量
使用 Pascal命名法
,枚舉的屬性
使用 全部字母大寫,單詞間下劃線分隔
的命名方式。
示例:
var TargetState = {
READING: 1,
READED: 2,
APPLIED: 3,
READY: 4
};
[強(qiáng)制] 命名空間
使用 Camel命名法
。
示例:
equipments.heavyWeapons = {};
[強(qiáng)制] 由多個(gè)單詞組成的縮寫詞,在命名中,根據(jù)當(dāng)前命名法和出現(xiàn)的位置,所有字母的大小寫與首字母的大小寫保持一致。
示例:
function XMLParser() {
}
function insertHTML(element, html) {
}
var httpRequest = new HTTPRequest();
[強(qiáng)制] 類名
使用 名詞
。
示例:
function Engine(options) {
}
[建議] 函數(shù)名
使用 動(dòng)賓短語
。
示例:
function getStyle(element) {
}
[建議] boolean
類型的變量使用 is
或 has
開頭。
示例:
var isReady = false;
var hasMoreCommands = false;
[建議] Promise對(duì)象
用 動(dòng)賓短語的進(jìn)行時(shí)
表達(dá)。
示例:
var loadingData = ajax.get('url');
loadingData.then(callback);
2.4 注釋
2.4.1 單行注釋
[強(qiáng)制] 必須獨(dú)占一行。//
后跟一個(gè)空格,縮進(jìn)與下一行被注釋說明的代碼一致。
2.4.2 多行注釋
[建議] 避免使用 /*...*/
這樣的多行注釋。有多行注釋內(nèi)容時(shí),使用多個(gè)單行注釋。
2.4.3 文檔化注釋
[強(qiáng)制] 為了便于代碼閱讀和自文檔化,以下內(nèi)容必須包含以 /**...*/
形式的塊注釋中。
解釋:
- 文件
- namespace
- 類
- 函數(shù)或方法
- 類屬性
- 事件
- 全局變量
- 常量
- AMD 模塊
[強(qiáng)制] 文檔注釋前必須空一行。
[建議] 自文檔化的文檔說明 what,而不是 how。
2.4.4 類型定義
[強(qiáng)制] 類型定義都是以 {
開始, 以 }
結(jié)束。
解釋:
常用類型如:{string}, {number}, {boolean}, {Object}, {Function}, {RegExp}, {Array}, {Date}。
類型不僅局限于內(nèi)置的類型,也可以是自定義的類型。比如定義了一個(gè)類 Developer,就可以使用它來定義一個(gè)參數(shù)和返回值的類型。
[強(qiáng)制] 對(duì)于基本類型 {string}, {number}, {boolean},首字母必須小寫。
類型定義 | 語法示例 | 解釋 |
---|---|---|
String | {string} | -- |
Number | {number} | -- |
Boolean | {boolean} | -- |
Object | {Object} | -- |
Function | {Function} | -- |
RegExp | {RegExp} | -- |
Array | {Array} | -- |
Date | {Date} | -- |
單一類型集合 | {Array.<string>} | string 類型的數(shù)組 |
多類型 | {(number|boolean)} | 可能是 number 類型, 也可能是 boolean 類型 |
允許為null | {?number} | 可能是 number, 也可能是 null |
不允許為null | {!Object} | Object 類型, 但不是 null |
Function類型 | {function(number, boolean)} | 函數(shù), 形參類型 |
Function帶返回值 | {function(number, boolean):string} | 函數(shù), 形參, 返回值類型 |
Promise | Promise.<resolveType, rejectType> | Promise,成功返回的數(shù)據(jù)類型,失敗返回的錯(cuò)誤類型 |
參數(shù)可選 | @param {string=} name | 可選參數(shù), =為類型后綴 |
可變參數(shù) | @param {...number} args | 變長(zhǎng)參數(shù), ...為類型前綴 |
任意類型 | {*} | 任意類型 |
可選任意類型 | @param {*=} name | 可選參數(shù),類型不限 |
可變?nèi)我忸愋?/td> | @param {...*} args | 變長(zhǎng)參數(shù),類型不限 |
2.4.5 文件注釋
[強(qiáng)制] 文件頂部必須包含文件注釋,用 @file
標(biāo)識(shí)文件說明。
示例:
/**
* @file Describe the file
*/
[建議] 文件注釋中可以用 @author
標(biāo)識(shí)開發(fā)者信息。
解釋:
開發(fā)者信息能夠體現(xiàn)開發(fā)人員對(duì)文件的貢獻(xiàn),并且能夠讓遇到問題或希望了解相關(guān)信息的人找到維護(hù)人。通常情況文件在被創(chuàng)建時(shí)標(biāo)識(shí)的是創(chuàng)建者。隨著項(xiàng)目的進(jìn)展,越來越多的人加入,參與這個(gè)文件的開發(fā),新的作者應(yīng)該被加入 @author
標(biāo)識(shí)。
@author
標(biāo)識(shí)具有多人時(shí),原則是按照 責(zé)任
進(jìn)行排序。通常的說就是如果有問題,就是找第一個(gè)人應(yīng)該比找第二個(gè)人有效。比如文件的創(chuàng)建者由于各種原因,模塊移交給了其他人或其他團(tuán)隊(duì),后來因?yàn)樾略鲂枨螅渌嗽谛略龃a時(shí),添加 @author
標(biāo)識(shí)應(yīng)該把自己的名字添加在創(chuàng)建人的前面。
@author
中的名字不允許被刪除。任何勞動(dòng)成果都應(yīng)該被尊重。
業(yè)務(wù)項(xiàng)目中,一個(gè)文件可能被多人頻繁修改,并且每個(gè)人的維護(hù)時(shí)間都可能不會(huì)很長(zhǎng),不建議為文件增加 @author
標(biāo)識(shí)。通過版本控制系統(tǒng)追蹤變更,按業(yè)務(wù)邏輯單元確定模塊的維護(hù)責(zé)任人,通過文檔與wiki跟蹤和查詢,是更好的責(zé)任管理方式。
對(duì)于業(yè)務(wù)邏輯無關(guān)的技術(shù)型基礎(chǔ)項(xiàng)目,特別是開源的公共項(xiàng)目,應(yīng)使用 @author
標(biāo)識(shí)。
示例:
/**
* @file Describe the file
* @author author-name(mail-name@domain.com)
* author-name2(mail-name2@domain.com)
*/
2.4.6 命名空間注釋
[建議] 命名空間使用 @namespace
標(biāo)識(shí)。
示例:
/**
* @namespace
*/
var util = {};
2.4.7 類注釋
[建議] 使用 @class
標(biāo)記類或構(gòu)造函數(shù)。
解釋:
對(duì)于使用對(duì)象 constructor
屬性來定義的構(gòu)造函數(shù),可以使用 @constructor
來標(biāo)記。
示例:
/**
* 描述
*
* @class
*/
function Developer() {
// constructor body
}
[建議] 使用 @extends
標(biāo)記類的繼承信息。
示例:
/**
* 描述
*
* @class
* @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.inherits(Fronteer, Developer);
[強(qiáng)制] 使用包裝方式擴(kuò)展類成員時(shí), 必須通過 @lends
進(jìn)行重新指向。
解釋:
沒有 @lends
標(biāo)記將無法為該類生成包含擴(kuò)展類成員的文檔。
示例:
/**
* 類描述
*
* @class
* @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.extend(
Fronteer.prototype,
/** @lends Fronteer.prototype */{
getLevel: function () {
// TODO
}
}
);
[強(qiáng)制] 類的屬性或方法等成員信息不是 public
的,應(yīng)使用 @protected
或 @private
標(biāo)識(shí)可訪問性。
解釋:
生成的文檔中將有可訪問性的標(biāo)記,避免用戶直接使用非 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 函數(shù)/方法注釋
[強(qiáng)制] 函數(shù)/方法注釋必須包含函數(shù)說明,有參數(shù)和返回值時(shí)必須使用注釋標(biāo)識(shí)。
解釋:
當(dāng) return
關(guān)鍵字僅作退出函數(shù)/方法使用時(shí),無須對(duì)返回值作注釋標(biāo)識(shí)。
[強(qiáng)制] 參數(shù)和返回值注釋必須包含類型信息,且不允許省略參數(shù)的說明。
[建議] 當(dāng)函數(shù)是內(nèi)部函數(shù),外部不可訪問時(shí),可以使用 @inner
標(biāo)識(shí)。
示例:
/**
* 函數(shù)描述
*
* @param {string} p1 參數(shù)1的說明
* @param {string} p2 參數(shù)2的說明,比較長(zhǎng)
* 那就換行了.
* @param {number=} p3 參數(shù)3的說明(可選)
* @return {Object} 返回值描述
*/
function foo(p1, p2, p3) {
var p3 = p3 || 10;
return {
p1: p1,
p2: p2,
p3: p3
};
}
[強(qiáng)制] 對(duì) Object 中各項(xiàng)的描述, 必須使用 @param
標(biāo)識(shí)。
示例:
/**
* 函數(shù)描述
*
* @param {Object} option 參數(shù)描述
* @param {string} option.url option項(xiàng)描述
* @param {string=} option.method option項(xiàng)描述,可選參數(shù)
*/
function foo(option) {
// TODO
}
[建議] 重寫父類方法時(shí), 應(yīng)當(dāng)添加 @override
標(biāo)識(shí)。如果重寫的形參個(gè)數(shù)、類型、順序和返回值類型均未發(fā)生變化,可省略 @param
、@return
,僅用 @override
標(biāo)識(shí),否則仍應(yīng)作完整注釋。
解釋:
簡(jiǎn)而言之,當(dāng)子類重寫的方法能直接套用父類的方法注釋時(shí)可省略對(duì)參數(shù)與返回值的注釋。
2.4.9 事件注釋
[強(qiáng)制] 必須使用 @event
標(biāo)識(shí)事件,事件參數(shù)的標(biāo)識(shí)與方法描述的參數(shù)標(biāo)識(shí)相同。
示例:
/**
* 值變更時(shí)觸發(fā)
*
* @event Select#change
* @param {Object} e e描述
* @param {string} e.before before描述
* @param {string} e.after after描述
*/
this.fire(
'change',
{
before: 'foo',
after: 'bar'
}
);
[強(qiáng)制] 在會(huì)廣播事件的函數(shù)前使用 @fires
標(biāo)識(shí)廣播的事件,在廣播事件代碼前使用 @event
標(biāo)識(shí)事件。
[建議] 對(duì)于事件對(duì)象的注釋,使用 @param
標(biāo)識(shí),生成文檔時(shí)可讀性更好。
示例:
/**
* 點(diǎn)擊處理
*
* @fires Select#change
* @private
*/
Select.prototype.clickHandler = function () {
/**
* 值變更時(shí)觸發(fā)
*
* @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 常量注釋
[強(qiáng)制] 常量必須使用 @const
標(biāo)記,并包含說明和類型信息。
示例:
/**
* 常量說明
*
* @const
* @type {string}
*/
var REQUEST_URL = 'myurl.do';
2.4.11 復(fù)雜類型注釋
[建議] 對(duì)于類型未定義的復(fù)雜結(jié)構(gòu)的注釋,可以使用 @typedef
標(biāo)識(shí)來定義。
示例:
// `namespaceA~` 可以換成其它 namepaths 前綴,目的是為了生成文檔中能顯示 `@typedef` 定義的類型和鏈接。
/**
* 服務(wù)器
*
* @typedef {Object} namespaceA~Server
* @property {string} host 主機(jī)
* @property {number} port 端口
*/
/**
* 服務(wù)器列表
*
* @type {Array.<namespaceA~Server>}
*/
var servers = [
{
host: '1.2.3.4',
port: 8080
},
{
host: '1.2.3.5',
port: 8081
}
];
2.4.12 AMD 模塊注釋
[強(qiáng)制] AMD 模塊使用 @module
或 @exports
標(biāo)識(shí)。
解釋:
@exports 與 @module 都可以用來標(biāo)識(shí)模塊,區(qū)別在于 @module 可以省略模塊名稱。而只使用 @exports 時(shí)在 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 標(biāo)識(shí):
define(
function (require) {
/**
* module description.
*
* @module foo
*/
var exports = {};
/**
* bar description
*
*/
exports.bar = function () {
// TODO
};
return exports;
}
);
如果直接使用 factory 的 exports 參數(shù),還可以:
/**
* module description.
*
* @module
*/
define(
function (require, exports) {
/**
* bar description
*
*/
exports.bar = function () {
// TODO
};
return exports;
}
);
[強(qiáng)制] 對(duì)于已使用 @module
標(biāo)識(shí)為 AMD模塊 的引用,在 namepaths
中必須增加 module:
作前綴。
解釋:
namepaths 沒有 module: 前綴時(shí),生成的文檔中將無法正確生成鏈接。
示例:
/**
* 點(diǎn)擊處理
*
* @fires module:Select#change
* @private
*/
Select.prototype.clickHandler = function () {
/**
* 值變更時(shí)觸發(fā)
*
* @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'
}
);
};
[建議] 對(duì)于類定義的模塊,可以使用 @alias
標(biāo)識(shí)構(gòu)建函數(shù)。
示例:
/**
* A module representing a jacket.
* @module jacket
*/
define(
function () {
/**
* @class
* @alias module:jacket
*/
var Jacket = function () {
};
return Jacket;
}
);
[建議] 多模塊定義時(shí),可以使用 @exports
標(biāo)識(shí)各個(gè)模塊。
示例:
// 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;
}
);
[建議] 對(duì)于 exports 為 Object 的模塊,可以使用@namespace
標(biāo)識(shí)。
解釋:
使用 @namespace 而不是 @module 或 @exports 時(shí),對(duì)模塊的引用可以省略 module: 前綴。
[建議] 對(duì)于 exports 為類名的模塊,使用 @class
和 @exports
標(biāo)識(shí)。
示例:
// 只使用 @class Bar 時(shí),類方法和屬性都必須增加 @name Bar#methodName 來標(biāo)識(shí),與 @exports 配合可以免除這一麻煩,并且在引用時(shí)可以省去 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 細(xì)節(jié)注釋
對(duì)于內(nèi)部實(shí)現(xiàn)、不容易理解的邏輯說明、摘要信息等,我們可能需要編寫細(xì)節(jié)注釋。
[建議] 細(xì)節(jié)注釋遵循單行注釋的格式。說明必須換行時(shí),每行是一個(gè)單行注釋的起始。
示例:
function foo(p1, p2, opt_p3) {
// 這里對(duì)具體內(nèi)部邏輯進(jìn)行說明
// 說明太長(zhǎng)需要換行
for (...) {
....
}
}
[強(qiáng)制] 有時(shí)我們會(huì)使用一些特殊標(biāo)記進(jìn)行說明。特殊標(biāo)記必須使用單行注釋的形式。下面列舉了一些常用標(biāo)記:
解釋:
- TODO: 有功能待實(shí)現(xiàn)。此時(shí)需要對(duì)將要實(shí)現(xiàn)的功能進(jìn)行簡(jiǎn)單說明。
- FIXME: 該處代碼運(yùn)行沒問題,但可能由于時(shí)間趕或者其他原因,需要修正。此時(shí)需要對(duì)如何修正進(jìn)行簡(jiǎn)單說明。
- HACK: 為修正某些問題而寫的不太好或者使用了某些詭異手段的代碼。此時(shí)需要對(duì)思路或詭異手段進(jìn)行描述。
- XXX: 該處存在陷阱。此時(shí)需要對(duì)陷阱進(jìn)行描述。
3 語言特性
3.1 變量
[強(qiáng)制] 變量、函數(shù)在使用前必須先定義。
解釋:
不通過 var 定義變量將導(dǎo)致變量污染全局環(huán)境。
示例:
// good
var name = 'MyName';
// bad
name = 'MyName';
原則上不建議使用全局變量,對(duì)于已有的全局變量或第三方框架引入的全局變量,需要根據(jù)檢查工具的語法標(biāo)識(shí)。
示例:
/* globals jQuery */
var element = jQuery('#element-id');
[強(qiáng)制] 每個(gè) var
只能聲明一個(gè)變量。
解釋:
一個(gè) var
聲明多個(gè)變量,容易導(dǎo)致較長(zhǎng)的行長(zhǎng)度,并且在修改時(shí)容易造成逗號(hào)和分號(hào)的混淆。
示例:
// good
var hangModules = [];
var missModules = [];
var visited = {};
// bad
var hangModules = [],
missModules = [],
visited = {};
[強(qiáng)制] 變量必須 即用即聲明
,不得在函數(shù)或其它形式的代碼塊起始位置統(tǒng)一聲明所有變量。
解釋:
變量聲明與使用的距離越遠(yuǎn),出現(xiàn)的跨度越大,代碼的閱讀與維護(hù)成本越高。雖然JavaScript的變量是函數(shù)作用域,還是應(yīng)該根據(jù)編程中的意圖,縮小變量出現(xiàn)的距離空間。
示例:
// 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 條件
[強(qiáng)制] 在 Equality Expression 中使用類型嚴(yán)格的 ===
。僅當(dāng)判斷 null
或 undefined
時(shí),允許使用 == null
。
解釋:
使用 ===
可以避免等于判斷中隱式的類型轉(zhuǎn)換。
示例:
// good
if (age === 30) {
// ......
}
// bad
if (age == 30) {
// ......
}
[建議] 盡可能使用簡(jiǎn)潔的表達(dá)式。
示例:
// 字符串為空
// good
if (!name) {
// ......
}
// bad
if (name === '') {
// ......
}
// 字符串非空
// good
if (name) {
// ......
}
// bad
if (name !== '') {
// ......
}
// 數(shù)組非空
// 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') {
// ......
}
[建議] 按執(zhí)行頻率排列分支的順序。
解釋:
按執(zhí)行頻率排列分支的順序好處是:
- 閱讀的人容易找到最常見的情況,增加可讀性。
- 提高執(zhí)行效率。
[建議] 對(duì)于相同變量或表達(dá)式的多值條件,用 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') {
// ......
}
[建議] 如果函數(shù)或全局中的 else
塊后沒有任何語句,可以刪除 else
。
示例:
// good
function getName() {
if (name) {
return name;
}
return 'unnamed';
}
// bad
function getName() {
if (name) {
return name;
}
else {
return 'unnamed';
}
}
3.3 循環(huán)
[建議] 不要在循環(huán)體中包含函數(shù)表達(dá)式,事先將函數(shù)提取到循環(huán)體外。
解釋:
循環(huán)體中的函數(shù)表達(dá)式,運(yùn)行過程中會(huì)生成循環(huán)次數(shù)個(gè)函數(shù)對(duì)象。
示例:
// 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 () {});
}
[建議] 對(duì)循環(huán)內(nèi)多次使用的不變值,在循環(huán)外用變量緩存。
示例:
// 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';
// ......
}
[建議] 對(duì)有序集合進(jìn)行遍歷時(shí),緩存 length
。
解釋:
雖然現(xiàn)代瀏覽器都對(duì)數(shù)組長(zhǎng)度進(jìn)行了緩存,但對(duì)于一些宿主對(duì)象和老舊瀏覽器的數(shù)組對(duì)象,在每次 length
訪問時(shí)會(huì)動(dòng)態(tài)計(jì)算元素個(gè)數(shù),此時(shí)緩存 length
能有效提高程序性能。
示例:
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
// ......
}
[建議] 對(duì)有序集合進(jìn)行順序無關(guān)的遍歷時(shí),使用逆序遍歷。
解釋:
逆序遍歷可以節(jié)省變量,代碼比較優(yōu)化。
示例:
var len = elements.length;
while (len--) {
var element = elements[len];
// ......
}
3.4 類型
3.4.1 類型檢測(cè)
[建議] 類型檢測(cè)優(yōu)先使用 typeof
。對(duì)象類型檢測(cè)使用 instanceof
。null
或 undefined
的檢測(cè)使用 == 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 類型轉(zhuǎn)換
[建議] 轉(zhuǎn)換成 string
時(shí),使用 + ''
。
示例:
// good
num + '';
// bad
new String(num);
num.toString();
String(num);
[建議] 轉(zhuǎn)換成 number
時(shí),通常使用 +
。
示例:
// good
+str;
// bad
Number(str);
[建議] string
轉(zhuǎn)換成 number
,要轉(zhuǎn)換的字符串結(jié)尾包含非數(shù)字并期望忽略時(shí),使用 parseInt
。
示例:
var width = '200px';
parseInt(width, 10);
[強(qiáng)制] 使用 parseInt
時(shí),必須指定進(jìn)制。
示例:
// good
parseInt(str, 10);
// bad
parseInt(str);
[建議] 轉(zhuǎn)換成 boolean
時(shí),使用 !!
。
示例:
var num = 3.14;
!!num;
[建議] number
去除小數(shù)點(diǎn),使用 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 字符串
[強(qiáng)制] 字符串開頭和結(jié)束使用單引號(hào) '
。
解釋:
- 輸入單引號(hào)不需要按住
shift
,方便輸入。 - 實(shí)際使用中,字符串經(jīng)常用來拼接 HTML。為方便 HTML 中包含雙引號(hào)而不需要轉(zhuǎn)義寫法。
示例:
var str = '我是一個(gè)字符串';
var html = '<div class="cls">拼接HTML可以省去雙引號(hào)轉(zhuǎn)義</div>';
[建議] 使用 數(shù)組
或 +
拼接字符串。
解釋:
- 使用
+
拼接字符串,如果拼接的全部是 StringLiteral,壓縮工具可以對(duì)其進(jìn)行自動(dòng)合并的優(yōu)化。所以,靜態(tài)字符串建議使用+
拼接。 - 在現(xiàn)代瀏覽器下,使用
+
拼接字符串,性能較數(shù)組的方式要高。 - 如需要兼顧老舊瀏覽器,應(yīng)盡量使用數(shù)組拼接字符串。
示例:
// 使用數(shù)組拼接字符串
var str = [
// 推薦換行開始并縮進(jìn)開始第一個(gè)字符串, 對(duì)齊代碼, 方便閱讀.
'<ul>',
'<li>第一項(xiàng)</li>',
'<li>第二項(xiàng)</li>',
'</ul>'
].join('');
// 使用 `+` 拼接字符串
var str2 = '' // 建議第一個(gè)為空字符串, 第二個(gè)換行開始并縮進(jìn)開始, 對(duì)齊代碼, 方便閱讀
+ '<ul>',
+ '<li>第一項(xiàng)</li>',
+ '<li>第二項(xiàng)</li>',
+ '</ul>';
[建議] 使用字符串拼接的方式生成HTML,需要根據(jù)語境進(jìn)行合理的轉(zhuǎn)義。
解釋:
在 JavaScript
中拼接,并且最終將輸出到頁面中的字符串,需要進(jìn)行合理轉(zhuǎn)義,以防止安全漏洞。下面的示例代碼為場(chǎng)景說明,不能直接運(yùn)行。
示例:
// HTML 轉(zhuǎn)義
var str = '<p>' + htmlEncode(content) + '</p>';
// HTML 轉(zhuǎn)義
var str = '<input type="text" value="' + htmlEncode(value) + '">';
// URL 轉(zhuǎn)義
var str = '<a href="/?key=' + htmlEncode(urlEncode(value)) + '">link</a>';
// JavaScript字符串 轉(zhuǎn)義 + HTML 轉(zhuǎn)義
var str = '<button onclick="check(\'' + htmlEncode(strLiteral(name)) + '\')">提交</button>';
[建議] 復(fù)雜的數(shù)據(jù)到視圖字符串的轉(zhuǎn)換過程,選用一種模板引擎。
解釋:
使用模板引擎有如下好處:
- 在開發(fā)過程中專注于數(shù)據(jù),將視圖生成的過程由另外一個(gè)層級(jí)維護(hù),使程序邏輯結(jié)構(gòu)更清晰。
- 優(yōu)秀的模板引擎,通過模板編譯技術(shù)和高質(zhì)量的編譯產(chǎn)物,能獲得比手工拼接字符串更高的性能。
- 模板引擎能方便的對(duì)動(dòng)態(tài)數(shù)據(jù)進(jìn)行相應(yīng)的轉(zhuǎn)義,部分模板引擎默認(rèn)進(jìn)行HTML轉(zhuǎn)義,安全性更好。
- artTemplate: 體積較小,在所有環(huán)境下性能高,語法靈活。
- dot.js: 體積小,在現(xiàn)代瀏覽器下性能高,語法靈活。
- etpl: 體積較小,在所有環(huán)境下性能高,模板復(fù)用性高,語法靈活。
- handlebars: 體積大,在所有環(huán)境下性能高,擴(kuò)展性高。
- hogon: 體積小,在現(xiàn)代瀏覽器下性能高。
- nunjucks: 體積較大,性能一般,模板復(fù)用性高。
3.6 對(duì)象
[強(qiáng)制] 使用對(duì)象字面量 {}
創(chuàng)建新 Object
。
示例:
// good
var obj = {};
// bad
var obj = new Object();
[建議] 對(duì)象創(chuàng)建時(shí),如果一個(gè)對(duì)象的所有 屬性
均可以不添加引號(hào),建議所有 屬性
不添加引號(hào)。
示例:
var info = {
name: 'someone',
age: 28
};
[建議] 對(duì)象創(chuàng)建時(shí),如果任何一個(gè) 屬性
需要添加引號(hào),則所有 屬性
建議添加 '
。
解釋:
如果屬性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。
示例:
// good
var info = {
'name': 'someone',
'age': 28,
'more-info': '...'
};
// bad
var info = {
name: 'someone',
age: 28,
'more-info': '...'
};
[強(qiáng)制] 不允許修改和擴(kuò)展任何原生對(duì)象和宿主對(duì)象的原型。
示例:
// 以下行為絕對(duì)禁止
String.prototype.trim = function () {
};
[建議] 屬性訪問時(shí),盡量使用 .
。
解釋:
屬性名符合 Identifier 的要求,就可以通過 .
來訪問,否則就只能通過 [expr]
方式訪問。
通常在 JavaScript 中聲明的對(duì)象,屬性命名是使用 Camel 命名法,用 .
來訪問更清晰簡(jiǎn)潔。部分特殊的屬性(比如來自后端的 JSON ),可能采用不尋常的命名方式,可以通過 [expr]
方式訪問。
示例:
info.age;
info['more-info'];
[建議] for in
遍歷對(duì)象時(shí), 使用 hasOwnProperty
過濾掉原型中的屬性。
示例:
var newInfo = {};
for (var key in info) {
if (info.hasOwnProperty(key)) {
newInfo[key] = info[key];
}
}
3.7 數(shù)組
[強(qiáng)制] 使用數(shù)組字面量 []
創(chuàng)建新數(shù)組,除非想要?jiǎng)?chuàng)建的是指定長(zhǎng)度的數(shù)組。
示例:
// good
var arr = [];
// bad
var arr = new Array();
[強(qiáng)制] 遍歷數(shù)組不使用 for in
。
解釋:
數(shù)組對(duì)象可能存在數(shù)字以外的屬性, 這種情況下 for in
不會(huì)得到正確結(jié)果。
示例:
var arr = ['a', 'b', 'c'];
// 這里僅作演示, 實(shí)際中應(yīng)使用 Object 類型
arr.other = 'other things';
// 正確的遍歷方式
for (var i = 0, len = arr.length; i < len; i++) {
console.log(i);
}
// 錯(cuò)誤的遍歷方式
for (var i in arr) {
console.log(i);
}
[建議] 不因?yàn)樾阅艿脑蜃约簩?shí)現(xiàn)數(shù)組排序功能,盡量使用數(shù)組的 sort
方法。
解釋:
自己實(shí)現(xiàn)的常規(guī)排序算法,在性能上并不優(yōu)于數(shù)組默認(rèn)的 sort
方法。以下兩種場(chǎng)景可以自己實(shí)現(xiàn)排序:
- 需要穩(wěn)定的排序算法,達(dá)到嚴(yán)格一致的排序結(jié)果。
- 數(shù)據(jù)特點(diǎn)鮮明,適合使用桶排。
[建議] 清空數(shù)組使用 .length = 0
。
3.8 函數(shù)
3.8.1 函數(shù)長(zhǎng)度
[建議] 一個(gè)函數(shù)的長(zhǎng)度控制在 50
行以內(nèi)。
解釋:
將過多的邏輯單元混在一個(gè)大函數(shù)中,易導(dǎo)致難以維護(hù)。一個(gè)清晰易懂的函數(shù)應(yīng)該完成單一的邏輯單元。復(fù)雜的操作應(yīng)進(jìn)一步抽取,通過函數(shù)的調(diào)用來體現(xiàn)流程。
特定算法等不可分割的邏輯允許例外。
示例:
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;
}
}
// 直接閱讀該函數(shù)會(huì)難以明確其主線邏輯,因此下方是一種更合理的表達(dá)方式:
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 參數(shù)設(shè)計(jì)
[建議] 一個(gè)函數(shù)的參數(shù)控制在 6
個(gè)以內(nèi)。
解釋:
除去不定長(zhǎng)參數(shù)以外,函數(shù)具備不同邏輯意義的參數(shù)建議控制在 6
個(gè)以內(nèi),過多參數(shù)會(huì)導(dǎo)致維護(hù)難度增大。
某些情況下,如使用 AMD Loader 的 require
加載多個(gè)模塊時(shí),其 callback
可能會(huì)存在較多參數(shù),因此對(duì)函數(shù)參數(shù)的個(gè)數(shù)不做強(qiáng)制限制。
[建議] 通過 options
參數(shù)傳遞非數(shù)據(jù)輸入型參數(shù)。
解釋:
有些函數(shù)的參數(shù)并不是作為算法的輸入,而是對(duì)算法的某些分支條件判斷之用,此類參數(shù)建議通過一個(gè) options
參數(shù)傳遞。
如下函數(shù):
/**
* 移除某個(gè)元素
*
* @param {Node} element 需要移除的元素
* @param {boolean} removeEventListeners 是否同時(shí)將所有注冊(cè)在元素上的事件移除
*/
function removeElement(element, removeEventListeners) {
element.parent.removeChild(element);
if (removeEventListeners) {
element.clearEventListeners();
}
}
可以轉(zhuǎn)換為下面的簽名:
/**
* 移除某個(gè)元素
*
* @param {Node} element 需要移除的元素
* @param {Object} options 相關(guān)的邏輯配置
* @param {boolean} options.removeEventListeners 是否同時(shí)將所有注冊(cè)在元素上的事件移除
*/
function removeElement(element, options) {
element.parent.removeChild(element);
if (options.removeEventListeners) {
element.clearEventListeners();
}
}
這種模式有幾個(gè)顯著的優(yōu)勢(shì):
-
boolean
型的配置項(xiàng)具備名稱,從調(diào)用的代碼上更易理解其表達(dá)的邏輯意義。 - 當(dāng)配置項(xiàng)有增長(zhǎng)時(shí),無需無休止地增加參數(shù)個(gè)數(shù),不會(huì)出現(xiàn)
removeElement(element, true, false, false, 3)
這樣難以理解的調(diào)用代碼。 - 當(dāng)部分配置參數(shù)可選時(shí),多個(gè)參數(shù)的形式非常難處理重載邏輯,而使用一個(gè) options 對(duì)象只需判斷屬性是否存在,實(shí)現(xiàn)得以簡(jiǎn)化。
3.8.3 閉包
[建議] 在適當(dāng)?shù)臅r(shí)候?qū)㈤]包內(nèi)大對(duì)象置為 null
。
解釋:
在 JavaScript 中,無需特別的關(guān)鍵詞就可以使用閉包,一個(gè)函數(shù)可以任意訪問在其定義的作用域外的變量。需要注意的是,函數(shù)的作用域是靜態(tài)的,即在定義時(shí)決定,與調(diào)用的時(shí)機(jī)和方式?jīng)]有任何關(guān)系。
閉包會(huì)阻止一些變量的垃圾回收,對(duì)于較老舊的 JavaScript 引擎,可能導(dǎo)致外部所有變量均無法回收。
首先一個(gè)較為明確的結(jié)論是,以下內(nèi)容會(huì)影響到閉包內(nèi)變量的回收:
- 嵌套的函數(shù)中是否有使用該變量。
- 嵌套的函數(shù)中是否有 直接調(diào)用eval。
- 是否使用了 with 表達(dá)式。
Chakra、V8 和 SpiderMonkey 將受以上因素的影響,表現(xiàn)出不盡相同又較為相似的回收策略,而 JScript.dll 和 Carakan 則完全沒有這方面的優(yōu)化,會(huì)完整保留整個(gè) LexicalEnvironment 中的所有變量綁定,造成一定的內(nèi)存消耗。
由于對(duì)閉包內(nèi)變量有回收優(yōu)化策略的 Chakra、V8 和 SpiderMonkey 引擎的行為較為相似,因此可以總結(jié)如下,當(dāng)返回一個(gè)函數(shù) fn 時(shí):
- 如果 fn 的
[[Scope]]
是 ObjectEnvironment(with 表達(dá)式生成 ObjectEnvironment,函數(shù)和 catch 表達(dá)式生成 DeclarativeEnvironment),則:- 如果是 V8 引擎,則退出全過程。
- 如果是 SpiderMonkey,則處理該 ObjectEnvironment 的外層 LexicalEnvironment。
- 獲取當(dāng)前 LexicalEnvironment 下的所有類型為 Function 的對(duì)象,對(duì)于每一個(gè) Function 對(duì)象,分析其 FunctionBody:
- 如果 FunctionBody 中含有 直接調(diào)用 eval,則退出全過程。
- 否則得到所有的 Identifier。
- 對(duì)于每一個(gè) Identifier,設(shè)其為 name,根據(jù)查找變量引用的規(guī)則,從 LexicalEnvironment 中找出名稱為 name 的綁定 binding。
- 對(duì) binding 添加 notSwap 屬性,其值為
true
。
- 檢查當(dāng)前 LexicalEnvironment 中的每一個(gè)變量綁定,如果該綁定有 notSwap 屬性且值為
true
,則:- 如果是 V8 引擎,刪除該綁定。
- 如果是 SpiderMonkey,將該綁定的值設(shè)為
undefined
,將刪除 notSwap 屬性。
對(duì)于 Chakra 引擎,暫無法得知是按 V8 的模式還是按 SpiderMonkey 的模式進(jìn)行。
如果有 非常龐大 的對(duì)象,且預(yù)計(jì)會(huì)在 老舊的引擎 中執(zhí)行,則使用閉包時(shí),注意將閉包不需要的對(duì)象置為空引用。
[建議] 使用 IIFE
避免 Lift 效應(yīng)
。
解釋:
在引用函數(shù)外部變量時(shí),函數(shù)執(zhí)行時(shí)外部變量的值由運(yùn)行時(shí)決定而非定義時(shí),最典型的場(chǎng)景如下:
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]();
}
以上代碼對(duì) tasks 中的函數(shù)的執(zhí)行均會(huì)輸出 Current cursor is at 5
,往往不符合預(yù)期。
此現(xiàn)象稱為 Lift 效應(yīng) 。解決的方式是通過額外加上一層閉包函數(shù),將需要的外部變量作為參數(shù)傳遞來解除變量的綁定關(guān)系:
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 空函數(shù)
[建議] 空函數(shù)不使用 new Function()
的形式。
示例:
var emptyFunction = function () {};
[建議] 對(duì)于性能有高要求的場(chǎng)合,建議存在一個(gè)空函數(shù)的常量,供多處使用共享。
示例:
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 面向?qū)ο?/h3>
[強(qiáng)制] 類的繼承方案,實(shí)現(xiàn)時(shí)需要修正 constructor
。
constructor
。解釋:
通常使用其他 library 的類繼承方案都會(huì)進(jìn)行 constructor
修正。如果是自己實(shí)現(xiàn)的類繼承方案,需要進(jìn)行 constructor
修正。
示例:
/**
* 構(gòu)建類之間的繼承關(guān)系
*
* @param {Function} subClass 子類函數(shù)
* @param {Function} superClass 父類函數(shù)
*/
function inherits(subClass, superClass) {
var F = new Function();
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}
[建議] 聲明類時(shí),保證 constructor
的正確性。
示例:
function Animal(name) {
this.name = name;
}
// 直接prototype等于對(duì)象時(shí),需要修正constructor
Animal.prototype = {
constructor: Animal,
jump: function () {
alert('animal ' + this.name + ' jump');
}
};
// 這種方式擴(kuò)展prototype則無需理會(huì)constructor
Animal.prototype.jump = function () {
alert('animal ' + this.name + ' jump');
};
[建議] 屬性在構(gòu)造函數(shù)中聲明,方法在原型中聲明。
解釋:
原型對(duì)象的成員被所有實(shí)例共享,能節(jié)約內(nèi)存占用。所以編碼時(shí)我們應(yīng)該遵守這樣的原則:原型對(duì)象包含程序不會(huì)修改的成員,如方法函數(shù)或配置項(xiàng)。
function TextNode(value, engine) {
this.value = value;
this.engine = engine;
}
TextNode.prototype.clone = function () {
return this;
};
[強(qiáng)制] 自定義事件的 事件名
必須全小寫。
解釋:
在 JavaScript 廣泛應(yīng)用的瀏覽器環(huán)境,絕大多數(shù) DOM 事件名稱都是全小寫的。為了遵循大多數(shù) JavaScript 開發(fā)者的習(xí)慣,在設(shè)計(jì)自定義事件時(shí),事件名也應(yīng)該全小寫。
[強(qiáng)制] 自定義事件只能有一個(gè) event
參數(shù)。如果事件需要傳遞較多信息,應(yīng)仔細(xì)設(shè)計(jì)事件對(duì)象。
解釋:
一個(gè)事件對(duì)象的好處有:
- 順序無關(guān),避免事件監(jiān)聽者需要記憶參數(shù)順序。
- 每個(gè)事件信息都可以根據(jù)需要提供或者不提供,更自由。
- 擴(kuò)展方便,未來添加事件信息時(shí),無需考慮會(huì)破壞監(jiān)聽器參數(shù)形式而無法向后兼容。
[建議] 設(shè)計(jì)自定義事件時(shí),應(yīng)考慮禁止默認(rèn)行為。
解釋:
常見禁止默認(rèn)行為的方式有兩種:
- 事件監(jiān)聽函數(shù)中
return false
。 - 事件對(duì)象中包含禁止默認(rèn)行為的方法,如
preventDefault
。
3.10 動(dòng)態(tài)特性
3.10.1 eval
[強(qiáng)制] 避免使用直接 eval
函數(shù)。
解釋:
直接 eval
,指的是以函數(shù)方式調(diào)用 eval
的調(diào)用方法。直接 eval
調(diào)用執(zhí)行代碼的作用域?yàn)楸镜刈饔糜颍瑧?yīng)當(dāng)避免。
如果有特殊情況需要使用直接 eval
,需在代碼中用詳細(xì)的注釋說明為何必須使用直接 eval
,不能使用其它動(dòng)態(tài)執(zhí)行代碼的方式,同時(shí)需要其他資深工程師進(jìn)行 Code Review。
[建議] 盡量避免使用 eval
函數(shù)。
3.10.2 動(dòng)態(tài)執(zhí)行代碼
[建議] 使用 new Function
執(zhí)行動(dòng)態(tài)代碼。
解釋:
通過 new Function
生成的函數(shù)作用域是全局使用域,不會(huì)影響當(dāng)當(dāng)前的本地作用域。如果有動(dòng)態(tài)代碼執(zhí)行的需求,建議使用 new Function
。
示例:
var handler = new Function('x', 'y', 'return x + y;');
var result = handler($('#x').val(), $('#y').val());
3.10.3 with
[建議] 盡量不要使用 with
。
解釋:
使用 with
可能會(huì)增加代碼的復(fù)雜度,不利于閱讀和管理;也會(huì)對(duì)性能有影響。大多數(shù)使用 with
的場(chǎng)景都能使用其他方式較好的替代。所以,盡量不要使用 with
。
3.10.4 delete
[建議] 減少 delete
的使用。
解釋:
如果沒有特別的需求,減少或避免使用 delete
。delete
的使用會(huì)破壞部分 JavaScript 引擎的性能優(yōu)化。
[建議] 處理 delete
可能產(chǎn)生的異常。
解釋:
對(duì)于有被遍歷需求,且值 null
被認(rèn)為具有業(yè)務(wù)邏輯意義的值的對(duì)象,移除某個(gè)屬性必須使用 delete
操作。
在嚴(yán)格模式或 IE 下使用 delete
時(shí),不能被刪除的屬性會(huì)拋出異常,因此在不確定屬性是否可以刪除的情況下,建議添加 try-catch
塊。
示例:
try {
delete o.x;
}
catch (deleteError) {
o.x = null;
}
3.10.5 對(duì)象屬性
[建議] 避免修改外部傳入的對(duì)象。
解釋:
JavaScript 因其腳本語言的動(dòng)態(tài)特性,當(dāng)一個(gè)對(duì)象未被 seal 或 freeze 時(shí),可以任意添加、刪除、修改屬性值。
但是隨意地對(duì) 非自身控制的對(duì)象 進(jìn)行修改,很容易造成代碼在不可預(yù)知的情況下出現(xiàn)問題。因此,設(shè)計(jì)良好的組件、函數(shù)應(yīng)該避免對(duì)外部傳入的對(duì)象的修改。
下面代碼的 selectNode 方法修改了由外部傳入的 datasource 對(duì)象。如果 datasource 用在其它場(chǎng)合(如另一個(gè) Tree 實(shí)例)下,會(huì)造成狀態(tài)的混亂。
function Tree(datasource) {
this.datasource = datasource;
}
Tree.prototype.selectNode = function (id) {
// 從datasource中找出節(jié)點(diǎn)對(duì)象
var node = this.findNode(id);
if (node) {
node.selected = true;
this.flushView();
}
};
對(duì)于此類場(chǎng)景,需要使用額外的對(duì)象來維護(hù),使用由自身控制,不與外部產(chǎn)生任何交互的 selectedNodeIndex 對(duì)象來維護(hù)節(jié)點(diǎn)的選中狀態(tài),不對(duì) datasource 作任何修改。
function Tree(datasource) {
this.datasource = datasource;
this.selectedNodeIndex = {};
}
Tree.prototype.selectNode = function (id) {
// 從datasource中找出節(jié)點(diǎn)對(duì)象
var node = this.findNode(id);
if (node) {
this.selectedNodeIndex[id] = true;
this.flushView();
}
};
除此之外,也可以通過 deepClone 等手段將自身維護(hù)的對(duì)象與外部傳入的分離,保證不會(huì)相互影響。
[建議] 具備強(qiáng)類型的設(shè)計(jì)。
解釋:
- 如果一個(gè)屬性被設(shè)計(jì)為
boolean
類型,則不要使用1
或0
作為其值。對(duì)于標(biāo)識(shí)性的屬性,如對(duì)代碼體積有嚴(yán)格要求,可以從一開始就設(shè)計(jì)為number
類型且將0
作為否定值。 - 從 DOM 中取出的值通常為
string
類型,如果有對(duì)象或函數(shù)的接收類型為number
類型,提前作好轉(zhuǎn)換,而不是期望對(duì)象、函數(shù)可以處理多類型的值。
4 瀏覽器環(huán)境
4.1 模塊化
4.1.1 AMD
[強(qiáng)制] 使用 AMD
作為模塊定義。
解釋:
AMD 作為由社區(qū)認(rèn)可的模塊定義形式,提供多種重載提供靈活的使用方式,并且絕大多數(shù)優(yōu)秀的 Library 都支持 AMD,適合作為規(guī)范。
目前,比較成熟的 AMD Loader 有:
[強(qiáng)制] 模塊 id
必須符合標(biāo)準(zhǔn)。
解釋:
模塊 id 必須符合以下約束條件:
- 類型為 string,并且是由
/
分割的一系列 terms 來組成。例如:this/is/a/module
。 - term 應(yīng)該符合 [a-zA-Z0-9_-]+ 規(guī)則。
- 不應(yīng)該有 .js 后綴。
- 跟文件的路徑保持一致。
4.1.2 define
[建議] 定義模塊時(shí)不要指明 id
和 dependencies
。
解釋:
在 AMD 的設(shè)計(jì)思想里,模塊名稱是和所在路徑相關(guān)的,匿名的模塊更利于封包和遷移。模塊依賴應(yīng)在模塊定義內(nèi)部通過 local require
引用。
所以,推薦使用 define(factory)
的形式進(jìn)行模塊定義。
示例:
define(
function (require) {
}
);
[建議] 使用 return
來返回模塊定義。
解釋:
使用 return 可以減少 factory 接收的參數(shù)(不需要接收 exports 和 module),在沒有 AMD Loader 的場(chǎng)景下也更容易進(jìn)行簡(jiǎn)單的處理來偽造一個(gè) Loader。
示例:
define(
function (require) {
var exports = {};
// ...
return exports;
}
);
4.1.3 require
[強(qiáng)制] 全局運(yùn)行環(huán)境中,require
必須以 async require
形式調(diào)用。
解釋:
模塊的加載過程是異步的,同步調(diào)用并無法保證得到正確的結(jié)果。
示例:
// good
require(['foo'], function (foo) {
});
// bad
var foo = require('foo');
[強(qiáng)制] 模塊定義中只允許使用 local require
,不允許使用 global require
。
解釋:
- 在模塊定義中使用
global require
,對(duì)封裝性是一種破壞。 - 在 AMD 里,
global require
是可以被重命名的。并且 Loader 甚至沒有全局的require
變量,而是用 Loader 名稱做為global require
。模塊定義不應(yīng)該依賴使用的 Loader。
[強(qiáng)制] Package 在實(shí)現(xiàn)時(shí),內(nèi)部模塊的 require
必須使用 relative id
。
解釋:
對(duì)于任何可能通過 發(fā)布-引入 的形式復(fù)用的第三方庫、框架、包,開發(fā)者所定義的名稱不代表使用者使用的名稱。因此不要基于任何名稱的假設(shè)。在實(shí)現(xiàn)源碼中,require
自身的其它模塊時(shí)使用 relative id
。
示例:
define(
function (require) {
var util = require('./util');
}
);
[建議] 不會(huì)被調(diào)用的依賴模塊,在 factory
開始處統(tǒng)一 require
。
解釋:
有些模塊是依賴的模塊,但不會(huì)在模塊實(shí)現(xiàn)中被直接調(diào)用,最為典型的是 css
/ js
/ tpl
等 Plugin 所引入的外部?jī)?nèi)容。此類內(nèi)容建議放在模塊定義最開始處統(tǒng)一引用。
示例:
define(
function (require) {
require('css!foo.css');
require('tpl!bar.tpl.html');
// ...
}
);
4.2 DOM
4.2.1 元素獲取
[建議] 對(duì)于單個(gè)元素,盡可能使用 document.getElementById
獲取,避免使用document.all
。
[建議] 對(duì)于多個(gè)元素的集合,盡可能使用 context.getElementsByTagName
獲取。其中 context
可以為 document
或其他元素。指定 tagName
參數(shù)為 *
可以獲得所有子元素。
[建議] 遍歷元素集合時(shí),盡量緩存集合長(zhǎng)度。如需多次操作同一集合,則應(yīng)將集合轉(zhuǎn)為數(shù)組。
解釋:
原生獲取元素集合的結(jié)果并不直接引用 DOM 元素,而是對(duì)索引進(jìn)行讀取,所以 DOM 結(jié)構(gòu)的改變會(huì)實(shí)時(shí)反映到結(jié)果中。
示例:
<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>
[建議] 獲取元素的直接子元素時(shí)使用 children
。避免使用childNodes
,除非預(yù)期是需要包含文本、注釋和屬性類型的節(jié)點(diǎn)。
4.2.2 樣式獲取
[建議] 獲取元素實(shí)際樣式信息時(shí),應(yīng)使用 getComputedStyle
或 currentStyle
。
解釋:
通過 style 只能獲得內(nèi)聯(lián)定義或通過 JavaScript 直接設(shè)置的樣式。通過 CSS class 設(shè)置的元素樣式無法直接通過 style 獲取。
4.2.3 樣式設(shè)置
[建議] 盡可能通過為元素添加預(yù)定義的 className 來改變?cè)貥邮剑苊庵苯硬僮?style 設(shè)置。
[強(qiáng)制] 通過 style 對(duì)象設(shè)置元素樣式時(shí),對(duì)于帶單位非 0 值的屬性,不允許省略單位。
解釋:
除了 IE,標(biāo)準(zhǔn)瀏覽器會(huì)忽略不規(guī)范的屬性值,導(dǎo)致兼容性問題。
4.2.4 DOM 操作
[建議] 操作 DOM
時(shí),盡量減少頁面 reflow
。
解釋:
頁面 reflow 是非常耗時(shí)的行為,非常容易導(dǎo)致性能瓶頸。下面一些場(chǎng)景會(huì)觸發(fā)瀏覽器的reflow:
- DOM元素的添加、修改(內(nèi)容)、刪除。
- 應(yīng)用新的樣式或者修改任何影響元素布局的屬性。
- Resize瀏覽器窗口、滾動(dòng)頁面。
- 讀取元素的某些屬性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 。
[建議] 盡量減少 DOM
操作。
解釋:
DOM 操作也是非常耗時(shí)的一種操作,減少 DOM 操作有助于提高性能。舉一個(gè)簡(jiǎn)單的例子,構(gòu)建一個(gè)列表。我們可以用兩種方式:
- 在循環(huán)體中 createElement 并 append 到父元素中。
- 在循環(huán)體中拼接 HTML 字符串,循環(huán)結(jié)束后寫父元素的 innerHTML。
第一種方法看起來比較標(biāo)準(zhǔn),但是每次循環(huán)都會(huì)對(duì) DOM 進(jìn)行操作,性能極低。在這里推薦使用第二種方法。
4.2.5 DOM 事件
[建議] 優(yōu)先使用 addEventListener / attachEvent
綁定事件,避免直接在 HTML 屬性中或 DOM 的 expando
屬性綁定事件處理。
解釋:
expando 屬性綁定事件容易導(dǎo)致互相覆蓋。
[建議] 使用 addEventListener
時(shí)第三個(gè)參數(shù)使用 false
。
解釋:
標(biāo)準(zhǔn)瀏覽器中的 addEventListener 可以通過第三個(gè)參數(shù)指定兩種時(shí)間觸發(fā)模型:冒泡和捕獲。而 IE 的 attachEvent 僅支持冒泡的事件觸發(fā)。所以為了保持一致性,通常 addEventListener 的第三個(gè)參數(shù)都為 false。