导航
导航

JS高级程序设计读书笔记(中)

函数表达式

概念与特征

关于函数,我们知道定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。函数声明的语法是这样的:

1
2
3
function functionName(arg0, arg1, arg2) { 
//函数体
}

关于函数声明,它的一个重要特征就是函数声明提升,意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。例如:

1
2
3
4
sayHi(); 
function sayHi(){
alert("Hi!");
}

第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种形式。

1
2
3
var functionName = function(arg0, arg1, arg2){ 
//函数体
};

这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName。 这种情况下创建的函数叫做匿名函数,因为 function 关键字后面没有标识符。匿名函数的 name 属性是空字符串。

函数表达式没有变量提升,也就是说在使用前必须赋值,如果像上述函数声明那样直接使用的话,就会报错。

有一点需要注意的是,在 ES5 中,严格模式下,函数的声明只能在全局作用域或者函数内,其他块(if, for)都会报错。但是函数表达式就没有这样的限制,究其原因,还是因为变量提升。

1
2
3
4
5
6
// 如下代码ES5中都会报错

if(1){
function a() {……}
// 报错,ES6中不报错,是因为ES6支持了块作用域,所以在块作用域中声明函数是合理的。
}

关于递归

在使用函数声明进行递归调用时,如果操作不当,可能会发生错误:

1
2
3
4
5
6
7
8
9
10
11
function factorial(num){ 
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错!

虽然可以在函数声明时使用arguments.callee解决这个问题,但是在严格模式下,不能通过脚本访问arguments.callee,访问这个属性会导致错误,所以,我们可以用函数表达式来解决这个问题:

1
2
3
4
5
6
7
var factorial = (function f(num){ 
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});

以上代码创建了一个名为 f()的命名函数表达式,然后将它赋值给变量 factorial。即便把函数 赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和 非严格模式下都行得通。

闭包

闭包这个概念总是让一部分开发人员摸不着头脑,不少开发人员也会搞混匿名函数与闭包的概念,因此经常混用。我们先来看一下高程是怎么解释闭包的概念的:

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数

我们先来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createComparisonFunction(propertyName) {

return function(object1, object2){

var value1 = object1[propertyName];
var value2 = object2[propertyName];

if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}

在这个例子中,value1 和 value2 是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量 propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量 propertyName 。之所以还能够访问这个变量,是因为内部函数的作用域链中包含 createComparisonFunction()的作用域。

而为了理解清楚闭包的原理,我们有必要理解清楚作用域与作用域链的问题

作用域与作用域链

我们先理清几个概念:

  • 执行环境(执行上下文)
  • 执行环境栈(执行上下文栈)
  • 变量对象
  • 活动对象
  • 作用域链
执行环境(执行上下文)

每当浏览器在转到可执行代码的时候,就会创造一个执行上下文:

全局代码:

全局代码不包含任何函数体内的代码,这个执行环境是默认的执行环境,一旦代码被载入,引擎最先进入的就是这个环境

函数代码:

任何一个函数体内的代码都是函数代码,需要注意的是,具体的函数代码不会包含该函数内部函数的代码

执行环境栈(执行上下文栈)

我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(i) {
if (i < 0) return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(2);

// 输出:

// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2

由于js是单线程的,也就是说在同一时间内只会执行一行代码或者说一个函数,其他的后续代码会被放入执行栈中排队

当浏览器首次载入你的脚本,它将默认进入全局执行上下文。如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。

如果你调用当前函数内部的其他函数,相同的事情会在此上演。代码的执行流程进入内部函数,创建一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。

变量对象

JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字,对象属性的值对应它们的值。但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到的

活动对象

有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了,此时变量对象就变成了活动对象,活动对象在函数执行完毕后就会被销毁(闭包函数所引用的函数活动对象除外)

作用域链

我们先看高程上的一个概念:

当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。 然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
function compare(value1, value2){ 
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}

var result = compare(5, 10);

以上代码先定义了一个compare函数,然后再全局作用域调用了它。当第一次调用compare()时,会创建一个包含this、arguments、valuel1 和value2的活动对象。全局执行环境的变量对象,其中又包含有:this、result、和compare。compare函数执行时,有这样一个作用域链,其中compare()自身的活动对象处于第一位,全局执行环境的变量对象处于第二位:

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建 compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用 compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中 compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

内存泄漏问题

由于闭包在使用时会一直引用作用域链中的活动对象,如果不及时进行无用闭包的内存释放,同时又大量使用了闭包的话,就有可能造成内存泄漏,虽然大多数现代浏览器会引擎会尝试回收被闭包占用的内存,但是毕竟也还是会有回收不到的情况,况且还有一些比较蠢的浏览器是不会进行回收的。所以在使用闭包时需要程序员考虑是否必须使用闭包,是否有可以替代的方法,同时使用闭包过后需要及时进行内存回收。

事件

事件流

事件流描述的是从页面中接收事件的顺序

需要注意的是,事件流有截然相反的两种概念,一个是事件冒泡流,一个是事件捕获流

事件冒泡

事件冒泡流的事件首先在具体事件上发生,之后事件沿DOM树向上传播,在每一级节点上都会发生,直至传播到document对象:

事件捕获

事件捕获与事件冒泡截然相反,事件首先在document对象上发生,然后沿DOM树依次向下,一直传播到事件目标:

由于老版本的浏览器不支持事件捕获,所以在兼容性上,事件冒泡要优于事件簿或

DOM事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获、处于目标、事件冒泡

首先发生的是事件捕获,为事件的截取提供了机会,然后是目标事件发生,最后一个阶段是冒泡。需要注意的是,规范规定处于目标这个阶段被看做事件冒泡的一部分,在事件捕获阶段不会捕获到处于目标这个层级。但是各个浏览器的高版本都会在捕获阶段触发事件对象上的事件。结果就是有两次机会来处理目标事件。

事件处理程序

DOM0级事件处理程序

通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这 种为事件处理程序赋值的方法是在第四代 Web 浏览器中出现的,而且至今仍然为所有现代浏览器所支 持。原因一是简单,二是具有跨浏览器的优势。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。

每个元素(包括 window 和 document)都有自己的事件处理程序属性,这些属性通常全部小写, 例如 onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:

1
2
3
4
var btn = document.getElementById("myBtn"); 
btn.onclick = function(){
alert("Clicked");
};

使用 DOM0 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在 元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。

1
2
3
4
var btn = document.getElementById("myBtn"); 
btn.onclick = function(){
alert(this.id); //"myBtn"
};

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理

DOM2级事件处理程序

“DOM2 级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener() 和 removeEventListener()。所有 DOM 节点中都包含这两个方法,并且它们都接受 3 个参数:要处 理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获 阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。

要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:

1
2
3
4
5
6
7
8
9
var btn = document.getElementById("myBtn"); 

btn.addEventListener("click", function(){
alert(this.id);
}, false);

btn.addEventListener("click", function(){
alert("Hello world!");
}, false);

使用DOM2级事件的好处是可以同时添加多个事件,触发顺序为定义时的顺序

通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除;移 除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener()添加的匿名函数将无法移除,例如上面例子中的两个事件

1
2
3
4
5
6
7
var btn = document.getElementById("myBtn"); 
var handler = function(){
alert(this.id);
};

btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false); //有效!

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览 器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需 要,我们不建议在事件捕获阶段注册事件处理程序。

IE事件处理程序

众所周知,IE有毒,所以在事件处理上也有体现,IE 实现了与 DOM 中类似的两个方法:attachEvent()和 detachEvent()。这两个方法接受相同 的两个参数:事件处理程序名称与事件处理程序函数。由于 IE8 及更早版本只支持事件冒泡,所以通过 attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

要使用 attachEvent()为按钮添加事件处理程序,可以使用以下代码:

1
2
3
4
5
6
7
8
var btn = document.getElementById("myBtn"); 
btn.attachEvent("onclick", function(){
alert("Clicked");
});

btn.attachEvent("onclick", function(){
alert("Hello world!");
});

这里需要注意几个问题:

  • attachEvent()的第一个参数是”onclick”,而非 DOM 的 addEventListener()方法中 的 “click”
  • 在使用 DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此 this 分别指向 当前元素 和 window。

与 addEventListener()类似,attachEvent()方法也可以用来为一个元素添加多个事件处理程序,同样的,也无法取消一个匿名函数的事件

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的 信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件 对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有 浏览器都支持 event 对象,但支持方式不同。

DOM事件对象

兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。无论指定事件处理程序时使用什 么方法(DOM0 级或 DOM2 级),都会传入 event 对象。来看下面的例子。

1
2
3
4
var btn = document.getElementById("myBtn"); 
btn.onclick = function(event){
alert(event.type); //"click"
};

在通过 HTML 特性指定事件处理程序时,变量 event 中保存着 event 对象。请看下面的例子。

1
<input type="button" value="Click Me" onclick="alert(event.type)"/>

下图中列出了event对象的所有属性和说明

其中需要重点区别三个属性

  • currentTarget属性指的是事件处理程序正在处理的那个元素,即处理事件的元素,this将始终指向currentTarget
  • target是指事件的目标,例如被点击的元素

我们来看两个例子:

1
2
3
4
5
6
var btn = document.getElementById("myBtn"); 

btn.onclick = function(event){
alert(event.currentTarget === this); //true
alert(event.target === this); //true
};

如果直接将事件处理程序指定给了目标元素,则 this、currentTarget 和 target 包含相同的值。

1
2
3
4
5
document.body.onclick = function(event){ 
alert(event.currentTarget === document.body); //true
alert(this === document.body); //true
alert(event.target === document.getElementById("myBtn")); //true
};

如果事件处理程序存在于按钮的父节点中(例如 document.body),那么这些值是不相同的。当单击这个例子中的按钮时,this 和 currentTarget 都等于 document.body,因为事件处理程 序是注册到这个元素上的。然而,target 元素却等于按钮元素,因为它是 click 事件真正的目标。由 于按钮上并没有注册事件处理程序,结果 click 事件就冒泡到了 document.body,在那里事件才得到了处理。

IE中的事件对象

在IE中使用DOM0级方法定义时,event对象作为window对象的一个属性存在:

1
2
3
4
5
6
var btn = document.getElementById("myBtn"); 

btn.onclick = function(){
var event = window.event;
alert(event.type); //"click"
};

如果是通过 HTML特性指定的事件处理程序,则与之前的DOM事件相同

下表列出了IE的event属性与方法:

因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为 this 会始终等于事件目 标。故而,最好还是使用 event.srcElement 比较保险。例如:

1
2
3
4
5
6
7
8
var btn = document.getElementById("myBtn"); 
btn.onclick = function(){
alert(window.event.srcElement === this); //true
};

btn.attachEvent("onclick", function(event){
alert(event.srcElement === this); //false
});

注意 — 由于 IE 不支持事件捕获,因而只能取消事件冒泡

事件类型

UI事件

UI 事件指的是那些不一定与用户操作有关的事件。这些事件在 DOM 规范出现之前,都是以这种或 那种形式存在的,而在 DOM 规范中保留是为了向后兼容。现有的 UI 事件如下:

  • DOMActivate:表示元素已经被用户操作(通过鼠标或键盘)激活。这个事件在 DOM3 级事件中被废弃,但 Firefox 2+和 Chrome 支持它。考虑到不同浏览器实现的差异,不建议使用这个事件
  • load:当页面完全加载后在 window 上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>元素上面触发,或者当嵌入的内容加载完毕时在<object>元素上面触发
  • unload:当页面完全卸载后在 window 上面触发,当所有框架都卸载后在框架集上面触发,或 者当嵌入的内容卸载完毕后在<object>元素上面触发
  • abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上面触发
  • error:当发生 JavaScript 错误时在 window 上面触发,当无法加载图像时在<img>元素上面触发,当无法加载嵌入内容时在<object>元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发
  • select:当用户选择文本框(<input><texterea>)中的一或多个字符时触发
  • resize:当窗口或框架的大小变化时在 window 或框架上面触发
  • scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。<body>元素中包含所加载页面的滚动条

焦点事件

焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与 document.hasFocus()方法及 document.activeElement 属性配合,可以知晓用户在页面上的行踪。

  • blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它
  • DOMFocusIn:在元素获得焦点时触发。这个事件与 HTML 事件 focus 等价,但它冒泡。只有 Opera 支持这个事件。DOM3 级事件废弃了 DOMFocusIn,选择了 focusin
  • DOMFocusOut:在元素失去焦点时触发。这个事件是 HTML 事件 blur 的通用版本。只有 Opera 支持这个事件。DOM3 级事件废弃了 DOMFocusOut,选择了 focusout
  • focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它
  • focusin:在元素获得焦点时触发。这个事件与 HTML 事件 focus 等价,但它冒泡。支持这个事件的浏览器有IE5.5+、Safari 5.1+、Opera 11.5+和 Chrome
  • focusout:在元素失去焦点时触发。这个事件是 HTML 事件 blur 的通用版本。支持这个事件 的浏览器有 IE5.5+、Safari 5.1+、Opera 11.5+和 Chrome

这一类事件中最主要的两个是 focus 和 blur,它们都是 JavaScript 早期就得到所有浏览器支持的 事件。这些事件的最大问题是它们不冒泡。因此,IE 的 focusin 和 focusout 与 Opera 的 DOMFocusIn 和 DOMFocusOut 才会发生重叠。IE 的方式最后被 DOM3 级事件采纳为标准方式。 当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:

  1. focusout 在失去焦点的元素上触发;
  2. focusin 在获得焦点的元素上触发;
  3. blur 在失去焦点的元素上触发;
  4. DOMFocusOut 在失去焦点的元素上触发;
  5. focus 在获得焦点的元素上触发;
  6. DOMFocusIn 在获得焦点的元素上触发。

鼠标与滚轮事件

鼠标事件是 Web 开发中最常用的一类事件,毕竟鼠标还是最主要的定位设备。DOM3 级事件中定 义了 9 个鼠标事件,简介如下:

  • click:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。这一点对确保 易访问性很重要,意味着 onclick 事件处理程序既可以通过键盘也可以通过鼠标执行
  • dblclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不 是 DOM2 级事件规范中规定的,但鉴于它得到了广泛支持,所以 DOM3 级事件将其纳入了标准
  • mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件
  • mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且 在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它 纳入了规范。IE、Firefox 9+和 Opera 支持这个事件
  • mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且 在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它 纳入了规范。IE、Firefox 9+和 Opera 支持这个事件
  • mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件
  • mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另 一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件
  • mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件
  • mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件

只有在同一个元素上相继触发 mousedown 和 mouseup 事件, 才会触发 click 事件;如果 mousedown 或 mouseup 中的一个被取消,就不会触发 click 事件。类似地,只有触发两次 click 事 件,才会触发一次 dblclick 事件。如果有代码阻止了连续两次触发 click 事件(可能是直接取消 click 事件,也可能通过取消 mousedown 或 mouseup 间接实现),那么就不会触发 dblclick 事件了。这 4 个事件触发的顺序始终如下:

  1. mousedown
  2. mouseup
  3. click
  4. mousedown
  5. mouseup
  6. click
  7. dblclick

显然,click 和 dblclick 事件都会依赖于其他先行事件的触发;而 mousedown 和 mouseup 则 不受其他事件的影响。 IE8 及之前版本中的实现有一个小 bug,因此在双击事件中,会跳过第二个 mousedown 和 click 事件,其顺序如下:

  1. mousedown
  2. mouseup
  3. click
  4. mouseup
  5. dblclick

IE9 修复了这个 bug,之后顺序就正确了

键盘与文本事件

“DOM3 级事件”为键盘事件制定了规范,IE9 率先完全实现了该规范。其他浏览器也在着手实现这 一标准,但仍然有很多遗留的问题。 有 3 个键盘事件,简述如下:

  • keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件
  • keypress:当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。 按下 Esc 键也会触发这个事件。Safari 3.1 之前的版本也会在用户按下非字符键时触发 keypress 事件
  • keyup:当用户释放键盘上的键时触发

只有一个文本事件:textInput。这个事件是对 keypress 的补充,用意是在将文本显示给用户之 前更容易拦截文本。在文本插入文本框之前会触发 textInput 事件。

复合事件

复合事件(composition event)是 DOM3 级事件中新添加的一类事件,用于处理 IME 的输入序列。 IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符。例如,使用拉 丁文键盘的用户通过 IME 照样能输入日文字符。IME 通常需要同时按住多个键,但最终只输入一个字 符。复合事件就是针对检测和处理这种输入而设计的。有以下三种复合事件:

  • compositionstart:在 IME 的文本复合系统打开时触发,表示要开始输入了
  • compositionupdate:在向输入字段中插入新字符时触发
  • compositionend:在 IME 的文本复合系统关闭时触发,表示返回正常键盘输入状态。 复合事件与文本事件在很多方面都很相似。在触发复合事件时,目标是接收文本的输入字段。但它比文本事件的事件对象多一个属性 data,其中包含以下几个值中的一个
    • 如果在 compositionstart 事件发生时访问,包含正在编辑的文本(例如,已经选中的需要马 上替换的文本
    • 如果在 compositionupdate 事件发生时访问,包含正插入的新字符
    • 如果在 compositionend 事件发生时访问,包含此次输入会话中插入的所有字符。 与文本事件一样,必要时可以利用复合事件来筛选输入

变动事件

DOM2 级的变动(mutation)事件能在 DOM 中的某一部分发生变化时给出提示。变动事件是为 XML 或 HTML DOM 设计的,并不特定于某种语言。DOM2 级定义了如下变动事件:

  • DOMSubtreeModified:在 DOM 结构中发生任何变化时触发。这个事件在其他任何事件触发 后都会触发
  • DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发
  • DOMNodeRemoved:在节点从其父节点中被移除时触发
  • DOMNodeInsertedIntoDocument:在一个节点被直接插入文档或通过子树间接插入文档之后 触发。这个事件在 DOMNodeInserted 之后触发
  • DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移 除之前触发。这个事件在 DOMNodeRemoved 之后触发
  • DOMAttrModified:在特性被修改之后触发
  • DOMCharacterDataModified:在文本节点的值发生变化时触发

HTML5 事件(部分)

  • contextmenu 事件

    Windows 95 在 PC 中引入了上下文菜单的概念,即通过单击鼠标右键可以调出上下文菜单。不久, 这个概念也被引入了 Web 领域。为了实现上下文菜单,开发人员面临的主要问题是如何确定应该显示 上下文菜单(在 Windows 中,是右键单击;在 Mac 中,是 Ctrl+单击),以及如何屏蔽与该操作关联的 默认上下文菜单。为解决这个问题,就出现了 contextmenu 这个事件,用以表示何时应该显示上下文 菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单

  • DOMContentLoaded 事件

    window 的 load 事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要 加载的外部资源过多而颇费周折。而 DOMContentLoaded 事件则在形成完整的 DOM 树之后就会触发, 不理会图像、JavaScript 文件、CSS 文件或其他资源是否已经下载完毕。 与 load 事件不同,DOMContentLoaded 支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互

  • readystatechange 事件

    IE 为 DOM 文档中的某些部分提供了 readystatechange 事件。这个事件的目的是提供与文档或 元素的加载状态有关的信息,但这个事件的行为有时候也很难预料。支持 readystatechange 事件的 每个对象都有一个 readyState 属性,可能包含下列 5 个值中的一个

    • uninitialized(未初始化):对象存在但尚未初始化
    • loading(正在加载):对象正在加载数据
    • loaded(加载完毕):对象加载数据完成
    • interactive(交互):可以操作对象了,但还没有完全加载
    • complete(完成):对象已经加载完毕

事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事 件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就 是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事 件处理程序。以下面的 HTML 代码为例:

1
2
3
4
5
<ul id="myLinks"> 
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>

其中包含 3 个被单击后会执行操作的列表项。按照传统的做法,需要像下面这样为它们添加 3 个事件处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var item1 = document.getElementById("goSomewhere"); 
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");

EventUtil.addHandler(item1, "click", function(event){
location.href = "http://www.wrox.com";
});

EventUtil.addHandler(item2, "click", function(event){
document.title = "I changed the document's title";
});

EventUtil.addHandler(item3, "click", function(event){
alert("hi");
});

如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不 清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var list = document.getElementById("myLinks");

EventUtil.addHandler(list, "click", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);

switch(target.id){
case "doSomething":
document.title = "I changed the document's title";
break;

case "goSomewhere":
location.href = "http://www.wrox.com";
break;

case "sayHi":
alert("hi");
break;
}
});

如果可行的话,也可以考虑为 document 对象添加一个事件处理程序,用以处理页面上发生的某种 特定类型的事件。这样做与采取传统的做法相比具有如下优点:

  • document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序 (无需等待 DOMContentLoaded 或 load 事件)。换句话说,只要可单击的元素呈现在页面上,就可以立即具备适当的功能
  • 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的 DOM 引用更少,所花的时间也更少
  • 整个页面占用的内存空间更少,能够提升整体性能