你是否曾经见过像 $(".cta").click(function(){})
这样的JavaScrip代码?或许你还会思考下 $('#X')
是什么,如果看到这些你都觉得摸不着头脑,那请一定要读完这篇文章。如果你觉得上述的代码片段是不能正常工作的,那请先看一些jQuery的代码范例,你会发现链接中的代码都是这样的结构。
这篇文章将会分析下面代码片段(动画化一个方形)中出现的一些关键知识点。你可能不会经常接触这样的代码,但了解一下这段代码的机制有助于你理解jQuery:
我们将会逐字逐句地解释上述的代码,告诉你JavaScript函数、jQuery对象还有事件驱动编程的具体细节。希望看完这篇文章以后,再遇到神秘的jQuery代码时你不会再头疼。
$是什么?
在你第一眼看到$的时候,有一种高大上的猜测在你心中盘旋:这一定是个很特别很复杂的JS方法。事实上,它很普通,也没有什么特殊的含义。$就是一个函数,是jQuery函数的另一个名字罢了。
jQuery是一个比较简单的JavaScript库,它在浏览器兼容方面做得很好,而且还提供了许多很有用的特性用来操作网页或者做些动画效果。你可以先引入jQuery库的地址,然后就能使用jQuery函数(比如$)了:
或者你也可以直接在jQuery官网上下载它。
jQuery函数通常只需要一个参数,这个参数可以是一个选择器也可以是JS引用页面上的内容(比如document)。
选择器就是CSS的一个片段,写在{…}之前的内容就是选择器了。所以,$(“div”)和jQuery(“div”)是一个意思,就是简单粗暴地把页面上所有的<div>标签都选中,和在CSS中使用.div获得的是同一个结果。
还记得在代码的最开头有一个$(document)吗?这一步是要把JS变量document传入jQuery方法当中。document是由浏览器来定义的,可以直接使用,它指的是文档对象模型(DOM)的最顶层。DOM指的是各个浏览器是如何来解释页面的整个HTML结构。用jQuery写的程序是基于DOM的。jQuery中的$(‘div’)和document.getElementsByTagNae(“div”)得到的结果大致上是一样的。
关键点
$只是一个方法,它是jQuery方法的简写也是它另一个名字。
点
在$(document)之后的点(’.’)预示着有许多方法可以被调用。一定要是一个JS对象才能使用这个点。说个最简单的,一个JS对象就是一堆属性的集合:
上面代码中,变量digger是一个对象,我们赋值了三个子对象给它:species,name和color。在面向对象编程中,这三个变量被称为成员变量。你可能更简洁地写成这样:你也可以把方法当做属性赋值给一个变量。沙鼠(Gerbil)大部分时候都很安静,但他们偶尔也会发出高频meeping sort of noise。在JS中,可以这么来表示:
在JS中,变量、方法和对象之间的界限是很模糊的,所以一个方法也可以被赋值到一个(成员)变量上:
现在你可一个调用这个方法来让沙鼠发出叫声来:
在面向对象语法中,digger.speak();是一个成员方法或者函数。在同一个对象中的方法可以互相引用,它们还能引用其他的成员变量。想象一下Digger学会了说英语,这得是多牛X的一件事啊:
在myNameIs函数中,this指的是包含它的对象,this.species就是digger.species,它的值就是’gerbil’。如果你想要不通过对象直接调用myNameIs(),那么this指的就是JS的window对象,this.species就是window.species,,在这里它的值是undefined。页面的弹框中的文字会变成”Meep! I am a undefined”。
对象也可以作为函数的返回值,我个人也推荐这么使用:
这么写的话,返回的就是(全局)变量/对象digger的引用,它和最初的digger是完全一样的,所以操作它的时候你不需要有什么顾虑。
你也可以不通过digger2这个中间值,而是直接在giveMeTheGerbil的返回值上调用sayMayName:
先不考虑内部代码,这种程序结构和例子里第一行代码是一样的:
下一节你将知道ready的作用是什么。
关键点
将对象简写:{name:”Digger”, species:”gerbil”},在方法中使用到的this是依附于一个对象(或者一个方法)的,它指向包含它的对象。
匿名函数
在JS中,创建函数的方法多种多样。只要你有一点编程经验那对下面的函数声明就不会陌生:
在上文里我们已经知道了函数是可以被赋值到变量上的。我们创建了meepMeep函数,并将它赋值到digger.speak上。事实上,函数还可以被匿名地创建出来(我们称呼这样的函数为:函数表达式),它们在声明时是没有任何名字的,声明后再被赋值到一个变量上:
在JS中,函数可以被赋值到变量上,还能像变量一样到处传递。让我们看看下面这个例子:
runMe函数有一个传入参数f,它将这个传入参数视作一个函数,还调用的这个函数。所以你可以这么使用runMe:
这样meepMeep函数就会被成功调用。如果在这个方法里,你连meepMeep的名字都不需要了,那事情就会更有趣些了。你可以直接创建它,当需要的时候再把它传入runMe来调用这个函数:
事实上,哪里都会出现meepMeep,等同于它的匿名函数也是这样的。这么调用:
不像上面那样,你可以用匿名函数替换掉meepMeep,虽然使用匿名函数的时候你需要在最外层添加一组括号:
在JS中,这种写法常常是用在制造变量作用域上。你能不能猜到下面这段代码的输出是什么呢?
在匿名函数里的var是解题的关键点。通过var,我们在函数内定义了一个局部变量x,它的值是4,然后通过console.log输出这个值。因为var这个关键词,函数内的x和函数外的值为3的x就是互相独立的。因此这段代码会将4和3先后打印出来。
现在我们的沙鼠已经不会发出尖锐的声音了,所以在代码中我们不再使用alert改用console.log来打印它的结果。在现代浏览器中console.log*是可以使用的(换言之,IE浏览器低版本中无法使用它),使用console.log就能安静地在浏览器控制台中输出信息。
我们接着就要讲匿名函数了。jQuery的ready方法可以说是上文中的runMe函数的延时版。ready方法中的内容会等到DOM完全加载完会后在运行。所以等到document加载完成了,下面的匿名函数才会运行:
如果需要在HTML文档加载完后再执行一些动作的话,程序员们通常会使用$(document).ready(…)。
关键点
匿名函数就是没有名字的函数,像function(){alert(1);}这样。它们可以被赋值到变量上、被传递到其他函数中也可以立即执行以创建出一个作用域来。
方法链
在更详细地分析代码之前,我们要先介绍JS中一个常见的内容:方法链。方法链指的是在一行代码中执行多个函数。这真的只是上述giveMeTheGerbil()的一个扩展:
现在让我们要重新定义一下gerbil相关的方法来返回他们的引用。
这两个函数都是对digger做了一些处理后返回digger对象。代码没有做什么改动,但是将digger对象返回以后,就可以把函数串在一起使用:
giveMeTheGerbil先运行,返回了digger对象的引用。所以上面那行代码等价于:
下一步,digger对象的speak方法运行后弹窗出’meep meep’。这也能返回digger的引用,然后这行代码就变成:
在这之后,sayMyName运行后返回digger的引用……运行后会出现三个警告框:‘meep meep. Meep! I am a gerbil, meep meep’。这样的链式效果常常出现在JS中,你可能在字符串(string)对象中见到这个:
上面的代码是获取字符串s中的子字符串,再将子字符串中的字母’a’用’i’代替,替换后的结果(也就是’digger’)被转为大写,然后返回打印到控制台上。
当然,jQuery中到处都是方法链,在我们的例子中也能看到:
$(“div”)将页面上所有的div元素获取到然后作为jQuery对象的一部分返回。基于jQuery对象调用animate方法,然后再在每个jQuery对象上执行append。这样的作用链可以很长很长,下面这个是典型的长jQuery方法链:
总的来说,使用这样的长方法链会造成debug和维护代码的困难。所以尽量避免使用这样的长链,不过在压缩时它们还是常常被使用。
关键点
对象(比如对象中的方法)的方法会返回对象的引用,然后就能基于引用使用方法链,而不需要在执行多个方法的时候还要储存中间值。
jQuery对象
我们的例子里用了好几个jQuery方法:ready、click、animate和append。这些方法都是与jQuery对象结合使用的,和上文中digger对象的speak方法和myNameIs方法类似的机制,也和string对象的substr方法、replace方法和toUpperCase方法类似。
这些函数都是jQuery对象的方法,它们也都会返回一个jQuery对象。不过比起我们例子里的digger对象和string对象,jQuery对象相对而言要复杂许多。就像早前提过的,JS中各个概念之前的界限其实比较模糊。你可以在使用方法链的时候把它视作一个对象,但是你也可以把它当做一个数组来对待:
在这里例子中,$(“div”)将页面上所有的div元素都存储一个jQuery对象中,然后赋值到变量mydivs中。这个jQuery对象会被当做一个数组(其实是一个NodeList)进入迭代。每次迭代都会对DOM中选出的节点做一些操作,这些节点在迭代里也是当做对象的,所以它们也有自己的属性,比如outerHTML和innerHTML。
也可以先把这些节点转成jQuery对象,也就是在取得节点后将它们用$()包起来(你可以把任何代码传入$中,都能将它们转成jQuery对象),再之后通过jQuery方法html()也可以得到相同的结果。
上面两个方法都可以将页面上的div元素中的HTML内容打印到控制台中。
当你在运行像$(“div”).animate(…).append(…);这样的代码的时候,动画是会发生在所有的div元素上的,然后这些div元素会被作为jQuery对象的一部分传到方法链中的下一个函数中(在大部分jQuery函数中都是这么实现的,具体请看文档)。
关键点
jQuery的$函数还有像click、animate这样会返回jQuery对象的方法,它们都是对象或者数组的一部分。类似数组的这部分会包含DOM中节点的引用。
总的来看
现在我们可以全局地来看这个例子了,$(document)返回的是页面本身的jQuery对象。将一个方法传入.ready(…)中,等到页面已经解析完了DOM也已经加载完成,ready(…)中的方法就会运行。
这个方法将页面中的button元素都获取到了,然后返回一个绑定了click方法的jQuery对象。click方法中还有一个匿名函数:
上述的函数获取了所有的div元素,然后返回一个jQuery对象,在这个对象上显示调用了它的animate方法。传入jQuery的animate方法中的参数是animate的一系列属性,这些属性是对象的简写形式,{height:”toggle”}这句是告诉jQuery对页面上所有的div元素的高度都使用toggle效果:一开始div的高度会变成0,接着它们的高度又会动画地变回原来的值。
animate方法也会返回一个jQuery对象,执行完animate方法后执行append方法:每当button被点击了,就在每个div元素中添加”hi”字符串。运行下面的HTML代码来看看我们说的效果是什么样的,在线demo在此:
每次button被点击了,绿色的div就会收起或者展开,然后添加一个新的“hi”到div中。
事件驱动造成的问题
下面这段代码看起来够简单的吧:
你可能只是希望div的高度到200px,但是事实上从*h*被赋值为200到动画真正发生之间还可能发生了很多事情导致最终的结果和你所期望的不一样。在一个复杂的jQuery应用中,变量*h*可能会被反复使用或者它的值被改写。你可能会发现div的高度只会达到50px而不是期望中的200px。这时候你需要去看看是不是别的代码改写了h的值,当执行*for (h=1; h<50; h++) {…}*来改变h的值时,你可能会有所发现。
坦白来说,这个问题并不是由jQuery或者匿名函数造成的,而是事件驱动编程本身就会遇到的问题。上述的代码的片段其实是在不同的时间点被执行的:
- 首次执行时($(document).ready(…))
- 页面加载完成后($(“button”).click(…))
- button被点击后($(“div”).animate(…))
服务端的代码(比如PHP的程序)运行是有按照从头到尾的顺序的, 从开始到结束,输入HTML以显示页面。JS也可以做到这一点,但是它如果和事件结合起来才能发挥最大作用,比如button点击事件。这就是事件驱动编程,可不仅仅只有JS是这样的编程哦。手机应用背后的程序很多也都是事件驱动的,比如Objective-C、Java或者C++在处理用户与屏幕互动这块也是使用事件驱动编程的。
如果上面的代码转成Java后再Android手机中运行,那么在最里层的函数中的h的引用就会出现错误。这是因为h并没有被声明为全局(或者是Java中的static)变量,所以里层的代码不知道h的值应该是什么。虽然了解这点也解决不了事件驱动造成的问题,不过至少以后你会想清楚要怎么使用变量。
避免上述问题的一个解决办法就是将你的变量放在适当的作用域中。在第一个匿名函数中声明var h变量来解决这个问题,这样局部变量h的优先级高于其他任何的全局变量h:
如果你一定要使用全局变量,那就将这些全局变量命名、组合好,并在你的代码中加上适当的comment:
结论
这篇文章是一篇针对初学者的介绍JS语法和如何使用jQuery使用的指南。jQuery只是一个JS库,它有一个很看起来很特别的函数:$,推荐在jQuery中使用对象的简写形式、匿名函数还有方法链。类似的库还有YUI(Yahoo User Interface)。
现在再看jQuery的代码时,你是不是不会再抱有过去的疑问和不确定了呢?你已经知道它要做什么了。虽然由于事件驱动编程的复杂性,你可能不确定什么时候使用它,但是你会知道怎么做。