Mozilla JavaScript extension: __noSuchMethod__
http://www.nczonline.net/blog/2009/02/17/mozilla-javascript-extension-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__()将是一个非常强有力的工具。