放假的电话

Effective JavaScript读书笔记

‘use strict’

  1. 注意JS的版本
  2. 如果JS文件会拼接成一个大文件,注意‘use strict’。理想情况是把每个文件中的函数都放到一个IIFE里,并在这个IIFE里指定strict。
  3. 注意测试JS在不同版本的JS engine中的运行情况,特别是如果你还用到了JS的一些新特性,或者没有被ECMAScript写入标准的特性。

Floating Number

JS中的所有数字都是‘double’类型的。不过在进行bitwise的操作时,会转成32bit integer运算。
例如:

1
2
3
4
5
8 | 1 === 9
(8).toString(2) === '1000'
(1).toString(2) === '0001'
parseInt('1001', 2) === 9

因为是double类型的,所以JS中的数字运算并不是绝对精确的:

1
2
3
4
0.1 + 0.2; // 0.30000000000000004
(0.1 + 0.2) + 0.3; // 0.6000000000000001
0.1 + (0.2 + 0.3); // 0.6

如果精度很重要,可以采用先扩大倍数,再缩小倍数的方法来减小精度损失。例如上面的运算相当于:

1
(1 + 2 + 3) / 10

Implicit Coercion

JS中的隐式类型转换往往会产生意想不到的结果。

判断一个值是NaN, 唯一可靠的就是x !== x,因为NaN是唯一一个不等于自身的值。

false, 0, -0, “”, NaN, null, undefined会隐式转换成false,其他值在逻辑运算中都转成true。

Prefer Primitives to Object Wrappers

1
'hello world'.toUpperCase() == new String('hello world').toUpperCase()

primitive在调用方法的时候,会隐式转换成对应的wrapper类型。

Avoid using == with Mixed Types

==判等时,如果比较的数据类型不一致,会进行隐式类型转换。由于转换的不确定性,容易产生未知的结果。所以,尽量使用===

Learn the Limits of Semicolon Insertion

在以(, [, +, -, /开头的的语句前加;,可以保证避免因为上一句语句没有;而导致的错误,特别是在有文件拼接的情况下。

Think of Strings As Sequences of 16-Bit Code Units

JS一开始实现的string,通过2字节定长来表示一个字符。但是由于unicode字符集的扩展,2字节不够用,UTF-16这种变长的编码方式会用到2或者4个字节来表示一个字符。因此,JS在判断字符串长度,charAt()之类操作上会有问题,主要是字符串里有4字节的字符的情况下。JS默认是以2字节为单位来处理字符串的。如果要处理unicode,最好是依赖一些3rd party的library。

Minimize Use of the Global Object

尽量避免使用global变量,不过通过global变量来判断platform是否支持某些特性是个好主意。

Get Comfortable with Closures

Closure如果访问了外部变量,其实它保存的是一个外部变量的引用。因此,closure也可以修改外部变量。

Understand Variable Hoisting

JS只有词法作用域,或者说函数作用域,因此所有变量声明都会被提升到函数最开始的地方。唯一的一个例外是try...catch...语句,catch捕获的变量的作用域只在catch语句内。

Beware of Unportable Scoping of Block-Local Function Declarations

不要在{}block里定义函数。 如果要根据条件变化函数,记得用var先声明。

1
2
3
4
5
6
7
8
9
10
function f() { return "global"; }
function test(x) {
var g = f, result = [];
if (x) {
g = function() { return "local"; };
result.push(g());
}
result.push(g()); return result;
}

Avoid Creating Local Variables with eval

eval会污染作用域,跟strict模式也不能很好相处,如果非要用eval,比较安全的做法是把它放到一个IIFE里:

1
2
3
4
5
6
7
var y = "global";
function test(src) {
(function() { eval(src); })();
return y;
}
test("var y = 'local';"); // "global"
test("var z = 'local';"); // "global"

Prefer indirect eval over direct eval

使用indirect的eval更安全,更高效,因为indirect的eval只是用global作用域,使用的方法有:

1
2
3
4
5
6
7
var x = "global";
function test() {
var x = "local";
var f = eval;
return f("x"); // indirect eval
}
test(); // "global"

或者

1
(0, eval)(src)

Understand the Difference between Function, Method, and Constructor Calls

在ES5里,‘use strict’规定默认的this不再绑定到全局对象,而是绑定到undefined。这会尽快暴露出可能的错误,因为访问undefined.xxx会直接报错,但是访问window.xxx可能返回undefined

Item 21: Use apply to Call Functions with Different Numbers of Arguments

很多地方说.apply()的用法跟.call()差不多,都可以绑定this,只是传参数的方法不同。但是这里讲到了另一个.apply()的用法,更实际一些。例如,有个函数average,接受任意多个参数,average(1, 2, 3, ....)。现在,如果有个一个array叫做score,我怎么算score的平均值呢?答案就是用average.apply(null, score)

Use a Variable to Save a Reference to arguments

如果使用了了arguments,注意function嵌套。内层的函数会隐式创建一个新的arguments。如果要用到外层的arguments,记得用一个变量引用外层的arguments。

Never modify __proto__

通过__proto__修改原型是很危险的,例如下面的例子,即使发生了继承,任然可以动态给其他变量增加方法,甚至修改方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function A() {};
A.prototype.saya = function() {
console.log('a');
}
var a = new A();
a.saya();
function B() {}
B.prototype = new A();
B.prototype.sayb = function() {
console.log('b');
}
var b = new B();
b.__proto__.__proto__.sayb = function() {
console.log('new b');
}
b.saya();
b.sayb();
a.sayb();
console.log(b.__proto__.__proto__);

Make Your Constructors new-Agnostic

通过下面的方法,是你的constructor在没有new的情况下,也能正确工作。

1
2
3
4
5
6
7
function User(name, passwordHash) {
if (!(this instanceof User)) {
return new User(name, passwordHash);
}
this.name = name;
this.passwordHash = passwordHash;
}

不过我觉得这么做倒是没什么必要,毕竟code review或者代码里会很快发现没有new这种问题。我觉得直接use strict,让它尽快报错就可以了。

Call Superclass Constructors from Subclass Constructors

看一下下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent(name) {
console.log('init parent');
this.name = name;
this.secret = 'xxx';
}
function Child(name, age) {
console.log('init child');
Parent.call(this, name)
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
// Child.prototype = new Parent();
Child.prototype.constructor = Child;
var a = new Child('Tom', 14);
var b = new Child('John', 15);
console.log(a.name, a.__proto__.name);
console.log(a, b);

建议使用Child.prototype = Object.create(Parent.prototype)而不是Child.prototype = new Parent('Papa')。原因如下:

  1. 首先,我们可能没有一个合适的name来进行new Parent(),如果不使用任何参数,new Parent()可能会报错。
  2. 其次,new Parent()操作是多余的。
  3. 另外,通过a.__proto__.name,会发现如果用了new,prototype也会有个name属性,而实际上name属性应该是属于Child的实例的。

Note: 其实如果不考虑原型链之类的,javascript里所有的东西都可以当做函数。prototype类似于名空间。一般情况下只要通过call和apply指定正确的this,任何函数都是可以直接执行的。

记住: The superclass constructor should only be invoked from the subclass constructor, not when creating the subclass prototype.

Use null Prototypes to Prevent Prototype Pollution

为了避免XXX.prototype.xxx = xxx污染prototype,有些人就出了把null作为prototype,例如A.prototype=null。然而,这种方法在ES5之前并没有用,new A()产生的对象的prototype还是Object。不过,在ES5里,可以通过Object.create()实现:

1
2
var o = Object.create(null);
Object.getPrototypeOf(o) === null; // true

Never Add Enumerable Properties to Object.prototype

给Object.prototype添加属性会污染prototype。ES5里添加了新的defineProperty方法,可以设置enumerable避免该属性被遍历。

1
2
3
4
5
6
7
8
9
10
11
12
Object.defineProperty(Object.prototype, "allKeys", {
value: function() {
var result = [];
for (var key in this) {
result.push(key);
}
return result;
},
writable: true,
enumerable: false,
configurable: true }
);

Reuse Generic Array Methods on Array-Like Objects

Array.prototype的所有方法都足够generic,可以用在Arrary-like的object上,例如函数的arguments。唯独一个方法,concat,会测试参数的[[Class]]。如果不是Arrary,会当作一个元素加入array。解决的办法就是先生成一个真正的Array。

1
2
3
4
function namesColumn() {
return ["Names"].concat([].slice.call(arguments));
}
namesColumn("Alice", "Bob", "Chris"); // ["Names", "Alice", "Bob", "Chris"]

Distinguish between Array and Array-Like

ES5引入的Array.isArray方法,比``x instanceof Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## Use Recursion for Asynchronous Loops
异步的递归函数,可以大大减少stack的利用。
```javascript
function downloadOneAsync(urls, onsuccess, onfailure) {
var n = urls.length;
function tryNextURL(i) {
if (i >= n) {
onfailure("all downloads failed");
return;
}
downloadAsync(urls[i], onsuccess, function() { tryNextURL(i + 1);});
}
tryNextURL(0);
}

每次执行tryNextURL都会马上返回,不会压栈。回掉函数会负责执行下一个递归。感觉有点像广度优先的搜索。

Don’t Block the Event Queue on Computation

尽管有event queue,如果handler耗时很长,还是会破坏用户体验。解决的几个常见办法是:

  1. 使用Worker线程。不过并不是所有平台都实现了Worker线程,而且跟Worker线程的通信也是很耗费资源的。
  2. 把很花时间的计算break,并加入一个callback函数,变成异步的。循环可以通过setTimeout变成异步的。

Never Call Asynchronous Callbacks Synchronously

不这么做的主要原因是,会破坏api调用者的期望。

1
2
3
4
downloadCachingAsync("file.txt", function(file) {
console.log("finished"); // might happen first
});
console.log("starting");

例如上面的例子里,如果’finished’有时在‘starting’之前输出,有时在之后,会给人造成很大困扰。