JavaScript语言是当前十分流行的语言。它从前端页面走来,产生了像jQuery这样的强大框架;它向后端服务器走去,有了Node.js;它更被微软接受为平等于C#和VB.net的第三种可用于开发Windows8应用的语言。
不过说到底,JavaScript的本家还是在前段网页界面的上的使用,而真正让JavaScript变得如此强大的还在于它能对文档对象模型(Document Object Model),也就是俗称的DOM进行操作。这其中,又要数“事件处理(Event Handling)”让用户交互(User Interaction)体验得到了巨大的提升和丰富。
如果没有事件处理,其实根本不会需要添加JavaScript到页面上,本篇文章就来总结一下JavaScript中DOM事件绑定和激发的一些方式和机制,有助于在以后的开发上有更好的设计和理解。
事件绑定方式
总得来说DOM的事件绑定一共有三种方式:初级式、中级式、高级式。好吧,开玩笑的,但其实并不完全是玩笑,通俗一些说来应该是:
(注:属性式和方法式只是作者本人针对该种事件绑定方式的特点所起的名字,并非广为使用的命名;括号中的英文名字,则来自于QuirksMode的Introduction to Events一文,该名称根据绑定方法的出现历史时间顺序进行分类。)
下边就来一一介绍这三种方式的格式,当你看到它们的写法的时候,你可能就会觉得很熟悉了。
内嵌式(Inline)
这是最早的绑定方式,早到当JavaScript出现的时候它也随之同时出现了,早到这是当时唯一的方式,也随着历史一直被最常使用着:
[codesyntax lang=”xml”]
<button onclick="alert('called from inline');">Inline Click Event</button>
[/codesyntax]
之所以称之为“内嵌式”,是因为它出现在HTMT标签中当做属性(attribute)来使用,其值则是所要执行的JavaScript脚本内容。还有常见的使用方式是将函数定义在<script>标签内,然后将方法调用作为onclick等事件的值:
[codesyntax lang=”xml”]
<button onclick="func4inline('call function from inline');">Inline Click Event Call Function</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function func4inline(str) { alert(str); } </script>
[/codesyntax]
属性式(Traditional)
onclick不仅是HTML标签的属性(attribute),也是DOM对象的属性(property),JavaScript的代码可以作为值在标签中赋给onclick
,DOM对象的onclick
则接受函数作为值。
[codesyntax lang=”xml”]
<button id="btn_traditional">Property of Click Event</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> var btn = document.getElementById("btn_traditional"); function func4traditional() { alert("call function from DOM property"); } btn.onclick = func4traditional; </script>
[/codesyntax]
如果赋予的值不是方法,那么事件激发时就会将其忽略。
方法式(Advanced)
这种方式是最先进的一种方式,其先进性我慢慢道来。这种方式通过调用DOM对象的事件绑定方法来进行绑定,不过这种方法又分为W3C和Microsoft两种标准。当然显而易见后者只在IE上实现,而前者被其它大部分浏览器实现(其中也包括IE 8-10)。
W3C格式
W3C格式的事件绑定方法是addEventListener,该方法接受3个参数分别为:type
(事件类型),listener
(事件监听方法),useCapture
(指定该方法是捕获式还是冒泡是)。先来看个示例:
[codesyntax lang=”xml”]
<button id="btn_w3c">W3C Advanced Event Bind</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function func4w3c() { alert("call function bind with addEventListener"); } var btn = document.getElementById("btn_w3c"); btn.addEventListener("click", func4w3c, false); </script>
[/codesyntax]
跟属性式很像,只是属性名字变成了第一个参数(注意:没有on前缀),而本来付给属性的方法,现在作为了第二个参数。至于第三个参数的意义,以后再做介绍,一般情况下都传入false,或者不传入(默认为false)。
Microsoft格式
Microsoft格式其实跟W3C格式很像,只是方法名字变成了attachEvent,并且只接受两个参数:type和listener。
[codesyntax lang=”xml”]
<button id="btn_ms">Microsoft Advanced Event Bind</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function func4ms() { alert("call function bind with attachEvent"); } var btn = document.getElementById("btn_ms"); btn.attachEvent("onclick", func4ms, false); </script>
[/codesyntax]
注意,这里是有on前缀的。
事件绑定方式比较
介绍完了JavaScript三个DOM事件基本绑定方式后,再看比较一下这三种方法有何异同,在使用的时候又有哪些地方需要注意的,以及该挑选那一种方式来进行事件绑定。
内嵌式 vs. 属性式
JavaScript写多了应该都知道大部分HTML标签的属性(attribute)差不多都有对应的DOM对象属性(property),它们有些有着一模一样的名字,比如style
、id
;有些只是略微不同,比如HTML中的class
对应DOM上的className
。修改任一一项都能对相应的对象呈现产生作用。
之前也看到内嵌式和属性式用着相同的名称,这是否也暗示着它们指代相同的内容呢?事实的确如此,只是略有细微的不同。
首先内嵌式直接接受字符串形式的JavaScript脚本,而属性式则接受JavaScript的函数。但实际上内嵌式也是生成一个匿名的函数方法,并将字符串中的脚本包裹在内。比如下边的代码:
[codesyntax lang=”xml”]
<button onclick="alert(this.onclick);">Check onclick property</button>
[/codesyntax]
在button被点击之后,alert
会跳出对话框显示this
的onclick
属性的内容,而这里的this我们知道就是指代button自身。Chrome的显示如下:
可以看到显示的内容为:
[codesyntax lang=”javascript”]
function onclick(event) { alert(this.onclick); }
[/codesyntax]
很明显的可以看出一个onclick
方法包裹了内嵌的代码段。并且这个方法接受一个event
的参数,这个event
其实就是激发起这个方法的事件。
下边我们在标签中内嵌onclick
方法,同时使用属性式对onclick
赋予其它的方法,看看会有什么样的效果:
[codesyntax lang=”xml”]
<button id="btn_inline" onclick="alert(this.onclick);">Override onclick property</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> var btn = document.getElementById("btn_inline"); btn.onclick = function() { alert(this.onclick); } </script>
[/codesyntax]
当button被点击后,alert出来的结果与之前的已经不再相同,而是通过DOM属性(property)新赋值上去的匿名方法:
这也就说明内嵌式和属性式是相通的。那么应该使用内嵌式还是属性式来绑定事件呢?总得来说,既然是相同的,那么两者都可以。但是一般偏向于使用属性式,原因有以下几点:
首先内嵌式不方面写比较长的代码,当然可以只写一个方法调用,但是被调用的方法必须定义在全局下,如此的话JavaScript强大的闭包(Closure)就无法使用了;若要分享数据,则变量也要定义成全局的,这会造成变量污染;另外也看到对于内嵌式的代码,浏览器会创建一个新的方法包裹它,本来已经定义了一个方法,现在又多了一个无谓的方法。
其次是特殊变量的调用上不方便,这个特殊变量是指this
(事件所绑定的DOM对象)和 event
(具体激发的事件对象),因为所调用的方法已经不再属于对象的域,所以常常会看到这样的代码来传递这两个特殊的变量:
[codesyntax lang=”xml”]
<button onclick="funcWithParam(event, this);">Pass event and this</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function funcWithParam(e, btn) { alert(btn); alert(e); } </script>
[/codesyntax]
但如果使用属性式,就可以直接在方法内使用this
,只是event
还是需要作为参数传入:
[codesyntax lang=”xml”]
<button id="btn_this_event">Pass event only</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function funcWithEvent(e) { alert(this); alert(e); } document.getElementById("btn_this_event").onclick = funcWithEvent; </script>
[/codesyntax]
也许在一开始的代码中并不需要用到被激发的对象或者event
,因此并没有传递this
过去,但是或者后来的修改中却需要用到的,此时就要找回到内嵌代码的地方,将变量传入并,修改调用方法的参数。而使用属性式,this
随时都可以使用,若要event
,只需在方法定义上添加一个参数即可。
此外,使用属性式,还可以做到页面呈现和功能的分离。毕竟HTML和CSS主要式用来呈现界面的,而JavaScript则主要式用来交互的,两者的分离有助于模块化,便于维护和修改。比如有很多的个类似的text input,它们都需要在onblur时调用一个validate的验证方法来判定自身是否非空,在将来某个时候想要添加另一个验证长度不大于一个数值,如果使用的是内嵌式的事件绑定,那么就需要寻到每个内嵌的地方添加新的验证;若是使用属性式,那么只需要在JavaScript中修改某个遍历方法即可,而不需要修改页面的代码,这就是一种松耦合的体现。
既然已经知道内嵌式和属性式基本一致的,下边就针对属性式和方法式来比较一下,看看两者有什么区别。不过在此之前先来看看方法式的W3C格式的addEventListener
和Microsoft格式的attachEvent
不同点。
attachEvent vs. addEventListener
之前已经介绍,Microsoft格式的attachEvent
主要用于IE,而W3C格式的addEventListener
则是被更多浏览器广为接受和实现的标准。attachEvent
比addEventListener
少一个Boolean的参数,这个参数指定绑定的方法是以捕获式激发还是冒泡式激发,attachEvent
绑定方法只能式冒泡式激发。这也让addEventListener
更加强大。
不仅如此,attachEvent
绑定的方法被激发时,this
并不指代被触发事件的DOM对象,而是window
对象,也就是说方法只是简单的函数被调用,而非当做DOM对象的方法被调用。
另外因为attachEvent
主要在早期的IE浏览器中被实现,最新IE9以及IE10都开始使用addEventListener
而不再实现attachEvent
。所以接下来的对比中,也主要是针对addEventListener
来讲。
不过为了兼容性很多时候,一般检查方法是否有定义,然后使用该方法进行绑定。以下代码就是来自jQuery:
[codesyntax lang=”javascript”]
if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); }
[/codesyntax]
属性式 vs. 方法式
方法式相较于属性式的有点在于:可以绑定多个方法到同一种事件类型上。比如我们可以将两个不同的函数通过addEventListener
都绑定给同一个button的click上:
[codesyntax lang=”xml”]
<button id="btn_multi_bind">Mutilple Events</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function func4multi1(e) { alert("hello world"); } function func4multi2(e) { alert("welcome to ider"); } var btn = document.getElementById("btn_multi_bind"); btn.addEventListener("click", func4multi1, false); btn.addEventListener("click", func4multi2, false); </script>
[/codesyntax]
两个方法会按绑定顺序先后被调用(其实调用顺序并没有文档明确指出是怎么样的,只是实验结果如此)。
而属性式,就只能绑定一个方法上去,第二个方法若赋给相同的事件类型,则只会覆盖前者。当然可以把多个方法包裹在一个函数中再赋值上去,但是这样在被包裹的方法内部,就无法使用到this
和event
变量必须靠包裹方法传递进来,而方法式绑定的每个方法都可以正常使用this
和event
变量。
但是要注意是,如果相同的方法绑定在相同的事件类型之上,并且第三个参数的布尔值也相同,那么addEventListener
则视为相同绑定不会绑定多遍。所以下边代码虽然绑定了两边方法,但是只有一个alert
产生:
[codesyntax lang=”xml”]
<button id="btn_multi_with_one">Mutilple Events with on Function</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> function func4multi(e) { alert("hello, welcome to ider"); } var btn = document.getElementById("btn_multi_with_one"); btn.addEventListener("click", func4multi, false); btn.addEventListener("click", func4multi, false); </script>
[/codesyntax]
不过匿名函数比较特别,每次定义时都是产生一个新的Function对象,即使内容相同,也是不同的方法:
[codesyntax lang=”xml”]
<button id="btn_multi_anonym">Mutilple Anonymous Function</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> var btn = document.getElementById("btn_multi_anonym"); for (var i = 1; i <= 2; i++) btn.addEventListener("click", function(){alert("hello, welcome");}, false); </script>
[/codesyntax]
有绑定,也有取消绑定。属性式取消绑定的方式很简单,只要将null
值赋给相应的事件属性(property)即可,那么相应的事件激发时就不会有方法可以调用。方法式取消绑定同样要调用另一个方法removeEventListener(type, listener, useCapture) (Microsoft格式是detachEvent(event, function)),取消时必须保持跟绑定时所使用的参数对象完全一致,如果该事件没有绑定,调用removeEventListener
也不会用任何的副作用,所以可以安心的反复调用多遍。
因为取消绑定要求参数一致,所以这里就出现了方法式比属性式事件绑定的一个弊端。对于属性式而言无论式什么样的方法,都可以简单用null
覆盖来取消绑定。而用addEventListener
绑定的匿名方法, 因为在外部已经失去了对匿名方法的引用,也就不能传递给removeEventListener
,所以就无法用任何的方式来取消绑定,只能通过刷新页面来重置所有的内容。不过一般而言很少会遇到需要取消事件绑定的情况。
EventListener接口
什么?JavaScirpt还有接口?如果自己看addEventListener和removeEventListener第二参数的类型,会发现其要求是EventListener,而它的定义却是一个接口(interface)该接口只包含了handleEvent的方法。不过JavaScript的这样弱类型的语言,接口作用只是达成约束该对象需要存在该方法而不会强制必须实现该方法。所以使用方法式绑定的时候,除了可以传方法进去,还可以使用“实现”了EventListener接口的对象。
[codesyntax lang=”xml”]
<button id="btn_interface">Implement Interface</button>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> var obj = { handleEvent:function() { alert("handleEvent function from object"); }}; var btn = document.getElementById("btn_interface"); btn.addEventListener("click", obj, false); </script>
[/codesyntax]
因为JavaScript的接口只是要求而非强制,所以即使使用没有实现EventListener接口的对象来作事件绑定也不会有任何影响,当事件发生时该绑定就会被忽略了。而每一个JavaScript函数,根据MDN上的说法,都自动实现了该接口,所以才可以用来绑定。但是我们并不能在代码中主动的调用handleEvent方法,不知道里面是什么玄机。
使用对象的好处在于可以额外的提供一些辅助方法和变量,但因为拥有强大的闭包,很多时候更倾向使用闭包,不过多一种选择往往在某些时候可以找到更好的实现方式。
最后要提一下的是,当用JavaScript,使用DOM的cloneNode方法拷贝出新对象时,不会将任何绑定的方法拷贝过去,也不会让新对象绑定相同的事件处理方法,也就是说新对象没有任何事件处理方法。不过,如果用jQuery的clone方法可以指定是否要将事件和数据都拷贝过来。
本文通过例子和对比介绍了JavaScript中DOM对象的事件绑定方法,看过之后应该觉得并不是很复杂吧,不过有时为了考虑兼容性和扩展性还需要一些考虑和选择,希望本篇文章对此会有帮助。不过有jQuery这个强大的框架作为开发利器,事件绑定只要一个on方法足以,何须顾虑那么多呢。