1、Type - 类型
1)Object类型。在Prototype的基本类型中,因为js是无类型语言这种无类型,纵观全局,Object提供一些供全局静态调用的一些方法。它们包括:检测,扩展。且没有污染到Object的原型对象。(这一点很重要)
2)String类型。它将String对象原型进行进一步的扩展,扩展的方法都是一些常见的strip, stripTags, truncate之类的,有几个方法值得一提:gsub方法,这个方法用来作一个全局的替换,传入的参数有两个,一个是pattern,为正则表达式,第二个是replacement,要替换的数符串。
3)Array类型。它并无特别之处,只是将一些常用功能进行扩展。
4)Hash类型。并非是JS内置内型,是一个属于Prototype自定义的一种类型,支持json类型的序列化和反序列化
5)Function类型。可以说这是几个类型里扩展得比较特别的地方。
argumentNames。得到函数的形参名称,并返回一个形参(字符串型)的一个数组,主要用于动态的调用。与invoke可以形成一个simple Factory pattern。
bind,通俗的说,bind不会让this指针被劫持。说得理论一些,就是用于绑定上下文。关于这一点,我在下面的Context节中会有详细一些的描述。bindAsEventListener,与bind类似,主要区别在于它传递了event对象。
curry方法克里化方法。用于动态的传递参数。比如
Function.prototype.curry = function() {
if (!arguments.length) return this;
var __method = this, args = Array.prototype.slice.call(arguments,0);
return function() {
return __method.apply(this, args.concat(Array.prototype.slice.call(arguments,0)));
}
}
var F=function(){alert(Array.prototype.slice.call(arguments,0).join(' '))};
F.curry('I').curry('am').curry('never-online').curry('http://www.never-online.net')();
</script>
delay函数延迟执行。
wrap把自身函数进行包装,并把包装好的函数作为形参函数中所传递的第一个参数,简单的说就是自身封装。动态改变的将已包装函数的上下文(包装函数的this与执行时的this是一致的)看个示例吧。
Function.prototype.bind = function() {
if (arguments.length < 2 && typeof(arguments[0])=='undefined') return this;
var __method = this, args = Array.prototype.slice.call(arguments,0), object = args.shift();
return function() {
return __method.apply(object, args.concat(Array.prototype.slice.call(arguments,0)));
}
}
Function.prototype.wrap = function(wrapper) {
var __method = this;
return function() {
return wrapper.apply(this, [__method.bind(this)].concat(Array.prototype.slice.call(arguments,0)));
}
}
var a = {
b: {
c: function () {
}
},
f: function () {
alert(this==a.b);
}
};
a.b.c = a.f.wrap(function (F) {
F();
});
a.b.c();
</script>
a.f被wrap,在匿名函数中,F参数就是a.f,这个被包装好的函数被赋给a.b.c这个函数。我们最后一行调用了a.b.c这个函数,从源代码里可以看到this原来是属于a这个对象,但根据示例之前所说的,这个wrap会动态的替换this。所谓动态,就是看你是怎么调用的。a.f方法的this被谁替换的答案就是,因为a.f从上面的a.b.c()执行,它的对象就是a.b,如果是这样
F();
});
f();
this就会是window,自然上面运行的结果就是false。这个奥秘就是在返回的闭包里。如果你理解闭包,理解apply,就可以从wrap原型中找到答案了。具体的要用书面来解释的话,还是有太多题外话要说,有不清楚的话就在评论中留言吧。
methodize
将动态的指针this作为参数传入闭包中。这个方法,我不是很清楚它的具体作用。看代码是比较简单的,但为何要这样做我也不得而知。
2、Hack
我把这个词从css hack中取过来,暂且放到这里,意指,灵活运用js机制从而实现简化代码的作用。
1) 先不说这个Hack的具体内容,我们不妨思考一个问题,在可列举型的对象上,如:Array, Object等对象,如何实现一个迭代器接口,即each, 利用each实现all(并在每次迭代中执行函数,如果执行函数返回false,则跳出each迭代,也跳出all函数)等方法。
首先, 先看普通的实现方法, 它将会有一些限制.
/**
* 如上文所说,先实现each,参数有一个fn,
* 用apply去动态的调用这个函数并给予形参value还有index
*
* @method each
* @param {function}
* @return void
*/
Array.prototype.each = function (fn) {
var self = this; var i = this.length;
while(i--) fn.apply(null,[self[i],i]);
}
/**
* 但在all的实现中就不好实现了
* 因为each是循环的执行某一函数,
* 不论函数何值都会执行到最后一个元素
* 因此我们不能够从函数中安全的返回
*
* @method each
* @param {function}
* @return void
*/
Array.prototype.all = function (fn) {
this.each(
function (value, index) {
if (false===fn.apply(null,[value,index])) {
return false;
}
alert('this is a test for outer Function run count as ' +(value));
//执行为false时已经将这层函数返回,因此这里将会出现9个alert,很明显不符合我们预期(预期是执行为false时将all函数也返回)
}
);
};
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.all(function (value, index) {
var result = value==6?false:true;
return result;
});
</script>
从上面的例子来看,我们为什么不在all中单独的实现呢,而是调用each来实现? 当然, 我们可以在all中直接而显式的编码代码
var self = this; var i = this.length;
while(i--) if (!fn.apply(null,[self[i],i])) return;
};
现在再来解释为何不单独实现. 我们可以单独的把each作为一个迭代器, 它是抽象的, 设计模式中说:一个类可以扩展,但不要修改它.另外一点,用each来实现all(当然可以是其它的), 可以节省很多代码
下面看Prototype实现, 用throw 。这个throw,通常用于抛出一个异常给上层代码,让上层代码捕获并加以处理。Prototype给了我们(无论是否是其发明的这种Hack)另外一种用法,从而改变了所有可列举对象接口的实现。
var $exception = '$$EXCEPTION$$';
Array.prototype.each = function (fn) {
var self = this; var i = this.length;
try { while(i--) fn.apply(null,[self[i],i]); }
catch (e) { if (e!=$exception) throw e; }
}
Array.prototype.all = function (fn) {
this.each(
function (value, index) {
if (false===fn.apply(null,[value,index])) {
throw $exception;
}
alert('this is a test for outer Function run count as ' +(value));
//执行为false时已经将这层函数返回,因此这里将会出现9个alert,很明显不符合我们预期(预期是执行为false时将all函数也返回)
}
);
};
var a = [1,2,3,4,5,6,7,8,9,10];
a.all(function (value, index) {
var result = value==6?false:true;
return result;
});
</script>
这个Hack也利用了异常机制, 在要返回自身的上层函数时,抛出异常,在each时捕获。从而可以用each来all。
2)extend。最原始的目的,为了扩展对象方法或属性而实现的一个函数,它是Object的静态方法。这里我们讲述的是Hack方法。比如:
var defaultConfig = {
duration: 72,
delay: 10
}
function jsclass (objConfig) {
objConfig.duration = defaultConfig.duration||72;
objConfig.delay = defaultConfig.delay||10;
return objConfig;
}
//Hack版
var defaultConfig = {
duration: 72,
delay: 10
}
function jsclass (objConfig) {
Object.extend(objConfig||{}, defaultConfig);
return objConfig;
}
上面的代码可以看出,用extend可以进行一个简单的验证过程。从而简化了代码。
三、闭包 Closure
在Prototype中,闭包随处可见。最常见的就是在扩展Function原型时,几乎每一个扩展原型的实现,都是一个闭包,且。该扩展是可任意组合的。比如克里化可以有这样的调用方法(上文中已经提到了此方法的使用):
Function.prototype.curry = function() {
if (!arguments.length) return this;
var __method = this, args = Array.prototype.slice.call(arguments,0);
return function() {
return __method.apply(this, args.concat(Array.prototype.slice.call(arguments,0)));
}
}
var F = function () { alert(Array.prototype.slice.call(arguments,0).join(' ')); }
F.curry('I').curry('am').curry('never-online')();
</script>
当然,bind, wrap这些都是返回闭包。因为使用闭包,js才更吸引人,也正是因为使用闭包,js才更灵活,可以实现很多在传统编程里意想不到的东西。但反思后会发现,闭包的滥用会导致debug的困难,可读性差。这个问题留到后面再来讨论。
四、上下文 context
在我的理解中,上下文简单的说就是指针。在js的动态特性中,用apply和call是最有趣的地方之一,也是实现改变上下文的关键所在。举个简单的例子:
this.name = 'never-online';
this.alert = function () {
alert(this.name);
}
return this;
}
var C = function () {
this.name = 'Rank';
this.alert = function () {
alert(this.name);
}
return this;
}
var oF = new F();
var oC = new C();
关于改变context在很多时候是有用的。这里就不再赘述一些代码的例子了。
五、Lazy function pattern
有人称为惰性函数模式。貌似很理论化的东西,事实上看下面一个例子就明白了,注意看在getXMLHttpRequest里执行了几次alert
ie: !!window.ActiveXObject,
ie7: /msie\s*?7\.0/i.test(window.navigator.userAgent)
}
var getXMLHttpRequest = function () {
if (browser.ie && !browser.ie7)
getXMLHttpRequest = function () {
return new ActiveXObject('Microsoft.XMLHTTP');
}
else
getXMLHttpRequest = function () {
return new XMLHttpRequest();
}
alert('看看在getXMLHttpRequest里执行了几次alert');
return getXMLHttpRequest();
}
var req = getXMLHttpRequest();
alert(req)
var http = getXMLHttpRequest();
alert(http)
上面的在getXMLHttpRequest里只执行了一次alert,由此可以看出,通过js的动态重写函数,达到兼容的目的。第一次调用后,重写自身。动态的重写这是js里又一个很有意思的地方。
我的体会是,Prototype的设计上考虑得很全面。包括层次,接口,模式都用得很到位。这一点是我觉得Prototype优雅的地方,新颖的语法是一大亮点(虽然现在都知道他的$, Try These extend...),它借鉴了许多语言的许多语法,包括Ruby,python等。
Prototype里的相互依赖太紧密。藕合太高,也许Prototype的团队开发的想法是把整个Prototype看作是一个大module(这个粒度是不是有点大-_-!)。闭包大量的使用,调试是一个问题(也许是我水平有限,Prototype的实现某些方法有点“丑陋”),闭包毕竟是不可大量使用的。所以使用时需要注意。因为把可列举对象抽象Enumerable。实现接口_each。用_each来实现each。再用each来实现各集合的方法。把迭代器封装,造成的缺点是让使用者无需考虑迭代细节,如果数据量大,建议不要用Prototype的迭代器。上文中的lazy function pattern这样的东西还是少用为妙。
其它的对象的迭代实现都是扩展自Enumerable这个对象。_each方法是接口,所有的对象都要实现这个方法,Enumerable则象是基类对象。
3)inheritance. 在新版Prototype中,实现了新的Class.Create的方式,是代替原来的extend object方案,实现起来复杂得多,大量动态的调用了其它类型方法。
主要思路是用原型继承的方式实现继承,但在实现中用了N个闭包。N>=3层。包括实现$supper这个方法,与传统OO不同的是,这个$supper方法,是作为参数传入子类中(且为第一个参数)才有效。(Prototype的方法是基于Alex Arnell写的plugin)
4)Template. 模板类提供了基于json数据的调用。比如一个字符串用Template可以作
var a = new Template(s);
var o = { name: 'never-online', uri: 'http://www.never-online.net/blog' };
a.evaluate(o);
//得到 never-online's website is http://www.never-online.net/blog
而这个evaluate方法依赖String中的原型方法gsub。
5)Selector. 基于yui-ext的selector。这个的思路比较多,只是大略的了解了DOMquery(yui-ext)的思路,就是compileMatch里采用组装字符串,形成function,动态解析selector。几个selector都是一点是一样的,都有一个cache数组存储临时数据。
6)Event. 在新版Prototype中,有一句评论是这么说的。从原来最差的Event模式,到了最好的Event模式。管理事件都是基于可列举对象的基类,比如在用于事件中的pluck,without等,在实现Event事件里,用了不少的"private"方法,而最后返回的只有几个方法,包括observe, fire和stopObserving。我们也可以简洁的用图来表示
7)Ajax. 大概的图就是下面这样,感觉实现得还不错的(对于扩展来说)
8)Chaining stuff. 这个在jquery上面用得是最多的,当时jquery出现,推广该框架的时候就是用鼓励和使用Chaining, 这也成为jquery的代表了.
实际上Chaining就是一个循环. chaining是很有用的, 它可以用一种自然语言的方式来编程,而非命令式编式,但会消耗效率,因为迭代器使用过多.Prototype也用了部分链,这个链是从Element开始的,详细的实现我就不说了,原理也是很简单的:
$(id).observe('onclick',handler)就是一个例子,将Event继承到Element上即可.
methodize可以理解为预先将this作为第一个参数填好了,以后就可以只填后面的参数,在Element中用得很多
addClassName: function(element, className) {
if (!(element = $(element))) return;
if (!element.hasClassName(className))
element.className += (element.className ? ' ' : '') + className;
return element;
},
prototype中的each感觉还是很不爽,宁可用for~
序
这篇文章不是用来说明怎么用prototype,不是教程,而是作为一个理论研究,看网络上都是写Prototype的使用,而没有多少人去解释这些设计与实现,给予后来的人以提示和学习。感觉实在是有必要写写了。但一直没有机会写,一来是觉得自己的水平还欠缺,二来是没有太多的时间来写文,三是表述起来比较困难,因为毕竟在框架中交互实现的地方太多,设计依赖也有很多。很难定下来如何描述谁先谁先。如何将这些设计与实现描述出来,且让人容易理解我在说什么,能够不将整个框架说得过于主观,这些都是比较头疼的问题。这次能有这一个机会来让我描述一下这个框架的设计,纯属偶然,没有太多时间来准备,因此,我没有写过多的细节,如果有遗漏之处,还请指出。
概要
Prototype是一个优雅的框架,在何处优雅,它带给我们什么启示?为何我们要学习它?下面我将从Type(类型),Hack,Closure(闭包),Context(上下文),Lazy function pattern(惰性函数模式)和Design(设计)客观的看来prototype。
总览、 Design设计
假设你已经明天了怎么样去实现模块,了解了上面Prototype的实现,如果让你来编写一个框架,是否你能够胜任呢?是否能够写得比Prototype好呢?很抱歉,这个问题我不能回答。不管答案怎么样,学习一下总没坏事,下面我们来讨论一下Prototype的是如何将他的代码组织起来的。
1)Object. 这是Prototype实现检测和扩展的核心,全部是静态方法。它提供了全局范围内的验证。
2)Enumerable. 它是全部可列举对象的核心所在。比如: Array, Hash, Object, Element对象的迭代实现。 他们都要实现一个接口。_each方法,每个对象的each都是通过本对象中实现的each来实现的。以下图可以大概表示他们的层次关系