Programming Practices 编程实践
避免二次评估
JavaScript 与许多脚本语言一样,允许你在程序中获取一个包含代码的字符串然后运行它。有四种标准 方法可以实现:eval(),Function()构造器,setTimeout()和 setInterval()。每个函数允许你传入一串 JavaScript 代码,然后运行它。例如
var num1 = 5, num2 = 6, //eval_r() evaluating a string of code
result = eval("num1 + num2"), //Function() evaluating strings of code
sum = new Function("arg1", "arg2", "return arg1 + arg2"); //setTimeout()evaluating a string of code
setTimeout("sum = num1 + num2", 100); //setInterval() evaluating a string of code
setInterval("sum = num1 + num2", 100);
结果都是11
避免二次评估是实现优化的 JavaScript 运行时性能的关键。
作为一个比较点,不同浏览器上访问一个数组项所占用的时间各有不同,但如果使用 eval()访问其结 果将大相径庭。例如:
//faster
var item = array[0];
//slower
var item = eval_r("array[0]");
若使用 eval()代替直接代码访问 10’000 个数组项,在不同浏览器上的差异非常巨大。是因为每次调用 eval()时要创建一个新的解释/编译实例。同样的过程 也发生在 Function(),setTimeout()和 setInterval()上,自动使代码执行速度变慢。
大多数情况下,没必要使用 eval()或 Function(),如果可能的话,尽量避免使用它们。至于另外两个函 数,setTimeout()和 setInterval(),建议第一个参数传入一个函数而不是一个字符串。例如:
setTimeout(function () {
sum = num1 + num2;
}, 100);
setInterval(function () {
sum = num1 + num2;
}, 100);
使用对象/数组直接量
典型的对象创建和赋值是这样的:
//create an object
var myObject = new Object();
myObject.name = "Nicholas";
myObject.count = 50;
myObject.flag = true;
myObject.pointer = null;
//create an array
var myArray = new Array();
myArray[0] = "Nicholas";
myArray[1] = 50;
myArray[2] = true;
myArray[3] = null;
但是,直接量赋值很快。作为一个额外的好处,直接量在你的代码中占 用较少空间,所以整个文件尺寸可以更小。
//create an object
var myObject = {name: "Nicholas", count: 50, flag: true, pointer: null};
//create an array
var myArray = ["Nicholas", 50, true, null];
不要重复工作
也许常见的重复工作类型是浏览器检测。大量代码依赖于浏览器的功能。以事件句柄的添加和删除为 例,典型的跨浏览器代码如下:
function addHandler(target, eventType, handler) {
if (target.addEventListener) { //DOM2 Events
target.addEventListener(eventType, handler, false);
} else { //IE
target.attachEvent("on" + eventType, handler);
}
}
function removeHandler(target, eventType, handler) {
if (target.removeEventListener) { //DOM2 Events
target.removeEventListener(eventType, handler, false);
} else { //IE
target.detachEvent("on" + eventType, handler);
}
}
乍一看,这些函数为实现它们的目的已经足够优化。隐藏的性能问题在于每次函数调用时都执行重复工 作。每一次,都进行同样的检查,看看某种方法是否存在。如果你假设 target 唯一的值就是 DOM 对象, 而且用户不可能在页面加载时魔术般地改变浏览器,那么这种判断就是重复的。如果 addHandler()一上来 就调用addEventListener()那么每个后续调用都要出现这句代码。在每次调用中重复同样的工作是一种浪费, 有多种办法避免这一点。
延迟加载
第一种消除函数中重复工作的方法称作延迟加载。延迟加载意味着在信息被使用之前不做任何工作。在
前面的例子中,不需要判断使用哪种方法附加或分离事件句柄,直到有人调用此函数。使用延迟加载的函
数如下:
function addHandler(target, eventType, handler) {
//overwrite the existing function
if (target.addEventListener) { //DOM2 Events
addHandler = function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
};
} else { //IE
addHandler = function (target, eventType, handler) {
target.attachEvent("on" + eventType, handler);
};
}
//call the new function
addHandler(target, eventType, handler);
}
function removeHandler(target, eventType, handler) {
//overwrite the existing function
if (target.removeEventListener) { //DOM2 Events
removeHandler = function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
};
} else { //IE
removeHandler = function (target, eventType, handler) {
target.detachEvent("on" + eventType, handler);
};
}
//call the new function
removeHandler(target, eventType, handler);
}
这两个函数依照延迟加载模式实现。这两个方法第一次被调用时,检查一次并决定使用哪种方法附加或分离事件句柄。然后,原始函数就被包含适当操作的新函数覆盖了。后调用新函数并将原始参数传给它。以后再调用 addHandler()或者 removeHandler()时不会再次检测,因为检测代码已经被新函数覆盖了。
调用一个延迟加载函数总是在第一次使用较长时间,因为它必须运行检测然后调用另一个函数以完成任务。但是,后续调用同一函数将快很多,因为不再执行检测逻辑了。延迟加载适用于函数不会在页面上立即被用到的场合。
条件预加载
它在脚本加载之前提前进行检查,而不等待函数调用。 这样做检测仍只是一次,但在此过程中来的更早。例如:
var addHandler = document.body.addEventListener ? function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
} : function (target, eventType, handler) {
target.attachEvent("on" + eventType, handler);
};
var removeHandler = document.body.removeEventListener ? function (target, eventType, handler) {
target.removeEventListener(eventType, handler, false);
} : function (target, eventType, handler) {
target.detachEvent("on" + eventType, handler);
};
这个例子检查 addEventListener()和 removeEventListener()是否存在,然后根据此信息指定合适的函数。 三元操作符返回 DOM 级别 2 的函数,如果它们存在的话,否则返回 IE 特有的函数。然后,调用 addHandler() 和 removeHandler()同样很快,虽然检测功能提前了。
条件预加载确保所有函数调用时间相同。其代价是在脚本加载时进行检测。预加载适用于一个函数马上 就会被用到,而且在整个页面生命周期中经常使用的场合。
位操作运算符
计算对 2 取模,需要用这个数除以 2 然后查看余数。如果你看到 32 位数字的底层(二进制)表示法, 你会发现偶数的低位是 0,奇数的低位是 1。如果此数为偶数,那么它和 1 进行位与操作的结果就是 0; 如果此数为奇数,那么它和 1 进行位与操作的结果就是 1。也就是说上面的代码可以重写如下:
for (var i = 0, len = rows.length; i < len; i++) {
if (i & 1) {
className = "odd"; //奇数
} else {
className = "even";//偶数
}
//apply class
}
比取余运算要快;i%2
;
第二种使用位操作的技术称作位掩码。位掩码在计算机科学中是一种常用的技术,可同时判断多个布尔 选项,快速地将数字转换为布尔标志数组。掩码中每个选项的值都等于 2 的幂。例如:
var OPTION_A = 1;
var OPTION_B = 2;
var OPTION_C = 4;
var OPTION_D = 8;
var OPTION_E = 16;
var options = OPTION_A | OPTION_C | OPTION_D;
//is option A in the list?
if (options & OPTION_A) {
//do something
console.log(options);
}
//is option B in the list?
if (options & OPTION_D) {
//do something
console.log(options);
}
结果:13 13
像这样的位掩码操作非常快,正因为前面提到的原因,操作发生在系统底层。如果许多选项保存在一起 并经常检查,位掩码有助于加快整体性能。
原生方法
- 尽量使用Math对象,避免大量的数学逻辑;
- 使用原生的选择器 API;如 querySelector()和 querySelectorAll();
Summary 总结
- 通过避免使用 eval_r()和 Function()构造器避免二次评估。此外,给 setTimeout()和 setInterval()传递函数参 数而不是字符串参数
- 创建新对象和数组时使用对象直接量和数组直接量。它们比非直接量形式创建和初始化更快。
- 避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载。
- 当执行数学运算时,考虑使用位操作,它直接在数字底层进行操作。
- 原生方法总是比 JavaScript 写的东西要快。尽量使用原生方法。