# JavaScript 編碼規范指南
#### 以下文檔大多來自:
* [Google JavaScript 編碼規范指南](http://wyz.67ge.com/google-js/javascriptguide.xml)
* [Idiomatic 風格](https://github.com/rwldrn/idiomatic.js/tree/master/translations/zh_CN)
#### 參考規范
* [ECMAScript 5.1 注解版](http://es5.github.com/)
* [EcmaScript 語言規范, 5.1 版](http://ecma-international.org/ecma-262/5.1/)
#### 基本原則: 無論有多少人在維護,所有在代碼倉庫中的代碼理應看起來像同一個人寫的。
------------------------------------------------
## 前言
下面的章節描述的是一個 _合理_ 的現代 JavaScript 開發風格指南,并非硬性規定。其想送出的核心理念是*高度統一的代碼風格*(the law of code style consistency)。你為項目所擇風格都應為最高準則。作為一個描述放置于你的項目中,并鏈接到這個文檔作為代碼風格一致性、可讀性和可維護性的保證。
### 項目約定
#### 項目規范
1. JS/CSS 文件編碼統一采用 UTF8編碼
1. 代碼縮進使用4個空格縮進替代 tab 縮進
1. 一行代碼長度盡量保持80列左右
1. 如果編輯器支持在文件保存的時候自動刪除行末和空行中的空格(注意:要么全部采用,要么全不采用,否則會產生過多的diff信息)
1. JS/CSS 最終發布到產品中的時候需要被壓縮,以減小靜態資源文件大小,提升頁面加載速度
1. JS里聲明變量必須加上 var 關鍵字,推薦一個 var 同時聲明多個變量,或者一組有邏輯關系的變量,避免一個變量一個 var.
1. 使用 Array 和 Object 語法直接聲明并將其初始化,更易讀且性能更好, 而不使用 Array 和 Object 構造器.
1. JS里使用單引號 (') 優于雙引號 (").
1. JS代碼結尾統一約定加';'
1. 沒有特殊原因避免使用 with/eval
1. 對于if/else等后面的語句即使只有一行代碼也需要在該行代碼的首尾加上'{}'.
1. 字符串拼接在少量(次數為個位數)的情況下可以使用'+', 大量的時候使用數組 join(), 或者盡可能采用模板引擎渲染:比如jsRender等, 如果是Extjs可以采用XTemplate.
1. 對于數組賦值操作快于 push()操作, 所以盡量使用賦值操作.
1. for循環遍歷:for(var i = 0, l = arr.length; i < l; i++){// doSomething here } 采用這種方式而不是 i < arr.length, 前一種方式只會計算一次 arr 的長度,而后一種方式會計算數組長度 arr.length + 1 次,效率比較低
1. 字符串轉換為整數,推薦使用 parseInt(num, 10) 這種方式,+num 寫法簡單,在操作次數極少的情況下也可以酌情使用。
1. 變量比較的時候總是判斷最好、最精確的值,推薦使用'==='少用'=='(可以參考[jQuery](https://github.com/jquery/jquery/blob/master/src/core.js)代碼里面, 可以看到只有在'== null'的時候才可能使用'==',其他情況一律使用的是'===').
1. JS里變量命名規范使用 functionNamesLikeThis, variableNamesLikeThis, ClassNamesLikeThis, EnumNamesLikeThis, methodNamesLikeThis, 和 SYMBOLIC_CONSTANTS_LIKE_THIS, 尤其不要跟python里面的變量命名方式混淆了.
1. JS文件名應該使用小寫字符, 以避免在有些系統平臺上不識別大小寫的命名方式. 文件名以.js結尾, 不要包含除 '-' 和 '_' 外的標點符號(使用 '-' 優于 '_'), 我們約定統一使用js-file-name.js這種類型,對于template文件命名方式為 template_name.html形式.
1. 所有的html DOM里面的id, Extjs配置項里面的id 以及所有樣式里面的 class命名使用中劃線,如'id-name'/'class-name'.
1. 公共的js第三方類庫放在static/js/common/lib下,jQuery相關類庫放在static/js/common/lib/jQplugin下,我們自己開發的公共類庫放在static/js/common下
1. 鑒于有很多代碼是復制粘貼過來的,所以大家要保證自己的代碼風格良好且易于閱讀,不然別人拷過去后不好的風格就蔓延開了,而且會導致其他人效仿。
1. 對于復制粘貼然后做相應修改以實現功能的代碼,請務必清理干凈,不要有'忘了刪除的不影響邏輯的代碼',同時記得將變量名改成適合當前業務場景的有意義的變量名, 不要因為不影響邏輯就保留原來的不適合當前場景的名字
1. 對于系統中出現的大段注釋的、過時的、廢棄的代碼務必及時清理干凈,誰制造誰清理,否則其他人也不敢清理,越積越多
1. 不要使用魔法數字,盡量定義一個常量來表示該數字,并加上相應的注釋,否則后期可能出現因為數字變化而導致牽一發而動全身,需要到處修改,增加維護成本
1. 注釋盡量采用jsdoc的代碼注釋風格,普通業務代碼不做要求,不過通用js類庫要求盡量詳盡以方便其他人閱讀使用
1. 在開發相應功能的時候盡量抽象化、組件化、通用化:考慮這個東西其他地方會不會用到,能不能做成一個組件?而不是類似的代碼到處復制、修改或者讓大家都去寫一遍
1. 類似地,在解決問題的時候要考慮下其他地方會不會存在同樣的問題?能不能統一解決掉?尤其對于類似ExtJs的Bug這種,能不能做最少的改動解決所有同樣的問題,類似于全局補丁.
1. 代碼風格跟其他JS文件的代碼風格保持一致
1. 新增、修改、查看等表單在 popup的時候需要重新初始化清除原來數據
1. 代碼提交前用JSHint檢查一下,可以通過 grunt check 命令來執行檢查,grunt具體配置可以參考[文檔](http://pha.hzdiv.qizhitech.com/w/guides/grunt_build/)
#### 前端資源Build
項目必須總是提供一些通用的方法來檢驗(can be linted)、測試和壓縮源碼以為產品階段使用做準備。對于此類工作 Ben Alman 所寫的 [grunt](https://github.com/cowboy/grunt) 可謂首屈一指。通過簡單的配置即可完成自動對CSS進行檢查/壓縮/合并,對JS進行檢查/壓縮/合并,對html文件進行壓縮,刪除創建目錄,拷貝文件,壓縮打包等,十分方便。
參考的grunt配置文件:
```
/**
* Grunt file for front end resource check/minify/compress.
* Date : 2013/07/23
* copyright : (c) 2013 by QiZhi.
*/
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
options: {
asi : true,
curly : false,
eqeqeq : true,
immed : false,
latedef : true,
newcap : true,
noarg : true,
sub : true,
undef : true,
boss : true,
eqnull : true,
smarttabs : true,
browser : true,
jquery : true,
white : false,
laxbreak : false,
laxcomma : true,
expr : true,
devel : false,
globals : {
module : true,
Mousetrap : true,
jQuery : true
}
},
all: ['Gruntfile.js', 'js/**/*.js', '!js/**/*.min.js', '!js/json2.js', '!js/jquery.simplemodal.js']
},
clean: {
release: {
src: ['release/']
}
},
uglify: {
options: {
banner: '/*! <%= pkg.description %> Ver: <%= pkg.version %> Date: <%= grunt.template.today("yyyy/mm/dd HH:MM:ss") %> */\n'
},
dist: {
// Grunt will search for "**/?.js" under "js/" when the "uglify" task
// runs and build the appropriate src-dest file mappings then, so you
// don't need to update the Gruntfile when files are added or removed.
files: [
{
expand : true, // Enable dynamic expansion.
cwd : 'js/', // Src matches are relative to this path.
src : ['jquery.simplemodal.js','index.js'], // Actual pattern(s) to match.
dest : 'release/js/' // Destination path prefix.
// ext : '.js' // Dest filepaths will have this extension. '.min.js' is recommeded.
}
]
}
},
cssmin: {
minify: {
expand : true,
cwd : 'style/',
src : ['*.css'],
dest : 'release/style/',
ext : '.css'
}
},
htmlmin: { // Task
dist: { // Target
options: { // Target options
removeComments: true,
collapseWhitespace: true
},
files: { // Dictionary of files
'release/index.html': 'index.html' // 'destination': 'source'
}
}
},
copy: {
main: {
files: [
{expand: true, cwd: '.', src: ['js/*.min.js', 'js/json2.js', 'images/*'], dest: 'release/'}
]
}
},
compress: {
main: {
options: {
archive: 'release/executable.zip' // make a zipfile
},
files: [
// {src: ['release/**'], dest: 'release/'}, // includes files in path and its subdirs
{expand: true, cwd: 'release/', src: ['**'], dest: 'executable/'} // makes all src relative to cwd
]
}
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-htmlmin');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-compress');
grunt.registerTask('min' , ['clean', 'uglify', 'cssmin', 'htmlmin', 'copy', 'compress']);
grunt.registerTask('check' , ['jshint']);
grunt.registerTask('default', ['jshint', 'clean', 'uglify', 'cssmin', 'htmlmin', 'copy', 'compress']);
};
```
### 規范詳情
#### JavaScript 編碼風格
##### 空白
- 永遠都不要混用空格和Tab。
- 開始一個項目,在寫代碼之前,選擇軟縮進(空格)或者 Tab(作為縮進方式),并將其作為**最高準則**。
- 為了可讀, 我總是推薦在你的編輯中設計4個字母寬度的縮進 — 這等同于四個空格或者四個空格替代一個 Tab。
- 如果你的編輯器支持,請總是打開 “顯示不可見字符” 這個設置。好處是:
- 保證一致性
- 去掉行末的空格
- 去掉空行的空格
- 提交和對比更具可讀性
##### 行末和空行
留白會破壞 diff 并使diff 結果變得更不可讀。考慮包括一個預提交的 hook 自動刪除行末和空行中的空格。
##### 花括號, 換行
```
// if/else/for/while/try 通常都有小括號、花括號和多行
// 這有助于可讀, 以下是很糟糕的寫法
if(condition) doSomething();
while(condition) iterating++;
for(var i=0;i<100;i++) someIterativeFn();
```
##### 命名
通常, 使用 functionNamesLikeThis, variableNamesLikeThis, ClassNamesLikeThis, EnumNamesLikeThis, methodNamesLikeThis, 和 SYMBOLIC_CONSTANTS_LIKE_THIS.
###### 屬性和方法
文件或類中的 私有 屬性, 變量和方法名應該以下劃線 "_" 開頭.
保護 屬性, 變量和方法名不需要下劃線開頭, 和公共變量名一樣.
###### 方法和函數參數
可選參數以 opt_ 開頭.
函數的參數個數不固定時, 應該添加最后一個參數 var_args 為參數的個數. 你也可以不設置 var_args而取代使用 arguments.
可選和可變參數應該在 @param 標記中說明清楚. 雖然這兩個規定對編譯器沒有任何影響, 但還是請盡量遵守
###### Getters 和 Setters
Getters 和 setters 并不是必要的. 但只要使用它們了, 就請將 getters 命名成 getFoo() 形式, 將 setters 命名成 setFoo(value) 形式. (對于布爾類型的 getters, 使用 isFoo() 也可.)
##### 命名空間
JavaScript 不支持包和命名空間.
不容易發現和調試全局命名的沖突, 多個系統集成時還可能因為命名沖突導致很嚴重的問題. 為了提高 JavaScript 代碼復用率, 我們遵循下面的約定以避免沖突.
###### 為全局代碼使用命名空間
在全局作用域上, 使用一個唯一的, 與工程/庫相關的名字作為前綴標識. 比如, 你的工程是 "Project Sloth", 那么命名空間前綴可取為 sloth.*.
```
var sloth = {};
sloth.sleep = function() {
...
};
```
許多 JavaScript 庫, 包括 the Closure Library and Dojo toolkit 為你提供了聲明你自己的命名空間的函數. 比如:
```
goog.provide('sloth');
sloth.sleep = function() {
...
};
```
###### 明確命名空間所有權
當選擇了一個子命名空間, 請確保父命名空間的負責人知道你在用哪個子命名空間, 比如說, 你為工程 'sloths' 創建一個 'hats' 子命名空間, 那確保 Sloth 團隊人員知道你在使用 sloth.hats.
###### 外部代碼和內部代碼使用不同的命名空間
"外部代碼" 是指來自于你代碼體系的外部, 可以獨立編譯. 內外部命名應該嚴格保持獨立. 如果你使用了外部庫, 他的所有對象都在 foo.hats.* 下, 那么你自己的代碼不能在 foo.hats.*下命名, 因為很有可能其他團隊也在其中命名.
```
foo.require('foo.hats');
/**
* WRONG -- Do NOT do this.
* @constructor
* @extend {foo.hats.RoundHat}
*/
foo.hats.BowlerHat = function() {
};
```
##### 重命名那些名字很長的變量, 提高可讀性
主要是為了提高可讀性. 局部空間中的變量別名只需要取原名字的最后部分.
```
/**
* @constructor
*/
some.long.namespace.MyClass = function() {
};
/**
* @param {some.long.namespace.MyClass} a
*/
some.long.namespace.MyClass.staticHelper = function(a) {
...
};
myapp.main = function() {
var MyClass = some.long.namespace.MyClass;
var staticHelper = some.long.namespace.MyClass.staticHelper;
staticHelper(new MyClass());
};
```
##### 除非是枚舉類型, 不然不要訪問別名變量的屬性.
```
/** @enum {string} */
some.long.namespace.Fruit = {
APPLE: 'a',
BANANA: 'b'
};
myapp.main = function() {
var Fruit = some.long.namespace.Fruit;
switch (fruit) {
case Fruit.APPLE:
...
case Fruit.BANANA:
...
}
};
myapp.main = function() {
var MyClass = some.long.namespace.MyClass;
MyClass.staticHelper(null);
};
```
不要在全局范圍內創建別名, 而僅在函數塊作用域中使用.
##### 文件名
文件名應該使用小寫字符, 以避免在有些系統平臺上不識別大小寫的命名方式. 文件名以.js結尾, 不要包含除 - 和 _ 外的標點符號(使用 - 優于 _).
##### 自定義 toString() 方法
應該總是成功調用且不要拋異常.
可自定義 toString() 方法, 但確保你的實現方法滿足: (1) 總是成功 (2) 沒有其他負面影響. 如果不滿足這兩個條件, 那么可能會導致嚴重的問題, 比如, 如果 toString() 調用了包含 assert 的函數, assert 輸出導致失敗的對象, 這在 toString() 也會被調用.
##### 延遲初始化
可以
沒必要在每次聲明變量時就將其初始化.
##### 明確作用域
任何時候都需要
任何時候都要明確作用域 - 提高可移植性和清晰度. 例如, 不要依賴于作用域鏈中的 window 對象. 可能在其他應用中, 你函數中的 window 不是指之前的那個窗口對象.
##### 代碼格式化
###### 大括號
分號會被隱式插入到代碼中, 所以你務必在同一行上插入大括號. 例如:
```
if (something) {
// ...
} else {
// ...
}
```
###### 數組和對象的初始化
如果初始值不是很長, 就保持寫在單行上:
```
var arr = [1, 2, 3]; // No space after [ or before ].
var obj = {a: 1, b: 2, c: 3}; // No space after { or before }.
```
初始值占用多行時, 縮進2個空格.
```
// Object initializer.
var inset = {
top: 10,
right: 20,
bottom: 15,
left: 12
};
// Array initializer.
this.rows_ = [
'"Slartibartfast" <fjordmaster@magrathea.com>',
'"Zaphod Beeblebrox" <theprez@universe.gov>',
'"Ford Prefect" <ford@theguide.com>',
'"Arthur Dent" <has.no.tea@gmail.com>',
'"Marvin the Paranoid Android" <marv@googlemail.com>',
'the.mice@magrathea.com'
];
// Used in a method call.
goog.dom.createDom(goog.dom.TagName.DIV, {
id: 'foo',
className: 'some-css-class',
style: 'display:none'
}, 'Hello, world!');
```
###### 函數參數
盡量讓函數參數在同一行上. 如果一行超過 80 字符, 每個參數獨占一行, 并以4個空格縮進, 或者與括號對齊, 以提高可讀性. 盡可能不要讓每行超過80個字符. 比如下面這樣:
```
// Four-space, wrap at 80. Works with very long function names, survives
// renaming without reindenting, low on space.
goog.foo.bar.doThingThatIsVeryDifficultToExplain = function(
veryDescriptiveArgumentNumberOne, veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy, artichokeDescriptorAdapterIterator) {
// ...
};
// Four-space, one argument per line. Works with long function names,
// survives renaming, and emphasizes each argument.
goog.foo.bar.doThingThatIsVeryDifficultToExplain = function(
veryDescriptiveArgumentNumberOne,
veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy,
artichokeDescriptorAdapterIterator) {
// ...
};
// Parenthesis-aligned indentation, wrap at 80. Visually groups arguments,
// low on space.
function foo(veryDescriptiveArgumentNumberOne, veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy, artichokeDescriptorAdapterIterator) {
// ...
}
// Parenthesis-aligned, one argument per line. Visually groups and
// emphasizes each individual argument.
function bar(veryDescriptiveArgumentNumberOne,
veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy,
artichokeDescriptorAdapterIterator) {
// ...
}
```
###### 傳遞匿名函數
如果參數中有匿名函數, 函數體從調用該函數的左邊開始縮進2個空格, 而不是從 function 這個關鍵字開始. 這讓匿名函數更加易讀 (不要增加很多沒必要的縮進讓函數體顯示在屏幕的右側).
```
var names = items.map(function(item) {
return item.name;
});
prefix.something.reallyLongFunctionName('whatever', function(a1, a2) {
if (a1.equals(a2)) {
someOtherLongFunctionName(a1);
} else {
andNowForSomethingCompletelyDifferent(a2.parrot);
}
});
```
###### 更多的縮進
事實上, 除了 初始化數組和對象 , 和傳遞匿名函數外, 所有被拆開的多行文本要么選擇與之前的表達式左對齊, 要么以4個(而不是2個)空格作為一縮進層次.
```
someWonderfulHtml = '' +
getEvenMoreHtml(someReallyInterestingValues, moreValues,
evenMoreParams, 'a duck', true, 72,
slightlyMoreMonkeys(0xfff)) +
'';
thisIsAVeryLongVariableName =
hereIsAnEvenLongerOtherFunctionNameThatWillNotFitOnPrevLine();
thisIsAVeryLongVariableName = 'expressionPartOne' + someMethodThatIsLong() +
thisIsAnEvenLongerOtherFunctionNameThatCannotBeIndentedMore();
someValue = this.foo(
shortArg,
'Some really long string arg - this is a pretty common case, actually.',
shorty2,
this.bar());
if (searchableCollection(allYourStuff).contains(theStuffYouWant) &&
!ambientNotification.isActive() && (client.isAmbientSupported() ||
client.alwaysTryAmbientAnyways()) {
ambientNotification.activate();
}
```
###### 空行
使用空行來劃分一組邏輯上相關聯的代碼片段.
```
doSomethingTo(x);
doSomethingElseTo(x);
andThen(x);
nowDoSomethingWith(y);
andNowWith(z);
```
###### 二元和三元操作符
操作符始終跟隨著前行, 這樣就不用顧慮分號的隱式插入問題. 如果一行實在放不下, 還是按照上述的縮進風格來換行.
```
var x = a ? b : c; // All on one line if it will fit.
// Indentation +4 is OK.
var y = a ?
longButSimpleOperandB : longButSimpleOperandC;
// Indenting to the line position of the first operand is also OK.
var z = a ?
moreComplicatedB :
moreComplicatedC;
```
###### 括號
只在需要的時候使用
不要濫用括號, 只在必要的時候使用它.
對于一元操作符(如delete, typeof 和 void ), 或是在某些關鍵詞(如 return, throw, case, new )之后, 不要使用括號.
###### 字符串
使用 ' 優于 "
無論你選擇單引號還是雙引號都無所謂,在 JavaScript 中它們在解析上沒有區別。而**絕對需要**強制的是一致性。 **永遠不要在同一個項目中混用兩種引號,選擇一種,并保持一致**。
單引號 (') 優于雙引號 ("). 當你創建一個包含 HTML 代碼的字符串時就知道它的好處了.
```
var msg = 'This is some HTML';
```
###### 前置逗號(Comma First)
請勿使用。所有使用這個文檔作為基本風格指南的項目都不允許前置逗號的代碼格式,除非明確指定或者作者要求。
###### 注釋
* 單行注釋放于代碼上方為首選
* 多行也可以
* 行末注釋應被避免!
* JSDoc 的方式也不錯,但需要比較多的時間
###### 使用 JSDoc
我們使用 JSDoc 中的注釋風格. 行內注釋使用 // 變量 的形式. 另外, 我們也遵循 C++ 代碼注釋風格 . 這也就是說你需要:
* 版權和著作權的信息,
* 文件注釋中應該寫明該文件的基本信息(如, 這段代碼的功能摘要, 如何使用, 與哪些東西相關), 來告訴那些不熟悉代碼的讀者.
* 類, 函數, 變量和必要的注釋,
* 期望在哪些瀏覽器中執行,
* 正確的大小寫, 標點和拼寫.
* 為了避免出現句子片段, 請以合適的大/小寫單詞開頭, 并以合適的標點符號結束這個句子.
現在假設維護這段代碼的是一位初學者. 這可能正好是這樣的!
目前很多編譯器可從 JSDoc 中提取類型信息, 來對代碼進行驗證, 刪除和壓縮. 因此, 你很有必要去熟悉正確完整的 JSDoc .
###### 頂層/文件注釋
頂層注釋用于告訴不熟悉這段代碼的讀者這個文件中包含哪些東西. 應該提供文件的大體內容, 它的作者, 依賴關系和兼容性信息. 如下:
```
// Copyright 2009 Google Inc. All Rights Reserved.
/**
* @fileoverview Description of file, its uses and information
* about its dependencies.
* @author user@google.com (Firstname Lastname)
*/
```
###### 類注釋
每個類的定義都要附帶一份注釋, 描述類的功能和用法. 也需要說明構造器參數. 如果該類繼承自其它類, 應該使用 @extends 標記. 如果該類是對接口的實現, 應該使用 @implements 標記.
```
/**
* Class making something fun and easy.
* @param {string} arg1 An argument that makes this more interesting.
* @param {Array.<number>} arg2 List of numbers to be processed.
* @constructor
* @extends {goog.Disposable}
*/
project.MyClass = function(arg1, arg2) {
// ...
};
goog.inherits(project.MyClass, goog.Disposable);
```
###### 方法與函數的注釋
提供參數的說明. 使用完整的句子, 并用第三人稱來書寫方法說明.
```
/**
* Converts text to some completely different text.
* @param {string} arg1 An argument that makes this more interesting.
* @return {string} Some return value.
*/
project.MyClass.prototype.someMethod = function(arg1) {
// ...
};
/**
* Operates on an instance of MyClass and returns something.
* @param {project.MyClass} obj Instance of MyClass which leads to a long
* comment that needs to be wrapped to two lines.
* @return {boolean} Whether something occured.
*/
function PR_someMethod(obj) {
// ...
}
```
對于一些簡單的, 不帶參數的 getters, 說明可以忽略.
```
/**
* @return {Element} The element for the component.
*/
goog.ui.Component.prototype.getElement = function() {
return this.element_;
};
```
###### 屬性注釋
也需要對屬性進行注釋.
```
/**
* Maximum number of things per pane.
* @type {number}
*/
project.MyClass.prototype.someProperty = 4;
```
###### JSDoc 縮進
如果你在 @param, @return, @supported, @this 或 @deprecated 中斷行, 需要像在代碼中一樣, 使用4個空格作為一個縮進層次.
```
/**
* Illustrates line wrapping for long param/return descriptions.
* @param {string} foo This is a param with a description too long to fit in
* one line.
* @return {number} This returns something that has a description too long to
* fit in one line.
*/
project.MyClass.prototype.method = function(foo) {
return 5;
};
```
不要在 @fileoverview 標記中進行縮進.
雖然不建議, 但也可對說明文字進行適當的排版對齊. 不過, 這樣帶來一些負面影響, 就是當你每次修改變量名時, 都得重新排版說明文字以保持和變量名對齊.
```
/**
* This is NOT the preferred indentation method.
* @param {string} foo This is a param with a description too long to fit in
* one line.
* @return {number} This returns something that has a description too long to
* fit in one line.
*/
project.MyClass.prototype.method = function(foo) {
return 5;
};
```
###### 枚舉
```
/**
* Enum for tri-state values.
* @enum {number}
*/
project.TriState = {
TRUE: 1,
FALSE: -1,
MAYBE: 0
};
```
注意一下, 枚舉也具有有效類型, 所以可以當成參數類型來用.
```
/**
* Sets project state.
* @param {project.TriState} state New project state.
*/
project.setState = function(state) {
// ...
};
```
#### JavaScript 語言規范
##### 變量
聲明變量必須加上 var 關鍵字.
當你沒有寫 var, 變量就會暴露在全局上下文中, 這樣很可能會和現有變量沖突. 另外, 如果沒有加上, 很難明確該變量的作用域是什么, 變量也很可能像在局部作用域中, 很輕易地泄漏到 Document 或者 Window 中, 所以務必用 var 去聲明變量.
```
// 變量
var foo = "bar",
num = 1,
undef;
// 字面量標識:
var array = [],
object = {};
// 在一個作用域(函數)內只使用一個 `var` 有助于提升可讀性
// 并且讓你的聲明列表變得有條不紊 (還幫你省了幾次鍵盤敲擊)
// 不好
var foo = "";
var bar = "";
var qux;
// 好的做法
var foo = "",
bar = "",
quux;
// 或者..
var // 對這些變量的注釋
foo = "",
bar = "",
quux;
// `var` 語句必須總是在各自作用域(函數)頂部
// 同樣適應于來自 ECMAScript 6 的常量
// 不好
function foo() {
// 在變量前有語句
var bar = "",
qux;
}
// 好
function foo() {
var bar = "",
qux;
// 所有語句都在變量之后
}
```
##### 常量
常量的形式如: NAMES_LIKE_THIS, 即使用大寫字符, 并用下劃線分隔. 你也可用 @const 標記來指明它是一個常量. 但請永遠不要使用 const 關鍵詞.
Decision:
對于基本類型的常量, 只需轉換命名.
```
/**
* The number of seconds in a minute.
* @type {number}
*/
goog.example.SECONDS_IN_A_MINUTE = 60;
```
對于非基本類型, 使用 @const 標記.
```
/**
* The number of seconds in each of the given units.
* @type {Object.<number>}
* @const
*/
goog.example.SECONDS_TABLE = {
minute: 60,
hour: 60 * 60,
day: 60 * 60 * 24
}
```
這標記告訴編譯器它是常量.
至于關鍵詞 const, 因為 IE 不能識別, 所以不要使用.
##### 分號
總是使用分號.
如果僅依靠語句間的隱式分隔, 有時會很麻煩. 你自己更能清楚哪里是語句的起止. 而且有些情況下, 漏掉分號會很危險,可能會導致代碼合并錯誤等,又比如:
```
// 1.
MyClass.prototype.myMethod = function() {
return 42;
} // No semicolon here.
(function() {
// Some initialization code wrapped in a function to create a scope for locals.
})();
var THINGS_TO_EAT = [apples, oysters, sprayOnCheese] // No semicolon here.
// 2. conditional execution a la bash
-1 == resultOfOperation() || die();
```
這段代碼會發生些什么詭異事呢?
1. 報 JavaScript 錯誤 - 例子1上的語句會解釋成, 一個函數帶一匿名函數作為參數而被調用, 返回42后, 又一次被"調用", 這就導致了錯誤.
1. 當 resultOfOperation() 返回非 NaN 時, 就會調用die, 其結果也會賦給 THINGS_TO_EAT.
為什么?
JavaScript 的語句以分號作為結束符, 除非可以非常準確推斷某結束位置才會省略分號. 上面的例子產出錯誤, 均是在語句中聲明了函數/對象/數組直接量, 但 閉括號('}'或']')并不足以表示該語句的結束. 在 JavaScript 中, 只有當語句后的下一個符號是后綴或括號運算符時, 才會認為該語句的結束. 參考:[JS分號自動插入機制](http://justjavac.iteye.com/blog/1852405)
遺漏分號有時會出現很奇怪的結果, 所以確保語句以分號結束.
##### 塊內函數聲明
不要在塊內聲明一個函數,不要寫成:
```
if (x) {
function foo() {}
}
```
雖然很多 JS 引擎都支持塊內聲明函數, 但它不屬于 ECMAScript 規范 (見 [ECMA-262](http://www.ecma-international.org/publications/standards/Ecma-262.htm), 第13和14條). 各個瀏覽器糟糕的實現相互不兼容, 有些也與未來 ECMAScript 草案相違背. ECMAScript 只允許在腳本的根語句或函數中聲明函數. 如果確實需要在塊中定義函數, 建議使用函數表達式來初始化變量:
```
if (x) {
var foo = function() {}
}
```
##### 標準特性
標準特性總是優于非標準特性.
最大化可移植性和兼容性, 盡量使用標準方法而不是用非標準方法, (比如, 優先用string.charAt(3) 而不用 string[3] , 通過 DOM 原生函數訪問元素, 而不是使用應用封裝好的快速接口.
##### 不要封裝基本類型
沒有任何理由去封裝基本類型, 另外還存在一些風險:
```
var x = new Boolean(false);
if (x) {
alert('hi'); // Shows 'hi'.
}
```
除非明確用于類型轉換, 其他情況請千萬不要這樣做!
```
var x = Boolean(0);
if (x) {
alert('hi'); // This will never be alerted.
}
typeof Boolean(0) == 'boolean';
typeof new Boolean(0) == 'object';
```
有時用作 number, string 或 boolean時, 類型的轉換會非常實用.
##### 閉包
可以, 但小心使用.
閉包也許是 JS 中最有用的特性了. 有一份比較好的介紹閉包原理的[文檔](http://jibbering.com/faq/notes/closures/).
有一點需要牢記, 閉包保留了一個指向它封閉作用域的指針, 所以, 在給 DOM 元素附加閉包時, 很可能會產生循環引用, 進一步導致內存泄漏. 比如下面的代碼:
```
function foo(element, a, b) {
element.onclick = function() { /* uses a and b */ };
}
```
這里, 即使沒有使用 element, 閉包也保留了 element, a 和 b 的引用, 由于 element 也保留了對閉包的引用, 這就產生了循環引用, 這就不能被 GC 回收. 這種情況下, 可將代碼重構為:
```
function foo(element, a, b) {
element.onclick = bar(a, b);
}
function bar(a, b) {
return function() { /* uses a and b */ }
}
```
##### eval()
只用于解析序列化串 (如: 解析 RPC 響應), 而且解析序列號字符串用JSON.parse()會更好,所以最好放棄使用 eval().
eval() 會讓程序執行的比較混亂, 當 eval() 里面包含用戶輸入的話就更加危險. 可以用其他更佳的, 更清晰, 更安全的方式寫你的代碼, 所以一般情況下請不要使用 eval(). 當碰到一些需要解析序列化串的情況下(如, 計算 RPC 響應), 使用 eval 很容易實現.
解析序列化串是指將字節流轉換成內存中的數據結構. 比如, 你可能會將一個對象輸出成文件形式:
```
users = [
{
name: 'Eric',
id: 37824,
email: 'jellyvore@myway.com'
},
{
name: 'xtof',
id: 31337,
email: 'b4d455h4x0r@google.com'
},
...
];
```
很簡單地調用 eval 后, 把表示成文件的數據讀取回內存中.
類似的, eval() 對 RPC 響應值進行解碼. 例如, 你在使用 XMLHttpRequest 發出一個 RPC 請求后, 通過 eval () 將服務端的響應文本轉成 JavaScript 對象:
```
var userOnline = false;
var user = 'nusrat';
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', 'http://chat.google.com/isUserOnline?user=' + user, false);
xmlhttp.send('');
// Server returns:
// userOnline = true;
if (xmlhttp.status == 200) {
eval(xmlhttp.responseText);
}
// userOnline is now true.
```
##### with() {}
不要使用
使用 with 讓你的代碼在語義上變得不清晰. 因為 with 的對象, 可能會與局部變量產生沖突, 從而改變你程序原本的用義. 下面的代碼是干嘛的?
```
with (foo) {
var x = 3;
return x;
}
```
答案: 任何事. 局部變量 x 可能被 foo 的屬性覆蓋, 當它定義一個 setter 時, 在賦值 3 后會執行很多其他代碼. 所以不要使用 with 語句.
##### this
僅在對象構造器, 方法, 閉包中使用.
this 的語義很特別. 有時它引用一個全局對象(大多數情況下), 調用者的作用域(使用 eval時), DOM 樹中的節點(添加事件處理函數時), 新創建的對象(使用一個構造器), 或者其他對象(如果函數被 call() 或 apply()).
使用時很容易出錯, 所以只有在下面兩個情況時才能使用:
* 在構造器中
* 對象的方法(包括創建的閉包)中
##### for-in 循環
只用于 object/map/hash 的遍歷
對 Array 用 for-in 循環有時會出錯. 因為它并不是從 0 到 length - 1 進行遍歷, 而是所有出現在對象及其原型鏈的鍵值. 下面就是一些失敗的使用案例:
```
function printArray(arr) {
for (var key in arr) {
print(arr[key]);
}
}
printArray([0,1,2,3]); // This works.
var a = new Array(10);
printArray(a); // This is wrong.
a = document.getElementsByTagName('*');
printArray(a); // This is wrong.
a = [0,1,2,3];
a.buhu = 'wine';
printArray(a); // This is wrong again.
a = new Array;
a[3] = 3;
printArray(a); // This is wrong again.
// 而遍歷數組通常用最普通的 for 循環.
function printArray(arr) {
var l = arr.length;
for (var i = 0; i < l; i++) {
print(arr[i]);
}
}
```
##### 多行字符串
不要使用. 不要這樣寫長字符串:
```
var myString = 'A rather long string of English text, an error message \
actually that just keeps going and going -- an error \
message to make the Energizer bunny blush (right through \
those Schwarzenegger shades)! Where was I? Oh yes, \
you\'ve got an error and all the extraneous whitespace is \
just gravy. Have a nice day.';
```
在編譯時, 不能忽略行起始位置的空白字符; "\" 后的空白字符會產生奇怪的錯誤; 雖然大多數腳本引擎支持這種寫法, 但它不是 ECMAScript 的標準規范.
##### Array 和 Object 直接量
使用 Array 和 Object 語法, 而不使用 Array 和 Object 構造器.
使用 Array 構造器很容易因為傳參不恰當導致錯誤.
```
// Length is 3.
var a1 = new Array(x1, x2, x3);
// Length is 2.
var a2 = new Array(x1, x2);
// If x1 is a number and it is a natural number the length will be x1.
// If x1 is a number but not a natural number this will throw an exception.
// Otherwise the array will have one element with x1 as its value.
var a3 = new Array(x1);
// Length is 0.
var a4 = new Array();
```
如果傳入一個參數而不是2個參數, 數組的長度很有可能就不是你期望的數值了.
為了避免這些歧義, 我們應該使用更易讀的直接量來聲明.
```
var a = [x1, x2, x3];
var a2 = [x1, x2];
var a3 = [x1];
var a4 = [];
```
雖然 Object 構造器沒有上述類似的問題, 但鑒于可讀性和一致性考慮, 最好還是在字面上更清晰地指明. 例如:
```
var o = new Object();
var o2 = new Object();
o2.a = 0;
o2.b = 1;
o2.c = 2;
o2['strange key'] = 3;
```
應該寫成:
```
var o = {};
var o2 = {
a: 0,
b: 1,
c: 2,
'strange key': 3
};
```
##### 不要修改內置對象的原型
千萬不要修改內置對象, 如 Object.prototype 和 Array.prototype 的原型. 而修改內置對象, 如 Function.prototype 的原型, 雖然少危險些, 但仍會導致調試時的詭異現象. 所以也要避免修改其原型.
#### JavaScript 小技巧(Tips and Tricks)
##### True 和 False 布爾表達式
下面的布爾表達式都返回 false:
```
null
undefined
'' // 空字符串
0 //數字0
```
但小心下面的, 可都返回 true:
```
'0' // 字符串0
[] // 空數組
{} // 空對象
```
下面段比較糟糕的代碼:
```
while (x != null) {
```
你可以直接寫成下面的形式(只要你希望 x 不是 0 和空字符串, 和 false):
```
while (x) {
```
如果你想檢查字符串是否為 null 或空:
```
if (y != null && y != '') {
```
但這樣會更好:
```
if (y) {
```
注意: 還有很多需要注意的地方, 如:
```
Boolean('0') == true
'0' != true
0 != null
0 == []
0 == false
Boolean(null) == false
null != true
null != false
Boolean(undefined) == false
undefined != true
undefined != false
Boolean([]) == true
[] != true
[] == false
Boolean({}) == true
{} != true
{} != false
```
##### 條件(三元)操作符 (?:)
三元操作符用于替代下面的代碼:
```
if (val != 0) {
return foo();
} else {
return bar();
}
```
你可以寫成:
```
return val ? foo() : bar();
```
在生成 HTML 代碼時也是很有用的:
```
var html = '<input type="checkbox"' +
(isChecked ? ' checked' : '') +
(isEnabled ? '' : ' disabled') +
' name="foo">';
```
##### && 和 ||
二元布爾操作符是可短路的, 只有在必要時才會計算到最后一項.
"||" 被稱作為 'default' 操作符, 因為可以這樣:
```
/** @param {*=} opt_win */
function foo(opt_win) {
var win;
if (opt_win) {
win = opt_win;
} else {
win = window;
}
// ...
}
```
你可以使用它來簡化上面的代碼:
```
/** @param {*=} opt_win */
function foo(opt_win) {
var win = opt_win || window;
// ...
}
```
"&&" 也可簡短代碼.比如:
```
if (node) {
if (node.kids) {
if (node.kids[index]) {
foo(node.kids[index]);
}
}
}
```
你可以像這樣來使用:
```
if (node && node.kids && node.kids[index]) {
foo(node.kids[index]);
}
```
或者:
```
var kid = node && node.kids && node.kids[index];
if (kid) {
foo(kid);
}
```
不過這樣就有點兒過頭了:
```
node && node.kids && node.kids[index] && foo(node.kids[index]);
```
##### 使用 join() 來創建字符串
通常是這樣使用的:
```
function listHtml(items) {
var html = '<div class="foo">';
for (var i = 0; i < items.length; ++i) {
if (i > 0) {
html += ', ';
}
html += itemHtml(items[i]);
}
html += '</div>';
return html;
}
```
但這樣在 IE 下非常慢, 可以用下面的方式:
```
function listHtml(items) {
var html = [];
for (var i = 0; i < items.length; ++i) {
html[i] = itemHtml(items[i]);
}
return '<div class="foo">' + html.join(', ') + '</div>';
}
```
你也可以是用數組作為字符串構造器, 然后通過 myArray.join('') 轉換成字符串. 不過由于賦值操作快于數組的 push(), 所以盡量使用賦值操作.
##### 類型檢測 (來源于 jQuery Core Style Guidelines)
* 直接類型(實際類型,Actual Types)
String:
typeof variable === "string"
Number:
typeof variable === "number"
Boolean:
typeof variable === "boolean"
Object:
typeof variable === "object"
Array:
Array.isArray(arrayLikeObject)
(如果可能的話)
Node:
elem.nodeType === 1
null:
variable === null
null or undefined:
variable == null
undefined:
全局變量:
typeof variable === "undefined"
局部變量:
variable === undefined
屬性:
object.prop === undefined
object.hasOwnProperty(prop)
"prop" in object
* 轉換類型(強制類型,Coerced Types)
考慮下面這個的含義...
給定的 HTML:
```
<input type="text" id="foo-input" value="1">
```
```
// `foo` 已經被賦予值 `0`,類型為 `number`
var foo = 0;
// typeof foo;
// "number"
...
// 在后續的代碼中,你需要更新 `foo`,賦予在 input 元素中得到的新值
foo = document.getElementById("foo-input").value;
// 如果你現在測試 `typeof foo`, 結果將是 `string`
// 這意味著你在 if 語句檢測 `foo` 有類似于此的邏輯:
if (foo === 1) {
importantTask();
}
// `importantTask()` 將永遠不會被執行,即使 `foo` 有一個值 "1"
// 你可以巧妙地使用 + / - 一元運算符強制轉換類型以解決問題:
foo = +document.getElementById("foo-input").value;
// ^ + 一元運算符將它右邊的運算對象轉換為 `number`
// typeof foo;
// "number"
if (foo === 1) {
importantTask();
}
// `importantTask()` 將被調用
```
對于強制類型轉換這里有幾個例子:
```
var number = 1,
string = "1",
bool = false;
number;
// 1
number + "";
// "1"
string;
// "1"
+string;
// 1
+string++;
// 1
string;
// 2
bool;
// false
+bool;
// 0
bool + "";
// "false"
```
```
var number = 1,
string = "1",
bool = true;
string === number;
// false
string === number + "";
// true
+string === number;
// true
bool === number;
// false
+bool === number;
// true
bool === string;
// false
bool === !!string;
// true
```
```
var num = 2.5;
parseInt(num, 10);
// 等價于...
~~num;
num >> 0;
num >>> 0;
// 結果都是 2
// 時刻牢記心底, 負值將被區別對待...
var neg = -2.5;
parseInt(neg, 10);
// 等價于...
~~neg;
neg >> 0;
// 結果都是 -2
// 但是...
neg >>> 0;
// 結果即是 4294967294
```
* 字符串轉換為整數
將字符串轉換為整數有以下幾種方式,可以在[這里](http://jsperf.com/converting-string-to-int/2)進行測試對比:
```
var number1 = "45";
// ParseInt() Test on chrome for mac:31% slower
var i = parseInt(number1);
// Using unary Test on chrome for mac:67% slower
var j = +number1;
// Number constructor Test on chrome for mac:59% slower
var k = Number(number1);
// By multplication Test on chrome for mac:69% slower
var l = number1 * 1;
// parseInt with Test on chrome for mac:radix ±3.85% fastest
var m = parseInt(number1, 10);
```
所以推薦使用 parseInt(number1, 10) 這種方式,不過 +number1 更為簡單,在操作次數極少的情況下也可以酌情使用。
##### 對比運算
```
// 當只是判斷一個 array 是否有長度,相對于使用這個:
if (array.length > 0) ...
// ...判斷真偽, 請使用這種:
if (array.length) ...
// 當只是判斷一個 array 是否為空,相對于使用這個:
if (array.length === 0) ...
// ...判斷真偽, 請使用這種:
if (!array.length) ...
// 當只是判斷一個 string 是否為空,相對于使用這個:
if (string !== "") ...
// ...判斷真偽, 請使用這種:
if (string) ...
// 當只是判斷一個 string 是為空,相對于使用這個:
if (string === "") ...
// ...判斷真偽, 請使用這種:
if (!string) ...
// 當只是判斷一個引用是為真,相對于使用這個:
if (foo === true) ...
// ...判斷只需像你所想,享受內置功能的好處:
if (foo) ...
// 當只是判斷一個引用是為假,相對于使用這個:
if (foo === false) ...
// ...使用嘆號將其轉換為真
if (!foo) ...
// ...需要注意的是:這個將會匹配 0, "", null, undefined, NaN
// 如果你 _必須_ 是布爾類型的 false,請這樣用:
if (foo === false) ...
// 如果想計算一個引用可能是 null 或者 undefined,但并不是 false, "" 或者 0,
// 相對于使用這個:
if (foo === null || foo === undefined) ...
// ...享受 == 類型強制轉換的好處,像這樣:
if (foo == null) ...
// 謹記,使用 == 將會令 `null` 匹配 `null` 和 `undefined`
// 但不是 `false`,"" 或者 0
null == undefined
```
總是判斷最好、最精確的值,上述是指南而非教條。
```
// 類型轉換和對比運算說明
// 首次 `===`,`==` 次之 (除非需要松散類型的對比)
// `===` 總不做類型轉換,這意味著:
"1" === 1;
// false
// `==` 會轉換類型,這意味著:
"1" == 1;
// true
// 布爾, 真 & 偽
// 布爾:
true, false
// 真:
"foo", 1
// 偽:
"", 0, null, undefined, NaN, void 0
```
##### Misc
這個部分將要說明的想法和理念都并非教條。相反更鼓勵對現存實踐保持好奇,以嘗試提供完成一般 JavaScript 編程任務的更好方案。
* 提前返回值提升代碼的可讀性并且沒有太多性能上的差別
```
// 不好:
function returnLate(foo) {
var ret;
if (foo) {
ret = "foo";
} else {
ret = "quux";
}
return ret;
}
// 好:
function returnEarly(foo) {
if (foo) {
return "foo";
}
return "quux";
}
```
* for循環遍歷
```
// for循環遍歷:
for(var i = 0, l = arr.length; i < l; i++){
// doSomething here
}
// 上面的方式優于:
for(var i = 0; i < arr.length; i++){
// doSomething here
}
// 前一種方式只會計算一次 arr 的長度,而后一種方式會計算 arr.length + 1 次,效率比較低
```
## 保持一致性.
當你在編輯代碼之前, 先花一些時間查看一下現有代碼的風格. 如果他們給算術運算符添加了空格, 你也應該添加. 如果他們的注釋使用一個個星號盒子, 那么也請你使用這種方式.
代碼風格中一個關鍵點是整理一份常用詞匯表, 開發者認同它并且遵循, 這樣在代碼中就能統一表述. 我們在這提出了一些全局上的風格規則, 但也要考慮自身情況形成自己的代碼風格. 但如果你添加的代碼和現有的代碼有很大的區別, 這就讓閱讀者感到很不和諧. 所以, 避免這種情況的發生.