(转)窥探jQuery——面向JavaScript程序员
当 jQuery 在2006年1月现身时,给我的第一印象,是这玩意儿构造得很精明。基于CSS选择器(CSS selectors)来打点一切,其思路相当灵巧(参考getElementsBySelector)。但链盒工事(chaining stuff)看起来更像个噱头,并且整体看来,jQuery库提供的功能并不能覆盖所有基础性的东西。因此我断定,jQuery只会昙花一现。
几
个月以来,我逐渐明白自己想错了。从技术工艺上考量,jQuery十分凌厉。它用简洁的方法,把大量常用功能封装起来,并提供精巧的插入式API,来满足
标准库之外的功能模块的实现。jQuery秉持的核心,乃DOM元素的集合(译注:通常是某些子集合)——它把元素集合作为一个根本,给高度抽象出来了。
最重要的,是这种遵循最佳实践的抽象,能让jQuery与其他JavaScript代码相处融洽。
很多对jQuery的介绍,都是针对设计师和初级开发人员。接下来我想说明,为什么jQuery也会吸引那些富有经验的开发人员。
名称空间(Namespacing)
编写可重用的、优秀的JavaScript代码,其关键在于对名称空间的积极把控。JavaScript只拥有单一的、全局的名称空间(即window对象),而很多程序员(以及一些库)恣意地为之添加各种东西。要知道全局变量是魔鬼!聪明的开发人员,会使用类似组件模式的技术,来尽力减少全局对象的数量。
jQuery仅向全局名称空间引入一个标记:jQuery函数/对象。其余的要么是jQuery的直接属性(译注:原文‘directy property’系笔误,应是‘direct property’),要么就是调用jQuery函数所返回的对象的方法。
那“语
言升级”(language
enhancements)又是什么呢?大多数库会提供映射,过滤,剥离,往往是浏览器的JavaScript引擎所缺少的那些功能。还有一些库,直接扩
展了JavaScript内置的String和Array类,但这是冒险的做法。String.prototype和Array.prototype也有
各自的名称空间,在其内添加的属性一旦发生冲突,所带来的风险,不亚于在全局环境下的草率大意。
在语言升级方面,jQuery提供了很多函数(功能),但每个函数都被赋给jQuery对象的属性:jQuery.each,jQuery.extend,jQuery.grep,jQuery.map,jQuery.merge以及jQuery.trim。如此一来,它们就不会跟其他代码产生冲突。
声名狼藉的$函数(The infamous $ function)
刚才我说到,jQuery是唯一被引入的全局标记,其实并不尽然:$标记作为jQuery的快捷方式,也被引入进来。庆幸的是,$的存在不会带来负面影响:如果你需要让原始的$起死回生(比如,这之前你的代码使用了Prototype),你可以调用jQuery.noConflict()来恢复它。
如果你既想拥有$的便利,又不希望jQuery跟其他同样使用了全局$函数的代码发生冲突,可遵循jQuery文档所建议的惯用方式:
(function($) {
// 在这个函数体里,$可作为jQuery的引用
// 很方便,对吧?
})(jQuery);
把一切都附加到$标记的做法,曾让我认为jQuery华而不实。不过,从体系的角度来审视这种设计,一切又是非常明了的——尽管我常喜欢在代码中定义自己的$快捷方式。
选取元素(Selecting some elements)
jQuery的每个操作,都以选取DOM中一个或更多的节点(nodes)作为开始。jQuery(拥有一种真正的面向特定领域)的选取语法,是十分有趣的,它结合了CSS 1,CSS 2,部分CSS 3语法,一些XPath语法,以及一些特定的扩展。在这里我不会做详细介绍,我只列出几个有用的例子:
- jQuery('div.panel')
- 选取了所有class="panel"的div
- jQuery('p#intro')
- 选取了所有id="intro"的段落
- jQuery('div#content a:visible')
- 选取了id="content"的div中所有可见的链接
- jQuery('input[@name=email]')
- 选取了所有name="email"的输入域
- jQuery('table.orders tr:odd')
- 选取了类名为“orders”的表中所有的奇数行
- jQuery('a[@href^="http://"]')
- 选取了所有(以http://开头的)外部链接
- jQuery('p[a]')
- 选取了所有包含一个或多个链接的段落
上述例子中,:visible和:odd是jQuery实现的扩展,很具特色。而属性的选取使用@作为标记,其方式和XPath一样,要优于CSS 2。
jQuery的这套选取语法包罗万象,有些类似正则表达式,想完全消化是需要花上一段时间的。
把玩一下(Doing stuff with them)
通过jQuery的选取操作,我们能得到一些很棒的“素材”(beast)。它们是一个集合,包含了DOM元素,并且类似数组那样,拥有length属性;通过索引可以访问集合中的元素。在Firebug console的交互模式下,集合也被显示成一个数组,这个特性非常有用。集合实际上是一个jQuery对象,这个对象被赋予了很多方法(methods),用来查询,修改,扩展集合中的元素。
jQuery的方法(methods),本质上可分成三种:一种可以操作那些符合匹配的元素;一种可以返回第一个匹配到的对象的值;一种可以变更被选取的集合。
我不会列出所有的方法(可参考visualjquery.com),但我用例子做一下说明。如果你的浏览器装了Firebug,你可以以交互方式运行这些示例代码:首先使用这个bookmarklet(译注[1])把jQuery库载入至浏览器的任意页面,然后把示例代码粘贴到Firebug console中。
- jQuery('div#primary').width(300);
- 把id="primary"的div的宽度设为300px
- jQuery('p').css('line-height', '1.8em');
- 把所有段落的line-height设为1.8em
- jQuery('li:odd').css({color: 'white', backgroundColor: 'black'});
- 向间隔的list项添加两个CSS规则;注意css()函数可以用一个对象来代替两个字符串作为参数
- jQuery('a[@href^="http://"]').addClass('external').attr('target', '_blank');
- 向所有(以http://开头的)外部链接添加“external”类,然后策略性地加上target="_blank"属性。这个示例用到了链盒(chaining),稍后会做介绍。
- jQuery('blockquote').each(function(el) { alert(jQuery(this).text()) });
- 遍历页面上的每个<blockquote>,并显示出它的文字内容(包括HTML标签)
- jQuery('a').html('Click here!');
- 用阴险的“Click here!”代替页面上所有的链接<a>的文字
下面的示例展示了jQuery如何取得第一个匹配到的对象的值:
- var width = jQuery('div').width();
- 页面上第一个div的宽度
- var src = jQuery('img').attr('src');
- 页面上第一张图片的src属性值
- var color = jQuery('h1').css('color');
- 第一个<h1>的颜色样式值
在jQuery
的方法构造中,蕴含着令人惬意的对称性:当向方法传递两个参数或一个对象时,方法可被用来执行设置操作;如果只向方法传递一个参数,则可以让它执行取值操
作(译注:读者可对照上面的示例代码感受一下)。这种对称性设计贯穿了jQuery体系,使得API的文法更容易被记忆。
本节最后的例子,展示了一些可变更被选取的元素集合的方法。这些方法大多都提高了检索DOM的简易程度:
- jQuery('div').not('[@id]')
- 返回那些没有id属性的div
- jQuery('h2').parent()
- 返回那些是<h2>的直接父节点元素
- jQuery('blockquote').children()
- 返回所有<blockquote>的子节点元素
- jQuery('p').eq(4).next()
- 在页面上找到第五个段落(译注:因为集合的元素索引从0开始),然后根据节点的树层结构关系,找到并返回这个段落节点右侧的兄弟节点元素
- jQuery('input:text:first').parents('form')
- 找到并返回页面上第一个type="text"的输入域input所在的form节点元素,parents()的可选参数是另一个选择器
链盒(Chaining)
jQuery
开发团队经常夸耀jQuery的链盒理念(译注[2]),甚至在网站首页上宣扬“jQuery将改变你编写JavaScript的方式”。我个人感觉,这
么做多少有点误导大众,我愿意告诉大家,你完全可以取jQuery之长,却应避免冗长的方法链盒(chains of methods)。
也
就是说,链盒有时会像变戏法一样。除了使用链盒将各种操作DOM的方法粘到一起,你也可以使用jQuery的end()方法,来实现在特定范围内推进或回
溯你需要得到的元素。这个概念很难解释清楚。本质上讲,每次使用(诸如children()或filter())方法来改变元素集合时,你可以在这些方法
之后使用end(),来重新定位你最初选取的元素集合。关于这点,Jesse Skinner在他的Simplify Ajax development with jQuery(译注[3])教程中给出了实例:
$('form#login')
// 第一步,隐藏表单中那些带有'optional'类的<label>
.find('label.optional').hide().end()
// 第二步,为表单的密码输入域渲染上红色边框
.find('input:password').css('border', '1px solid red').end()
// 第三步,为表单加上提交处理
.submit(function(){
return confirm('Are you sure you want to submit?');
});
这个示例读起来就像句俏皮话。整个过程是,先选取一个表单,再在其中选取一些元素做修改,然后回溯到表单,为它定义一个submit()处理。
示例很酷,但如果你不习惯,也可以不这么用。我就很乐意用自定义变量来规划代码。
操作DOM(DOM Manipulation)
jQuery提供了几个大规模操作DOM的卓越方法。第一种非常让人惊叹:jQuery()函数能把HTML片段插入DOM元素中(实际上,函数会留意以'<'打头的字符串参数):
var div = jQuery('<div>Some text</div>');
一旦你创建好了div,便可以继续用链盒向其添加属性:
var div = jQuery('<div>Some text</div>').addClass('inserted').attr('id', 'foo');
现在把div加到body上:
div.appendTo(document.body) ;//没有 '$’
或用选择器把div加到已知元素的前面:
div.prependTo('div#primary')
处理事件(Handling events)
任
何JavaScript库都需要事件处理能力,jQuery也不例外。类似attr()和css()的行为,各种与事件处理相关的方法也有双重用途:一种
是把函数当作参数,赋给事件处理器;一种是不带参数,可以模拟事件被触发(译注:前提是事件已经定义,可参考visualjquery.com > Events > click()):
- jQuery('p').click(function() { jQuery(this).css('background-color', 'red'); });
- 为所有段落增加点击事件,当你点击它们时,段落背景会变成红色
- jQuery('p:first').click()
- 然后在第一个段落上模拟点击的动作,它的背景会变成红色
类似的函数还包括mouseover,keyup等,对应着浏览器通常支持的那些动作。留意一下事件处理中的'this'关键字,它代表触发事件的元素;jQuery(this)是一种惯用语法,可以让this所代表的元素应用各种jQuery方法。
这里有两个与事件相关的函数值得仔细说一下:
jQuery('a').hover(function() {
jQuery(this).css('background-color', 'orange');
}, function() {
jQuery(this).css('background-color', 'white');
});
hover()可设定两个函数,分别对应onmouseover和onmouseout事件。
jQuery('p').one('click', function() { alert(jQuery(this).html()); });
one()设定的事件在第一次被触发后便被移除。上面的示例会让所有段落在第一次被点击时显示其文字内容。
凭借bind()和trigger()方法,jQuery也可以支持自定义事件(click()家族仅仅是便捷方法,只支持有限的事件)。自定义事件可接受参数,trigger()可接受数组作为参数,来做各种处理操作:
jQuery(document).bind('stuffHappened', function(event, msg) {
alert('stuff happened: ' + msg);
});
jQuery(document).trigger('stuffHappened', ['Hello!']);
渐进式编码(Unobtrusive scripting)
本小节的标题很令我钟意。我一直认为,最好的Web应用程序,往往是那些在脚本被禁用后仍能正常使用的程序。想建立这样的应用程序,最好的方法就是遵循渐进式编码,让普通页面完全加载后,再为页面中的元素赋以事件处理(更多信息可参考渐进式编码和Hijax)。
jQuery对这种编码策略提供了绝好支持。首先,从整体上看,节点选取暗合jQuery以及渐进式编码的核心理念。其次,针对window.onload问题,jQuery提供了一套解决方案,这套方案借鉴了Dean Edward的成果,使得以“DOM加载完毕”为信号的事件能跨浏览器工作。你可以在浏览器完全加载DOM后设定并运行一个函数,如下所示:
jQuery(document).ready(function() {
alert('The DOM is ready!');
});
你甚至可以直接传递一个函数给jQuery(),以更简洁的方式达到同样效果:
jQuery(function() {
alert('The DOM is ready!');
});
jQuery与Ajax(jQuery and Ajax)
在我所知道的主流JavaScript库中,jQuery拥有最棒的Ajax API。最简单的Ajax调用如:
jQuery('div#intro').load('/some/fragment.html');
代码以GET请求方式,从/some/fragment.html文件中获取HTML片段,并把片段装载到id="intro"的div中。
当
我第一次看到这行代码时,几乎对它没什么印象。这看起来非常简洁,但如果你想用jQuery做些更复杂的事情,比如显示Ajax装载进度,该如何做呢?
jQuery为你准备了一些可自定义的事件(ajaxStart,ajaxComplete,ajaxError等等),来实现你想要的代码。同时
jQuery也提供了广泛的底层API,来实现更复杂的Ajax交互:
jQuery.get('/some/script.php', {'name': 'Simon'}, function(data) {
alert('The server said: ' + data);
}); // 以GET方式通过/some/script.php?name=Simon获取数据
jQuery.post('/some/script.php', {'name': 'Simon'}, function(data) {
alert('The server said: ' + data);
}); // 以POST方式向/some/script.php发送请求
jQuery.getJSON('/some.json', function(json) {
alert('JSON rocks: ' + json.foo + ' ' + json.bar);
}); // 从/some.json接收并解析数据,把数据转换成JSON格式
jQuery.getScript('/script.js'); // 以GET方式获取/script.js脚本并用eval()执行
插件(Plugins)
就你所能获得的功能的数量而言,jQuery库其实是相当小的——对代码做紧凑处理后只有20KB左右,甚至用gzip压缩后会变得更小。向标准库添加额外功能时,需用插件的方式来做,它可以(也确实能够)向现有的jQuery实例对象添加全新的方法。如果你想执行:
jQuery('p').bounceAroundTheScreenAndTurnGreen();
jQuery的插件机制提供了文档说明型的挂载方式(documented hooks),可以实现把上述方法添加到jQuery中。这种简易的创建形式,吸引了很多插件作者,他们让人印象深刻;现在插件目录中已经有超过100个插件了。
真正绝妙的,是你可以像自定义方法那样,来定义选择器。比如,moreSelectors插件实现了诸如“div:color(red)”的方法,来匹配包含红色文本的div。
并非天衣无缝(Leaky abstractions)
在
发掘jQuery各种特性的同时,我也被某个我视之为教条(philosophical
blocker)的东西所折磨着。几年来,我总是建议大家使用一种JavaScript库,前提是你们愿意梳理它的源码,并把它的工作原理彻底搞懂。我发
出如此论调,是基于Joel Spolsky的不健全抽象的法则(译注[4])。在那篇文章中,Joel指出,API把复杂性隐藏的越多,当它出现无法应付的意外时,你越有可能遭遇更多的麻烦。浏览器平台是不健全抽象的最佳代表,所以当库无法帮你摆脱困境时,你要自寻解药。保持警觉非常重要。
jQuery
使用了相当不可思议的技术,以求实现它所设想的各种功能——其中一些(比如选择器的代码)真是震天骇地。如果有必要彻底搞懂一个库的工作原理,那么对大多
数开发人员来说,jQuery不会是上佳之选。然而,jQuery拥有极高的人气,并且没有太多与之相关的恐怖经典流于街巷(译注:原文是a
distinct lack of horror
stories,比如微软Win95的“蓝屏”就是恐怖经典:),所以具体到jQuery所用技艺的邪正之分,也就变得不那么重要了。
我
想,我必须重新审视曾给大家的建议。库的运作机制并不是问题焦点:关键是应看清更具普遍性的潜在问题,知晓浏览器之间的差别,以及你使用库的哪种技术,来
消除差别造成的负面影响。没有哪种库可以一劳永逸地帮你克服浏览器的古怪行为。但只要你对应付潜在问题训练有素,便可把握经脉,指出问题的源头——无论它
们来自你自己编写的代码,还是库或者应付策略本身。
结语(To conclude)
我费了那么多口舌,希望能让大家明白,jQuery不只是又一个JavaScript库那么简单——它蕴含了很多值得品味揣摩的理念,甚至能启迪那些骨灰级的JavaScript程序员。如果你不打算尝试jQuery,但仍值得去花些时间探索一下jQuery的生态体系(the jQuery ecosystem)。
Simon Willison写于2007年8月15日 凌晨2:27
译注:
[1]
bookmarklet在原文中指的是一段“Insert jQuery”的JavaScript代码,由于译者使用Google
Docs进行在线翻译,链接中的JS代码被编辑器屏蔽掉了,下面列出的代码可粘贴到浏览器的地址栏中执行,执行后才可以继续用示例代码查看jQuery的
选取效果:
javascript:void(function(){var s=document.createElement('script');s.src='http://code.jquery.com/jquery-1.1.2.js';document.getElementsByTagName('head')[0].appendChild(s);}())
[2] 本文使用的术语“链盒”,大抵可对应单词chain/chaining/chainable;译者在参考jQuery Magazine issue 1对jQuery
选择器运行方式的图解后,确定了这种译法。汉字是象形文字,按“盒”字的构造来体会jQuery颇有意趣:比如上面的“人”字,不正是选择器“吐出”特定
元素的“嘴”吗?而“人”字下面“一”“口”“皿”的逐层累积,其形状又类似jQuery的Logo,并让人联想到链式选取过程中不同的元素集合;“盒”
即是“桶”(bucket),译者自以为绝妙!
[3] IBM developerWorks中国的官方翻译版本《使用jQuery简化Ajax开发》;译言版本分1、2两部分,由令狐葱翻译。
[4] Law of Leaky Abstractions: All non-trivial abstractions, to some degree, are leaky. 详细解读请见原文。