Yet Another JavaScript Nerd
 

搬家

限于blogspot的各种限制,决定将博客平台更改为WordPress,相应的这个博客也将停止更新,以前的内容将陆续迁移到新的地址(http://oragg.com)。
 

Mozilla专有JavaScript扩展之一(__noSuchMethod__)

JavaScript中有很多内部属性和方法,在大多数情况下,只有JavaScript引擎才可以访问,但不论什么都是有特例的,在这里就是指Mozilla的JavaScript引擎,包括SpiderMonkey和Rhino,都提供了若干接口来访问这些内部属性,如果加以合理利用的话,不仅可以让JavaScript更加健壮,还可以开发出一些有意思的功能,比如本文介绍的__noSuchMethod__()方法就是其中之一。

著作权声明

本文译自Nicholas C. Zakas于2009年2月17日在个人网站上发表的《Mozilla JavaScript extension: __noSuchMethod__》。原文是唯一的正式版,本文是经过原作者(Nicholas C. Zakas)授权的简体中文翻译版(Simplified Chinese Translation)。译者(明达)在翻译的准确性上做了大量的努力,并承诺译文的内容完全忠于原文,但可能还是包含疏漏和不妥之处,欢迎大家指正。译注的内容是非正式的,仅代表译者个人观点。

以下是对原文的翻译:

和其它浏览器相比,Mozilla的JavaScript引擎总会有一些与众不同的亮点。SpiderMonkey和他的好搭档Rhino(用 Java实现的JavaScript引擎)都提供了很多扩展特性,可以帮助我们开发更加健壮的JavaScript应用。其中之一就是本地对象的__noSuchMethod__()方法。在大多数JavaScript引擎中,访问一个不存在的方法时都只会简单的抛出错误,而在Mozilla的引擎中,这只是默认行为,我们可以通过覆盖某个对象的__noSuchMethod__()方来来重新定义这个行为。当这个对象试图调用一个不存在的方法时,就会触发该对象的__noSuchMethod__()方法。

当__noSuchMethod__()方法被调用时,JavaScript引擎会传入两个参数,一个是调用方法的名称,一个是参数数组,对应于要传递给调用方法的所有参数,注意这个参数数组应该是一个Array对象,而不是arguments对象,而且就算没有参数,也要传递一个空数组,下面举一个简单的例子加以说明:
// 注意,下面的代码只有在使用SpiderMonkey或者Rhino的浏览器中才会被正确解析
var person = {
name: "Nicholas",
__noSuchMethod__: function(name, args) {
alert("Method called '" + name + "' executed with arguments [" + args + "]");
}
}

//"Method called 'sayName' executed with arguments []"
person.sayName();

//"Method called 'phone' executed with arguments [Mike]"
person.phone("Mike");
这段代码定义了一个person对象,并重写了该对象的__noSuchMethod__()方法。当调用person对象的sayName()方法和phone()方法时,由于这两个方法都不存在,所以__noSuchMethod__()方法会被自动调用,这样我们就避免了一个错误的显示,并且可以做出相应的处理。在上面这个例子中,我们所做的处理就是直接将方法的名称和传递的参数直接显示出来。

但话说回来,在运行时才发现未定义方法的情况,是不该出现在正常的开发实践中的,那简直就是自取烦恼。对于通常的情况来说,这个方法似乎没有什么作用,但他确实为我们提供了一个可能性,来创建一些有趣的动态工具,比如说建立一个可以输出有效XHTML文档的对象:
function HTMLWriter() {
this._work = [];
}
HTMLWriter.prototype = {
escape: function(text) {
return text.replace(/[><"&]/g,
function(c) {
switch (c) {
case ">":
return ">";
case "<":
return "<";
case "\"":
return """;
case " & ": return " & ";
}
});
},

startTag: function(tagName, attributes){
this._work.push(" < " + tagName);

if (attributes){
var name, value;
for (name in attributes){
if (attributes.hasOwnProperty(name)){
value = this.escape(attributes[name]);
this._work.push("" + name + " = \"" + value + "\"");
}
}
}
this._work.push(">");
},
text: function(text) {
this._work.push(this.escape(text));
},
endTag: function(tagName) {
this._work.push("");
},
toString: function() {
return this._work.join("");
}
};
var writer = new HTMLWriter();
writer.startTag("html");
writer.startTag("head");
writer.startTag("title");
writer.text("Example & Test");
writer.endTag("title");
writer.endTag("head");
writer.startTag("body", {
style: "background-color: red"
});
writer.text("Hello world!");
writer.endTag("body");
writer.endTag("html");
alert(writer);
这段代码通过三个方法来完成任务,分别是:startTag()、endTag()和text()。而上面给出的调用例子,却显得非常冗长。想象一下,如果不使用startTag()、endTag()和text()这三个方法,而是为每一个有效的XHTML标记都建立一个方法,那么这个例子的调用方法可能就会变成下面这个样子了:
var writer = new HTMLWriter();
var result = writer.html().head().title().text("Example & Test").xtitle().xhead()
.body().text("Hell world!").xbody().xhtml().toString();
由于每个标签的实现大致相同,所以我们可能需要为HTMLWriter对象复制一系列非常相似的方法,这无疑是一种严重的浪费行为。而这正是__noSuchMethod__()发挥真正作用的时候。让我们来看看用__noSuchMethod__()来实现这种效果到底有多么简单:
function HTMLWriter(){
this._work = [];
}

HTMLWriter.prototype = {

escape: function (text){
return text.replace(/[><"&]/g, function(c){
switch(c){
case ">": return ">";
case "<": return "<";
case "\"": return """;
case "&": return "&";
}
});
},

text: function(text){
this._work.push(this.escape(text));
return this;
},

toString: function(){
return this._work.join("");
},

__noSuchMethod__: function(name, args){
var tags = [
"a", "abbr", "acronym", "address", "applet", "area",
"b", "base", "basefont", "bdo", "big", "blockquote",
"body", "br", "button",
"caption", "center", "cite", "code", "col", "colgroup",
"dd", "del", "dir", "div", "dfn", "dl", "dt",
"em",
"fieldset", "font", "form", "frame", "frameset",
"h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "html",
"i", "iframe", "img", "input", "ins", "isindex",
"kbd",
"label", "legend", "li", "link",
"map", "menu", "meta",
"noframes", "noscript",
"object", "ol", "optgroup", "option",
"p", "param", "pre",
"q",
"s", "samp", "script", "select", "small", "span", "strike",
"strong", "style", "sub", "sup",
"table", "tbody", "td", "textarea", "tfoot", "th", "thead",
"title", "tr", "tt",
"u", "ul",
"var"
];

var closeTag = (name.charAt(0) == "x"),
tagName = closeTag ? name.substring(1) : name;

if (tags.indexOf(tagName) > -1){
if (!closeTag){
this._work.push("<" + tagName);

if (args.length){
var attributes = args[0],
name, value;
for (name in attributes){
if (attributes.hasOwnProperty(name)){
value = this.escape(attributes[name]);
this._work.push(" " + name + "=\"" +
value + "\"");
}
}
}

this._work.push(">");
} else {
this._work.push("");
}
return this;
} else {
throw new Error("Method '" + name + "' is undefined.");
}

}

};
这段代码的主要功能都是在__noSuchMethod__()中实现的。它包含一个数组,对应于全部有效的XHTML标签,用于查找可以调用的方法。如果需要关闭标签,只需要在方法名前面加一个“x”就可以,在__noSuchMethod__()中,会对方法名的首字母进行判断,如果首字母是“x”,就会标记为结束标签,并将“x”从方法名称中去掉。接下来,会通过Mozilla的数组扩展 indexOf()来判断方法名称是否在可用标签的数组里面,如果方法名是无效的,就会抛出一个错误;如果方法名是有效的,就会返回生成的标签字符串。代码所支持的标签数量是可以动态设置的,我们只需要对标签列表数组进行修改,就可以达到添加或者删除“方法”的目的。

很显然,由于这个特性不能跨浏览器,所以肯定不能用于通常的情况下。但对于那些专门针对Mozilla引擎(比如Firefox等)的JavaScript应用来说,这无疑为我们敞开了一道神奇的大门,尤其在开发动态接口时,__noSuchMethod__()将是一个非常强有力的工具。
 

在JavaScript中,为什么要尽可能使用局部变量?

JavaScript中,我们应该尽可能的用局部变量来代替全局变量,这句话所有人都知道,可是这句话是谁先说的?为什么要这么做?有什么根据么?不这么做,对性能到底能带来多大的损失?本文就来探讨这些问题的答案,从根本上了解变量的读写性能都和哪些因素有关。

著作权声明

本文译自Nicholas C. Zakas于2009年2月10日在个人网站上发表的《JavaScript Variable Performance》。原文是唯一的正式版,本文是经过原作者(Nicholas C. Zakas)授权的简体中文翻译版(Simplified Chinese Translation)。译者(明达)在翻译的准确性上做了大量的努力,并承诺译文的内容完全忠于原文,但可能还是包含疏漏和不妥之处,欢迎大家指正。译注的内容是非正式的,仅代表译者个人观点。

以下是对原文的翻译:

在如何提高JavaScript性能这个问题上,大家最常听到的建议应该就是尽量使用局部变量(local variables)来代替全局变量(global variables)。在我从事Web开发工作的九年时间里,这条建议始终萦绕在我的耳边,并且从来没有质疑过,而这条建议的基础,则来自于 JavaScript处理作用域(scoping)和标识符解析(identifier resolution)的方法。

首先我们要明确,函数在JavaScript中具体表现为对象,创建一个函数的过程,其实也就是创建一个对象的过程。每个函数对象都有一个叫做 [[Scope]]的内部属性,这个内部属性包含创建函数时的作用域信息。实际上,[[Scope]]属性对应的是一个对象(Variable Objects)列表,列表中的对象是可以从函数内部访问的。比如说我们建立一个全局函数A,那么A的[[Scope]]内部属性中只包含一个全局对象(Global Object),而如果我们在A中创建一个新的函数B,那么B的[[Scope]]属性中就包含两个对象,函数A的Activation Object对象在前面,全局对象(Global Object)排在后面。

当一个函数被执行的时候,会自动创建一个可以执行的对象(Execution Object),并同时绑定一个作用域链(Scope Chain)。作用域链会通过下面两个步骤来建立,用于进行标识符解析。

1. 首先将函数对象[[Scope]]内部属性中的对象,按顺序复制到作用域链中。
2. 其次,在函数执行时,会创建一个新的Activation Object对象,这个对象中包含了this、参数(arguments)、局部变量(包括命名的参数)的定义,这个Activation Object对象会被置于作用域链的最前面。

在执行JavaScript代码的过程中,当遇到一个标识符,就会根据标识符的名称,在执行上下文(Execution Context)的作用域链中进行搜索。从作用域链的第一个对象(该函数的Activation Object对象)开始,如果没有找到,就搜索作用域链中的下一个对象,如此往复,直到找到了标识符的定义。如果在搜索完作用域中的最后一个对象,也就是全局对象(Global Object)以后也没有找到,则会抛出一个错误,提示用户该变量未定义(undefined)。这是在ECMA-262标准中描述的函数执行模型和标识符解析(Identifier Resolution)的过程,事实证明,大部分的JavaScript引擎确实也是这样实现的。需要注意的是,ECMA-262并没有强制要求采用这种结构,只是对这部分功能加以描述而已。

了解标识符解析(Identifier Resolution)的过程以后,我们就能明白为什么局部变量的解析速度要比其他作用域的变量快,主要是由于搜索过程被大幅缩短了。但是,具体会快多少呢?为了回答这个问题,我模拟了一系列的测试,来测试不同作用域深度中变量的性能。

第一个测试是向一个变量中写入一个最简单的值(这里使用字面量的数值1),结果如下图显示,很有趣:



从结果中不难看出,当标识符解析的过程需要进行深度搜索时,会伴随性能损失,而且性能损失的程度会随着标识符深度的增加而递增。意料之中的是,Internet Explorer表现的是最差的(但公平的说,IE 8还是有一些改善的)。值得注意的是,这里有一些例外情况,Google Chrome和最新的WebKit午夜版在访问变量的时间保持得很稳定,不会随着作用域深度的递增而增长。当然,这应该归功于它们所使用的下一代 JavaScript引擎,V8和SquirrelFish。这些引擎在执行代码时进行了优化,而且很明显,这些优化使访问变量的速度比以往更快。 Opera表现的也很不错,比IE、Firefox和当前版本的Safari要快的多,但比基于V8和Squirrelfish的浏览器要慢。 Firefox 3.1 Beta 2的表现有点出人意料,对于局部变量执行的效率非常高,但随着作用域层数的增加,效率便大打折扣。需要注意的是,我这里使用的都是默认设置,也就是说 Firefox是没有开启Trace功能的。

上面的结果是通过对变量执行写操作而得出的,其实我很好奇,读取变量时的情况会不会有什么不同,于是接着做了下面的测试。结果发现,读的速度要比写的速度快一些,但是性能变化的趋势是一致的。



和上个测试一样,Internet Explorer和Firefox还是最慢的,Opera表现了非常抢眼的性能,而同样的,Chrome和最新版本的Webkit午夜版显示了和作用域深度无关的性能趋势,同样需要注意的是,Firefox 3.1 Beta 2的变量访问时间还是会伴随着深度出现一个奇怪的跳跃。

在测试的过程中,我发现一个有趣的现象,就是Chrome在访问全局变量的时候会有额外的性能损失。访问全局变量的时间和作用域层数没有关系,但是会比访问同样层数的局部变量的时间多出50%。

这两个测试可以给我们带来什么启示呢?首先是验证了那个古老的观点,就是要尽可能的使用局部变量。在所有的浏览器下,访问局部变量都比访问跨作用域的变量要快,当然也包括全局变量。下面这几点应该是通过这个测试得出的经验吧:

* 仔细检查函数中所有使用的变量,如果有一个变量不是当前作用域定义的,而且使用了不止一次,那么我们就应该把这个变量保存在局部变量中,而使用这个局部变量来进行读写操作。这样可以帮助我们将作用域外的变量的搜索深度减少到1.这对全局变量尤为重要,因为全局变量总是被放到作用域链的最后位置来搜索。
* 避免使用with语句。因为它会修改执行上下文(Execution Context)的作用域链,在最前面添加一个对象(Variable Object)。这就意味着在执行with的过程中,实际上的局部变量都被移到作用域链上的第二个位置,这会带来性能上的损失。
* 如果你确定一段代码肯定会抛出异常,那么就要避免使用try-catch,因为catch分支在作用域链上的处理方法和with是一样的。但try分支的代码是没有性能损失的,所以还是建议用try-catch来捕获那些不可预知的错误。

如果你想围绕这个话题展开更多的讨论,我在上个月的Mountain View JavaScript Meetup中曾经发表了一个小演讲。可以在SlideShare上下载幻灯片,或者观看聚会的完整视频,我的演讲大概从11分钟左右时开始。

译者笔记

大家如果在阅读本文的过程中,有什么疑惑,建议延伸阅读以下两篇文章:
* Richie写的《JavaScript对象模型-执行模型
* 《ECMA-262第三版》,主要看看第十章,就是执行上下文(Execution Context)那张,本文提到的名词在那里都有详细的解释。

在最后的时候,Nicholas提到一个Mountain View JavaScript Meetup,Meetup那个网站其实就是一个各种现实世界活动的组织网站,需要翻墙才能访问,住在California真幸福,有那么多的好活动可以参加,呵呵。
 

如何判断JavaScript变量的类型

据类型是所有开发语言的基础,JavaScript虽然是一个弱类型的脚本语言,但是在数据类型上也有很多讲究的,看了淘宝UED玉伯的一篇文章,末尾有一个判断数据类型的函数,仔细揣摩后发现有一些改进的余地,于是有了本文。目标就是一个可以提供足够类型参考信息的函数。

【本文地址】http://cuimingda.com/2009/02/how-to-detect-javascript-object-type.html
【测试地址1】http://lab.cuimingda.com/jquery/tof/
【测试地址2】http://lab.cuimingda.com/detection/window.html

在JavaScript中,变量中可以存储的值主要有两种类型:原始值(primitive value)和引用值(reference value)。前者通常是固定而又简单的数据,存储在栈(stack)中,而后者则是比较大的对象,存储在堆(heap)中,而对于后者的调用,是通过存储在栈中的指针来完成的。原始类型有五种:Number、String、Boolean、Null和Undefined,引用类型都继承自Object。

我们的最终目标就是一个叫做tof的全局函数,返回值为字符串,代表anything对应的类型,因为所有的引用类型都是object,所以这里返回的类型应该不局限于数据类型,应该尽可能的反映出anything的真实信息:
/*
var t = tof(anything);
*/
第一个实现肯定是利用传统的typeof来实现:
/*
function tof(val) {
return typeof(val);
}
*/
typeof可以得出的结论有五种:undefined、string、number、boolean和object。和原始类型比较还差个null,这个比较特殊,typeof(null)的返回值是object,这本来是JavaScript早期的bug,但后来却被写入了ECMAScript标准,可以理解为null是object的占位符。很明显,typeof无法满足我们的需求。

幸好Google的Mark Miller找到另外一个非常有效的判断数据类型的方法,就是利用Object.prototype.toString.call,用这个方法对Date类型数据进行计算,得到的结果是“[object Date]”,对于其他类型,前半部分始终是object,后半部分会发生变化,这样就很方便我们判断了。jQuery从1.3开始,也采用了这种方法判断array和function。下面我们来改进一下:
/*
function tof(val) {
return Object.prototype.toString.call(val).match(/object\s(\w+)/)[1];
}
*/
这个函数现在已经可以判断非常多的情况了,等等,undefined和null怎么返回的是Window(Firefox 3.0.6和Opera 9.63),在IE 7下返回Object,在Chrome返回builtins,在Safari 3.2下返回DOMWindow,明显优于这两个被定义为全局对象的属性,在不同的浏览器下挂到了不同的全局对象上。既然这么特殊,只能对他们单独有待一下了:
/*
function tof(val) {
var t;
switch(val) {
case null: t = "null"; break;
case undefined: t = "undefined"; break;
default:
t = Object.prototype.toString.call(val).match(/object\s(\w+)/)[1];
break;
}
return t.toLowerCase();
}
*/
到这里,我们已经可以判断很多object了,包括array、regexp、date等。让我们再多想一些,比如你碰到过需要判断DOM元素类型的情况么?这个首先要说明个问题,我们访问DOM,其实有两种方式,一种是通过树型的继承方式,就是Element为基础的,另外一种是构建在xml基础上的node模式,而对应每个节点都有tagName和nodeName两个属性,大部分时候要求是必须一样的,一些特殊节点只有nodeName,没有tagName,比如document的nodeName为“#document”,tagName为空值。
/*
function tof(val) {
var t;
switch(val) {
case null: t = "null"; break;
case undefined: t = "undefined"; break;
default:
t = val.nodeName || Object.prototype.toString.call(val).match(/object\s(\w+)/)[1];
break;
}
return t.toLowerCase();
}
*/
如果是DOM元素,首先取节点名称,取不到再用老办法判断对象类型。这样,不管是通过document.createElement构建的元素,还是通过document.getElementById获取的元素,都可以得到一个可以参考的类型了,注意此类型非彼类型,只是元素名称而已。经过了以上几个步骤,还有什么情况会出现object呢,其中一个就是我们自己定义的对象,或者称为类?这里我们采用构造函数来搞定。
/*
function tof(val) {
var t;
switch(val) {
case null: t = "null"; break;
case undefined: t = "undefined"; break;
default:
t = val.nodeName || Object.prototype.toString.call(val).match(/object\s(\w+)/)[1];
if(!!val.constructor && t.toLowerCase() === "object") {
t = val.constructor.toString().match(/^\s*function\s(\w+)/)[1];
}
break;
}
return t.toLowerCase();
}
*/
构造函数的判断放到最后,不得已而为之的情况。一是要处理那些还处于object状态的,二是要确保人家要有构造函数,可不是每个对象都有构造函数的。构造函数的类型是function,如果直接拿来比较在有些时候会有问题,比如不同框架的时候。所以我们将function的内容转换为字符串,通过正则表达式来取函数名。这里有个插曲就是,IE很缺德,在function前面加了一个空格,害我多调了好半天正则表达式。

基本上完结了,我们还是没有彻底的消除object,一个典型的情况就是通过{}直接定义对象,那个名字我是不会获取了,谁要是知道一定要告诉我。再比较特殊的就是IE了,有N多对象无法判断实际类型,只能给出object,暂时没有头绪。

2月7日更新,发现原来的代码还是存在很多漏洞的,更新了一下,并转换为jQuery插件:
/*
(function($){
$.extend({
tof: function(val, description) {
var result, constructorName;
switch(val) {
case null: result = "Null"; break;
case undefined: result = "Undefined"; break;
default:
result = Object.prototype.toString.call(val).match(/^\[object\s(\w+)\]$/)[1];

if(typeof(Node) !== "undefined") {
if(val instanceof Node) {
if(typeof(val.nodeName) === "string") {
result = val.nodeName;
}
}
}
else {
if(typeof(val.nodeName) === "string") {
result = val.nodeName;
}
}

if(result === "Object") {
// can not access fireunit's constructor
try {
if(typeof(val.constructor) !== "undefined") {
constructorName = val.constructor.toString().match(/^\s*function\s(\w+)/);
if(constructorName !== null) {
result = constructorName[1];
}
}
} catch(err) {}
}
break;
}
return result.toLowerCase();
}
});
})(jQuery);
*/
这里需要提一下的是,在IE中,无论怎么判断,alert、confirm和prompt三个函数的类型都是object,而在其他所有浏览器中都是function。

在玉伯那里,有一个这样的例子,经过测试是可以判断出数组的:
/*
function SubArray() {}
SubArray.prototype = [];
jstest.add(new SubArray(), "array", "prototype is array");
*/
对于数值来说,通过字面量定义的肯定返回number,而对于用new Number来定义的,虽然typeof的值是object,其实使用的过程中都会自动调用valueOf,所以在这里也会返回number的。而对于那些定义在全局变量上,和数值相关的特殊属性,获取类型也是number:
/*
jstest.add(0, "number", "literal number 0");
jstest.add(1, "number", "literal number 1");
jstest.add("1", "string", "string \"1\"");
jstest.add(new Number(1.5), "number", "new Number(1.5)");
jstest.add((new Number(1.5)).valueOf(), "number", "(new Number(1.5)).valueOf()");
jstest.add((new Number(1.5)).toString(), "string", "(new Number(1.5)).toString()");
jstest.add(NaN, "number", "NaN - not a number");
jstest.add(Infinity, "number", "Infinity");
jstest.add(-Infinity, "number", "-Infinity");
*/
和布尔值相关的测试用例:
/*
jstest.add(true, "boolean", "literal boolean true");
jstest.add(false, "boolean", "literal boolean false");
jstest.add(new Boolean(true), "boolean", "new Boolean(true)");
*/
和浏览器的内置对象相关的测试用例:
/*
jstest.add(new Error(), "error", "new Error()");
jstest.add(new EvalError(), "error", "new EvalError()");
jstest.add(new Date(), "date", "new Date()");
jstest.add(/abc/, "regexp", "literal regular expression");
jstest.add(new RegExp("a?"), "regexp", "new RegExp()");
jstest.add([], "array", "array by []");
jstest.add(new Array(), "array", "array by new Array()");
jstest.add(Math, "math", "Math");
*/
DOM对象的测试用例:
/*
jstest.add(document.createElement("div"), "div", "HTML Element");
jstest.add(document, "#document", "document");
*/
自定义对象的测试用例:
/*
function TestClass(){};
jstest.add(new TestClass(), "testclass", "custom object, new TestClass()");
*/

2009年2月10日更新,上面的测试用例,测试的内容没有变,但已经单独提出来了,这两天整理出一篇来。

测试环境:
* Firefox 3.0.6
* Internet Explorer 7.0
* Safari 3.2
* Opera 9.63
* Chrome 1.0.154.48

参考资料:
* http://lucassmith.name/pub/typeof.html
* http://blog.360.yahoo.com/blog-TBPekxc1dLNy5DOloPfzVvFIVOWMB0li?p=916
* http://lifesinger.org/blog/?p=1109
* http://lifesinger.org/blog/?p=1130
* http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html

写在最最后面的
现在的状态应该说只能算是第一个版本吧,应该还有很大的优化余地,最终的目标应该是一个非常增强的typeof,不止要能适应各个浏览器的各个版本,还要尽可能多的判断出各种对象。大家要有好的改进想法,记得告诉我哟。
 

如何提升JavaScript的运行速度(DOM篇)

Web开发中,JavaScript的一个很重要的作用就是对DOM进行操作,可你知道么?对DOM的操作是非常昂贵的,因为这会导致浏览器执行回流操作,而执行了过多的回流操作,你就会发现自己的网站变得越来越慢了,我们应该尽可能的减少DOM操作。本文是这个系列的最后一篇,给出了一些指导性原则,比如在什么时候应该对DOM可以进行什么样的操作等。

【原文】Nicholas C. Zakas - Speed up your JavaScript, Part 4
【译文】明达 - 如何提升JavaScript的运行速度(DOM篇)

以下是对原文的翻译:

在过去的几周中,我为大家介绍了几种可以加快JavaScript脚本运行速度的技术。第一节介绍了如何优化循环。第二节的重点放在优化函数内部代码上,还介绍了队列(queuing)和记忆化(memoization)两种技术,来减轻函数的工作负担。第三节就如何将递归转换为迭代循环或者记忆化方式的话题,展开了讨论。第四节是这个系列的最后一篇,也就是本文,将重点阐述过多的DOM操作所带来的影响。

我们都知道,DOM操作的效率是很低的,而且不是一般的慢,而且这也是引发性能问题的常见问题之一。为什么会慢呢?因为对DOM的修改为影响网页的用户界面,重绘页面是一项昂贵的操作。太多的DOM操作会导致一系列的重绘操作,为了确保执行结果的准确性,所有的修改操作是按顺序同步执行的。我们称这个过程叫做回流(reflow),同时这也是最昂贵的浏览器操作之一。回流操作主要会发生在几种情况下:

* 当对DOM节点执行新增或者删除操作时。
* 动态设置一个样式时(比如element.style.width="10px")。
* 当获取一个必须经过计算的尺寸值时,比如访问offsetWidth、clientHeight或者其他需要经过计算的CSS值(在兼容DOM的浏览器中,可以通过getComputedStyle函数获取;在IE中,可以通过currentStyle属性获取)。

解决问题的关键,就是限制通过DOM操作所引发回流的次数。大部分浏览器都不会在JavaScript的执行过程中更新DOM。相应的,这些浏览器将对对DOM的操作放进一个队列,并在JavaScript脚本执行完毕以后按顺序一次执行完毕。也就是说,在JavaScript执行的过程中,用户不能和浏览器进行互动,直到一个回流操作被执行。(失控脚本对话框会触发回流操作,因为他执行了一个中止JavaScript执行的操作,此时会对用户界面进行更新)

如果要减少由于DOM修改带来的回流操作,有两个基本的方法。第一个就是在对当前DOM进行操作之前,尽可能多的做一些准备工作。一个经典的例子就是向document对象中添加很多DOM节点:
/*
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
list.appendChild(item);
}
*/
这段代码的效率是很低的,因为他在每次循环中都会修改当前DOM结构。为了提高性能,我们需要将这个次数降到最低,对于这个案例来说,最好的办法是建立一个文档碎片(document fragment),作为那些已创建元素元素的临时容器,最后一次将容器的内容直接添加到父节点中:
/*
var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
fragment.appendChild(item);
}
list.appendChild(fragment);
*/
经过调整的代码,只会修改一次当前DOM的结构,就在最后一行,而在这之前,我们用文档碎片来保存那些中间结果。因为文档碎片没有任何可见内容,所以这类修改不会触发回流操作。实际上,文档碎片也不能被添加到DOM中,我们需要将它作为参数传给appendChild函数,而实际上添加的不是文档碎片本身,而是它下面的所有子元素。

避免不必要回流操作的另外一种方法,就是在对DOM操作之前,把要操作的元素,先从当前DOM结构中删除。对于删除一个元素,基本有两种方法:
1. 通过removeChild()或者replaceChild()实现真正意义上的删除。
2. 设置该元素的display样式为“none”。

而一旦修改操作完成,上面这个过程就需要反转过来,将删除的元素重新添加到当前的DOM结构中,我们还是拿上面的例子来做说明:
/*
list.style.display = "none";
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
list.appendChild(item);
}
list.style.display = "";
*/
将list的display样式设置为“none”后,就将这个元素从当前的DOM结构中删除了,因为这个节点不再可视。在将display属性设置回之前的默认值之前,向其下添加子元素是不会触发回流操作的。

另外一个经常引起回流操作的情况是通过style属性对元素的外观进行修改。比如下面这个例子:
/*
element.style.backgroundColor = "blue";
element.style.color = "red";
element.style.fontSize = "12em";
*/
这段代码修改了三个样式,同时也就触发了三次回流操作。每次修改元素的style属性,都肯定会触发回流操作。如果你要同时修改一个元素的很多样式,最好的办法是将这些样式放到一个class下,然后直接修改元素的class,这可比单独修改元素的样式要强得多。比如下面这个例子:
/*
.newStyle {
background-color: blue;
color: red;
font-size: 12em;
}
*/
这样我们在JavaScript代码中,只需下面这行代码就可以修改样式:
/*
element.className = "newStyle";
*/
修改元素的class属性,会一次将所有的样式应用在目标元素上,而且只会触发一次回流操作。这样做不止更加有效,而且还更容易维护

既然DOM几乎在所有情况下都很慢,就很有必要将获取的DOM数据缓存起来。这种方法,不仅对获取那些会触发回流操作的属性(比如offsetWidth等)尤为重要,就算对于一般情况,也同样适用。下面介绍一个效率低的夸张的例子:
/*
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
document.getElementById("myDiv").offsetWidth + "px";
*/
这里对getElementById()调用了三次,是一个很大的问题,访问DOM是很昂贵的,而这三个调用恰恰访问的是同一个元素,也许我们像下面这样写,会更好一些:
/*
var myDiv = document.getElementById("myDiv");
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";
*/
我们去掉了一些冗余操作,现在对DOM操作的次数已经被减小了。对于那些使用次数超过一次的DOM值,我们都应该缓冲起来,这样可以避免无谓的性能消耗。

也许,拖慢属性访问速度的罪魁祸首就是HTMLCollection对象。这些对象是object类型的,只要DOM需要返回一组节点时就会使用这个对象,也就是说childNodes属性和getElementsByTagName()的返回值都属于这种情况。我们可能经常会将HTMLCollection当作数组来使用,但实际上他是一个根据DOM结构自动变化的实体对象。每次你访问一个HTMLCollection对象的属性,他都会对DOM内所有的节点进行一次完整匹配,这意味着下面的代码将导致一个死循环:
/*
var divs = document.getElementsByTagName("div");
for (var i=0; i < divs.length; i++){ //infinite loop
document.body.appendChild(document.createElement("div"));
}
*/
这段代码为什么会变成死循环呢?因为在每次循环中,将会向document中新增一个div元素,同时也会更新divs这个集合,也就是说循环的索引永远都不会超过divs.length的值,因为divs.length的值是伴随着循环而递增的。每次访问divs.length,就会更新一次集合对象,这可比访问一个普通数组的length属性要付出更大的代价。当对HTMLCollection对象进行操作时,应该将访问的次数尽可能的降至最低,最简单的,你可以将length属性缓存在一个本地变量中,这样就能大幅度的提高循环的效率。
/*
var divs = document.getElementsByTagName("div");
for (var i=0, len=divs.length; i < len; i++){ //not an infinite loop
document.body.appendChild(document.createElement("div"));
}
*/
修改后的代码已经不是死循环了,因为在每次循环时,len的值都是保持固定不变的。将属性值缓存起来除了更加有效率,还可以保证document不会执行多于一次的查询。

本文是“Speed up your JavaScript”这个系列的最后一篇文章,我希望你现在已经知道如何避免那个脚本失控的对话框,以及如何让你的脚本运行的更快。我所提到的技巧很多别人已经提过了,我只是将它们组织到一起,这样大家可以更容易的找到这些信息。如果你有什么更好的话题需要来我整理,在评论中直接告诉我,或者直接联系我吧。
 

猜猜看,大家是不是喜欢jQuery 1.3这个版本?

jQuery 1.3在2009年1月14日发布,这个版本大家满意么?大家很关心这个问题,其实这也是整个jQuery团队非常关心的问题之一。John Resig通过两个在线工具Google Analytics和Google Trends的数据来做分析,从一个侧面反映了jQuery 1.3这个版本的状况,同时也对jQuery的历史发展做了相关的说明。

【原文】John Resig - jQuery 1.3 Aftermath
【译文】明达 - 猜猜看,大家是不是喜欢jQuery 1.3这个版本?

以下是对原文的翻译:

有一个问题其实很难回答:如何判断一个版本是否取得了成功?在jQuery这个项目中,我们试图寻找一系列可以作为依据的准则。

* 用户对这个版本是否满意?
* 用户是否接受这个版本?
* 我们是否满足了那些以前不使用jQuery的用户的需要?

这几个问题都没有足够精确的数字来支撑(但我们确实仔细的倾听了用户反馈,无论是在博客上、Twitter上、邮件列表上、或是其他的任何地方,但截止到目前,我们得到的大部分是积极的肯定),但我们确实有几个可以用来利用的工具,让这个问题变得更加简单一些,这些工具就是Google AnalyticsGoogle Trends



上面这个趋势图,是通过Google Analytics对jquery.com在2008年12月和2009年1月的访问记录进行比对的结果。

我们可以很容易发现,相比于12月份,每天都有30%左右的增长。这是我非常愿意看到的,1.3这个版本非常具有“粘性”(用户会经常回来看看,每日访问人数不会低于预发布阶段时的程度)。

注意比对两个时间段的结果,跳出率(bounce rate)出现增长,而平均每次访问页数(pages/visit)和平均停留时间(avg. time on site)出现降低,而导致出现这个变化的页面都集中在1.3和1.3.1的发布页面,大家都是过来检查最新的发布版本,然后直接离开的。

2009年1月14日是jQuery 1.3发布的日子(那一周的流量特别大,Ajaxian、Reddit、Hacker News以及很多博客都介绍了这个新版本的发布),在26号所在的那周我们上了Digg的首页,但是在流量方面没有看到明显的变化。

我将圣诞假期这个时间段的数据去掉了,因为那时候的流量非常低,不利于比较。

以上数据都只是专门针对jQuery的,jQuery UI和静态文件都采用单独的配置文件进行统计。



Google Trends可以帮助我们了解jQuery的使用情况,最典型的应用就是“圣诞节大衰退”。

关注jQuery的用户主要出现在工作日(注意前面的分析日志,周末会出现衰退)。每年的圣诞节假期左右(12月23日 - 次年1月3日),访问人数都会有很大幅度的下降,我们也可以在Google Trends的统计中看出这种关联性。

在维护jQuery的过程中,我们学习到了一件事情,就是在两个项目的交替时会流失用户。一个开发者在结束了一个项目后,会重新评估他的开发工具链,寻找可以改进的地方,这个时候,用户可能会更换所使用的工具,我们的使命就是确保始终提供最棒的工具和经验,这样大家就没有更换工具的必要了(比如提供更清晰的文档、更有效率的代码以及频繁的更新等)。

还有一个类似的情况发生在休假之后。当用户休息了一到两周,没有碰工作时的代码,在回到工作岗位后就存在更换工具的可能性,要么被这些用户放下,要么就做到足够强劲,吸引用户继续使用。

现在的问题已经演变为:我们如何在低迷的时候挽留住用户(最好是发现新的用户)?

当我们仔细研究2006-2007时的低迷期数据,我们会发现在用户休假归来的时候有一个快速的反弹。为什么?因为jQuery 1.1在那个时候发布了。

但是回顾2007-2008的数据,休假以后基本上没有反弹,而且花了将近半年的时间才恢复到之前的水平,顺便说一下,在一月份没有发布任何重大版本。

于是我们修正了这个时间周期,我们发布了jQuery 1.3,你可以注意到我们是如何赢回用户,甚至在一段时间内提高了市场占用率的。

从长远的角度看,我对jQuery 1.3的发布非常有信心,我认为我们将有一个非常辉煌的2009年。很可能这周我们就发布另外一个后续版本(1.3.2),除这以外还有一点很明确,我们将在1.3.3以及后续的版本中增加更多全新的功能。
 

屏蔽Ad blockers,可行么?

Firefox中最著名的扩展之一要属Adblock Plus了,有了这个扩展我们就可以清清静静的看网站,但是这对于网站的开发者来说可不是什么好消息,都把广告屏蔽了拿什么养家糊口啊。刚就在twitter中看到一个,只找到脚本的片段,但是其中的思想大家一看就可以理解了。但其实个人不是很推荐,如果网站的广告有针对性,大家不就没有理由屏蔽了,不是么?

【消息来源】http://twitter.com/schill/status/1167839656
【本文地址】http://cuimingda.com/2009/02/block-ad-blockers.html

先看截图,当用户使用广告屏蔽手段,而又没将这个网站加入白名单时,网站的所有内容就会被替换成下面的图片提示:


实现这个功能其实很简单,就是下面这一小段代码,我把完整的内容都粘贴过来了:
/* http://www.publicradio.org/config/cobrand/standard/js/apm001/national_fix.js
$(window).load(function() {
var $mainDiv = $('#apmAdsContainer');
var myHeight = $mainDiv.height();
if (myHeight == 0) {
$('body').css({
'background-image': 'url(http://www.publicradio.org/config/cobrand/standard/images/apm001/blockimg.gif)',
'background-position': 'center 5px',
'background-repeat': 'no-repeat'
});
}
});
*/
脚本的基本思路就是判断广告图层的高度,如果为0,表明被屏蔽了,就拒绝显示内容。这段代码依赖于jQuery,虽然作者加载的部分写的不是很好,但为了完整,也一起贴过来吧,唉,为什么要用document.write呢?
/* http://www.publicradio.org/config/cobrand/standard/js/apm001/jQ_detect.js
function include_js(js_url) {
document.write("\<script type=\"text/javascript\" ");
document.write("src='" + js_url + "'");
document.write("\>");
document.write("\</script\>");
}
if (typeof(window["jQuery"]) == "undefined") {
include_js("http://americanpublicmedia.publicradio.org/standard/js/all_domains/jquery/jquery_1.2.6.pack.js");
}
*/
对于Adblock Plus来说,除了可以隐藏元素以外,还可以组织脚本加载,这时候这段脚本可能就无效了,也许需要判断广告对象是否存在?而且让元素消失的方法也不是只有一种,应该多判断几种才能兼容各种情况。但其实最主要的,也许这样做会吓跑部分用户吧,呵呵,通过其他方式积极的引导用户将自己的网站加到Adblock Plus的白名单中,也许是个更好的主意。

历史上,加密和解密作为两大技术阵营,此消彼长,客观上促进了安全技术的发展。Ad和Ad blocker会不会也这样呢?

咳咳~~大家如果真的看到这里,感觉这个博客还不错,记得把他放进Adblock Plus的白名单啊,我这里可没有广告,连Adsense都没有,主要我得知道你什么时候来过哟(Adblock Plus China-list会屏蔽各种网站统计工具,比如我用的Google Analytics,^_^),呵呵,这样我才能更有动力嘛。