The Essentials of Writing High Quality JavaScript
文章目录

The Essentials of Writing High Quality JavaScript

最小全局变量(Minimizing Globals)

方法中的变量如果不添加var那么就是一个全局变量,同时多个赋值的时候每个变量前面都要带上var

  • Do not do this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function sum(x, y) {
    // 不推荐写法: 隐式全局变量
    result = x + y;
    return result;
    }
    // 反例,勿使用
    function foo() {
    var a = b = 0;
    // ...
    }
  • Do this

    1
    2
    3
    4
    5
    6
    7
    8
    function sum(x, y) {
    var result = x + y;
    return result;
    }
    function foo() {
    var a, b;
    // ... a = b = 0; // 两个均局部变量
    }

忘记var的副作用(Side Effects When Forgetting var)

隐式全局变量和明确定义的全局变量间有些小的差异,就是通过delete操作符让变量未定义的能力。

通过var创建的全局变量(任何函数之外的程序中创建)是不能被删除的。
无var创建的隐式全局变量(无视是否在函数中创建)是能被删除的。
这表明,在技术上,隐式全局变量并不是真正的全局变量,但它们是全局对象的属性。属性是可以通过delete操作符删除的,而变量是不能的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义三个全局变量
var global_var = 1;
global_novar = 2; // 反面教材
(function () {
global_fromfunc = 3; // 反面教材
}());

// 试图删除
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true

// 测试该删除
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"

在ES5严格模式下,未声明的变量(如在前面的代码片段中的两个反面教材)工作时会抛出一个错误。

预解析:var散布的问题(Hoisting: A Problem with Scattered vars)

JavaScript中,你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting(悬置/置顶解析/预解析)。

当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只 要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。看下面这个例子:

1
2
3
4
5
6
7
8
// 反例
myname = "global"; // 全局变量
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();

在这个例子中,你可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这种期许是可以理解的,因为在第一个alert 的时候,myname未声明,此时函数肯定很自然而然地看全局变量myname,但是,实际上并不是这么工作的。

第一个alert会弹 出”undefined”是因为myname被当做了函数的局部变量(尽管是之后声明的),所有的变量声明当被悬置到函数的顶部了。

因此,为了避免这种混 乱,最好是预先声明你想使用的全部变量。

上面的代码片段执行的行为可能就像下面这样:

1
2
3
4
5
6
7
8
myname = "global"; // global variable
function func() {
var myname; // 等同于 -> var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"
}
func();

为了完整,我们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段:

  • 第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。
  • 第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。
    但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,通常用来描述行为。

for循环的性能提升(for Loops)

1
2
3
4
// 次佳的循环
for (var i = 0; i < myarray.length; i++) {
// 使用myarray[i]做点什么
}

这种形式的循环的不足在于每次循环的时候数组的长度都要去获取下。这回降低你的代码,尤其当myarray不是数组,而是一个HTMLCollection对象的时候。

better solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//solution1
for (var i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做点什么
}

//solution2
function looper() {
var i = 0,
max,
myarray = [];
// ...
for (i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做点什么
}
}

还有两种变化的形式,其又有了些微改进,因为:

  • 少了一个变量(无max)
  • 向下数到0,通常更快,因为和0做比较要比和数组长度或是其他不是0的东西作比较更有效率
1
2
3
4
5
6
7
8
9
10
11
12
//第一种变化的形式:
var i, myarray = [];
for (i = myarray.length; i–-;) {
// 使用myarray[i]做点什么
}

//第二种使用while循环:
var myarray = [],
i = myarray.length;
while (i–-) {
// 使用myarray[i]做点什么
}

左花括号的位置(Opening Brace Location)

例子:

1
2
3
4
5
6
7
8
// 警告: 意外的返回值
function func() {
return
// 下面代码不执行
{
name : "Batman"
}
}

如果你希望函数返回一个含有name属性的对象,你会惊讶。由于隐含分号,函数返回undefined。前面的代码等价于:

1
2
3
4
5
6
7
8
// 警告: 意外的返回值
function func() {
return undefined;
// 下面代码不执行
{
name : "Batman"
}
}

总之,总是使用花括号,并始终把在与之前的语句放在同一行:

1
2
3
4
5
function func() {
return {
name : "Batman"
};
}

避免隐式类型转换(Avoiding Implied Typecasting )

JavaScript的变量在比较的时候会隐式类型转换。这就是为什么一些诸如:false == 0“” == 0 返回的结果是true。为避免引起混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用===和!==操作符。

var zero = 0;
if (zero === false) {
   // 不执行,因为zero为0, 而不是false
}

// 反面示例
if (zero == false) {
   // 执行了...
}

还有另外一种思想观点认为==就足够了===是多余的。

例如,当你使用typeof你就知道它会返回一个字符串,所以没有使用严格相等的理由。然而,JSLint要求严格相等,它使代码看上去更有一致性,可以降低代码阅读时的精力消耗。

函数表达式和函数声明

函数声明:

1
function 函数名称 (参数:可选){ 函数体 }

函数表达式:

1
function 函数名称(可选)(参数:可选){ 函数体 }

例子

1
2
3
4
5
6
7
8
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分

new function bar(){}; // 表达式,因为它是new表达式

(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();

还有一种函数表达式不太常见,就是被括号括住的(function foo(){}),他是表达式的原因是因为括号 ()是一个分组操作符,它的内部只能包含表达式,我们来看几个例子:

1
2
3
4
5
6
7
8
function foo(){} // 函数声明
(function foo(){}); // 函数表达式:包含在分组操作符内

try {
(var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
} catch(err) {
// SyntaxError
}

表达式和声明存在着十分微妙的差别,首先,函数声明会在任何表达式被解析和求值之前先被解析和求值,即使你的声明在代码的最后一行,它也会在同作用域内第一个表达式之前被解析/求值,参考如下例子,函数fn是在alert之后声明的,但是在alert执行的时候,fn已经有定义了:

1
2
3
4
5
alert(fn());

function fn() {
return 'Hello world!';
}

函数表达式在条件之中的意外情况

1
2
3
4
5
6
7
8
9
var f = function g() {
return 1;
};
if (false) {
f = function g(){
return 2;
};
}
g(); // 2

首先,g被当作函数声明解析,由于JScript中的函数声明不受条件代码块约束,所以在这个很恶的if分支中,g被当作另一个函数function g(){ return 2 },也就是又被声明了一次。然后,所有“常规的”表达式被求值,而此时f被赋予了另一个新创建的对象的引用。由于在对表达式求值的时候,永远不会进入“这个可恶if分支,因此f就会继续引用第一个函数function g(){ return 1 }。分析到这里,问题就很清楚了:假如你不够细心,在f中调用了g,那么将会调用一个毫不相干的g函数对象。

关于函数的内存释放

很奇葩的例子:

1
2
3
4
5
6
var f = (function(){
if (true) {
return function g(){};
}
return function g(){};
})();

我们显式断开对g函数的引用,否则它一直占着内存不放。

1
2
3
4
5
6
7
8
9
10
11
12
var f = (function(){
var f, g;
if (true) {
f = function g(){};
}
else {
f = function g(){};
}
// 设置g为null以后它就不会再占内存了
g = null;
return f;
})();