JavaScript中有很多内部属性和方法,在大多数情况下,只有JavaScript引擎才可以访问,但不论什么都是有特例的,在这里就是指Mozilla的JavaScript引擎,包括SpiderMonkey和Rhino,都提供了若干接口来访问这些内部属性,如果加以合理利用的话,不仅可以让JavaScript更加健壮,还可以开发出一些有意思的功能,比如本文介绍的__noSuchMethod__()方法就是其中之一。
// 注意,下面的代码只有在使用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__()方法会被自动调用,这样我们就避免了一个错误的显示,并且可以做出相应的处理。在上面这个例子中,我们所做的处理就是直接将方法的名称和传递的参数直接显示出来。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()来判断方法名称是否在可用标签的数组里面,如果方法名是无效的,就会抛出一个错误;如果方法名是有效的,就会返回生成的标签字符串。代码所支持的标签数量是可以动态设置的,我们只需要对标签列表数组进行修改,就可以达到添加或者删除“方法”的目的。在JavaScript中,我们应该尽可能的用局部变量来代替全局变量,这句话所有人都知道,可是这句话是谁先说的?为什么要这么做?有什么根据么?不这么做,对性能到底能带来多大的损失?本文就来探讨这些问题的答案,从根本上了解变量的读写性能都和哪些因素有关。


数据类型是所有开发语言的基础,JavaScript虽然是一个弱类型的脚本语言,但是在数据类型上也有很多讲究的,看了淘宝UED玉伯的一篇文章,末尾有一个判断数据类型的函数,仔细揣摩后发现有一些改进的余地,于是有了本文。目标就是一个可以提供足够类型参考信息的函数。
/*
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无法满足我们的需求。/*
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前面加了一个空格,害我多调了好半天正则表达式。/*
(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");
*/自定义对象的测试用例:2009年2月10日更新,上面的测试用例,测试的内容没有变,但已经单独提出来了,这两天整理出一篇来。/*
function TestClass(){};
jstest.add(new TestClass(), "testclass", "custom object, new TestClass()");
*/
在Web开发中,JavaScript的一个很重要的作用就是对DOM进行操作,可你知道么?对DOM的操作是非常昂贵的,因为这会导致浏览器执行回流操作,而执行了过多的回流操作,你就会发现自己的网站变得越来越慢了,我们应该尽可能的减少DOM操作。本文是这个系列的最后一篇,给出了一些指导性原则,比如在什么时候应该对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函数,而实际上添加的不是文档碎片本身,而是它下面的所有子元素。/*
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属性设置回之前的默认值之前,向其下添加子元素是不会触发回流操作的。/*
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属性,会一次将所有的样式应用在目标元素上,而且只会触发一次回流操作。这样做不止更加有效,而且还更容易维护。/*
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值,我们都应该缓冲起来,这样可以避免无谓的性能消耗。/*
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不会执行多于一次的查询。jQuery 1.3在2009年1月14日发布,这个版本大家满意么?大家很关心这个问题,其实这也是整个jQuery团队非常关心的问题之一。John Resig通过两个在线工具Google Analytics和Google Trends的数据来做分析,从一个侧面反映了jQuery 1.3这个版本的状况,同时也对jQuery的历史发展做了相关的说明。


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

/* 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的白名单中,也许是个更好的主意。