Item 47:不要给Object.prototype添加可枚举的属性
归根结底,还是在说for..in的特性带来的问题。像下面的代码在Object.prototype上定义了一个方法,结果导致一些问题:
Object.prototype.allKeys = function () {
var result = [];
for (var key in this) {
result.push(key);
}
return result;
};
({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b", "c"]
所以说不要在原型上面定义方法,解决方法是另外定义一个独立的函数:
function allKeys(obj) {
var result = [];
for (var key in obj) {
result.push(key);
}
return result;
}
但是更好的办法是使用ES5的新方法:Object.defineProperty。
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
});
通过声明一个属性的enumerable为false,就让for..in看不见它了。
Item 50:用迭代方法代替循環來遍歷數組
Item 51:在类似数组的对象上使用数组的泛型方法
有一些对象虽然本身不是数组对象,也没有继承自Array,但它们具有一些行为类似数组对象,作者说Array.prototype上面的函数是被设计用来可以在这些类数组的对象上重用的,虽然这看起来不太符合直觉。
有两个东西是典型的“类数组”,arguments和NodeList。
function highlight() {[].forEach.call(arguments, function (widget) {
widget.setBackground("yellow");
});
}
一个对象符合“类数组”有两个标准:
- 有length属性,并且其值根据元素增减;
- 有一个数字类型的索引属性键,它的值始终小于length。
所以普通对象也可以被改造成类数组:
var arrayLike = {
0: "a",
1: "b",
2: "c",
length: 3
};
var result = Array.prototype.map.call(arrayLike, function (s) {
return s.toUpperCase();
}); // ["A", "B", "C"]
不修改原数组值的数组方法也可以在String上面使用:
var result = Array.prototype.map.call("abc", function (s) {
return s.toUpperCase();
}); // ["A", "B", "C"]
唯一不能在类数组对象上直接使用的数组方法是concat。不过也有个解决办法,就是使用slice:
function namesColumn() {
return ["Names"].concat([].slice.call(arguments));
}
namesColumn("Alice", "Bob", "Chris"); // ["Names", "Alice", "Bob", "Chris"]
MDN的slice页面里有关于类数组的讨论。
Item 53:统一惯例
这一节讲的跟代码运行没有关系,说的的是编码风格和规范的问题。比如命名规则和规范要有内在的一致性。另外也可以尽量与流行的框架和库的做法保持一致。这里我插一句Dogulas Crockford的相关建议,JavaScript自己有自己的风格,比如说方法用lower carmel case,首先遵循JS自身的风格。
举例来说:
如果你有些方法的签名是function(width, heihgt),那么就都保持width在前,height在后,不要定义一个函数签名为:function(height, width);
如果你有属性访问方法是叫setWidth(),来设定width的值,就不要在其他类上面定义width()来做相同操作;
CSS里,接受正方形参数的时候,顺序是top, right, bottom, left,也就是顺时针,那么如果你有方法也接受类似信息,最好就沿用这个顺序。
Item 54:把undefined理解为“没有值”
四种情况可能导致你的操作返回的是undefined:
- 访问一个定义了但没有赋值的变量:
var x; x; // undefined
- 访问一个对象上没有定义变量:
var obj = {}; obj.x; // undefined
- 访问一个直接return,或者根本没有return语句的函数:
function f() { return; } function g() { } f(); // undefined g(); // undefined
- 调用一个函数却没有给它的参数传值:
function f(x) { return x; } f(); // undefined
作者讲到一种设计思路:
以undefined作为一种启用特殊处理机制的信号,比如说有一个HTML元素类,它有个方法是给它上色的,如果传递进来的是一个颜色值,就使用该颜色,如果是undefined,就启用随机颜色。如下:
element.highlight(); // use the default color
element.highlight("yellow"); // use a custom color
element.highlight(undefined); // use a random color
这个设计当然并不好,因为在有些情况下,传递undefined值进来是由于异常,而不是调用者的本意,这样的情况将无法区分。比如这个颜色设定值来自外部IO:
var config = JSON.parse(preferences);
// ...
element.highlight(config.highlightColor); // may be random
关于这种情况,作者建议了两个方案:
- 一:传递一个特殊字符串来作为信号
element.highlight("random");
- 二:传递一个设定对象
element.highlight({ random: true });
另一种设计思路:
在函数里验证arguments数组的长度。
function Server(port, hostname) {
if (arguments.length < 2) {
hostname = "localhost";
}
hostname = String(hostname);
// ...
}
var s1 = new Server(80, "example.com");
var s2 = new Server(80); // defaults to "localhost"
基于相同的原因,这个做法也不好。
总的来讲,明确地验证undefined是最万无一失的:
function Server(port, hostname) {
if (hostname === undefined) {
hostname = "localhost";
}
hostname = String(hostname);
// ...
}
function Server(port, hostname) {
hostname = String(hostname || "localhost");
// ...
}
还有一种设计思路:
通过if(variable)或者||来验证传递的参数是否无效,这种验证在undefined的情况下会得到预期的效果,可是它却对所有falsy的值都会做同样处理,包括null,空字符串,false和0,而这不见得是预期的。
function Element(width, height) {
this.width = width || 320; // wrong test
this.height = height || 240; // wrong test
// ...
}
var c1 = new Element(0, 0);
c1.width; // 320
c1.height; // 240
总之这个方案在有些情况下是可行的,可是比如,如果是数字的话,并且0是可以作为有效值的情况下,就要改成:
function Element(width, height) {
this.width = width === undefined ? 320 : width;
this.height = height === undefined ? 240 : height;
// ...
}
var c1 = new Element(0, 0);
c1.width; // 0
c1.height; // 0
var c2 = new Element();
c2.width; // 320
c2.height; // 240
好一些。
Item 55:用设定参数对象来传递参数
这一节关注的问题是函数的参数列表过长。参数过多会导致可读性降低,JS对此有一个策略就是传递一个对象,在对象上通过属性来表达参数。
写法就是:
function Alert(parent, message, opts) {
opts = opts || {}; // default to an empty options object
this.width = opts.width === undefined ? 320 : opts.width;
this.height = opts.height === undefined ? 240 : opts.height;
this.x = opts.x === undefined ? (parent.width / 2) - (this.width / 2) : opts.x;
this.y = opts.y === undefined ? (parent.height / 2) - (this.height / 2) : opts.y;
this.title = opts.title || "Alert";
this.titleColor = opts.titleColor || "gray";
this.bgColor = opts.bgColor || "white";
this.textColor = opts.textColor || "black";
this.icon = opts.icon || "info";
this.modal = !! opts.modal;
this.message = message;
}
var alert = new Alert(app, message, {
width: 150,
height: 100,
title: "Error",
titleColor: "blue",
bgColor: "white",
textColor: "black",
icon: "error",
modal: true
});
这个写法也可以更好地解决默认参数值和可选参数的问题。这个做法可以进一步优化,就是使用estend()方法来合并两个对象的属性:
function Alert(parent, message, opts) {
opts = extend({
width: 320,
height: 240
});
opts = extend({
x: (parent.width / 2) - (opts.width / 2),
y: (parent.height / 2) - (opts.height / 2),
title: "Alert",
titleColor: "gray",
bgColor: "white",
textColor: "black",
icon: "info",
modal: false
}, opts);
extend(this, opts);
}
function extend(target, source) {
if (source) {
for (var key in source) {
var val = source[key];
if (typeof val !== "undefined") {
target[key] = val;
}
}
}
return target;
}
有几点:
- 在使用extend的版本里,无论如何计算width和height的逻辑都会被执行,而第一个里在不需要的情况下是会被省略的,不过这个问题不大,效率上的影响可以忽略;
- 第一个版本会将空字符串也解读为没有提供值,而第二个则是验证undefined,这样更一致些。
Item 56:避免不必要的状态
这是个设计思路的问题。所谓状态指的是对象内部的属性的值,它们会影响对象上的一些方法的行为。举例来说,String.toUpperCase()的行为就是独立的,它不受对象的内部状态影响,这种就叫无状态API,相反,Date.now()返回的值就受到其内部状态的影响,这种就被称作状态化的API。
由此,作者提出的建议是,尽量令API无状态化,这样使用者会省去很多麻烦。为了做到这样的效果,往往就需要将一个操作所需要的所有数据都作为参数一次性传递给API。
作者给出了一个案例,假如我们需要设计API来读取配置文件里的设置,配置文件的格式如:
[Host]
address=172.0.0.1
name=localhost
[Connections]
timeout=10000
下面是一个不好的设计:
var ini = INI.parse(src);
ini.setSection("Host");
var addr = ini.get("address");
var hostname = ini.get("name");
ini.setSection("Connection");
var timeout = ini.get("timeout");
var server = new Server(addr, hostname, timeout);
下面是更好的设计:
var ini = INI.parse(src);
var server = new Server(ini.Host.address, ini.Host.name, ini.Connection.timeout);
Item 57:结构类型化(看不懂)
Item 58:區別數組與類數組對象
关于类数组,也可以参考MDN。
Item 61:不要令I/O事件阻碍事件队列
先总结下作者提及的跟这个话题相关的JavaScript原理:
- JavaScript对于那些耗时的操作,尤其是网络读写,提供了异步的机制,也就是你启动一个访问操作的时候提供一个回调函数,然后JS引擎继续去执行下面的其他代码,等这个事件完成了再去执行回调函数,这个机制最典型的就是Ajax了;
- JavaScript其实是使用一个队列来存储所有被触发的事件的,按照先后顺序处理这些事件的回调函数;
- 于是,看起来JS有能力并发性地接收事件。
作者这个标题的意思是,JS里有一类跟文件读写有关的函数并不遵照上面说的异步机制,而是同步的,也就是这类函数的执行会令整个程序的执行暂停下来,直到这个读写事件完成了,再继续执行下面的代码,这样的做法当然效率不高,因为耗时的读写操作阻滞了程序的运行,而作者在这一节里说的就是不要使用这类操作,可是其实我作为使用JS很多年的人,并没有听说过还有这类函数的存在,这一节似乎意义不大。
Item 62:对于一系列连续的异步调用,使用嵌套函数或者命名回调函数
这一节讲的都是关于可读性的问题。
当下面的代码:
db.lookupAsync("url", function (url) {
downloadAsync(url, function (text) {
console.log("contents of " + url + ": " + text);
});
});
演变成:
db.lookupAsync("url", function (url) {
downloadAsync(url, function (file) {
downloadAsync("a.txt", function (a) {
downloadAsync("b.txt", function (b) {
downloadAsync("c.txt", function (c) {
// ...
});
});
});
});
});
可以将代码改成:
db.lookupAsync("url", downloadURL);
function downloadURL(url) {
downloadAsync(url, function (text) { // still nested
showContents(url, text);
});
}
function showContents(url, text) {
console.log("contents of " + url + ": " + text);
}
或者在进一步,用bind完全抹掉嵌套回调函数:
db.lookupAsync("url", downloadURL);
function downloadURL(url) {
downloadAsync(url, showContents.bind(null, url));
}
function showContents(url, text) {
console.log("contents of " + url + ": " + text);
}
将上面那段极度长的代码按照这种思路重写就变成:
db.lookupAsync("url", downloadURLAndFiles);
function downloadURLAndFiles(url) {
downloadAsync(url, downloadABC.bind(null, url));
}
// awkward name
function downloadABC(url, file) {
downloadAsync("a.txt",
// duplicated bindings
downloadFiles23.bind(null, url, file));
}
// awkward name
function downloadBC(url, file, a) {
downloadAsync("b.txt",
// more duplicated bindings
downloadFile3.bind(null, url, file, a));
}
// awkward name
function downloadC(url, file, a, b) {
downloadAsync("c.txt",
// still more duplicated bindings
finish.bind(null, url, file, a, b));
}
function finish(url, file, a, b, c) {
// ...
}
如果你觉得也不是很好看,就折中下命名回调函数和直接嵌套的用法:
db.lookupAsync("url", function (url) {
downloadURLAndFiles(url);
});
function downloadURLAndFiles(url) {
downloadAsync(url, downloadFiles.bind(null, url));
}
function downloadFiles(url, file) {
downloadAsync("a.txt", function (a) {
downloadAsync("b.txt", function (b) {
downloadAsync("c.txt", function (c) {
// ...
});
});
});
}
Item 65:不要用复杂的计算阻滞事件队列
首先,再次强调,在JS代码运行的时候,浏览器是无暇处理其他事件的,比如用户点击一个按钮这样的用户事件,所以为了保证良好的用户体验,缩短每次JS的执行时间很重要。这个小节讲的话题是当计算量非常庞大时,如何减少执行时间。
第一个方案是Web Worker,这个在别的地方说过了,就不在这里重复了。
第二方案更通用,就是从算法上入手,如果一个复杂计算是可以分割成相互独立的步骤的话,就可以用循环与定时回调函数结合在一起把任务分割:
Member.prototype.inNetwork = function (other, callback) {
var visited = {};
var worklist = [this];
function next() {
if (worklist.length === 0) {
callback(false);
return;
}
var member = worklist.pop();
// ...
if (member === other) { // found?
callback(true);
return;
}
// ...
setTimeout(next, 0); // schedule the next iteration
}
setTimeout(next, 0); // schedule the first iteration
};
先把计算任务分割成若干子任务,setTimeout会把下一次执行的子任务放在回调函数里,并且在事件队列里插入一个新事件,等它被处理时下个子任务就会被执行了。这个做法的妙处在于,后续的子任务的执行要遵循事件的排队机制,在setTimeout执行前插入到队列里的事件会被先处理,这样一来,子任务的执行之间就会执行其他的任务,就不会阻碍其他任务了。上面这段代码也是可以进一步改进的,比如每次子任务里循环的次数。