Item 1:搞清楚你使用的JavaScript的版本
- 從ES5開始引入了嚴謹模式(strict mode);
- 嚴謹模式支持向上兼容,ES5以前的版本依然可以運行“strict mode”後面的代碼;
- “strict mode”指令必須被放在一段代碼的頂端,或者一個函數體內的頂端,否則便無效。
以上這些規則會在合併JS文件時造成一些困擾,假如有些JS文件有“strict mode”指令,而有些沒有,將它們合併后或許會導致一些問題,比如,本來打算在嚴謹模式下執行的代碼由於指令失效而在正常模式下運行,或者本來打算在正常指令下運行的代碼被放在了嚴謹模式運行,一個天然的解決辦法當然就是將兩類代碼分開放,所以你總得有兩個文件。
作者提出了一個新的解決辦法,可以將所有代碼保存在一個文件中,那就是用IIFE包住每個文件的內容:
// no strict-mode directive
(function() {
// file1.js
"use strict";
function f() {
// ...
}
// ...
})();
(function() {
// file2.js
// no strict-mode directive
function f() {
var arguments = [];
// ...
}
// ...
})();
Item 12:理解變量提升(Variable Hoisting)
這裡面作者強調提升的意思是:在一個代碼塊(比如if和for)里聲明的變量的作用域其實是超出了這個代碼塊,被提升到了包含它的最近的函數的函數作用域里的,因為JavaScript沒有代碼塊作用域。
簡言之,下面的代碼:
function trimSections(header, body, footer) {
for (var i = 0, n = header.length; i < n; i++) {
header[i] = header[i].trim();
}
for (var i = 0, n = body.length; i < n; i++) {
body[i] = body[i].trim();
}
for (var i = 0, n = footer.length; i < n; i++) {
footer[i] = footer[i].trim();
}
}
等效于:
function trimSections(header, body, footer) {
var i, n;
for (i = 0, n = header.length; i < n; i++) {
header[i] = header[i].trim();
}
for (i = 0, n = body.length; i < n; i++) {
body[i] = body[i].trim();
}
for (i = 0, n = footer.length; i < n; i++) {
footer[i] = footer[i].trim();
}
}
所以,總結:
- 千萬要小心重新定義了相同的變量,像上面的例子,作者本意是定義三個i和三個n,而結果是只定義了一個i和一個n;
- 既然JS有這樣的規則,不如把變量聲明都手動提前,就不會在寫代碼和讀代碼的時候造成誤解;
- try-catch語句里,catch收到的實際參數會被當做代碼塊作用域變量對待。
Item 8:避免使用全局对象
出于一些软件工程方面的考虑,比如说模块化,松耦合,避免命名冲突等等问题,作者对全局对象的使用归纳了几点:
- 尽少地声明全局变量;
- 尽可能多地使用本地变量;
- 尽可能不要给全局变量添加属性;
- 只在一些与平台有关的操作中使用全局对象。
这小节里作者提出了一个我之前没有考虑过的可能,对于一个变量名,同时出现使用var关键字和不使用:
this.foo; // undefined
foo = "global foo";
this.foo; // "global foo"
var foo = "global foo";
this.foo = "changed";
foo; // "changed"
从Dmitry.S的系列博客里,知道用var声明的情况下,变量是存储在变量体里的,可是全局变量的变量解析的过程还是应该有一些不同之处,除了在全局上下文的变量体上面寻找,它还它应该在全局对象上寻找,至于孰先孰后就暂不确定。
Item 9:使用局部变量
- 有意使用全局变量充其量只是糟糕的设计风格,但是意外地将本地变量声明为全局变量则是严重错误,可能导致严重后果;
- 一定要用var关键字声明本地变量;
- 最好使用lint这样的工具自动化找出因为漏掉var意外声明的全局变量。
(在Google里搜索“JavaScript lint”可以得到大量结果,看来lint只是个宽泛的概念,泛指检查JS语法错误的工具。)
Item 49:遍历数组时,用for循环而不是for...in循环
这个原因很明显,for...in访问到的是键-值对里面的键的值,即使是数组也是如此,而且数组的索引是以字符类型存储的,可以通过下面的示例代码演示:
var scores = [98, 74, 85, 77, 93, 100, 89];
var total = 0;
for (var score in scores) {
total += score;
}
var mean = total / scores.length;
console.log(mean); // ?
它输出的是17636.571428571428,而不是预期的88。
另外,作者也提醒了,从效率角度考虑,在遍历数组前最好先将数组的长度保存在一个本地变量里面,因为Array.length有可能被一再重复计算,而如果这个重复发生在循环里,就会对效率造成很大影响。
Item 11:适应闭包
作者用三个点概括了闭包:
- 函数可以访问在它以外定义的变量;
- 这些变量在其所被定义的函数之后,它们依然可以被访问;
- 闭包对于作用域里的变量的访问是通过引用,所以可以改变它们,并且改变之后会影响其后访问时的值。
作者用了三段代码来演示这三点:
function makeSandwich() {
var magicIngredient = "peanut butter";
function make(filling) {
return magicIngredient + " and " + filling;
}
return make("jelly");
}
makeSandwich(); // "peanut butter and jelly"
function sandwichMaker() {
var magicIngredient = "peanut butter";
function make(filling) {
return magicIngredient + " and " + filling;
}
return make;
}
var f = sandwichMaker();
f("jelly"); // "peanut butter and jelly"
f("bananas"); // "peanut butter and bananas"
f("marshmallows"); // "peanut butter and marshmallows"
function sandwichMaker(magicIngredient) {
function make(filling) {
return magicIngredient + " and " + filling;
}
return make;
}
var hamAnd = sandwichMaker("ham");
hamAnd("cheese"); // "ham and cheese"
hamAnd("mustard"); // "ham and mustard"
var turkeyAnd = sandwichMaker("turkey");
turkeyAnd("Swiss"); // "turkey and Swiss"
turkeyAnd("Provolone"); // "turkey and Provolone"
另外,作者对于闭包的定义比较简单:保留定义它们的作用域里的变量的函数就是闭包。也就是说符合一定条件的函数就是闭包,本质上就是函数是闭包,这一点和Dmitry的理解一样。
作者还说,函数表达式(function expression)就是JS为了方便创建闭包而提供的。
function sandwichMaker(magicIngredient) {
return function(filling) {
return magicIngredient + " and " + filling;
};
}
这一点我不是很理解!
作者也提醒了注意命名函数表达式(named function expression)。最后作者给出了一个闭包最常用的方法:
function box() {
var val = undefined;
return {
set: function(newVal) {
val = newVal;
},
get: function() {
return val;
},
type: function() {
return typeof val;
}
};
}
var b = box();
b.type(); // "undefined"
b.set(98.6);
b.get(); // 98.6
b.type(); // "number"
他称其为“盒子”(box),但是我见到过的文本一般都称之为模块(module)。
Item 52:创建数组时,首选使用字面表达(Array Literals),避免使用构造函数
声明数组有两种方法:
var a = [1, 2, 3, 4, 5];
或者:
var a = new Array(1, 2, 3, 4, 5);
作者的意思是选择第一个好一些,首选因为效率高。另外一个原因是当参数只有一个整形数字的时候,构造函数的行为会有些歧义:
var a = new Array(17);
上面语句的结果是创建一个数组,它的length属性是17,但它本身并不包含任何元素,然而你期望的结果或许是创建一个数字数组,只有一个元素,并且它的值是17,就像:
var a = [17];
其实不是这样的。所以这种歧义性容易导致阅读代码时导致误解,进而造成潜在的bug。
Item 2:理解浮点数(floating-piont number)
- 在JavaScript里,只有一种数字类型,就是Number;
- Number类型的数据是64位,其中最多53位可用来表示小数点左边的整数部分,所以JS里的Number类型可以表达的整数范围是从–9,007,199,254,740,992到9,007,199,254,740,992;
- JavaScript的代数计算有精准度上的缺陷,计算顺序的不同甚至可能导致计算结果的不同,解决办法是转换成整数再计算;
- JavaScript里,当对Number进行逻辑计算时,浮点数会被转换成32位整型数字,对整型数字进行位运算,然后在将结果转换回浮点数,这个原理可能会导致在某些JS环境里效率有些低。
Item 46:数组与无序集合
当你遍历一个集合的元素时,如果你对于输出的顺序比较在意,那就最好用数组,而不是用对象。枚举一个对象上的键值(key)时,它的顺序会因为JavaScript的运行环境而异,因为ECMA标准没有讲实现细节,所以各个JS引擎可以用自己的算法。
还有,由于在Item2提到浮点数的计算顺序会影响计算结果,所以当考虑用一个集合存储浮点数时,也要小心这个问题,因为计算顺序会是你无法预期的,所以导致结果会有差异。
Item 10:避免使用with
作者提出的原因主要都属于设计理念和工程管理层面的考虑。with会将参数对象作为一个作用域插入到作用域链的最前端,本意是让对这个对象上的属性的访问简单化。可是作者认为有两个问题:
- 首先是对于所有变量的访问看起来都是本地变量了,从可读性上,无法分清哪些变量是在with对象上,哪些是其余的本地变量,哪些是沿着作用域向上找的变量;
- 其次,也跟变量的同一划齐有关,万一with对象上面的属性和其他地方的变量有命名冲突,那么其他的变量就会被遮蔽,而这可能是计划之外的,于是就产生了bug。同时作者的解释对我也是个新的提醒,标识符解析的过程里,在作用域链上查找时,如果遇到了有原型属性的对象,会先沿着它的原型链查找,没有找到再回来作用域链继续,所以如果with对象是有一个原型链的话,这个原型链上的属性也会增加命名冲突的几率。
作者的演示代码如下:
function status(info) {
var widget = new Widget();
with(widget) {
setBackground("blue");
setForeground("white");
setText("Status: " + info); // ambiguous reference
show();
}
}
with区块内,setBackground(),info这些变量访问看起来都一样,而其实info是本地变量。它的变量查找顺序则是:
就像作者提醒的,因为with对象是普通对象,可以有原型属性,所以变量的搜索过程会先经过这个原型链。
下面代码则是会产生问题的地方:
status("connecting"); // Status: connecting
Widget.prototype.info = "[[widget info]]";
status("connected"); // Status: [[widget info]]
with的本意只是缩短代码,没有性能和功能上的意义,所以作者建议使用另一个方案来达到with的效果,就是把对象本身或者它的属性缓存到一个命名简短的变量里:
function status(info) {
var w = new Widget();
w.setBackground("blue");
w.setForeground("white");
w.addText("Status: " + info);
w.show();
}
另外在《High Performance JavaScript》和《YDtKJS》里也有讨论为何with是个糟糕的功能。
Item 16:避免用eval创建本地变量
作者不推荐使用主要是基于安全的考虑,eval里面声明的变量会影响原本代码里的变量作用域,所以作者给出一个解决方案,就是用IIFE包住eval,将它创建的新变量隔离出来:
var y = "global";
function test(src) {
(function() {
eval(src);
})();
return y;
}
test("var y ='local';"); // "global"
test("var z = 'local';"); // "global"
Item 17:尽量使用间接eval,而非直接eval
eval有两种用法,一般的常见的用法都是直接调用:
var x = "global";
function test() {
var x = "local";
return eval("x"); // direct eval
}
test(); // "local "
但是其实有另外一种呼叫方式,间接eval,而且它的效果与直接调用不同,它能访问到的作用域只限于全局对象:
var x = "global";
function test() {
var x = "local";
var f = eval;
return f("x"); // indirect eval
}
test(); // "global"
eval的规则确实与常态的JS代码不同。一个简洁的得到这个效果的语法是:
(0, eval)(src);