浏览器前端编程的面貌自2005年以来已经发生了深入的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得咱们能够更加方便的编写业务代码,更重要的是咱们看待前端技术的观念发生了重大转变,明确意识到了如何之前端特有的方式释放程序员的生产力。本文将结合jQuery源码的实现原理,对javascript中涌现出的编程范式和经常使用技巧做一简单介绍。
1. AJAX: 状态驻留,异步更新
首先来看一点历史。
A. 1995年Netscape公司的Brendan Eich开发了javacript语言,这是一种动态(dynamic)、弱类型(weakly typed)、基于原型(prototype-based)的脚本语言。
B. 1999年微软IE5发布,其中包含了XMLHTTP ActiveX控件。
C. 2001年微软IE6发布,部分支持DOM level 1和CSS 2标准。
D. 2002年Douglas Crockford发明JSON格式。
至此,能够说Web2.0所依赖的技术元素已经基本成形,可是并无马上在整个业界产生重大的影响。尽管一些“页面异步局部刷新”的技巧在程序员中间秘密的流传,甚至催生了bindows这样庞大臃肿的类库,但总的来讲,前端被看做是贫瘠而又肮脏的沼泽地,只有后台技术才是王道。到底还缺乏些什么呢?
当咱们站在今天的角度去回顾2005年以前的js代码,包括那些当时的牛人所写的代码,能够明显的感觉到它们在程序控制力上的孱弱。并非说2005年以前的js技术自己存在问题,只是它们在概念层面上是人心涣散,缺少统一的观念,或者说缺乏本身独特的风格, 本身的灵魂。当时大多数的人,大多数的技术都试图在模拟传统的面向对象语言,利用传统的面向对象技术,去实现传统的GUI模型的仿制品。
2005年是变革的一年,也是创造概念的一年。伴随着Google一系列让人耳目一新的交互式应用的发布,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被广为传播。Ajax这一前端特有的概念迅速将众多分散的实践统一在同一口号之下,引起了Web编程范式的转换。所谓名不正则言不顺,这下无名群众可找到组织了。在未有Ajax以前,人们早已认识到了B/S架构的本质特征在于浏览器和服务器的状态空间是分离的,可是通常的解决方案都是隐藏这一区分,将前台状态同步到后台,由后台统一进行逻辑处理,例如ASP.NET。由于缺少成熟的设计模式支持前台状态驻留,在换页的时候,已经装载的js对象将被迫被丢弃,这样谁还能期望它去完成什么复杂的工做吗?
Ajax明确提出界面是局部刷新的,前台驻留了状态,这就促成了一种须要:须要js对象在前台存在更长的时间。这也就意味着须要将这些对象和功能有效的管理起来,意味着更复杂的代码组织技术,意味着对模块化,对公共代码基的渴求。
jQuery现有的代码中真正与Ajax相关(使用XMLHTTP控件异步访问后台返回数据)的部分其实不多,可是若是没有Ajax, jQuery做为公共代码基也就缺少存在的理由。
2. 模块化:管理名字空间
当大量的代码产生出来之后,咱们所须要的最基础的概念就是模块化,也就是对工做进行分解和复用。工做得以分解的关键在于各人独立工做的成果能够集成在一块儿。这意味着各个模块必须基于一致的底层概念,能够实现交互,也就是说应该基于一套公共代码基,屏蔽底层浏览器的不一致性,并实现统一的抽象层,例如统一的事件管理机制等。比统一代码基更重要的是,各个模块之间必须没有名字冲突。不然,即便两个模块之间没有任何交互,也没法共同工做。
jQuery目前鼓吹的主要卖点之一就是对名字空间的良好控制。这甚至比提供更多更完善的功能点都重要的多。良好的模块化容许咱们复用任何来源的代码,全部人的工做得以积累叠加。而功能实现仅仅是一时的工做量的问题。jQuery使用module pattern的一个变种来减小对全局名字空间的影响,仅仅在window对象上增长了一个jQuery对象(也就是$函数)。
所谓的module pattern代码以下,它的关键是利用匿名函数限制临时变量的做用域。
var feature =(function() {
// 私有变量和函数
var privateThing = 'secret',
publicThing = 'not secret',
changePrivateThing = function() {
privateThing = 'super secret';
},
sayPrivateThing = function() {
console.log(privateThing);
changePrivateThing();
};
// 返回对外公开的API
return {
publicThing : publicThing,
sayPrivateThing : sayPrivateThing
}
})();
js自己缺少包结构,不过通过多年的尝试以后业内已经逐渐统一了对包加载的认识,造成了RequireJs库这样获得必定共识的解决方案。jQuery能够与RequireJS库良好的集成在一块儿, 实现更完善的模块依赖管理。http://requirejs.org/docs/jquery.html
require(["jquery", "jquery.my"], function() {
//当jquery.js和jquery.my.js都成功装载以后执行
$(function(){
$('#my').myFunc();
});
});
经过如下函数调用来定义模块my/shirt, 它依赖于my/cart和my/inventory模块,
require.def("my/shirt",
["my/cart", "my/inventory"],
function(cart, inventory) {
// 这里使用module pattern来返回my/shirt模块对外暴露的API
return {
color: "blue",
size: "large"
addToCart: function() {
// decrement是my/inventory对外暴露的API
inventory.decrement(this);
cart.add(this);
}
}
}
);
3. 神奇的$:对象提高
当你第一眼看到$函数的时候,你想到了什么?传统的编程理论老是告诉咱们函数命名应该准确,应该清晰无误的表达做者的意图,甚至声称长名字要优于短名字,由于减小了出现歧义的可能性。可是,$是什么?乱码?它所传递的信息实在是太隐晦,太暧昧了。$是由prototype.js库发明的,它真的是一个神奇的函数,由于它能够将一个原始的DOM节点提高(enhance)为一个具备复杂行为的对象。在prototype.js最初的实现中,$函数的定义为
var $ = function (id) {
return "string" == typeof id ? document.getElementById(id) : id;
};
这基本对应于以下公式
e = $(id)
这毫不仅仅是提供了一个聪明的函数名称缩写,更重要的是在概念层面上创建了文本id与DOM element之间的一一对应。在未有$以前,id与对应的element之间的距离十分遥远,通常要将element缓存到变量中,例如
var ea = docuement.getElementById('a');
var eb = docuement.getElementById('b');
ea.style....
可是使用$以后,却随处可见以下的写法
$('header_'+id).style...
$('body_'+id)....
id与element之间的距离彷佛被消除了,能够很是紧密的交织在一块儿。
prototype.js后来扩展了$的含义,
function $() {
var elements = new Array();
for (var i = 0; i < arguments.length; i++) {
var element = arguments[i];
if (typeof element == 'string')
element = document.getElementById(element);
if (arguments.length == 1)
return element;
elements.push(element);
}
return elements;
}
这对应于公式
[e,e] = $(id,id)
很遗憾,这一步prototype.js走偏了,这一作法不多有实用的价值。
真正将$发扬光大的是jQuery, 它的$对应于公式
[o] = $(selector)
这里有三个加强
A. selector再也不是单一的节点定位符,而是复杂的集合选择符
B. 返回的元素不是原始的DOM节点,而是通过jQuery进一步加强的具备丰富行为的对象,能够启动复杂的函数调用链。
C. $返回的包装对象被造型为数组形式,将集合操做天然的整合到调用链中。
固然,以上仅仅是对神奇的$的一个过度简化的描述,它的实际功能要复杂得多. 特别是有一个很是经常使用的直接构造功能.
$("<table><tbody><tr><td>...</td></tr></tbody></table>")....
jQuery将根据传入的html文本直接构造出一系列的DOM节点,并将其包装为jQuery对象. 这在某种程度上能够看做是对selector的扩展: html内容描述自己就是一种惟一指定.
$(function{})这一功能就实在是让人有些无语了, 它表示当document.ready的时候调用此回调函数。真的,$是一个神奇的函数, 有任何问题,请$一下。
总结起来, $是从普通的DOM和文本描述世界到具备丰富对象行为的jQuery世界的跃迁通道。跨过了这道门,就来到了理想国。
4. 无定形的参数:专一表达而不是约束
弱类型语言既然头上顶着个"弱"字, 总不免让人有些先天不足的感受. 在程序中缺少类型约束, 是否真的是一种重大的缺憾? 在传统的强类型语言中, 函数参数的类型,个数等都是由编译器负责检查的约束条件, 但这些约束仍然是远远不够的. 通常应用程序中为了增强约束, 总会增长大量防护性代码, 例如在C++中咱们经常使用ASSERT, 而在java中也常常须要判断参数值的范围
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
很显然, 这些代码将致使程序中存在大量无功能的执行路径, 即咱们作了大量判断, 代码执行到某个点, 系统抛出异常, 大喊此路不通. 若是咱们换一个思路, 既然已经作了某种判断,可否利用这些判断的结果来作些什么呢? javascript是一种弱类型的语言,它是没法自动约束参数类型的, 那若是顺势而行,进一步弱化参数的形态, 将"弱"推动到一种极致, 在弱无可弱的时候, weak会不会成为标志性的特色?
看一下jQuery中的事件绑定函数bind,
A. 一次绑定一个事件 $("#my").bind("mouseover", function(){});
B. 一次绑定多个事件 $("#my").bind("mouseover mouseout",function(){})
C. 换一个形式, 一样绑定多个事件
$("#my").bind({mouseover:function(){}, mouseout:function(){});
D. 想给事件监听器传点参数
$('#my').bind('click', {foo: "xxxx"}, function(event) { event.data.foo..})
E. 想给事件监听器分个组
$("#my").bind("click.myGroup″, function(){});
F. 这个函数为何尚未疯掉???
就算是类型不肯定, 在固定位置上的参数的意义总要是肯定的吧? 退一万步来讲, 就算是参数位置不重要了,函数自己的意义应该是肯定的吧? 但这是什么?
取值 value = o.val(), 设置值 o.val(3)
一个函数怎么能够这样过度, 怎么能根据传入参数的类型和个数不一样而行为不一样呢? 看不顺眼是否是? 可这就是俺们的价值观. 既然不能防止, 那就故意容许. 虽然形式多变, 却无一句废话. 缺乏约束, 不妨碍表达(我不是出来吓人的).
5. 链式操做: 线性化的逐步细化
jQuery早期最主要的卖点就是所谓的链式操做(chain).
$('#content') // 找到content元素
.find('h3') // 选择全部后代h3节点
.eq(2) // 过滤集合, 保留第三个元素
.html('改变第三个h3的文本')
.end() // 返回上一级的h3集合
.eq(0)
.html('改变第一个h3的文本');
在通常的命令式语言中, 咱们总须要在重重嵌套循环中过滤数据, 实际操做数据的代码与定位数据的代码纠缠在一块儿. 而jQuery采用先构造集合而后再应用函数于集合的方式实现两种逻辑的解耦, 实现嵌套结构的线性化. 实际上, 咱们并不须要借助过程化的思想就能够很直观的理解一个集合, 例如 $('div.my input:checked')能够看做是一种直接的描述,而不是对过程行为的跟踪.
循环意味着咱们的思惟处于一种反复回绕的状态, 而线性化以后则沿着一个方向直线前进, 极大减轻了思惟负担, 提升了代码的可组合性. 为了减小调用链的中断, jQuery发明了一个绝妙的主意: jQuery包装对象自己相似数组(集合). 集合能够映射到新的集合, 集合能够限制到本身的子集合,调用的发起者是集合,返回结果也是集合,集合能够发生结构上的某种变化但它仍是集合, 集合是某种概念上的不动点,这是从函数式语言中吸收的设计思想。集合操做是太常见的操做, 在java中咱们很容易发现大量所谓的封装函数其实就是在封装一些集合遍历操做, 而在jQuery中集合操做由于太直白而不须要封装.
链式调用意味着咱们始终拥有一个“当前”对象,全部的操做都是针对这一当前对象进行。这对应于以下公式
x += dx
调用链的每一步都是对当前对象的增量描述,是针对最终目标的逐步细化过程。Witrix平台中对这一思想也有着普遍的应用。特别是为了实现平台机制与业务代码的融合,平台会提供对象(容器)的缺省内容,而业务代码能够在此基础上进行逐步细化的修正,包括取消缺省的设置等。
话说回来, 虽然表面上jQuery的链式调用很简单, 内部实现的时候却必须本身多写一层循环, 由于编译器并不知道"自动应用于集合中每一个元素"这回事.
$.fn['someFunc'] = function(){
return this.each(function(){
jQuery.someFunc(this,...);
}
}
6. data: 统一数据管理
做为一个js库,它必须解决的一个大问题就是js对象与DOM节点之间的状态关联与协同管理问题。有些js库选择以js对象为主,在js对象的成员变量中保存DOM节点指针,访问时老是以js对象为入口点,经过js函数间接操做DOM对象。在这种封装下,DOM节点其实只是做为界面展示的一种底层“汇编”而已。jQuery的选择与Witrix平台相似,都是以HTML自身结构为基础,经过js加强(enhance)DOM节点的功能,将它提高为一个具备复杂行为的扩展对象。这里的思想是非侵入式设计(non-intrusive)和优雅退化机制(graceful degradation)。语义结构在基础的HTML层面是完整的,js的做用是加强了交互行为,控制了展示形式。
若是每次咱们都经过$('#my')的方式来访问相应的包装对象,那么一些须要长期保持的状态变量保存在什么地方呢?jQuery提供了一个统一的全局数据管理机制。
获取数据 $('#my').data('myAttr') 设置数据 $('#my').data('myAttr',3);
这一机制天然融合了对HTML5的data属性的处理
<input id="my" data-my-attr="4" ... />
经过 $('#my').data('myAttr')将能够读取到HTML中设置的数据。
第一次访问data时,jQuery将为DOM节点分配一个惟一的uuid, 而后设置在DOM节点的一个特定的expando属性上, jQuery保证这个uuid在本页面中不重复。
elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
以上代码能够同时处理DOM节点和纯js对象的状况。若是是js对象,则data直接放置在js对象自身中,而若是是DOM节点,则经过cache统一管理。
由于全部的数据都是经过data机制统一管理的,特别是包括全部事件监听函数(data.events),所以jQuery能够安全的实现资源管理。在clone节点的时候,能够自动clone其相关的事件监听函数。而当DOM节点的内容被替换或者DOM节点被销毁的时候,jQuery也能够自动解除事件监听函数, 并安全的释放相关的js数据。
7. event:统一事件模型
"事件沿着对象树传播"这一图景是面向对象界面编程模型的精髓所在。对象的复合构成对界面结构的一个稳定的描述,事件不断在对象树的某个节点发生,并经过冒泡机制向上传播。对象树很天然的成为一个控制结构,咱们能够在父节点上监听全部子节点上的事件,而不用明确与每个子节点创建关联。
jQuery除了为不一样浏览器的事件模型创建了统一抽象以外,主要作了以下加强:
A. 增长了自定制事件(custom)机制. 事件的传播机制与事件内容自己原则上是无关的, 所以自定制事件彻底能够和浏览器内置事件经过同一条处理路径, 采用一样的监听方式. 使用自定制事件能够加强代码的内聚性, 减小代码耦合. 例如若是没有自定制事件, 关联代码每每须要直接操做相关的对象
$('.switch, .clapper').click(function() {
var $light = $(this).parent().find('.lightbulb');
if ($light.hasClass('on')) {
$light.removeClass('on').addClass('off');
} else {
$light.removeClass('off').addClass('on');
}
});
而若是使用自定制事件,则表达的语义更加内敛明确,
$('.switch, .clapper').click(function() {
$(this).parent().find('.lightbulb').trigger('changeState');
});
B. 增长了对动态建立节点的事件监听. bind函数只能将监听函数注册到已经存在的DOM节点上. 例如
$('li.trigger').bind('click',function(){}}
若是调用bind以后,新建了另外一个li节点,则该节点的click事件不会被监听.
jQuery的delegate机制能够将监听函数注册到父节点上, 子节点上触发的事件会根据selector被自动派发到相应的handlerFn上. 这样一来如今注册就能够监听将来建立的节点.
$('#myList').delegate('li.trigger', 'click', handlerFn);
最近jQuery1.7中统一了bind, live和delegate机制, 天下一统, 只有on/off.
$('li.trigger’).on('click', handlerFn); // 至关于bind
$('#myList’).on('click', 'li.trigger', handlerFn); // 至关于delegate
8. 动画队列:全局时钟协调
抛开jQuery的实现不谈, 先考虑一下若是咱们要实现界面上的动画效果, 到底须要作些什么? 好比咱们但愿将一个div的宽度在1秒钟以内从100px增长到200px. 很容易想见, 在一段时间内咱们须要不时的去调整一下div的宽度, [同时]咱们还须要执行其余代码. 与通常的函数调用不一样的是, 发出动画指令以后, 咱们不能期待马上获得想要的结果, 并且咱们不能原地等待结果的到来. 动画的复杂性就在于:一次性表达以后要在一段时间内执行,并且有多条逻辑上的执行路径要同时展开, 如何协调?
伟大的艾萨克.牛顿爵士在《天然哲学的数学原理》中写道:"绝对的、真正的和数学的时间自身在流逝着". 全部的事件能够在时间轴上对齐, 这就是它们内在的协调性. 所以为了从步骤A1执行到A5, 同时将步骤B1执行到B5, 咱们只须要在t1时刻执行[A1, B1], 在t2时刻执行[A2,B2], 依此类推.
t1 | t2 | t3 | t4 | t5 ...
A1 | A2 | A3 | A4 | A5 ...
B1 | B2 | B3 | B4 | B5 ...
具体的一种实现形式能够是
A. 对每一个动画, 将其分装为一个Animation对象, 内部分红多个步骤.
animation = new Animation(div,"width",100,200,1000,
负责步骤切分的插值函数,动画执行完毕时的回调函数);
B. 在全局管理器中注册动画对象
timerFuncs.add(animation);
C. 在全局时钟的每个触发时刻, 将每一个注册的执行序列推动一步, 若是已经结束, 则从全局管理器中删除.
for each animation in timerFuncs
if(!animation.doOneStep())
timerFuncs.remove(animation)
解决了原理问题,再来看看表达问题, 怎样设计接口函数才可以以最紧凑形式表达咱们的意图? 咱们常常须要面临的实际问题:
A. 有多个元素要执行相似的动画
B. 每一个元素有多个属性要同时变化
C. 执行完一个动画以后开始另外一个动画
jQuery对这些问题的解答能够说是榨尽了js语法表达力的最后一点剩余价值.
$('input')
.animate({left:'+=200px',top:'300'},2000)
.animate({left:'-=200px',top:20},1000)
.queue(function(){
// 这里dequeue将首先执行队列中的后一个函数,所以alert("y")
$(this).dequeue();
alert('x');
})
.queue(function(){
alert("y");
// 若是不主动dequeue, 队列执行就中断了,不会自动继续下去.
$(this).dequeue();
});
A. 利用jQuery内置的selector机制天然表达对一个集合的处理.
B. 使用Map表达多个属性变化
C. 利用微格式表达领域特定的差量概念. '+=200px'表示在现有值的基础上增长200px
D. 利用函数调用的顺序自动定义animation执行的顺序: 在后面追加到执行队列中的动画天然要等前面的动画彻底执行完毕以后再启动.
jQuery动画队列的实现细节大概以下所示,
A. animate函数实际是调用queue(function(){执行结束时须要调用dequeue,不然不会驱动下一个方法})
queue函数执行时, 若是是fx队列, 而且当前没有正在运行动画(若是连续调用两次animate,第二次的执行函数将在队列中等待),则会自动触发dequeue操做, 驱动队列运行.
若是是fx队列, dequeue的时候会自动在队列顶端加入"inprogress"字符串,表示将要执行的是动画.
B. 针对每个属性,建立一个jQuery.fx对象。而后调用fx.custom函数(至关于start)来启动动画。
C. custom函数中将fx.step函数注册到全局的timerFuncs中,而后试图启动一个全局的timer.
timerId = setInterval( fx.tick, fx.interval );
D. 静态的tick函数中将依次调用各个fx的step函数。step函数中经过easing计算属性的当前值,而后调用fx的update来更新属性。
E. fx的step函数中判断若是全部属性变化都已完成,则调用dequeue来驱动下一个方法。
颇有意思的是, jQuery的实现代码中明显有不少是接力触发代码: 若是须要执行下一个动画就取出执行, 若是须要启动timer就启动timer等. 这是由于js程序是单线程的,真正的执行路径只有一条,为了保证执行线索不中断, 函数们不得不互相帮助一下. 能够想见, 若是程序内部具备多个执行引擎, 甚至无限多的执行引擎, 那么程序的面貌就会发生本质性的改变. 而在这种情形下, 递归相对于循环而言会成为更天然的描述.
9. promise模式:因果关系的识别
现实中,总有那么多时间线在独立的演化着, 人与物在时空中交错,却没有发生因果. 软件中, 函数们在源代码中排着队, 不免会产生一些疑问, 凭什么排在前面的要先执行? 难道没有它就没有我? 让全宇宙喊着1,2,3齐步前进, 从上帝的角度看,大概是管理难度过大了, 因而便有了相对论. 若是相互之间没有交换信息, 没有产生相互依赖, 那么在某个坐标系中顺序发生的事件, 在另一个坐标系中看来, 就多是颠倒顺序的. 程序员依葫芦画瓢, 便发明了promise模式.
promise与future模式基本上是一回事,咱们先来看一下java中熟悉的future模式.
futureResult = doSomething();
...
realResult = futureResult.get();
发出函数调用仅仅意味着一件事情发生过, 并没必要然意味着调用者须要了解事情最终的结果. 函数马上返回的只是一个将在将来兑现的承诺(Future类型), 实际上也就是某种句柄. 句柄被传来传去, 中间转手的代码对实际结果是什么,是否已经返回不闻不问. 直到一段代码须要依赖调用返回的结果, 所以它打开future, 查看了一下. 若是实际结果已经返回, 则future.get()马上返回实际结果, 不然将会阻塞当前的执行路径, 直到结果返回为止. 此后再调用future.get()老是马上返回, 由于因果关系已经被创建, [结果返回]这一事件必然在此以前发生, 不会再发生变化.
future模式通常是外部对象主动查看future的返回值, 而promise模式则是由外部对象在promise上注册回调函数.
function getData(){
return $.get('/foo/').done(function(){
console.log('Fires after the AJAX request succeeds');
}).fail(function(){
console.log('Fires after the AJAX request fails');
});
}
function showDiv(){
var dfd = $.Deferred();
$('#foo').fadeIn( 1000, dfd.resolve );
return dfd.promise();
}
$.when( getData(), showDiv() )
.then(function( ajaxResult, ignoreResultFromShowDiv ){
console.log('Fires after BOTH showDiv() AND the AJAX request succeed!');
// 'ajaxResult' is the server’s response
});
jQuery引入Deferred结构, 根据promise模式对ajax, queue, document.ready等进行了重构, 统一了异步执行机制. then(onDone, onFail)将向promise中追加回调函数, 若是调用成功完成(resolve), 则回调函数onDone将被执行, 而若是调用失败(reject), 则onFail将被执行. when能够等待在多个promise对象上. promise巧妙的地方是异步执行已经开始以后甚至已经结束以后,仍然能够注册回调函数
someObj.done(callback).sendRequest() vs. someObj.sendRequest().done(callback)
callback函数在发出异步调用以前注册或者在发出异步调用以后注册是彻底等价的, 这揭示出程序表达永远不是彻底精确的, 总存在着内在的变化维度. 若是能有效利用这一内在的可变性, 则能够极大提高并发程序的性能.
promise模式的具体实现很简单. jQuery._Deferred定义了一个函数队列,它的做用有如下几点:
A. 保存回调函数。
B. 在resolve或者reject的时刻把保存着的函数所有执行掉。
C. 已经执行以后, 再增长的函数会被马上执行。
一些专门面向分布式计算或者并行计算的语言会在语言级别内置promise模式, 好比E语言.
def carPromise := carMaker <- produce("Mercedes");
def temperaturePromise := carPromise <- getEngineTemperature()
...
when (temperaturePromise) -> done(temperature) {
println(`The temperature of the car engine is: $temperature`)
} catch e {
println(`Could not get engine temperature, error: $e`)
}
在E语言中, <-是eventually运算符, 表示最终会执行, 但不必定是如今. 而普通的car.moveTo(2,3)表示马上执行获得结果. 编译器负责识别全部的promise依赖, 并自动实现调度.
10. extend: 继承不是必须的
js是基于原型的语言, 并无内置的继承机制, 这一直让不少深受传统面向对象教育的同窗们耿耿于怀. 但继承必定是必须的吗? 它到底可以给咱们带来什么? 最纯朴的回答是: 代码重用. 那么, 咱们首先来分析一下继承做为代码重用手段的潜力.
曾经有个概念叫作"多重继承", 它是继承概念的超级赛亚人版, 很遗憾后来被诊断为存在着先天缺陷, 以至于出现了一种对于继承概念的解读: 继承就是"is a"关系, 一个派生对象"is a"不少基类, 必然会出现精神分裂, 因此多重继承是很差的.
class A{ public: void f(){ f in A } }
class B{ public: void f(){ f in B } }
class D: public A, B{}
若是D类从A,B两个基类继承, 而A和B类中都实现了同一个函数f, 那么D类中的f究竟是A中的f仍是B中的f, 抑或是A中的f+B中的f呢? 这一困境的出现实际上源于D的基类A和B是并列关系, 它们知足交换律和结合律, 毕竟,在概念层面上咱们可能难以承认两个任意概念之间会出现从属关系. 但若是咱们放松一些概念层面的要求, 更多的从操做层面考虑一下代码重用问题, 能够简单的认为B在A的基础上进行操做, 那么就能够获得一个线性化的结果. 也就是说, 放弃A和B之间的交换律只保留结合律, extends A, B 与 extends B,A 会是两个不一样的结果, 再也不存在诠释上的二义性. scala语言中的所谓trait(特性)机制实际上采用的就是这一策略.
面向对象技术发明好久以后, 出现了所谓的面向方面编程(AOP), 它与OOP不一样, 是代码结构空间中的定位与修改技术. AOP的眼中只有类与方法, 不知道什么叫作意义. AOP也提供了一种相似多重继承的代码重用手段, 那就是mixin. 对象被看做是能够被打开,而后任意修改的Map, 一组成员变量与方法就被直接注射到对象体内, 直接改变了它的行为.
prototype.js库引入了extend函数,
Object.extend = function(destination, source) {
for (var property in source) {
destination[property] = source[property];
}
return destination;
}
就是Map之间的一个覆盖运算, 但很管用, 在jQuery库中也获得了延用. 这个操做相似于mixin, 在jQuery中是代码重用的主要技术手段---没有继承也没什么大不了的.
11. 名称映射: 一切都是数据
代码好很差, 循环判断必须少. 循环和判断语句是程序的基本组成部分, 可是优良的代码库中却每每找不到它们的踪迹, 由于这些语句的交织会模糊系统的逻辑主线, 使咱们的思想迷失在疲于奔命的代码追踪中. jQuery自己经过each, extend等函数已经极大减小了对循环语句的需求, 对于判断语句, 则主要是经过映射表来处理. 例如, jQuery的val()函数须要针对不一样标签进行不一样的处理, 所以定义一个以tagName为key的函数映射表
valHooks: { option: {get:function(){}}}
这样在程序中就不须要处处写
if(elm.tagName == 'OPTION'){
return ...;
}else if(elm.tagName == 'TEXTAREA'){
return ...;
}
能够统一处理
(valHooks[elm.tagName.toLowerCase()] || defaultHandler).get(elm);
映射表将函数做为普通数据来管理, 在动态语言中有着普遍的应用. 特别是, 对象自己就是函数和变量的容器, 能够被看做是映射表. jQuery中大量使用的一个技巧就是利用名称映射来动态生成代码, 造成一种相似模板的机制. 例如为了实现myWidth和myHeight两个很是相似的函数, 咱们不须要
jQuery.fn.myWidth = function(){
return parseInt(this.style.width,10) + 10;
}
jQuery.fn.myHeight = function(){
return parseInt(this.style.height,10) + 10;
}
而能够选择动态生成
jQuery.each(['Width','Height'],function(name){
jQuery.fn['my'+name] = function(){
return parseInt(this.style[name.toLowerCase()],10) + 10;
}
});
12. 插件机制:其实我很简单
jQuery所谓的插件其实就是$.fn上增长的函数, 那这个fn是什么东西?
(function(window,undefined){
// 内部又有一个包装
var jQuery = (function() {
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery );
}
....
// fn实际就是prototype的简写
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function( selector, context, rootjQuery ) {... }
}
// 调用jQuery()就是至关于new init(), 而init的prototype就是jQuery的prototype
jQuery.fn.init.prototype = jQuery.fn;
// 这里返回的jQuery对象只具有最基本的功能, 下面就是一系列的extend
return jQuery;
})();
...
// 将jQuery暴露为全局对象
window.jQuery = window.$ = jQuery;
})(window);
显然, $.fn其实就是jQuery.prototype的简写.
无状态的插件仅仅就是一个函数, 很是简单.
// 定义插件
(function($){
$.fn.hoverClass = function(c) {
return this.hover(
function() { $(this).toggleClass(c); }
);
};
})(jQuery);
// 使用插件
$('li').hoverClass('hover');
对于比较复杂的插件开发, jQuery UI提供了一个widget工厂机制,
$.widget("ui.dialog", {
options: {
autoOpen: true,...
},
_create: function(){ ... },
_init: function() {
if ( this.options.autoOpen ) {
this.open();
}
},
_setOption: function(key, value){ ... }
destroy: function(){ ... }
});
调用 $('#dlg').dialog(options)时, 实际执行的代码基本以下所示:
this.each(function() {
var instance = $.data( this, "dialog" );
if ( instance ) {
instance.option( options || {} )._init();
} else {
$.data( this, "dialog", new $.ui.dialog( options, this ) );
}
}
能够看出, 第一次调用$('#dlg').dialog()函数时会建立窗口对象实例,并保存在data中, 此时会调用_create()和_init()函数, 而若是不是第一次调用, 则是在已经存在的对象实例上调用_init()方法. 屡次调用$('#dlg').dialog()并不会建立多个实例.
13. browser sniffer vs. feature detection
浏览器嗅探(browser sniffer)曾经是很流行的技术, 好比早期的jQuery中
jQuery.browser = {
version:(userAgent.match(/.+(?:rv|it|ra|ie)[/: ]([d.]+)/) || [0,'0'])[1],
safari:/webkit/.test(userAgent),
opera:/opera/.test(userAgent),
msie:/msie/.test(userAgent) && !/opera/.test(userAgent),
mozilla:/mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent)
};
在具体代码中能够针对不一样的浏览器做出不一样的处理
if($.browser.msie) {
// do something
} else if($.browser.opera) {
// ...
}
可是随着浏览器市场的竞争升级, 竞争对手之间的互相模仿和假装致使userAgent一片混乱, 加上Chrome的诞生, Safari的崛起, IE也开始加速向标准靠拢, sniffer已经起不到积极的做用. 特性检测(feature detection)做为更细粒度, 更具体的检测手段, 逐渐成为处理浏览器兼容性的主流方式.
jQuery.support = {
// IE strips leading whitespace when .innerHTML is used
leadingWhitespace: ( div.firstChild.nodeType === 3 ),
...
}
只基于实际看见的,而不是曾经知道的, 这样更容易作到兼容将来.
14. Prototype vs. jQuery
prototype.js是一个立意高远的库, 它的目标是提供一种新的使用体验,参照Ruby从语言级别对javascript进行改造,并最终真的极大改变了js的面貌。$, extends, each, bind...这些耳熟能详的概念都是prototype.js引入到js领域的. 它肆无忌惮的在window全局名字空间中增长各类概念, 大有谁先占坑谁有理, 舍我其谁的气势. 而jQuery则扣扣索索, 抱着比较实用化的理念, 目标仅仅是write less, do more而已.
不过等待激进的理想主义者的命运每每都是壮志未酬身先死. 当prototype.js标志性的bind函数等被吸取到ECMAScript标准中时, 便注定了它的没落. 处处修改原生对象的prototype, 这是prototype.js的独门秘技, 也是它的死穴. 特别是当它试图模仿jQuery, 经过Element.extend(element)返回加强对象的时候, 算是完全被jQuery给带到沟里去了. prototype.js与jQuery不一样, 它老是直接修改原生对象的prototype, 而浏览器倒是充满bug, 谎话, 历史包袱并夹杂着商业阴谋的领域, 在原生对象层面解决问题注定是一场悲剧. 性能问题, 名字冲突, 兼容性问题等等都是一个帮助库的能力所没法解决的. Prototype.js的2.0版本听说要作大的变革, 不知是要与历史决裂, 放弃兼容性, 仍是继续挣扎, 在夹缝中求生.javascript