事件
JavaScript和HTML之间的交互是通过事件 实现的。事件最早是在IE3和Netscape Navigator2中出现的,当时是作为分担服务器运算负载的一种手段。在IE4和Netscape Navigator4发布时,这两种浏览器都提供了相似但不相同的API。IE9、Firefox、Opera、Safari和Chrome全都实现了“DOM2级事件”模块的核心部分。IE8是最后一个仍然使用其专有事件系统的主要浏览器。
事件流
当浏览器发展到第四代(IE4和Netscape Navigator4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件? 两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。他们认为:如果你单击了某个按钮 ,这个单击事件并不仅仅发生在按钮 上。换句话说,你也单击了按钮的容器元素 ,甚至也单击了整个页面 。 事件流 描述的是从页面中接收事件的顺序 。有意思的是,IE和Netscape开发团队居然提出了差不多是完全相反 的事件流的概念。IE的事件流是事件冒泡流 ,而Netscape Communicator则是事件捕获流 。
事件冒泡
即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点(或文档) 。所有现代浏览器都支持事件冒泡。IE5.5-事件冒泡会跳过<html>
元素。IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。
事件捕获
事件捕获的顺序是从不太具体的节点到具体的节点 。事件捕获的用意在于事件到达预定目标之前捕获 它。虽然事件捕获是Netscape Communicator唯一支持的事件流模型,但IE9、Firefox、Chrome和Safari目前也都支持这种事件模型。“DOM 2级事件” 规范要求事件应该从document 对象开始传播,但这些浏览器都是从window 对象开始捕获事件的。
DOM事件流
“DOM2级事件” 规定的事件流包括三个阶段:事件捕获阶段 、处于目标阶段 和事件冒泡阶段 。首先发生的是事件捕获,为截取事件提供了机会。再是实际的目标接收事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。对于下面的div元素:
<!DOCTYPE html>
<html >
<head >
<title > example</title >
</head >
<body >
<div id ="myDiv" > click me</div >
</body >
</html >
单击div元素会按照下面的顺序触发事件: 在DOM事件流中,实际的目标(div)在捕获阶段不会接受到事件。下一个阶段是“处于目标”阶段,于是事件在div上发生,并在事件处理中被看成冒泡阶段的一部分。最后,冒泡事件发生,事件又传播回文档(这个是DOM2级事件规范)。不过,IE9、Firefox、Chrome、Safari和Opera9.5+都会在捕获阶段 触发事件对象上的事件。IE8-不支持DOM事件流。
事件处理程序
事件就是用户或浏览器自身执行的某个动作。诸如:click、load、mouseover,都是事件的名字。而响应事件的函数就叫做事件处理程序 (或事件监听器 )。事件处理程序的名字以“on” 开头,因此click事件的事件处理程序就是onclick,load就是onload。为事件指定处理程序的方式有以下几种。
HTML事件处理程序
顾名思义,我们可以直接通过元素的特性 为元素指定事件处理程序。这个特性的值是能够执行的JS代码。例如:
<div id ="myDiv" onclick="alert('clicked!');" >click me </div >
<div id ="myDiv" onclick="fun();" >click me </div >
<script >fun(){alert('clicked!');}</script >
这两种方式太常用了,不过我们还是要思考一下它具体是怎么工作的。一个最本质的问题,它的作用域是什么?通过第二种方式,可以看见全局作用域 在这段代码的作用域中(因为他调用了全局作用域下的一个函数)。书上的解释是这样的,这个特性相当于创建了如下一个函数:
function () {
with (document) {
with (this ) {
}
}
}
首先这里的this指代的是当前元素的引用 。这段代码的意义是为特性值创建了一个函数,且将document和this 依次加入这个函数的作用域的前端,所以执行这么一段代码时,会先在this 的作用域中寻找变量,再从document中寻找,最后因为这个匿名函数也是属于window的,所以最终会在window下查找。特殊情况 ,如果该元素是表单的一个输入元素 ,则作用域还会包含访问表单元素的入口,这个函数就变成了:
function () {
with (document) {
with (this .form) {
with (this ) {
}
}
}
}
<!DOCTYPE html>
<html >
<head >
<title > HTML Event Handler Example</title >
</head >
<body >
<form method ="post" >
<input type ="text" name ="username" value ="enter username" >
<input type ="button" value ="test1" onclick ="alert(value)" >
<input type ="button" value ="test2" onclick ="alert(username.value)" >
<input type ="button" value ="test3" onclick ="alert(value_test)" >
</form >
<script >
window.value_test = "window" ;
document.value_test = "document" ;
</script >
</body >
</html >
对于第一个按钮,会先搜索this下面的value,即“test1”。对于第二个按钮,由于form在其作用域中,结果也在预想之内。对于第三个按钮,会返回“document”,如果将document.value_test = “document”;这行删除,则会返回“window”。从上面的测试可以得出,它的作用域的确如上所说。 不过这里还有一个很容易犯错的地方。如果我不是写成alert(value)。而是写成:
<!DOCTYPE html>
<html >
<head >
<title > HTML Event Handler Example</title >
</head >
<body >
<form method ="post" >
<input type ="text" name ="username" value ="enter username" >
<input type ="button" value ="test1" onclick ="fun_alert()" >
<input type ="button" value ="test2" onclick ="alert(username.value)" >
<input type ="button" value ="test3" onclick ="alert(value_test)" >
</form >
<script >
window.value_test = "window" ;
document.value_test = "document" ;
window.value = "haha" ;
function fun_alert () {
alert(value);
}
</script >
</body >
</html >
其结果会是“haha”。理由也很简单,上面的写法相当于:
function () {
with (document) {
with (this .form) {
with (this ) {
fun_alert();
}
}
}
}
function fun_alert () {
alert(value);
}
window.value = "haha" ;
很明显这里的fun_alert()方法的作用域是函数内部->window ,所以其结果也会是“haha”。那么在这里有个问题。如果我把window.value = "haha";
这行去掉,那这个方法会不会正常返回this下面的value(test1)呢。自然也不会,这里还会抛一个异常(value is not defined)。因为整个执行过程是这样的。它先要执行fun_alert(),而fun_alert()方法需要查找,他依次从this->form->document->匿名函数内部(肯定没有,因为这个匿名函数就一个with块)->window 。直到找到了window下的fun_alert()再执行它。而fun_alert()的作用域则没有那么复杂,他的作用域就是函数内部->全局作用域window 。所以如果自身和window下面都没有value,这个函数即会报错。所以通过函数去调用alert,和直接写一个alert的区别还是挺大的 。 这里有一个问题,如果我在这个特性值内部声明变量,该变量是否会成为该元素对象的一个属性?我们来做一个实验(其实这里涉及到了with 的知识):
<!DOCTYPE html>
<html >
<head >
<title > HTML Event Handler Example</title >
</head >
<body >
<form method ="post" >
<input type ="text" name ="username" value ="enter username" >
<input type ="button" name ="xx" value ="test1" onclick ="alert(c);alert(value);
var c = 1;var value=2;alert(value);" >
</form >
<script >
window.value_test = "window" ;
document.value_test = "document" ;
</script >
</body >
</html >
首先要明确的是在with块里使用var 声明变量是不会将该变量设置成with中指定的作用域中的属性的(前提是该变量名是新的,如果已存在,则会修改,这种情况和这句话并不矛盾 )。所以this中不会添加c这个属性。那上面这段代码为什么不会报错呢?我在var c之前就先alert了。其实这里存在变量声明提升,此处的var c = 1相当于:
function () {
var c;
var value;
with (document ) {
with (this .form) {
with (this ) {
alert(c);// undefined
alert(value);// undefined 这个地方非常有意思,我觉得应该是this .value才对。
c = 1 ;
value = 2 ;// 这个操作也很有意思,如果真如我所想,这个操作能改变this .value;
alert(value);// 2 因为上一句话不能改变this .value。所以这个地方也应该alert(this .value),也不为2 才对。可见事情并没有我想的那么简单。
}
}
}
}
如果真如上面的代码,只要把上面的this改成通过document.getxx获得的节点引用,则结果完全不一样。所以,我觉得这里的作用域链更像是:匿名函数内部->this->form->document->window ,但是这样的逻辑按照我现在的认知还无法写出一套代码,等我以后再来解决。 这里还有一个问题,如果当前区域(或元素,这是另外一个例子)存在fun_alert()这个方法呢?结果会如何?还是会弹出“haha”吗?有可能是“test1”吗?我们稍微修改一下代码再做一个实验(我的想法:因为没有为fun_alert()指定调用的对象,fun_alert的内部的this必定为window,也就是说alert(this.value)必定是window.value。但alert(value)则不然 ):
<!DOCTYPE html>
<html >
<head >
<title > HTML Event Handler Example</title >
</head >
<body >
<form method ="post" >
<input type ="text" name ="username" value ="enter username" >
<input id ="ts" type ="button" name ="xx" value ="test1" onclick ="
function fun_alert() {
alert(value);
};fun_alert();" >
</form >
<script >
window.value_test = "window" ;
document.value_test = "document" ;
var value = "haha" ;
</script >
</body >
</html >
点击按钮后会输出test1,这里function fun_alert()就是一个闭包,它的value先是访问闭包内部,再接着就是之前那样的顺序 ,所以结果是test1也没有什么稀奇的。然后我把fun_alert设置为该节点的一个方法:
<!DOCTYPE html>
<html >
<head >
<title > HTML Event Handler Example</title >
</head >
<body >
<form method ="post" >
<input type ="text" name ="username" value ="enter username" >
<input id ="ts" type ="button" name ="xx" value ="test1" onclick ="
fun_alert();" >
</form >
<script >
document.getElementById("ts" ).fun_alert = function () {
alert(value);
};
window.value_test = "window" ;
document.value_test = "document" ;
var value = "haha" ;
</script >
</body >
</html >
结果为haha。其实这些例子更多和with,作用域链有关,这里是再巩固一下。之所以为haha,是因为这里找到了Node下的fun_alert(),但是这个fun_alert()的写法不是输出this.value(this对象是在运行时基于函数的执行环境绑定的,当函数作为某个对象的方法执行调用时,this等于那个对象 ),而是输出value。我们看一下这个方法的位置,它并不是一个闭包,复习前面的知识:在创建fun_alert函数时,会创建一个预先包含全局变量对象的作用域链,也就是说fun_alert函数的Scope Chain中的已经指向了全局变量对象了。然后它不是闭包(没有被其他函数包着),那么函数的Scope Chain就只包含全局作用域。最终在调用的时候,再加入函数内部的执行环境,也就是说它的作用域链中是不包含节点的(但是this依然指向节点,因为它是对象的一个方法,且我是在with(this){}内部寻找fun_alert。相当于with(this){this.fun_alert()} )。而fun_alert();只是返回了这样一个方法并且去执行,这个方法的作用域链中没有节点,所以最终会输出全局作用域下的value 。 这样指定事件处理程序会创建一个封装着元素属性值的函数 (可以想成之前的匿名函数),这个函数中除了arguments还有一个局部变量event (事件对象 ,下一篇文章细说)。这种写法的缺点是扩展作用域链 在不同浏览器可能会导致不一样的结果,且HTML和JavaScript代码紧密耦合。所以多数开发人员更乐意用JavaScript指定事件处理程序 。
DOM0级事件处理程序
通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性 。每个元素(包括window和document)都有自己的事件处理程序属性。这些属性通常全部小写 :
var btn = document.getElementById("myBtn" );
btn.onclick = function () { alert('clicked!' )};
---------------------------------------------------
<input type="button" id="myBtn" value="Click Me" onclick="alert('clicked!')" />
等价于btn.onclick = function () {
alert('clicked!' );
};
使用DOM0级方法指定的事件处理程序被认为是元素的方法。所以该方法中的this指向当前元素。以这种方式添加的事情处理程序会在事件流的冒泡阶段 被处理。通过btn.onclick = null;
也可以删除事件处理程序。再次单击按钮将不会有任何动作发生。
DOM2级事件处理程序
IE9+、其他浏览器支持DOM2级事件 。该模块定义了两个方法:addEventListener() 和removeEventListener() 。所有DOM节点都包含这两个方法,并且他们接收3个参数:要处理的事件名 、作为事件处理程序的函数 、一个布尔值 (false表示在冒泡阶段调用函数,true代表在捕获阶段调用函数,一般用false)。
var fun1 = function () {
alert(this .id);
}
var fun2 = function () {
alert("hello" );
}
var btn = document.getElementById("myBtn" );
btn.addEventListener("click" , fun1, false );
btn.addEventListener("click" , fun2, true );
btn.removeEventListener("click" , fun2, false );
可以多次添加事件处理程序 ,通过同样的参数也可以移除相应的事件处理程序 。如果add的时候传入了匿名函数 ,则无法通过removeEventListener移除,因为你没有匿名函数的引用。多次添加同一事件处理程序 (比如add fun2两次,且都在同一阶段,也就是第三个布尔值一致 )就只有一次的效果。
IE事件处理程序
IE中提供了和上述两个方法类似的attachEvent()和detachEvent() 。这两个方法只接受两个参数:事件名 和事件处理程序函数 。因为IE8-中只支持事件冒泡,所以attachEvent() 添加的事件处理程序都会被添加到冒泡阶段 。 在attachEvent()和detachEvent() 中,传入的第一个值是特性名,而不是省略“on”的名称。且attachEvent()添加函数顺序,与执行顺序刚好相反(IE8-,如果是IE9+则是和添加顺序相同),且添加的程序函数 的this指向window,而不是当前节点 :
<!DOCTYPE html>
<html >
<head >
<title > Internet Explorer Event Handler Example</title >
</head >
<body >
<input type ="button" id ="myBtn" value ="Click Me" />
<p > This example works only in Internet Explorer.</p >
<script type ="text/javascript" >
var btn = document.getElementById("myBtn" );
btn.attachEvent("onclick" , function () {
alert("Clicked" );
alert(this === window);
});
btn.attachEvent("onclick" , function () {
alert("Hello world!" );
});
</script >
</body >
</html >
跨浏览器的事件处理程序
了解了IE事件处理程序和一般的事件处理程序的区别后,我们可以写出一套通用的代码:
var EventUtil = {
addHandler: function (element, type , handler ){
if (element.addEventListener){
element.addEventListener(type , handler , false );
} else if (element.attachEvent){
element.attachEvent("on" + type , handler );
} else {
element["on" + type ] = handler;
}
},
removeHandler: function (element, type , handler ){
if (element.removeEventListener){
element.removeEventListener(type , handler , false );
} else if (element.detachEvent){
element.detachEvent("on" + type , handler );
} else {
element["on" + type ] = null ;
}
}
};