Javascript 编码指南

前言

总所周知,javascript 是一种语法极其灵活的语言。变量随时用随时可以声明;语句结束符可以不要;字符串和数字也可以相加;参数多一个少一个也不会报错。
没错,当你从 C/C++Java 严格的语法规定之下,转向 JavaScript 语言,会觉得自由了很多,轻松了很多。

语法松散是 JavaScript 重要的特征。它灵活易懂,给开发人员带来了很多方便,但如果编写过程中不注意,代码的调试成本和维护成本则会无形地增加。
JavaScript 编码会随应被直接发送到客户端的浏览器,代码规范不只是代码质量的保证,也影响到产品的长期信誉。

本文档的目标是使 JavaScript 代码风格保持一致,良好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。

JavaScript 语言规范

1
2
变量 常量 保留字 数组 字符串 函数 块内函数声明 闭包 Array和Object直接量
对象原型 True与False 类型分配&强制转换 浮点数精度 命名规范

JavaScript 编码风格

1
2
文件编码 分号 逗号 空格 大括号 单引号、双引号 空行 二元和三元操作符 语句块
注释 全局变量 全等

JavaScript 语言规范

变量

  • 声明变量必须加上 var 关键字

    当你没有写 var, 变量就会暴露在全局上下文中, 这样很可能会和现有变量冲突. 另外, 如果没有加上, 很难明确该变量的作用域是什么,

    变量也很可能像在局部作用域中, 很轻易地泄漏到 Document 或者 Window 中, 所以务必用 var 去声明变量.

常量

  • 常量使用大写字符并用下划线分隔,如:PAGE_CONFIG

保留字

  • 不要使用保留字,在IE8中不起作用
1
2
3
4
5
6
7
8
9
10
11
// good
var superman = {
defaults: { clark: 'kent' },
hidden: true
};
// bad
var superman = {
default: { clark: 'kent' },
private: true
};

数组

  • 添加数组元素时,使用push而不是直接添加
1
2
3
4
5
6
7
var someStack = [];
// good
someStack.push('abracadabra');
// bad
someStack[someStack.length] = 'abracadabra';
  • 需要复制数组时,可以使用slice
1
2
3
4
5
6
7
8
9
10
11
var len = items.length;
var itemsCopy = [];
var i;
// good
itemsCopy = items.slice();
// bad
for (i = 0; i < len; i++) {
itemsCopy[i] = items[i];
}
  • 使用slice将类数组对象转为数组
1
2
3
4
function trigger() {
var args = Array.prototype.slice.call(arguments);
...
}
  • 遍历数组不使用 for in

数组对象可能存在数字以外的属性, 这种情况下 for in 不会得到正确结果.

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

清空数组使用 .length = 0

字符串

  • 对字符串使用单引号

超过80个字符的字符串应该使用字符串连接符进行跨行(对长字符串过度使用连接符将会影响性能)

1
2
3
4
5
6
7
8
9
10
11
12
13
// good
var errorMessage = 'This is a super long error that was thrown because ' +
'of Batman. When you stop to think about how Batman had anything to do ' +
'with this, you would get nowhere fast.';
// bad
var errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';
// bad
var errorMessage = 'This is a super long error that was thrown because \
of Batman. When you stop to think about how Batman had anything to do \
with this, you would get nowhere \
fast.';
  • 使用 join() 来创建字符串

通常写法:

1
2
3
4
5
6
7
8
9
10
11
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 下非常慢, 可以用下面的方式

1
2
3
4
5
6
7
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(), 所以尽量使用赋值操作.

函数

  • 不要在非函数块中(if, while, etc)声明函数,尽管浏览器允许你分配函数给一个变量,但坏消息是,不同的浏览器用不同的方式解析它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// good
var test;
if (currentUser) {
test = function test() {
console.log('Yup.');
};
}
// bad
if (currentUser) {
function test() {
console.log('Nope.');
}
}
  • 不要命名一个参数为arguments,否则它将优先于传递给每个函数作用域中的arguments对象
1
2
3
4
5
6
7
8
9
// good
function yup(name, options, args) {
// ...stuff...
}
// bad
function nope(name, options, arguments) {
// ...stuff...
}
  • 在作用域顶端对变量赋值,这有助于避免变量声明问题和与声明提升相关的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// good
function() {
var name = getName();
test();
console.log('doing stuff..');
//..other stuff..
if (name === 'test') {
return false;
}
return name;
}
// bad
function() {
test();
console.log('doing stuff..');
//..other stuff..
var name = getName();
if (name === 'test') {
return false;
}
return name;
}
// good
function() {
if (!arguments.length) {
return false;
}
var name = getName();
return true;
}
// bad
function() {
var name = getName();
if (!arguments.length) {
return false;
}
return true;
}
  • 函数声明会提升变量名和函数体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// good
function example() {
superPower(); // => Flying
function superPower() {
console.log('Flying');
}
}
// bad
function example() {
superPower(); // => TypeError superPower is not a function
var superPower = function() {
console.log('Flying');
}
}

块内函数声明

  • 不要在块内声明函数
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    // good
    if (x) {
    var foo = function() {}
    }
    // bad
    if (x) {
    function foo() {}
    }

    块内声明函数, 但它不属于 ECMAScript 规范, 各个浏览器糟糕的实现相互不兼容, 有些也与未来 ECMAScript 草案相违背
    ECMAScript 只允许在脚本的根语句或函数中声明函数. 如果确实需要在块中定义函数, 建议使用函数表达式来初始化变量:

闭包

  • 小心使用闭包

闭包保留了一个指向它封闭作用域的指针, 所以, 在给 DOM 元素附加闭包时, 很可能会产生循环引用, 进一步导致内存泄漏. 比如下面的代码:

1
2
3
function foo(element, a, b) {
element.onclick = function() { /* uses a and b */ };
}

这里, 即使没有使用 element, 闭包也保留了 element, a 和 b 的引用, . 由于 element 也保留了对闭包的引用, 这就产生了循环引用, 这就不能被 GC 回收. 这种情况下, 可将代码重构为:

1
2
3
4
5
6
7
function foo(element, a, b) {
element.onclick = bar(a, b);
}
function bar(a, b) {
return function() { /* uses a and b */ }
}

Array 和 Object 直接量

  • 使用 ArrayObject 语法, 而不使用 ArrayObject 构造器.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//good
var a = [1, 2, 3];
var o2 = {
a: 0,
b: 1,
c: 2,
'strange key': 3
};
//bad
var a1 = new Array(1, 2, 3);
var o2 = new Object();
o2.a = 0;
o2.b = 1;
o2.c = 2;
o2['strange key'] = 3;

对象原型

  • 不要修改内置对象的原型,如 Object.prototype 和 Array.prototype 的原型,给添加内置原型方法很容易和其它库冲突或者可能与将来ES升级不兼容。

True 与 False

下面的布尔表达式都返回 false:

  • null
  • undefined
  • '' 空字符串
  • 0 数字0

但小心下面的, 可都返回 true:

  • '0' 字符串0
  • [] 空数组
  • {} 空对象

如果你想判断是一个变量是否为null/''/0/false

1
2
3
4
5
// good
if(x){ ... }
// bad
if(x != null){ ... }

还有很多需要注意的地方
以下都为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

遍历 Node List

Node lists 是通过给节点迭代器加一个过滤器来实现的. 这表示获取他的属性, 如 length 的时间复杂度为 O(n), 通过 length 来遍历整个列表需要 O(n^2).

1
2
3
4
var paragraphs = document.getElementsByTagName('p');
for (var i = 0; i < paragraphs.length; i++) {
doSomething(paragraphs[i]);
}

这样做会更好:

1
2
3
4
var paragraphs = document.getElementsByTagName('p');
for (var i = 0, paragraph; paragraph = paragraphs[i]; i++) {
doSomething(paragraph);
}

这种方法对所有的 collections 和数组(只要数组不包含 falsy 值) 都适用.

在上面的例子中, 也可以通过 firstChild 和 nextSibling 来遍历孩子节点.

1
2
3
4
var parentNode = document.getElementById('foo');
for (var child = parentNode.firstChild; child; child = child.nextSibling) {
doSomething(child);
}

类型分配&强制转换

  • 执行强制类型转换的语句
1
2
3
4
5
6
7
8
9
10
11
12
13
// => this.reviewScore = 9;
// good
var totalScore = this.reviewScore + ' total score';
// bad
var totalScore = this.reviewScore + '';
// good
var totalScore = '' + this.reviewScore;
// bad
var totalScore = '' + this.reviewScore + ' total score';
  • 使用parseInt对Numbers进行转换,并带一个进制作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var inputValue = '4';
// bad
var val = new Number(inputValue);
// bad
var val = +inputValue;
// bad
var val = inputValue >> 0;
// bad
var val = parseInt(inputValue);
// good
var val = Number(inputValue);
// good
var val = parseInt(inputValue, 10);

注意:
当使用位运算时,Numbers被视为64位值,但是位运算总是返回32位整型。
对于整型值大于32位的进行位运算将导致不可预见的行为。
最大的有符号32位整数是2,147,483,647

1
2
3
4
5
6
7
2147483647 >> 0 //=> 2147483647
2147483648 >> 0 //=> -2147483648
2147483649 >> 0 //=> -2147483647
~~2147483647 //=> 2147483647
~~2147483648 //=> -2147483648
~~2147483649 //=> -2147483647

浮点数精度

使用了IEEE 754 浮点数格式来存储浮点类型的任何编程语言(C/C++/C#/Java 等等)都存在精度丢失问题。

在 C#、Java 中,提供了 Decimal、BigDecimal 封装类来进行相应的处理,才避开了精度丢失。

原生JS并没有提供相应的API, 寻么就会出现以下类似怪异情况:

1
2
3
4
0.1 + 0.2 == 0.30000000000000004
9999999999999999 == 10000000000000000; // true
0.05 + 0.2 == 0.25 // true
0.05 + 0.9 == 0.95 // false

对于这人问题这里不做展开,只提供解决方案,可使用 math.js 对数据进行运算

详情解释见 玉伯的JavaScript 中小数和大整数的精度丢失

命名规范

  • 避免单字母名称,让名称具有描述性
1
2
3
4
5
6
7
8
9
// bad
function q() {
// ...stuff...
}
// good
function query() {
// ..stuff..
}
  • 当命名对象、函数和实例时使用骆驼拼写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
var OBJEcttsssss = {};
var this_is_my_object = {};
function c() {}
var u = new user({
name: 'Bob Parr'
});
// good
var thisIsMyObject = {};
function thisIsMyFunction() {}
var user = new User({
name: 'Bob Parr'
});
  • 当命名构造函数或类名时,使用驼峰式写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bad
function user(options) {
this.name = options.name;
}
var bad = new user({
name: 'nope'
});
// good
function User(options) {
this.name = options.name;
}
var good = new User({
name: 'yup'
});
  • 命名私有属性时使用前置下划线
1
2
3
4
5
6
// bad
this.__firstName__ = 'Panda';
this.firstName_ = 'Panda';
// good
this._firstName = 'Panda';
  • 保存this引用时使用_this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bad
function() {
var self = this;
return function() {
console.log(self);
};
}
// bad
function() {
var that = this;
return function() {
console.log(that);
};
}
// good
function() {
var _this = this;
return function() {
console.log(_this);
};
}

JavaScript 编码风格

文件编码

  • JavaScript 文件使用无 BOMUTF-8 编码。

分号

  • 总是以分号结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// good
var myMethod = function() {
return 42;
};
(function() {
// Some initialization code wrapped in a function to create a scope for locals.
})();
// bad
var myMethod = function() {
return 42;
}
(function() {
// Some initialization code wrapped in a function to create a scope for locals.
})();
// Uncaught TypeError: (intermediate value)(...) is not a function(…)
//语句会解释成, 一个函数带一匿名函数作为参数而被调用, 返回42后, 又一次被"调用", 这就导致了错误.
// good
(function() {
var name = 'Skywalker';
return name;
})();
;(function() {
var name = 'Skywalker';
return name;
})()
// bad
(function() {
var name = 'Skywalker'
return name
})()
JavaScript 的语句以分号作为结束符, 除非可以非常准确推断某结束位置才会省略分号. 语句中声明了函数/对象/数组直接量, 但 闭括号('}'或']')并不足以表示该语句的结束. 
在 JavaScript 中, 只有当语句后的下一个符号是后缀或括号运算符时, 才会认为该语句的结束.
遗漏分号有时会出现很奇怪的结果, 所以确保语句以分号结束.
逗号
  • 一次性申明多个变量或申明一个对象,逗号置尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// good
var foo = 1,
bar = 2,
baz = 3;
var obj = {
foo: 1,
bar: 2,
baz: 3
};
// bad
var foo = 1
, bar = 2
, baz = 3;
var obj = {
foo: 1
, bar: 2
, baz: 3
};
空格
  • 函数名称和条件语句后面不加空格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// good
function foo() {
return "bar";
}
if(true) {
//...
}
// bad
function foo () {
return "bar";
}
if (true) {
//...
}
  • 参数与括号之间无空格
1
2
3
4
5
6
7
8
9
10
11
12
13
// good
function fn(arg1, arg2) {
//or
if (true) {
// bad
function fn( arg1, arg2 ) {
// ...
}
if ( true ) {
// ...
}
  • 对象字面量冒号后面加空格,前面不加
1
2
3
4
5
6
7
8
9
10
11
12
13
// good
{
foo: 1,
bar: 2,
baz: 3
}
// bad
{
foo : 1,
bar : 2,
baz : 3
}
  • 在左大括号之前留一个空格
1
2
3
4
5
6
7
8
9
// good
function test() {
console.log('test');
}
// bad
function test(){
console.log('test');
}
  • 用空白分隔运算符
1
2
3
4
5
// good
var x = y + 5;
// bad
var x=y+5;
  • 使用软制表符设置两个空格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
function() {
∙∙∙∙var name;
}
// bad
function() {
var name;
}
// good
function() {
∙∙var name;
}
  • 当调用很长的方法链时使用缩进,可以强调这行是方法调用,不是新的语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// bad
$('#items').find('.selected').highlight().end().find('.open').updateCount();
// bad
$('#items').
find('.selected').
highlight().
end().
find('.open').
updateCount();
// good
$('#items')
.find('.selected')
.highlight()
.end()
.find('.open')
.updateCount();
// bad
var leds = stage.selectAll('.led').data(data).enter().append('svg:svg').classed('led', true)
.attr('width', (radius + margin) * 2).append('svg:g')
.attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
.call(tron.led);
// good
var leds = stage.selectAll('.led')
.data(data)
.enter().append('svg:svg')
.classed('led', true)
.attr('width', (radius + margin) * 2)
.append('svg:g')
.attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
.call(tron.led);
大括号
  • 表示区块起首的大括号,不要另起一行。
1
2
3
4
5
6
7
8
9
10
// good
return {
key : value;
};
// bad
return
{  
key:value;
};

代码的原意,是要返回一个对象,但实际上返回的是undefined,因为Javascript自动在return语句后面添加了分号。

单引号、双引号
  • 统一使用单引号
1
var msg = 'This is some HTML';
单引号 (') 优于双引号 ("). 当你创建一个包含 HTML 代码的字符串时就知道它的好处了.
空行
  • 使用空行来划分一组逻辑上相关联的代码片段.
1
2
3
4
5
6
7
doSomethingTo(x);
doSomethingElseTo(x);
andThen(x);
nowDoSomethingWith(y);
andNowWith(z);
二元和三元操作符
  • 操作符始终跟随着前行, 这样就不用顾虑分号的隐式插入问题. 如果一行实在放不下, 还是按照下面的缩进风格来换行.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
// 二元操作符后面的必须是一个完整的执行语句
// bad
var isShow = true, x1;
isShow ? x1=2;
//Uncaught SyntaxError: Unexpected token ;(…)
// good
var isShow = true, x1;
isShow ? (x1=2);
  • 二元布尔操作符是可短路的, 只有在必要时才会计算到最后一项.
    "||" 被称作为 default 操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(opt_win) {
var win;
if (opt_win) {
win = opt_win;
} else {
win = window;
}
// ...
}
// 可简化为
function foo(opt_win) {
var win = opt_win || window;
// ...
}

“&&” 也可简短代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
语句块
  • 对于使用if和else的多行语句块,把else和if语句块的右大括号放在同一行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// good
if (test) {
thing1();
thing2();
} else {
thing3();
}
// bad
if (test) {
thing1();
thing2();
}
else {
thing3();
}

注释

  • 多行注释使用/* … /,需包含一个描述、所有参数的具体类型和值以及返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// good
/**
* make() returns a new element
* based on the passed in tag name
*
* @param {String} tag
* @return {Element} element
*/
function make(tag) {
// ...stuff...
return element;
}
// bad
// make() returns a new element
// based on the passed in tag name
//
// @param {String} tag
// @return {Element} element
function make(tag) {
// ...stuff...
return element;
}
  • 单行注释使用//,把单行注释放在语句的上一行,并且在注释之前空一行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// good
function getType() {
console.log('fetching type...');
// set the default type to 'no type'
var type = this._type || 'no type';
return type;
}
// bad
function getType() {
console.log('fetching type...');
// set the default type to 'no type'
var type = this._type || 'no type';
return type;
}
  • 使用//TODO:给问题解决方案作注释
1
2
3
4
5
6
7
function Calculator() {
// TODO: total should be configurable by an options param
this.total = 0;
return this;
}
全局变量
  • 避免使用全局变量;如果不得不使用,用大写字母表示变量名,比如UPPER_CASE。

=====

  • 尽量使用’===’来进行逻辑等的判断,用’!==’进行逻辑不等的判断
  • ==作逻辑等判断时,会先进行类型转换后再进行比较。===则不会。因而,==进行的判断结果可能产生偏差。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var valueA = "1";
var valueB = 1;
if ( valueA == valueB) {
alert("Equal");
}
else {
alert("Not equal")
}
//output: "Equal"
if ( valueA === valueB) {
alert("Equal");
}
else {
alert("Not equal")
}
//output: "Not equal"
分享到