包含在
在这个例子中,外部文件 example.js 将被加载到当前页面中。外部文件只须包含通常要放在开始的
之间的那些 JavaScript 代码即可。与解析嵌入式 JavaScript 代码一样,在解析外部 JavaScript 文件(包括下载该文件)时,页面的处理也会暂时停止。如果是在 XHTML 文档中,也可以省略前面示例代码中结束的标签,例如:
<script type="text/javascript" src="example.js" />
**需要注意的是,带有 src 属性的
标签之间再包含额外的 JavaScript 代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。**
另外,通过
这样,位于外部域中的代码也会被加载和解析,就像这些代码位于加载它们的页面中一样。利用这一点就可以在必要时通过不同的域来提供 JavaScript 文件。不过,在访问自己不能控制的服务器上的JavaScript 文件时则要多加小心。如果不幸遇到了怀有恶意的程序员,那他们随时都可能替换该文件中的代码。因此,如果想包含来自不同域的代码,则要么你是那个域的所有者,要么那个域的所有者值得信赖。
按照传统的做法,所有
这个
<input type="button" value="Click Me" onclick="showMessage()" />
这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量 event,也就是事件对象:
<!-- 输出 "click" -->
<input type="button" value="Click Me" onclick="alert(event.type)">
通过 event 变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部, this 值等于事件的目标元素,例如:
<!-- 输出 "Click Me" -->
<input type="button" value="Click Me" onclick="alert(this.value)">
关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问 document 及该元素本身的成员。这个函数使用 with 像下面这样扩展作用域:
function(){
with(document){
with(this){
//元素属性值
}
}
}
如此一来,事件处理程序要访问自己的属性就简单多了。下面这行代码与前面的例子效果相同:
<!-- 输出 "Click Me" -->
<input type="button" value="Click Me" onclick="alert(value)">
如果当前元素是一个表单输入元素,则作用域中还会包含访问表单元素(父元素)的入口,这个函数就变成了如下所示:
function(){
with(document){
with(this.form){
with(this){
//元素属性值
}
}
}
}
实际上,这样扩展作用域的方式,无非就是想让事件处理程序无需引用表单元素就能访问其他表单字段。例如:
<form method="post">
<input type="text" name="username" value="">
<input type="button" value="Echo Username" onclick="alert(username.value)">
</form>
在这个例子中,单击按钮会显示文本框中的文本。值得注意的是,这里直接引用了 username 元素。
不过,在 HTML 中指定事件处理程序有两个缺点。首先,存在一个时差问题。因为用户可能会在HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行件。以前面的例子来说明,假设 showMessage()函数是在按钮下方、页面的最底部定义的。如果用户在页面解析 showMessage()函数之前就单击了按钮,就会引发错误。为此,很多 HTML 事件处理程序都会被封装在一个 try-catch 块中,以便错误不会浮出水面,如下面的例子所示:
<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}">
另一个缺点是,这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同 JavaScript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。
通过 HTML 指定事件处理程序的最后一个缺点是 HTML 与 JavaScript 代码紧密耦合。如果要更换事件处理程序,就要改动两个地方: HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的原因所在。
通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。原因一是简单,二是具有跨浏览器的优势。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素(包括 window 和 document)都有自己的事件处理程序属性,这些属性通常全部小写,例如 onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert("Clicked");
};
使用 DOM0 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert(this.id); //"myBtn"
};
也可以删除通过 DOM0 级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 即可:
btn.onclick = null; //删除事件处理程序
将事件处理程序设置为 null 之后,再单击按钮将不会有任何动作发生。
“DOM2 级事件” 定义了两个方法,用于处理指定和删除事件处理程序的操作: addEventListener()和 removeEventListener()。所有 DOM 节点中都包含这两个方法,并且它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。
要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
btn.addEventListener("click", function(){
alert("Hello world!");
}, false);
这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的 ID,其次会显示"Hello world!"消息。
通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener()添加的匿名函数将无法移除,如下面的例子所示。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
//这里省略了其他代码
btn.removeEventListener("click", function(){ //没有用!
alert(this.id);
}, false);
在这个例子中,我们使用 addEventListener()添加了一个事件处理程序。虽然调用 removeEventListener()时看似使用了相同的参数,但实际上,第二个参数与传入 addEventListener()中的那一个是完全不同的函数。而传入 removeEventListener()中的事件处理程序函数必须与传入addEventListener()中的相同,如下面的例子所示。
var btn = document.getElementById("myBtn");
var handler = function(){
alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!
大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,我们不建议在事件捕获阶段注册事件处理程序。
IE 实现了与 DOM 中类似的两个方法: attachEvent()和 detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。
要使用 attachEvent()为按钮添加一个事件处理程序,可以使用以下代码。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert("Clicked");
});
在 IE 中使用 attachEvent()与使用 DOM0 级方法的主要区别在于事件处理程序的作用域。在使用 DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert(this === window); //true
});
与 addEventListener()类似, attachEvent()方法也可以用来为一个元素添加多个事件处理程
序。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert("Clicked");
});
btn.attachEvent("onclick", function(){
alert("Hello world!");
});
这里调用了两次 attachEvent(),为同一个按钮添加了两个不同的事件处理程序。不过,与 DOM方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。单击这个例子中的按钮,首先看到的是"Hello world!",然后才是"Clicked"。
为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的 JavaScript 库,还有一些开发人员会自己开发最合适的事件处理的方法。自己编写代码其实也不难,只要恰当地使用能力检测即可(能力检测在第 9 章介绍过)。要保证处理事件的代码能在大多数浏览器下一致地运行,只需关注冒泡阶段。
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;
}
}
};
在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。
兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0 级或 DOM2 级),都会传入 event 对象。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
alert(event.type); //"click"
};
btn.addEventListener("click", function(event){
alert(event.type); //"click"
}, false);
与访问 DOM 中的 event 对象不同,要访问 IE 中的 event 对象有几种不同的方式,取决于指定事件处理程序的方法。在使用 DOM0 级方法添加事件处理程序时, event 对象作为 window 对象的一个属性存在。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
var event = window.event;
alert(event.type); //"click"
};
如果事件处理程序是使用 attachEvent()添加的,那么就会有一个 event 对象作为参数被传入事件处理程序函数中,如下所示。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
alert(event.type); //"click"
});
在像这样使用 attachEvent()的情况下,也可以通过 window 对象来访问 event 对象,就像使用DOM0 级方法时一样。如果是通过 HTML 特性指定的事件处理程序,那么还可以通过一个名叫 event 的变量来访问 event对象(与 DOM 中的事件模型相同)。再看一个例子。
<input type="button" value="Click Me" onclick="alert(event.type)">
虽然 DOM 和 IE 中的 event 对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。
var EventUtil = {
addHandler: function(element, type, handler){
//省略的代码
},
getEvent: function(event){
return event ? event : window.event;
},
getTarget: function(event){
return event.target || event.srcElement;
},
preventDefault: function(event){
if (event.preventDefault){
event.preventDefault();
} else {
event.returnValue = false;
}
},
removeHandler: function(element, type, handler){
//省略的代码
},
stopPropagation: function(event){
if (event.stopPropagation){
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
Web 浏览器中可能发生的事件有很多类型。如前所述,不同的事件类型具有不同的信息,而“DOM3级事件”规定了以下几类事件。
UI 事件指的是那些不一定与用户操作有关的事件。这些事件在 DOM 规范出现之前,都是以这种或那种形式存在的,而在 DOM 规范中保留是为了向后兼容。现有的 UI 事件如下。
load事件
JavaScript 中最常用的一个事件就是 load。当页面完全加载后(包括所有图像、 JavaScript 文件、CSS 文件等外部资源),就会触发 window 上面的 load 事件。
unload事件
与 load 事件对应的是 unload 事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生 unload 事件。而利用这个事件最多的情况是清除引用,以避免内存泄漏。
resize事件
当浏览器窗口被调整到一个新的高度或宽度时,就会触发 resize 事件。这个事件在 window(窗口)上面触发,因此可以通过 JavaScript 或者
元素中的 onresize 特性来指定事件处理程序。scroll事件
虽然 scroll 事件是在 window 对象上发生的,但它实际表示的则是页面中相应元素的变化。在混杂模式下,可以通过
元素的 scrollLeft 和 scrollTop 来监控到这一变化;而在标准模式下,除 Safari 之外的所有浏览器都会通过元素来反映这一变化焦点事件
焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与 document.hasFocus()方法及
document.activeElement 属性配合,可以知晓用户在页面上的行踪。有以下 6 个焦点事件。
当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:
(1) focusout 在失去焦点的元素上触发;
(2) focusin 在获得焦点的元素上触发;
(3) blur 在失去焦点的元素上触发;
(4) DOMFocusOut 在失去焦点的元素上触发;
(5) focus 在获得焦点的元素上触发;
(6) DOMFocusIn 在获得焦点的元素上触发。
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,之后顺序就正确了。
鼠标滚动事件
IE 6.0 首先实现了 mousewheel 事件。此后, Opera、 Chrome 和 Safari 也都实现了这个事件。当用户通过鼠标滚轮与页面交互、在垂直方向上滚动页面时(无论向上还是向下),就会触发 mousewheel事件。这个事件可以在任何元素上面触发,最终会冒泡到 document(IE8)或 window(IE9、 Opera、Chrome 及 Safari)对象。与 mousewheel 事件对应的 event 对象除包含鼠标事件的所有标准信息外,还包含一个特殊的 wheelDelta 属性。当用户向前滚动鼠标滚轮时, wheelDelta 是 120 的倍数;当用户向后滚动鼠标滚轮时, wheelDelta 是-120 的倍数。
触摸设备
iOS 和 Android 设备的实现非常特别,因为这些设备没有鼠标。在面向 iPhone 和 iPod 中的 Safari开发时,要记住以下几点。
用户在使用键盘时会触发键盘事件。有 3 个键盘事件,简述如下。
keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。
keypress:当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。
按下 Esc 键也会触发这个事件。Safari 3.1 之前的版本也会在用户按下非字符键时触发 keypress事件。
keyup:当用户释放键盘上的键时触发。
只有一个文本事件: textInput。这个事件是对 keypress 的补充,用意是在将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发 textInput 事件。
在用户按了一下键盘上的字符键时,首先会触发 keydown 事件,然后紧跟着是 keypress 事件,最后会触发 keyup 事件。 其中, keydown 和 keypress 都是在文本框发生变化之前被触发的;而 keyup事件则是在文本框已经发生变化之后被触发的。如果用户按下了一个字符键不放,就会重复触发keydown 和 keypress 事件,直到用户松开该键为止。
如果用户按下的是一个非字符键,那么首先会触发 keydown 事件,然后就是 keyup 事件。如果按住这个非字符键不放,那么就会一直重复触发 keydown 事件,直到用户松开这个键,此时会触发 keyup事件。
复合事件(composition event)是 DOM3 级事件中新添加的一类事件,用于处理 IME 的输入序列。IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符。例如,使用拉丁文键盘的用户通过 IME 照样能输入日文字符。 IME 通常需要同时按住多个键,但最终只输入一个字符。复合事件就是针对检测和处理这种输入而设计的。有以下三种复合事件。
DOM2 级的变动(mutation)事件能在 DOM 中的某一部分发生变化时给出提示。变动事件是为 XML
或 HTML DOM 设计的,并不特定于某种语言。 DOM2 级定义了如下变动事件。
contextmenu事件
Windows 95 在 PC 中引入了上下文菜单的概念,即通过单击鼠标右键可以调出上下文菜单。不久,这个概念也被引入了 Web 领域。为了实现上下文菜单,开发人员面临的主要问题是如何确定应该显示上下文菜单(在 Windows 中,是右键单击;在 Mac 中,是 Ctrl+单击),以及如何屏蔽与该操作关联的默认上下文菜单。为解决这个问题,就出现了 contextmenu 这个事件,用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单。
beforeunload事件
之所以有发生在 window 对象上的 beforeunload 事件,是为了让开发人员有可能在页面卸载前阻止这一操作。这个事件会在浏览器卸载页面之前触发,可以通过它来取消卸载并继续使用原有页面。但是,不能彻底取消这个事件,因为那就相当于让用户无法离开当前页面了。为此,这个事件的意图是将控制权交给用户。显示的消息会告知用户页面行将被卸载(正因为如此才会显示这个消息),询问用户是否真的要关闭页面,还是希望继续留下来。
DOMContentLoaded事件
如前所述, window 的 load 事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而颇费周折。而 DOMContentLoaded 事件则在形成完整的 DOM 树之后就会触发,不理会图像、 JavaScript 文件、 CSS 文件或其他资源是否已经下载完毕。与 load 事件不同,DOMContentLoaded 支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互。
readystatechange事件
IE 为 DOM 文档中的某些部分提供了 readystatechange 事件。这个事件的目的是提供与文档或元素的加载状态有关的信息,但这个事件的行为有时候也很难预料。支持 readystatechange 事件的每个对象都有一个 readyState 属性,可能包含下列 5 个值中的一个。
pageshow和pagehide事件
Firefox 和 Opera 有一个特性,名叫“往返缓存”(back-forward cache,或 bfcache),可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。这个缓存中不仅保存着页面数据,还保存了 DOM 和 JavaScript 的状态;实际上是将整个页面都保存在了内存里。如果页面位于 bfcache 中,那么再次打开该页面时就不会触发 load 事件。尽管由于内存中保存了整个页面的状态,不触发 load 事件也不应该会导致什么问题,但为了更形象地说明 bfcache 的行为, Firefox 还是提供了一些新事件。
第一个事件就是 pageshow,这个事件在页面显示时触发,无论该页面是否来自 bfcache。在重新加载的页面中, pageshow 会在 load 事件触发后触发;而对于 bfcache 中的页面, pageshow 会在页面状态完全恢复的那一刻触发。
hashchange事件
HTML5 新增了 hashchange 事件,以便在 URL 的参数列表(及 URL 中“#”号后面的所有字符串)发生变化时通知开发人员。之所以新增这个事件,是因为在 Ajax 应用中,开发人员经常要利用 URL 参数列表来保存状态或导航信息。
必须要把 hashchange 事件处理程序添加给 window 对象,然后 URL 参数列表只要变化就会调用它。此时的 event 对象应该额外包含两个属性: oldURL 和 newURL。这两个属性分别保存着参数列表变化前后的完整 URL。例如:
EventUtil.addHandler(window, "hashchange", function(event){
alert("Old URL: " + event.oldURL + "\nNew URL: " + event.newURL);
});
orientationchange事件
苹果公司为移动 Safari 中添加了 orientationchange 事件,以便开发人员能够确定用户何时将设备由横向查看模式切换为纵向查看模式。
MozOrientation事件
Firefox 3.6 为检测设备的方向引入了一个名为 MozOrientation 的新事件。(前缀 Moz 表示这是特定于浏览器开发商的事件,不是标准事件。)当设备的加速计检测到设备方向改变时,就会触发这个事件。
deviceorientation事件
本质上,DeviceOrientation Event 规范定义的 deviceorientation 事件与 MozOrientation 事件类似。它也是在加速计检测到设备方向变化时在 window 对象上触发,而且具有与 MozOrientation 事件相同的支持限制。
devicemotion事件
DeviceOrientation Event 规范还定义了一个 devicemotion 事件。这个事件是要告诉开发人员设备什么时候移动,而不仅仅是设备方向如何改变。例如,通过 devicemotion 能够检测到设备是不是正在往下掉,或者是不是被走着的人拿在手里。
触摸事件
包含 iOS 2.0 软件的 iPhone 3G 发布时,也包含了一个新版本的 Safari 浏览器。这款新的移动 Safari提供了一些与触摸(touch)操作相关的新事件。后来, Android 上的浏览器也实现了相同的事件。触摸事件会在用户手指放在屏幕上面时、在屏幕上滑动时或从屏幕上移开时触发。
手势事件
iOS 2.0 中的 Safari 还引入了一组手势事件。当两个手指触摸屏幕时就会产生手势,手势通常会改变显示项的大小,或者旋转显示项。
首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的角度出发,还是有一些方法能够提升性能的。
对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如, click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
以下面的 HTML 代码为例。
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
其中包含 3 个被单击后会执行操作的列表项。按照传统的做法,需要它们添加 3 个事件处理程序。
如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示。
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 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待 DOMContentLoaded 或 load 事件)。换句话说,只要可单击的元素呈现在页面上,就可以立即具备适当的功能。
在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的 DOM 引用更少,所花的时间也更少。
整个页面占用的内存空间更少,能够提升整体性能。
最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。虽然 mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。(因为当鼠标从一个元素移到其子节点时,或者当鼠标移出该元素时,都会触发 mouseout 事件。)
在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时。这可能是通过纯粹的 DOM 操作,例如使用 removeChild()和 replaceChild()方法,但更多地是发生在使用 innerHTML 替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。
<div id="myDiv">
<input type="button" value="Click Me" id="myBtn">
</div>
在
<div id="myDiv">
<input type="button" value="Click Me" id="myBtn">
</div>
在测试 Web 应用程序,模拟触发事件是一种极其有用的技术。
可以在 document 对象上使用 createEvent()方法创建 event 对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在 DOM2 级中,所有这些字符串都使用英文复数形式,而在 DOM3级中都变成了单数。这个字符串可以是下列几字符串之一。
JavaScript 最初的一个应用,就是分担服务器处理表单的责任,打破处处依赖服务器的局面。尽管目前的 Web 和 JavaScript 已经有了长足的发展,但 Web 表单的变化并不明显。由于 Web 表单没有为许多常见任务提供现成的解决手段,很多开发人员不仅会在验证表单时使用 JavaScirpt,而且还增强了一些标准表单控件的默认行为。
在 HTML 中,表单是由
var form = document.getElementById("form1");
其次,通过 document.forms 可以取得页面中所有的表单。在这个集合中,可以通过数值索引或name 值来取得特定的表单,如下面的例子所示。
var firstForm = document.forms[0]; //取得页面中的第一个表单
var myForm = document.forms["form2"]; //取得页面中名称为"form2"的表单
用户单击提交按钮或图像按钮时,就会提交表单。使用或
<!-- 通用提交按钮 -->
<input type="submit" value="Submit Form">
<!-- 自定义提交按钮 -->
<button type="submit">Submit Form</button>
<!-- 图像按钮 -->
<input type="image" src="graphic.gif">
只要表单中存在上面列出的任何一种按钮,那么在相应表单控件拥有焦点的情况下,按回车键就可以提交该表单。(textarea 是一个例外,在文本区中回车会换行。)如果表单里没有提交按钮,按回车键不会提交表单。
以这种方式提交表单时,浏览器会在将请求发送给服务器之前触发 submit 事件。这样,我们就有机会验证表单数据,并据以决定是否允许表单提交。阻止这个事件的默认行为就可以取消表单提交。例如,下列代码会阻止表单提交。
var form = document.getElementById("myForm");
EventUtil.addHandler(form, "submit", function(event){
//取得事件对象
event = EventUtil.getEvent(event);
//阻止默认事件
EventUtil.preventDefault(event);
});
在 JavaScript 中,以编程方式调用 submit()方法也可以提交表单。而且,这种方式无需表单包含提交按钮,任何时候都可以正常提交表单。来看一个例子。
var form = document.getElementById("myForm");
//提交表单
form.submit();
在以调用 submit()方法的形式提交表单时,不会触发 submit 事件,因此要记得在调用此方法之前先验证表单数据。
在用户单击重置按钮时,表单会被重置。使用 type 特性值为"reset"的或
<!-- 通用重置按钮 -->
<input type="reset" value="Reset Form">
<!-- 自定义重置按钮 -->
<button type="reset">Reset Form</button>
这两个按钮都可以用来重置表单。在重置表单时,所有表单字段都会恢复到页面刚加载完毕时的初始值。如果某个字段的初始值为空,就会恢复为空;而带有默认值的字段,也会恢复为默认值。
共有表单字段属性
表单字段共有的属性如下。
在涉及信用卡消费时,这就是个问题:因为会导致费用翻番。为此,最常见的解决方案,就是在第一次单击后就禁用提交按钮。只要侦听 submit 事件,并在该事件发生时禁用提交按钮即可。以下就是这样一个例子。
//避免多次提交表单
EventUtil.addHandler(form, "submit", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
//取得提交按钮
var btn = target.elements["submit-btn"];
//禁用它
btn.disabled = true;
});
以上代码为表单的 submit 事件添加了一个事件处理程序。事件触发后,代码取得了提交按钮并将其 disabled 属性设置为 true。注意,不能通过 onclick 事件处理程序来实现这个功能,原因是不同浏览器之间存在“时差”:有的浏览器会在触发表单的 submit 事件之前触发 click 事件,而有的浏览器则相反。对于先触发 click 事件的浏览器,意味着会在提交发生之前禁用按钮,结果永远都不会提交表单。
共有的表单字段方法
每个表单字段都有两个方法: focus()和 blur()。其中, focus()方法用于将浏览器的焦点设置到表单字段,即激活表单字段,使其可以响应键盘事件。例如,接收到焦点的文本框会显示插入符号,随时可以接收输入。使用 focus()方法,可以将用户的注意力吸引到页面中的某个部位。例如,在页面加载完毕后,将焦点转移到表单中的第一个字段。为此,可以侦听页面的 load 事件,并在该事件发生时在表单的第一个字段上调用 focus()方法,如下面的例子所示。
EventUtil.addHandler(window, "load", function(event){
document.forms[0].elements[0].focus();
});
与 focus()方法相对的是 blur()方法,它的作用是从元素中移走焦点。在调用 blur()方法时,并不会把焦点转移到某个特定的元素上;仅仅是将焦点从调用这个方法的元素上面移走而已。
共有的表单字段事件
除了支持鼠标、键盘、更改和 HTML 事件之外,所有表单字段都支持下列 3 个事件。
对于和
才会触发 change 事件。对于
在 HTML 中,有两种方式来表现文本框:一种是使用元素的单行文本框,另一种是使用
要表现文本框,必须将元素的 type 特性设置为"text"。而通过设置 size 特性,可以指定文本框中能够显示的字符数。通过 value 特性,可以设置文本框的初始值,而 maxlength 特性则用于指定文本框可以接受的最大字符数。
在文本框获得焦点时选择其所有文本,这是一种非常常见的做法,特别是在文本框包含默认值的时候。因为这样做可以让用户不必一个一个地删除文本。
选择(select)事件
与 select()方法对应的,是一个 select 事件。在选择了文本框中的文本时,就会触发 select事件。不过,到底什么时候触发 select 事件,还会因浏览器而异。另外,在调用 select()方法时也会触发 select 事件。
取得选择的文本
虽然通过 select 事件我们可以知道用户什么时候选择了文本,但仍然不知道用户选择了什么文本。HTML5 通过一些扩展方案解决了这个问题,以便更顺利地取得选择的文本。该规范采取的办法是添加两个属性: selectionStart 和 selectionEnd。这两个属性中保存的是基于 0 的数值,表示所选择文本的范围(即文本选区开头和结尾的偏移量)。因此,要取得用户在文本框中选择的文本,可以使用如下代码。
function getSelectedText(textbox){
return textbox.value.substring(textbox.selectionStart, textbox.selectionEnd);
}
因 为 substring() 方 法 基 于 字 符 串 的 偏 移 量 执 行 操 作 , 所 以 将 selectionStart 和selectionEnd 直接传给它就可以取得选中的文本。
选择部分文本
HTML5 也 为 选 择 文 本 框 中 的 部 分 文 本 提 供 了 解 决 方 案 , 即 最 早 由 Firefox 引 入 的setSelectionRange()方法。现在除select()方法之外,所有文本框都有一个setSelectionRange()方法。这个方法接收两个参数:要选择的第一个字符的索引和要选择的最后一个字符之后的字符的索引(类似于 substring()方法的两个参数)。
屏蔽字符
有时候,我们需要用户输入的文本中包含或不包含某些字符。例如,电话号码中不能包含非数值字符。如前所述,响应向文本框中插入字符操作的是 keypress 事件。因此,可以通过阻止这个事件的默认行为来屏蔽此类字符。在极端的情况下,可以通过下列代码屏蔽所有按键操作。
EventUtil.addHandler(textbox, "keypress", function(event){
event = EventUtil.getEvent(event);
EventUtil.preventDefault(event);
});
如果只想屏蔽特定的字符,则需要检测 keypress 事件对应的字符编码,然后再决定如何响应。例如,下列代码只允许用户输入数值。
EventUtil.addHandler(textbox, "keypress", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
var charCode = EventUtil.getCharCode(event);
if (!/\d/.test(String.fromCharCode(charCode))){
EventUtil.preventDefault(event);
}
});
操作剪贴板
HTML 5 后来也把剪贴板事件纳入了规范。下列就是 6 个剪贴板事件。
使用 JavaScript 可以从多个方面增强表单字段的易用性。其中,最常见的一种方式就是在用户填写完当前字段时,自动将焦点切换到下一个字段。通常,在自动切换焦点之前,必须知道用户已经输入了既定长度的数据(例如电话号码)。
开始的 tabForward()函数是实现“自动切换焦点”的关键所在。这个函数通过比较用户输入的值与文本框的 maxlength 特性,可以确定是否已经达到最大长度。如果这两个值相等(因为浏览器最终会强制它们相等,因此用户绝不会多输入字符),则需要查找表单字段集合,直至找到下一个文本框。找到下一个文本框之后,则将焦点切换到该文本框。然后,我们把这个函数指定为每个文本框的 onkeyup事件处理程序。由于 keyup 事件会在用户输入了新字符之后触发,所以此时是检测文本框中内容长度的最佳时机。这样一来,用户在填写这个简单的表单时,就不必再通过按制表键切换表单字段和提交表单了。
为了在将表单提交到服务器之前验证数据,HTML5 新增了一些功能。
必填字段
第一种情况是在表单字段中指定了 required 属性,如下面的例子所示:
<input type="text" name="username" required>
任何标注有 required 的字段,在提交表单时都不能空着。这个属性适用于、
其他输入类型
HTML5 为元素的 type 属性又增加了几个值。这些新的类型不仅能反映数据类型的信息,而且还能提供一些默认的验证功能。其中, "email"和"url"是两个得到支持最多的类型,各浏览器也都为它们增加了定制的验证机制。
数值范围
除了"email"和"url", HTML5 还定义了另外几个输入元素。这几个元素都要求填写某种基于数字的值: "number"、 "range"、 "datetime"、 "datetime-local"、 "date"、 "month"、 "week",还有"time"。浏览器对这几个类型的支持情况并不好,因此如果真想选用的话,要特别小心。
输入模式
HTML5 为文本字段新增了 pattern 属性。这个属性的值是一个正则表达式,用于匹配文本框中的值。例如,如果只想允许在文本字段中输入数值,可以像下面的代码一样应用约束:
<input type="text" pattern="\d+" name="count">
注意,模式的开头和末尾不用加^和$符号(假定已经有了)。这两个符号表示输入的值必须从头到尾都与模式匹配。
检测有效性
使用 checkValidity()方法可以检测表单中的某个字段是否有效。所有表单字段都有个方法,如果字段的值有效,这个方法返回 true,否则返回 false。字段的值是否有效的判断依据是本节前面介绍过的那些约束。换句话说,必填字段中如果没有值就是无效的,而字段中的值与 pattern 属性不匹配也是无效的。
禁用验证
通过设置 novalidate 属性,可以告诉表单不进行验证。
<form method="post" action="signup.php" novalidate>
<!--这里插入表单元素-->
</form>
选择框是通过
选择框的 type 属性不是"select-one",就是"select-multiple",这取决于 HTML 代码中有没有 multiple 特性。选择框的 value 属性由当前选中项决定,相应规则如下。
可以使用 JavaScript 动态创建选项,并将它们添加到选择框中。添加选项的方式有很多,第一种方式就是使用如下所示的 DOM 方法。
var newOption = document.createElement("option");
newOption.appendChild(document.createTextNode("Option text"));
newOption.setAttribute("value", "Option value");
selectbox.appendChild(newOption);
var newOption = new Option("Option text", "Option value");
selectbox.add(newOption, undefined); //最佳方案
与添加选项类似,移除选项的方式也有很多种。首先,可以使用 DOM 的 removeChild()方法,为其传入要移除的选项。
可以使用选择框的 remove()方法。这个方法接受一个参数,即要移除选项的索引。
就是将相应选项设置为 null。这种方式也是 DOM 出现之前浏览器的遗留机制。
在 DOM 标准出现之前,将一个选择框中的选项移动到另一个选择框中是非常麻烦的。整个过程要涉及从第一个选择框中移除选项,然后以相同的文本和值创建新选项,最后再将新选项添加到第二个选择框中。而使用 DOM 的 appendChild()方法,就可以将第一个选择框中的选项直接移动到第二个选择框中。我们知道,如果为 appendChild()方法传入一个文档中已有的元素,那么就会先从该元素的父节点中移除它,再把它添加到指定的位置。下面的代码展示了将第一个选择框中的第一个选项移动到第二个选择框中的过程。
var selectbox1 = document.getElementById("selLocations1");
var selectbox2 = document.getElementById("selLocations2");
selectbox2.appendChild(selectbox1.options[0]);
重排选项次序的过程也十分类似,最好的方式仍然是使用 DOM 方法。要将选择框中的某一项移动到特定位置,最合适的 DOM 方法就是 insertBefore(); appendChild()方法只适用于将选项添加到选择框的最后。要在选择框中向前移动一个选项的位置,可以使用以下代码:
var optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index-1]);
以上代码首先选择了要移动的选项,然后将其插入到了排在它前面的选项之前。实际上,第二行代码对除第一个选项之外的其他选项是通用的。类似地,可以使用下列代码将选择框中的选项向后移动一个位置。
var optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index+2]);
随着 Ajax 的出现,表单序列化已经成为一种常见需求(第 21 章将讨论 Ajax)。在 JavaScript 中,可以利用表单字段的 type 属性,连同 name 和 value 属性一起实现对表单的序列化。在编写代码之前,有必须先搞清楚在表单提交期间,浏览器是怎样将数据发送给服务器的。
富文本编辑,又称为 WYSIWYG(What You See Is What You Get,所见即所得)。在网页中编辑富文本内容,是人们对 Web 应用程序最大的期待之一。虽然也没有规范,但在 IE 最早引入的这一功能基础上,已经出现了事实标准。而且, Opera、 Safari、 Chrome 和 Firefox 都已经支持这一功能。这一技术的本质,就是在页面中嵌入一个包含空 HTML 页面的 iframe。通过设置 designMode 属性,这个空白的 HTML 页面可以被编辑,而编辑对象则是该页面
元素的 HTML 代码。 designMode 属性有两个可能的值: "off"(默认值)和"on"。在设置为"on"时,整个文档都会变得可以编辑(显示插入符号),然后就可以像使用字处理软件一样,通过键盘将文本内容加粗、变成斜体,等等。HTML5 添加的最受欢迎的功能就是
要使用
<canvas id="drawing" width=" 200" height="200">A drawing of something.</canvas>
与其他元素一样,
ps:牛逼的技术,感觉用不到,跳过跳过
跨文档消息传送(cross-document messaging),有时候简称为 XDM,指的是在来自不同域的页面间传递消息。例如, www.wrox.com 域中的页面与位于一个内嵌框架中的 p2p.wrox.com 域中的页面通信。在 XDM 机制出现之前,要稳妥地实现这种通信需要花很多工夫。 XDM 把这种机制规范化,让我们能既稳妥又简单地实现跨文档通信。
XDM 的核心是 postMessage()方法。在 HTML5 规范中,除了 XDM 部分之外的其他部分也会提到这个方法名,但都是为了同一个目的:向另一个地方传递数据。对于 XDM 而言, “另一个地方”指的是包含在当前页面中的
最早在网页中引入 JavaScript 拖放功能的是 IE4。当时,网页中只有两种对象可以拖放:图像和某些文本。拖动图像时,把鼠标放在图像上,按住鼠标不放就可以拖动它。拖动文本时,要先选中文本,然后可以像拖动图像一样拖动被选中的文本。在 IE 4 中,唯一有效的放置目标是文本框。到了 IE5,拖放功能得到扩展,添加了新的事件,而且几乎网页中的任何元素都可以作为放置目标。 IE5.5 更进一步,让网页中的任何元素都可以拖放。(IE6 同样也支持这些功能。) HTML5 以 IE 的实例为基础制定了拖放规范。 Firefox 3.5、 Safari 3+和 Chrome 也根据 HTML5 规范实现了原生拖放功能。
随着音频和视频在 Web 上的迅速流行,大多数提供富媒体内容的站点为了保证跨浏览器兼容性,不得不选择使用 Flash。 HTML5 新增了两个与媒体相关的标签,让开发人员不必依赖任何插件就能在网页中嵌入跨浏览器的音频和视频内容。这两个标签就是
历史状态管理是现代 Web 应用开发中的一个难点。在现代 Web 应用中,用户的每次操作不一定会打开一个全新的页面,因此“后退”和“前进”按钮也就失去了作用,导致用户很难在不同状态间切换。要解决这个问题,首选使用 hashchange 事件(第 13 章曾讨论过)。 HTML5 通过更新 history 对象为管理历史状态提供了方便。
IE、 Firefox、 Safari、 Chrome 和 Opera 等主流浏览器,都具有某种向用户报告 JavaScript 错误的机制。默认情况下,所有浏览器都会隐藏此类信息,毕竟除了开发人员之外,很少有人关心这些内容。因此,在基于浏览器编写 JavaScript 脚本时,别忘了启用浏览器的 JavaScript 报告功能,以便及时收到错误通知。
ECMA-262 第 3 版引入了 try-catch 语句,作为 JavaScript 中处理异常的一种标准方式。基本的语法如下所示,显而易见,这与 Java 中的 try-catch 语句是完全相同的。
try{
// 可能会导致错误的代码
} catch(error){
// 在错误发生时怎么处理
}
finally子句
虽然在 try-catch 语句中是可选的,但 finally 子句一经使用,其代码无论如何都会执行。换句话说, try 语句块中的代码全部正常执行, finally 子句会执行;如果因为出错而执行了 catch 语句块, finally 子句照样还会执行。只要代码中包含 finally 子句,则无论 try 或 catch 语句块中包含什么代码——甚至 return 语句,都不会阻止 finally 子句的执行。
错误类型
执行代码期间可能会发生的错误有多种类型。每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象。 ECMA-262 定义了下列 7 种错误类型:
利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误作出恰当的处理。要想知道错误的类型,可以像下面这样在 try-catch 语句的 catch 语句中使用 instanceof 操作符。
try {
someFunction();
} catch (error){
if (error instanceof TypeError){
//处理类型错误
} else if (error instanceof ReferenceError){
//处理引用错误
} else {
//处理其他类型的错误
}
}
使用 try-catch 最适合处理那些我们无法控制的错误。假设你在使用一个大型 JavaScript 库中的函数,该函数可能会有意无意地抛出一些错误。由于我们不能修改这个库的源代码,所以大可将对该函数的调用放在 try-catch 语句当中,万一有什么错误发生,也好恰当地处理它们。
在明明白白地知道自己的代码会发生错误时,再使用 try-catch 语句就不太合适了。例如,如果传递给函数的参数是字符串而非数值,就会造成函数出错,那么就应该先检查参数的类型,然后再决定如何去做。在这种情况下,不应用使用 try-catch 语句。
与 try-catch 语句相配的还有一个 throw 操作符,用于随时抛出自定义错误。抛出错误时,必须要给 throw 操作符指定一个值,这个值是什么类型,没有要求。下列代码都是有效的。
throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript"};
抛出错误的时机
下面的函数会在参数不是数组的情况下失败。
function process(values){
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
如果执行这个函数时传给它一个字符串参数,那么对 sort()的调用就会失败。对此,不同浏览器会给出不同的错误消息,但都不是特别明确,如下所示。
IE:属性或方法不存在。
Firefox: values.sort()不是函数。
Safari:值 undefined(表达式 values.sort 的结果)不是对象。
Chrome:对象名没有方法'sort'。
Opera:类型不匹配(通常是在需要对象的地方使用了非对象值)。
尽管 Firefox、 Chrome 和 Safari 都明确指出了代码中导致错误的部分,但错误消息并没有清楚地告诉我们到底出了什么问题,该怎么修复问题。在处理类似前面例子中的那个函数时,通过调试处理这些错误消息没有什么困难。但是,在面对包含数千行 JavaScript 代码的复杂的 Web 应用程序时,要想查找错误来源就没有那么容易了。这种情况下,带有适当信息的自定义错误能够显著提升代码的可维护性。来看下面的例子。
function process(values){
if (!(values instanceof Array)){
throw new Error("process(): Argument must be an array.");
}
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
在重写后的这个函数中,如果 values 参数不是数组,就会抛出一个错误。错误消息中包含了函数的名称,以及为什么会发生错误的明确描述。如果一个复杂的 Web 应用程序发生了这个错误,那么查找问题的根源也就容易多了。
抛出错误与使用try-catch
关于何时该抛出错误,而何时该使用 try-catch 来捕获它们,是一个老生常谈的问题。一般来说,应用程序架构的较低层次中经常会抛出错误,但这个层次并不会影响当前执行的代码,因而错误通常得不到真正的处理。如果你打算编写一个要在很多应用程序中使用的 JavaScript 库,甚至只编写一个可能会在应用程序内部多个地方使用的辅助函数,我都强烈建议你在抛出错误时提供详尽的信息。然后,即可在应用程序中捕获并适当地处理这些错误。
说到抛出错误与捕获错误,我们认为只应该捕获那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息
任何没有通过 try-catch 处理的错误都会触发 window 对象的 error 事件。这个事件是 Web 浏览器最早支持的事件之一, IE、 Firefox 和 Chrome 为保持向后兼容,并没有对这个事件作任何修改(Opera和 Safari 不支持 error 事件)。在任何 Web 浏览器中, onerror 事件处理程序都不会创建 event 对象,但它可以接收三个参数:错误消息、错误所在的 URL 和行号。多数情况下,只有错误消息有用,因为URL 只是给出了文档的位置,而行号所指的代码行既可能出自嵌入的 JavaScript 代码,也可能出自外部的文件。要指定 onerror 事件处理程序,必须使用如下所示的 DOM0 级技术,它没有遵循“DOM2 级事件”的标准格式。
window.onerror = function(message, url, line){
alert(message);
};
错误处理的核心,是首先要知道代码里会发生什么错误。由于 JavaScript 是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现。一般来说,需要关注三种错误:
以上错误分别会在特定的模式下或者没有对值进行足够的检查的情况下发生。
类型转换错误
容易发生类型转换错误的另一个地方,就是流控制语句。像 if 之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。尤其是 if 语句,如果使用不当,最容易出错。来看下面的例子。
function concat(str1, str2, str3){
var result = str1 + str2;
if (str3){ //绝对不要这样!!!
result += str3;
}
return result;
}
这个函数的用意是拼接两或三个字符串,然后返回结果。其中,第三个字符串是可选的,因此必须要检查。第 3 章曾经介绍过,未使用过的命名变量会自动被赋予 undefined 值。而 undefined 值可以被转换成布尔值 false,因此这个函数中的 if 语句实际上只适用于提供了第三个参数的情况。问题在于,并不是只有 undefined 才会被转换成 false,也不是只有字符串值才可以转换为 true。例如,假设第三个参数是数值 0,那么 if 语句的测试就会失败,而对数值 1 的测试则会通过。
在流控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此类错误,就要做到在条件比较时切实传入布尔值。实际上,执行某种形式的比较就可以达到这个目的。例如,我们可以将前面的函数重写如下。
function concat(str1, str2, str3){
var result = str1 + str2;
if (typeof str3 == "string"){ //恰当的比较
result += str3;
}
return result;
}
数据类型错误
JavaScript 是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。在将预料之外的值传递给函数的情况下,最容易发生数据类型错误。
//不安全的函数,任何非字符串值都会导致错误
function getQueryString(url){
var pos = url.indexOf("?");
if (pos > -1){
return url.substring(pos +1);
}
return "";
}
而添加一条简单的类型检测语句,就可以确保函数不那么容易出错。重写后的这个函数首先检查了传入的值是不是字符串。这样,就确保了函数不会因为接收到非字符串值而导致错误。
function getQueryString(url){
if (typeof url == "string"){ //通过检查类型确保安全
var pos = url.indexOf("?");
if (pos > -1){
return url.substring(pos +1);
}
}
return "";
}
另一个常见的错误就是将参数与 null 值进行比较,如下所示。
//不安全的函数,任何非数组值都会导致错误
function reverseSort(values){
if (values != null){ //绝对不要这样!!!
values.sort();
values.reverse();
}
}
与 null 进行比较只能确保相应的值不是 null 和 undefined(这就相当于使用相等和不相等操作)。要确保传入的值有效,仅检测 null 值是不够的;因此,不应该使用这种技术。同样,我们也不推荐将某个值与 undefined 作比较。另一种错误的做法,就是只针对要使用的某一个特性执行特性检测。来看下面的例子。
//还是不安全,任何非数组值都会导致错误
function reverseSort(values){
if (typeof values.sort == "function"){ //绝对不要这样!!!
values.sort();
values.reverse();
}
}
在这个例子中,代码首先检测了参数中是否存在 sort()方法。这样,如果传入一个包含 sort()方法的对象(而不是数组)当然也会通过检测,但在调用 reverse()函数时可能就会出错了。在确切知道应该传入什么类型的情况下,最好是使用 instanceof 来检测其数据类型,如下所示。
//安全,非数组值将被忽略
function reverseSort(values){
if (values instanceof Array){ //问题解决了
values.sort();
values.reverse();
}
}
通信错误
随着 Ajax 编程的兴起(第 21 章讨论 Ajax), Web 应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过, JavaScript 与服务器之间的任何一次通信,都有可能会产生错误。
第一种通信错误与格式不正确的 URL 或发送的数据有关。最常见的问题是在将数据发送给服务器之前,没有使用 encodeURIComponent()对数据进行编码。 例如,下面这个 URL 的格式就是不正确的。
区分非致命错误和致命错误的主要依据,就是看它们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实际上毫不相干的部分。
开发 Web 应用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。例如数据库和服务器错误都会定期写入日志,而且会按照常用 API 进行分类。在复杂的 Web 应用程序中,我们同样推荐你把 JavaScript 错误也回写到服务器。换句话说,也要将这些错误写入到保存服务器端错误的地方,只不过要标明它们来自前端。把前后端的错误集中起来,能够极大地方便对数据的分析。
这个方法接受两个参数:要解析的 XML 字符串和内容类型(内容类型始终都应该是"text/xml")。返回的值是一个 Document 的实例。来看下面的例子。
var parser = new DOMParser();
var xmldom = parser.parseFromString("<root><child/></root>", "text/xml");
alert(xmldom.documentElement.tagName); //"root"
alert(xmldom.documentElement.firstChild.tagName); //"child"
var anotherChild = xmldom.createElement("child");
xmldom.documentElement.appendChild(anotherChild);
var children = xmldom.getElementsByTagName("child");
alert(children.length); //2
DOMParser 只能解析格式良好的 XML,因而不能把 HTML 解析为 HTML 文档。在发生解析错误时,仍然会从 parseFromString()中返回一个 Document 对象,但这个对象的文档元素是
在引入 DOMParser 的同时, Firefox 还引入了 XMLSerializer 类型,提供了相反的功能:将 DOM文档序列化为 XML 字符串。
var serializer = new XMLSerializer();
var xml = serializer.serializeToString(xmldom);
alert(xml);
XMLSerializer 可以序列化任何有效的 DOM 对象,不仅包括个别的节点,也包括 HTML 文档。将 HTML 文档传入 serializeToString()以后, HTML 文档将被视为 XML 文档,因此得到的代码也将是格式良好的。
XPath 是设计用来在 DOM 文档中查找节点的一种手段,因而对 XML 处理也很重要。但是, DOM3级以前的标准并没有就 XPath 的 API 作出规定; XPath 是在 DOM3 级 XPath 模块中首次跻身推荐标准行列的。
XSLT 是与 XML 相关的一种技术,它利用 XPath 将文档从一种表现形式转换成另一种表现形式。与XML 和 XPath 不同, XSLT 没有正式的 API,在正式的 DOM 规范中也没有它的位置。结果,只能依靠浏览器开发商以自己的方式来实现它。
关于 JSON,最重要的是要理解它是一种数据格式,不是一种编程语言。虽然具有相同的语法形式,但 JSON 并不从属于 JavaScript。而且,并不是只有 JavaScript 才使用 JSON,毕竟 JSON 只是一种数据格式。很多编程语言都有针对 JSON 的解析器和序列化器。
JSON 的语法可以表示以下三种类型的值。
JSON 对象有两个方法: stringify()和 parse()。在最简单的情况下,这两个方法分别用于把JavaScript 对象序列化为 JSON 字符串和把 JSON 字符串解析为原生 JavaScript 值。例如:
var book = {
title: "Professional JavaScript",
authors: [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
};
var jsonText = JSON.stringify(book);
将 JSON 字符串直接传递给 JSON.parse()就可以得到相应的 JavaScript 值。例如,使用下列代码就可以创建与 book 类似的对象:
var bookCopy = JSON.parse(jsonText);
注意,虽然 book 与 bookCopy 具有相同的属性,但它们是两个独立的、没有任何关系的对象。如果传给 JSON.parse()的字符串不是有效的 JSON,该方法会抛出错误。
实际上, JSON.stringify()除了要序列化的 JavaScript 对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化 JavaScript 对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在 JSON 字符串中保留缩进。单独或组合使用这两个参数,可以更全面深入地控制 JSON 的序列化。
过滤结果
JSON.stringify()的第二个参数是一个数组,其中包含两个字符串: "title"和"edition"。这两个属性与将要序列化的对象中的属性是对应的,因此在返回的结果字符串中,就只会包含这两个属性。
var book = {
"title": "Professional JavaScript",
"authors": [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
};
var jsonText = JSON.stringify(book, function(key, value){
switch(key){
case "authors":
return value.join(",")
case "year":
return 5000;
case "edition":
return undefined;
default:
return value;
}
});
函数过滤器根据传入的键来决定结果。如果键为"authors",就将数组连接为一个字符串;如果键为"year",则将其值设置为 5000;如果键为"edition",通过返回 undefined 删除该属性。最后,一定要提供 default 项,此时返回传入的值,以便其他值都能正常出现在结果中。
字符串缩进
JSON.stringify()方法的第三个参数用于控制结果中的缩进和空白符。如果这个参数是一个数值,那它表示的是每个级别缩进的空格数。
toJSON()方法
有时候, JSON.stringify()还是不能满足对某些对象进行自定义序列化的需求。在这些情况下,
可以给对象定义 toJSON()方法,返回其自身的 JSON 数据格式。
var book = {
"title": "Professional JavaScript",
"authors": [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011,
toJSON: function(){
return this.title;
}
};
var jsonText = JSON.stringify(book);
以上代码在 book 对象上定义了一个 toJSON()方法,该方法返回图书的书名。与 Date 对象类似,这个对象也将被序列化为一个简单的字符串而非对象。可以让 toJSON()方法返回任何值,它都能正常工作。
toJSON()可以作为函数过滤器的补充,因此理解序列化的内部顺序十分重要。假设把一个对象传入 JSON.stringify(),序列化该对象的顺序如下。
JSON.parse()方法也可以接收另一个参数,该参数是一个函数,将在每个键值对儿上调用。为了区别 JSON.stringify()接收的替换(过滤)函数(replacer),这个函数被称为还原函数(reviver),但实际上这两个函数的签名是相同的——它们都接收两个参数,一个键和一个值,而且都需要返回一个值。
2005 年, Jesse James Garrett 发表了一篇在线文章,题为“Ajax: A new Approach to WebApplications”。他在这篇文章里介绍了一种技术,用他的话说,就叫 Ajax,是对 Asynchronous JavaScript + XML 的简写。这一技术能够向服务器请求额外的数据而无须卸载页面,会带来更好的用户体验。 Garrett 还解释了怎样使用这一技术改变自从 Web 诞生以来就一直沿用的“单击,等待”的交互模式。
Ajax 技术的核心是 XMLHttpRequest 对象(简称 XHR),这是由微软首先引入的一个特性,其他浏览器提供商后来都提供了相同的实现。在 XHR 出现之前, Ajax 式的通信必须借助一些 hack 手段来实现,大多数是使用隐藏的框架或内嵌框架。 XHR 为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器取得更多信息,意味着用户单击后,可以不必刷新页面也能取得新数据。也就是说,可以使用 XHR 对象取得新数据,然后再通过 DOM 将新数据插入到页面中。另外,虽然名字中包含 XML 的成分,但 Ajax 通信与数据格式无关;这种技术就是无须刷新页面即可从服务器取得数据,但不一定是 XML 数据。
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;
for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
这个函数中新增的代码首先检测原生 XHR 对象是否存在,如果存在则返回它的新实例。如果原生对象不存在,则检测 ActiveX 对象。如果这两种对象都不存在,就抛出一个错误。然后,就可以使用下面的代码在所有浏览器中创建 XHR 对象了。
var xhr = createXHR();
在使用 XHR 对象时,要调用的第一个方法是 open(),它接受 3 个参数:要发送的请求的类型("get"、 "post"等)、请求的 URL 和表示是否异步发送请求的布尔值。下面就是调用这个方法的例子。
xhr.open("get", "example.php", false);
这行代码会启动一个针对 example.php 的 GET 请求。有关这行代码,需要说明两点:一是 URL相对于执行代码的当前页面(当然也可以使用绝对路径);二是调用 open()方法并不会真正发送请求,而只是启动一个请求以备发送。
只能向同一个域中使用相同端口和协议的 URL 发送请求。如果 URL 与启动请求的页面有任何差别,都会引发安全错误。
要发送特定的请求,必须像下面这样调用 send()方法:
xhr.open("get", "example.txt", false);
xhr.send(null);
这里的 send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入 null,因为这个参数对有些浏览器来说是必需的。调用 send()之后,请求就会被分派到服务器。
由于这次请求是同步的, JavaScript 代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充 XHR 对象的属性,相关的属性简介如下。
在接收到响应后,第一步是检查 status 属性,以确定响应已经成功返回。一般来说,可以将 HTTP状态代码为 200 作为成功的标志。此时, responseText 属性的内容已经就绪,而且在内容类型正确的情况下, responseXML 也应该能够访问了。此外,状态代码为 304 表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本;当然,也意味着响应是有效的。为确保接收到适当的响应,应该像下面这样检查上述这两种状态代码。
xhr.open("get", "example.txt", false);
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
根据返回的状态代码,这个例子可能会显示由服务器返回的内容,也可能会显示一条错误消息。我们建议读者要通过检测 status 来决定下一步的操作,不要依赖 statusText,因为后者在跨浏览器使用时不太可靠。另外,无论内容类型是什么,响应主体的内容都会保存到 responseText 属性中;而对于非 XML 数据而言, responseXML 属性的值将为 null。
像前面这样发送同步请求当然没有问题,但多数情况下,我们还是要发送异步请求,才能让JavaScript 继续执行而不必等待响应。此时,可以检测 XHR 对象的 readyState 属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下。
var xhr = createXHR();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example.txt", true);
xhr.send(null);
另外,在接收到响应之前还可以调用 abort()方法来取消异步请求,如下所示:
xhr.abort();
调用这个方法后, XHR 对象会停止触发事件,而且也不再允许访问任何与响应有关的对象属性。在终止请求之后,还应该对 XHR 对象进行解引用操作。由于内存原因,不建议重用 XHR 对象。
只要 readyState 属性的值由一个值变成另一个值,都会触发一次 readystatechange 事件。可以利用这个事件来检测每次状态变化后 readyState 的值。通常,我们只对 readyState 值为 4 的阶
段感兴趣,因为这时所有数据都已经就绪。不过,必须在调用 open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性。
默认情况下,在发送 XHR 请求的同时,还会发送下列头部信息。
使用 setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。要成功发送请求头部信息,必须在调用 open()方法之后且调用 send()方法之前调用 setRequestHeader()。
调用 XHR 对象的 getResponseHeader()方法并传入头部字段名称,可以取得相应的响应头部信息。而调用 getAllResponseHeaders()方法则可以取得一个包含所有头部信息的长字符串。
GET 是最常见的请求类型,最常用于向服务器查询某些信息。必要时,可以将查询字符串参数追加到 URL 的末尾,以便将信息发送给服务器。对 XHR 而言,位于传入 open()方法的 URL 末尾的查询字符串必须经过正确的编码才行。
使用 GET 请求经常会发生的一个错误,就是查询字符串的格式有问题。查询字符串中每个参数的名称和值都必须使用 encodeURIComponent()进行编码,然后才能放到 URL 的末尾;而且所有名-值对儿都必须由和号(&)分隔。
下面这个函数可以辅助向现有 URL 的末尾添加查询字符串参数:
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
这个 addURLParam()函数接受三个参数:要添加参数的 URL、参数的名称和参数的值。这个函数首先检查 URL 是否包含问号(以确定是否已经有参数存在)。如果没有,就添加一个问号;否则,就添加一个和号。然后,将参数名称和值进行编码,再添加到 URL 的末尾。最后返回添加参数之后的 URL。
下面是使用这个函数来构建请求 URL 的示例。
var url = "example.php";
//添加参数
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
//初始化请求
xhr.open("get", url, false);
在这里使用 addURLParam()函数可以确保查询字符串的格式良好,并可靠地用于 XHR 对象。
默认情况下,服务器对 POST 请求和提交 Web 表单的请求并不会一视同仁。因此,服务器端必须有程序来读取发送过来的原始数据,并从中解析出有用的部分。不过,我们可以使用 XHR 来模仿表单提交:首先将 Content-Type 头部信息设置为 application/x-www-form-urlencoded,也就是表单提交时的内容类型,其次是以适当的格式创建一个字符串。第 14 章曾经讨论过, POST 数据的格式与查询字符串格式相同。如果需要将页面中表单的数据进行序列化,然后再通过 XHR 发送到服务器,那么就可以使用第 14 章介绍的 serialize()函数来创建这个字符串:
function submitData(){
var xhr = createXHR();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post", "postexample.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var form = document.getElementById("user-info");
xhr.send(serialize(form));
}
鉴于 XHR 已经得到广泛接受,成为了事实标准, W3C 也着手制定相应的标准以规范其行为。XMLHttpRequest 1 级只是把已有的 XHR 对象的实现细节描述了出来。而 XMLHttpRequest 2 级则进一步发展了 XHR。并非所有浏览器都完整地实现了 XMLHttpRequest 2 级规范,但所有浏览器都实现了它规定的部分内容。
下面的代码创建了一个 FormData 对象,并向其中添加了一些数据。
var data = new FormData();
data.append("name", "Nicholas");
这个 append()方法接收两个参数:键和值,分别对应表单字段的名字和字段中包含的值。可以像这样添加任意多个键值对儿。而通过向 FormData 构造函数中传入表单元素,也可以用表单元素的数据预先向其中填入键值对儿:
var data = new FormData(document.forms[0]);
创建了 FormData 的实例后,可以将它直接传给 XHR 的 send()方法,如下所示:
var xhr = createXHR();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post","postexample.php", true);
var form = document.getElementById("user-info");
xhr.send(new FormData(form));
使用 FormData 的方便之处体现在不必明确地在 XHR 对象上设置请求头部。 XHR 对象能够识别传入的数据类型是 FormData 的实例,并配置适当的头部信息。
IE8 为 XHR 对象添加了一个 timeout 属性,表示请求在等待响应多少毫秒之后就终止。在给timeout 设置一个数值后,如果在规定的时间内浏览器还没有接收到响应,那么就会触发 timeout 事件,进而会调用 ontimeout 事件处理程序。这项功能后来也被收入了 XMLHttpRequest 2 级规范中。来看下面的例子。
var xhr = createXHR();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
try {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
} catch (ex){
//假设由 ontimeout 事件处理程序处理
}
}
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; //将超时设置为 1 秒钟(仅适用于 IE8+)
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);
Firefox 最早引入了 overrideMimeType()方法,用于重写 XHR 响应的 MIME 类型。这个方法后来也被纳入了 XMLHttpRequest 2 级规范。因为返回响应的 MIME 类型决定了 XHR 对象如何处理它,所以提供一种方法能够重写服务器返回的 MIME 类型是很有用的。比如,服务器返回的 MIME 类型是 text/plain,但数据中实际包含的是 XML。根据 MIME 类型,即使数据是 XML, responseXML 属性中仍然是 null。通过调用 overrideMimeType()方法,可以保证把响应当作 XML 而非纯文本来处理。
var xhr = createXHR();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
这个例子强迫 XHR 对象将响应当作 XML 而非纯文本来处理。调用 overrideMimeType()必须在send()方法之前,才能保证重写响应的 MIME 类型。
Progress Events 规范是 W3C 的一个工作草案,定义了与客户端服务器通信有关的事件。这些事件最早其实只针对 XHR 操作,但目前也被其他 API 借鉴。有以下 6 个进度事件。
每个请求都从触发 loadstart 事件开始,接下来是一或多个 progress 事件,然后触发 error、abort 或 load 事件中的一个,最后以触发 loadend 事件结束。目前还没有浏览器支持 loadend 事件。
只要浏览器接收到服务器的响应,不管其状态如何,都会触发 load 事件。而这意味着你必须要检查 status 属性,才能确定数据是否真的已经可用了。
var xhr = createXHR();
xhr.onload = function(){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
Mozilla 对 XHR 的另一个革新是添加了 progress 事件,这个事件会在浏览器接收新数据期间周期性地触发。而 onprogress 事件处理程序会接收到一个 event 对象,其 target 属性是 XHR 对象,但包含着三个额外的属性: lengthComputable、 position 和 totalSize。其中, lengthComputable是一个表示进度信息是否可用的布尔值, position 表示已经接收的字节数, totalSize 表示根据Content-Length 响应头部确定的预期字节数。有了这些信息,我们就可以为用户创建一个进度指示器了。下面展示了为用户创建进度指示器的一个示例。
var xhr = createXHR();
xhr.onload = function(event){
if ((xhr.status >= 200 && xhr.status < 300) ||
xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.onprogress = function(event){
var divStatus = document.getElementById("status");
if (event.lengthComputable){
divStatus.innerHTML = "Received " + event.position + " of " +
event.totalSize +" bytes";
}
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下, XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。
CORS(Cross-Origin Resource Sharing,跨源资源共享)是 W3C 的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。 CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain。在发送该请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的一个示例:
Origin: http://www.nczonline.net
如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源
信息(如果是公共资源,可以回发"*")。例如:
Access-Control-Allow-Origin: http://www.nczonline.net
如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含 cookie 信息
微软在 IE8 中引入了 XDR(XDomainRequest)类型。这个对象与 XHR 类似,但能实现安全可靠的跨域通信。 XDR 对象的安全机制部分实现了 W3C 的 CORS 规范。以下是 XDR 与 XHR 的一些不同之处。
这些变化使 CSRF(Cross-Site Request Forgery,跨站点请求伪造)和 XSS(Cross-Site Scripting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)来决定是否设置 Access-Control- Allow-Origin 头部。作为请求的一部分, Origin 头部的值表示请求的来源域,以便远程资源明确地识别 XDR 请求。
Firefox 3.5+、 Safari 4+、 Chrome、 iOS 版 Safari 和 Android 平台中的 WebKit 都通过 XMLHttpRequest对象实现了对 CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。 要请求位于另一个域中的资源,使用标准的 XHR 对象并在 open()方法中传入绝对 URL 即可,例如:
var xhr = createXHR();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null);
CORS 通过一种叫做 Preflighted Requests 的透明服务器验证机制支持开发人员使用自定义的头部、GET 或 POST 之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个 Preflight 请求。这种请求使用 OPTIONS 方法,发送下列头部。
默 认 情 况 下, 跨 源 请 求不 提 供 凭 据(cookie 、 HTTP 认 证 及 客户 端 SSL 证明 等 )。 通 过 将withCredentials 属性设置为 true,可以指定某个请求应该发送凭据。
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是, responseText 中将是空字符串, status 的值为 0,而且会调用 onerror()事件处理程序)。
即使浏览器对 CORS 的支持程度并不都一样,但所有浏览器都支持简单的(非 Preflight 和不带凭据的)请求,因此有必要实现一个跨浏览器的方案。检测 XHR 是否支持 CORS 的最简单方式,就是检查是否存在 withCredentials 属性。再结合检测 XDomainRequest 对象是否存在,就可以兼顾所有浏览器了。
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
vxhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("get", "http://www.somewhere-else.com/page/");
if (request){
request.onload = function(){
//对 request.responseText 进行处理
};
request.send();
}
在 CORS 出现以前,要实现跨域 Ajax 通信颇费一些周折。开发人员想出了一些办法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。
上述第一种跨域请求技术是使用标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。
动态创建图像经常用于图像 Ping。图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。来看下面的例子。
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";
这里创建了一个 Image 的实例,然后将 onload 和 onerror 事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置 src 属性那一刻开始,而这个例子在请求中发送了一个 name 参数。图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数。图像 Ping 有两个主要的缺点,一是只能发送 GET 请求,二是无法访问服务器的响应文本。因此,图像 Ping 只能用于浏览器与服务器间的单向通信。
JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种新方法,在后来的 Web 服务中非常流行。 JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON
JSONP 之所以在开发人员中极为流行,主要原因是它非常简单易用。与图像 Ping 相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过, JSONP 也有两点不足。
首先, JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。因此在使用不是你自己运维的 Web 服务时,一定得保证它安全可靠。
其次,要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给
<!-- 使用事件处理程序属性值的紧密耦合的 HTML/JavaScript -->
<input type="button" value="Click Me" onclick="doSomething()" />
虽然这些从技术上来说都是正确的,但是实践中,它们将表示数据的 HTML 和定义行为的 JavaScript紧密耦合在了一起。理想情况是, HTML 和 JavaScript 应该完全分离,并通过外部文件和使用 DOM 附加行为来包含 JavaScript。
HTML 和 JavaScript 的紧密耦合也可以在相反的关系上成立: JavaScript 包含了 HTML。这通常会出现在使用 innerHTML 来插入一段 HTML 文本到页面上这种情况中,如下面的例子所示:
//将 HTML 紧密耦合到 JavaScript
function insertMessage(msg){
var container = document.getElementById("container");
container.innerHTML = "<div class=\"msg\"><p class=\"post\">" + msg + "</p>" +
"<p><em>Latest message above.</em></p></div>";
}
一般来说,你应该避免在 JavaScript 中创建大量 HTML。再一次重申要保持层次的分离,这样可以很容易的确定错误来源。当使用上面这个例子的时候,有一个页面布局的问题,可能和动态创建的 HTML没有被正确格式化有关。不过,要定位这个错误可能非常困难,因为你可能一般先看页面的源代码来查找那段烦人的 HTML,但是却没能找到,因为它是动态生成的。对数据或者布局的更改也会要求更改JavaScript,这也表明了这两个层次过于紧密地耦合了。
解耦CSS/JavaScript
另一个 Web 层则是 CSS,它主要负责页面的显示。 JavaScript 和 CSS 也是非常紧密相关的:他们都是 HTML 之上的层次,因此常常一起使用。但是,和 HTML 与 JavaScript 的情况一样, CSS 和 JavaScript也可能会过于紧密地耦合在一起。最常见的紧密耦合的例子是使用 JavaScript 来更改某些样式,如下所示:
//CSS 对 JavaScript 的紧密耦合
element.style.color = "red";
element.style.backgroundColor = "blue";
由于 CSS 负责页面的显示,当显示出现任何问题时都应该只是查看 CSS 文件来解决。然而,当使用了 JavaScript 来更改某些样式的时候,比如颜色,就出现了第二个可能已更改和必须检查的地方。结果是 JavaScript 也在某种程度上负责了页面的显示,并与 CSS 紧密耦合了。如果未来需要更改样式表,CSS 和 JavaScript 文件可能都需要修改。这就给开发人员造成了维护上的噩梦。所以在这两个层次之间必须有清晰的划分。
现代 Web 应用常常要使用 JavaScript 来更改样式,所以虽然不可能完全将 CSS 和 JavaScript 解耦,
但是还是能让耦合更松散的。这是通过动态更改样式类而非特定样式来实现的,如下例所示:
//CSS 对 JavaScript 的松散耦合
element.className = "edit";
通过只修改某个元素的 CSS 类,就可以让大部分样式信息严格保留在 CSS 中。 JavaScript 可以更改样式类,但并不会直接影响到元素的样式。只要应用了正确的类,那么任何显示问题都可以直接追溯到CSS 而非 JavaScript。
解耦应用逻辑/事件处理程序
每个 Web 应用一般都有相当多的事件处理程序,监听着无数不同的事件。然而,很少有能仔细得将应用逻辑从事件处理程序中分离的。请看以下例子:
function handleKeyPress(event){
event = EventUtil.getEvent(event);
if (event.keyCode == 13){
var target = EventUtil.getTarget(event);
var value = 5 * parseInt(target.value);
if (value > 10){
document.getElementById("error-msg").style.display = "block";
}
}
}
较好的方法是将应用逻辑和事件处理程序相分离,这样两者分别处理各自的东西。一个事件处理程序应该从事件对象中提取相关信息,并将这些信息传送到处理应用逻辑的某个方法中。例如,前面的代码可以被重写为:
function validateValue(value){
value = 5 * parseInt(value);
if (value > 10){
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event){
event = EventUtil.getEvent(event);
if (event.keyCode == 13){
var target = EventUtil.getTarget(event);
validateValue(target.value);
}
}
改动过的代码合理将应用逻辑从事件处理程序中分离了出来。 handleKeyPress() 函数确认是按下了 Enter 键(event.keyCode 为 13),取得了事件的目标并将 value 属性传递给 validateValue()函数,这个函数包含了应用逻辑。注意 validateValue()中没有任何东西会依赖于任何事件处理程序逻辑,它只是接收一个值,并根据该值进行其他处理。
ps:简单理解,就是 validateValue() 函数可以实现复用了
以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:
ps:就是上面的小例子,大概就是 validateValue()函数可以复用了
尊重对象所有权
JavaScript 的动态性质使得几乎任何东西在任何时间都可以修改。
也许在企业环境中最重要的编程实践就是尊重对象所有权,它的意思是你不能修改不属于你的对象。简单地说,如果你不负责创建或维护某个对象、它的对象或者它的方法,那么你就不能对它们进行修改。更具体地说:
问题在于开发人员会假设浏览器环境按照某个特定方式运行,而对于多个人都用到的对象进行改动就会产生错误。如果某人期望叫做 stopEvent()的函数能取消某个事件的默认行为,但是你对其进行了更改,然后它完成了本来的任务,后来还追加了另外的事件处理程序,那肯定会出现问题了。其他开发人员会认为函数还是按照原来的方式执行,所以他们的用法会出错并有可能造成危害,因为他们并不知道有副作用。
所以,最佳的方法便是永远不修改不是由你所有的对象。所谓拥有对象,就是说这个对象是你创建的,比如你自己创建的自定义类型或对象字面量。而 Array、 document 这些显然不是你的,它们在你的代码执行前就存在了。你依然可以通过以下方式为对象创建新的功能:
避免全局量
与尊重对象所有权密切相关的是尽可能避免全局变量和函数。这也关系到创建一个脚本执行的一致的和可维护的环境。最多创建一个全局变量,让其他对象和函数存在其中。请看以下例子:
//两个全局量——避免!!
var name = "Nicholas";
function sayName(){
alert(name);
}
这段代码包含了两个全局量:变量 name 和函数 sayName()。其实可以创建一个包含两者的对象,如下例所示:
//一个全局量——推荐
var MyApplication = {
name: "Nicholas",
sayName: function(){
alert(this.name);
}
};
在大多数情况下,可以是开发代码的公司的名字,例如 YAHOO 或者 Wrox。你可以如下例所示开始创建命名空间来组合功能。
//创建全局对象
var Wrox = {};
//为 Professional JavaScript 创建命名空间
Wrox.ProJS = {};
//将书中用到的对象附加上去
Wrox.ProJS.EventUtil = { ... };
Wrox.ProJS.CookieUtil = { ... };
避免与 null 进行比较
由于 JavaScript 不做任何自动的类型检查,所有它就成了开发人员的责任。因此,在 JavaScript 代码中其实很少进行类型检测。最常见的类型检测就是查看某个值是否为 null。但是,直接将值与 null比较是使用过度的,并且常常由于不充分的类型检查导致错误。看以下例子:
function sortArray(values){
if (values != null){ //避免!
values.sort(comparator);
}
}
在前面的范例中, values 参数应该是一个数组,那么就要检查它是不是一个数组,而不是检查它是否非 null。函数按照下面的方式修改会更加合适:
function sortArray(values){
if (values instanceof Array){ //推荐
values.sort(comparator);
}
}
如果看到了与 null 比较的代码,尝试使用以下技术替换:
如果值应为一个引用类型,使用 instanceof 操作符检查其构造函数;
如果值应为一个基本类型,使用 typeof 检查其类型;
如果是希望对象包含某个特定的方法名,则使用 typeof 操作符确保指定名字的方法存在于对象上。
代码中的 null 比较越少,就越容易确定代码的目的,并消除不必要的错误。
使用常量
关键在于将数据和使用它的逻辑进行分离。要注意的值的类型如下所示。
重复值——任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了 CSS 类名。
用户界面字符串 —— 任何用于显示给用户的字符串,都应被抽取出来以方便国际化。
URLs ——在 Web 应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的 URL。
任意可能会更改的值 —— 每当你在用到字面量值的时候,你都要问一下自己这个值在未来是不
是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量。对于企业级的 JavaScript 开发而言,使用常量是非常重要的技巧,因为它能让代码更容易维护,并且在数据更改的同时保护代码。
Chrome 是第一款内置优化引擎,将 JavaScript 编译成本地代码的浏览器。此后,主流浏览器纷纷效仿,陆续实现了 JavaScript 的编译执行。
随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。
避免全局查找
可能优化脚本性能最重要的就是注意全局查找。使用全局变量和函数肯定要比局部的开销更大,因为要涉及作用域链上的查找。请看以下函数:
function updateUI(){
var imgs = document.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
该函数可能看上去完全正常,但是它包含了三个对于全局 document 对象的引用。如果在页面上有多个图片,那么 for 循环中的 document 引用就会被执行多次甚至上百次,每次都会要进行作用域链查找。通过创建一个指向 document 对象的局部变量,就可以通过限制一次全局查找来改进这个函数的性能:
function updateUI(){
var doc = document;
var imgs = doc.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
这里,首先将 document 对象存在本地的 doc 变量中;然后在余下的代码中替换原来的 document。与原来的的版本相比,现在的函数只有一次全局查找,肯定更快。将在一个函数中会用到多次的全局对象存储为局部变量总是没错的
ps:这是什么原理,完全不懂
避免使用with语句
在性能非常重要的地方必须避免使用 with 语句。和函数类似, with 语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度。由于额外的作用域链查找,在 with 语句中执行的代码肯定会比外面执行的代码要慢。
避免不必要的属性查找
ps:感觉吹毛求疵,没什么特别的用处
优化循环
循环是编程中最常见的结构,在 JavaScript 程序中同样随处可见。优化循环是性能优化过程中很重要的一个部分,由于它们会反复运行同一段代码,从而自动地增加执行时间。在其他语言中对于循环优化有大量研究,这些技术也可以应用于 JavaScript。一个循环的基本优化步骤如下所示。
展开循环
当循环的次数是确定的,消除循环并使用多次函数调用往往更快 。请看一下前面的例子。如果数组的长度总是一样的,对每个元素都调用 process()可能更优,如以下代码所示:
//消除循环
process(values[0]);
process(values[1]);
process(values[2]);
这个例子假设 values 数组里面只有 3 个元素,直接对每个元素调用 process()。这样展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行得更快。
如果循环中的迭代次数不能事先确定,那可以考虑使用一种叫做 Duff 装置的技术。这个技术是以其创建者 Tom Duff 命名的,他最早在 C 语言中使用这项技术。正是 Jeff Greenberg 用 JavaScript 实现了Duff 装置。 Duff 装置的基本概念是通过计算迭代的次数是否为 8 的倍数将一个循环展开为一系列语句。请看以下代码:
//credit: Jeff Greenberg for JS implementation of Duff’s Device
//假设 values.length > 0
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;
do {
switch(startAt){
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt = 0;
} while (--iterations > 0);
Duff 装置的实现是通过将 values 数组中元素个数除以 8 来计算出循环需要进行多少次迭代的。然后使用取整的上限函数确保结果是整数。如果完全根据除 8 来进行迭代,可能会有一些不能被处理到的元素,这个数量保存在 startAt 变量中。首次执行该循环时,会检查 StartAt 变量看有需要多少额外调用。
避免双重解释
当 JavaScript 代码想解析 JavaScript 的时候就会存在双重解释惩罚。当使用 eval()函数或者是Function 构造函数以及使用 setTimeout()传一个字符串参数时都会发生这种情况。下面有一些例子:
//某些代码求值——避免!!
eval("alert('Hello world!')");
//创建新函数——避免!!
var sayHi = new Function("alert('Hello world!')");
//设置超时——避免!!
setTimeout("alert('Hello world!')", 500);
只有极少的情况下 eval()是绝对必须的,所以尽可能避免使用。在这个例子中,代码其实可以直接内嵌在原代码中。对于 Function 构造函数,完全可以直接写成一般的函数,调用 setTimeout()可以传入函数作为第一个参数。以下是一些例子:
//已修正
alert('Hello world!');
//创建新函数——已修正
var sayHi = function(){
alert('Hello world!');
};
//设置一个超时——已修正
setTimeout(function(){
alert('Hello world!');
}, 500);
性能的其他注意事项
当评估脚本性能的时候,还有其他一些可以考虑的东西。下面并非主要的问题,不过如果使用得当也会有相当大的提升。
JavaScript 代码中的语句数量也影响所执行的操作的速度。完成多个操作的单个语句要比完成单个操作的多个语句快。所以,就要找出可以组合在一起的语句,以减少脚本整体的执行时间。
多变量声明
有个地方很多开发人员都容易创建很多语句,那就是多个变量的声明。很容易看到代码中由多个var 语句来声明多个变量,如下所示:
//4 个语句—— 很浪费
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();
在强类型语言中,不同的数据类型的变量必须在不同的语句中声明。然而,在 JavaScript 中所有的变量都可以使用单个 var 语句来声明。前面的代码可以如下重写:
//一个语句
var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
此处,变量声明只用了一个 var 语句,之间由逗号隔开。在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明快很多。
插入迭代值
当使用迭代值(也就是在不同的位置进行增加或减少的值)的时候,尽可能合并语句。请看以下代码:
var name = values[i];
i++;
前面这 2 句语句各只有一个目的:第一个从 values 数组中获取值,然后存储在 name 中;第二个
给变量 i 增加 1。这两句可以通过迭代值插入第一个语句组合成一个语句,如下所示:
var name = values[i++];
使用数组和对象字面量
你可能看过两种创建数组和对象的方法:使用构造函数或者是使用字面量。使用构造函数总是要用到更多的语句来插入元素或者定义属性,而字面量可以将这些操作在一个语句中完成。请看以下例子:
//用 4 个语句创建和初始化数组——浪费
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//用 4 个语句创建和初始化对象——浪费
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
};
这段代码中,只创建和初始化了一个数组和一个对象。各用了 4 个语句:一个调用构造函数,其他3 个分配数据。其实可以很容易地转换成使用字面量的形式,如下所示:
//只用一条语句创建和初始化数组
var values = [123, 456, 789];
//只用一条语句创建和初始化对象
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};
DOM 操作与交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一部分。进一步说,看似细微的操作也可能要花很久来执行,因为 DOM 要处理非常多的信息。理解如何优化与 DOM 的交互可以极大得提高脚本完成速度。
最小化现场更新
一旦你需要访问的 DOM 部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。请看以下例子:
var list = document.getElementById("myList"),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
这段代码为列表添加了 10 个项目。添加每个项目时,都有 2 个现场更新:一个添加
var list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
使用innerHTML
有两种在页面上创建 DOM 节点的方法:使用诸如 createElement()和 appendChild()之类的DOM 方法,以及使用 innerHTML。对于小的 DOM 更改而言,两种方法效率都差不多。然而,对于大的 DOM 更改,使用 innerHTML 要比使用标准 DOM 方法创建同样的 DOM 结构快得多。
使用 innerHTML 的关键在于(和其他 DOM 操作一样)最小化调用它的次数。例如,下面的代码在这个操作中用到 innerHTML 的次数太多了:
var list = document.getElementById("myList"),
i;
for (i=0; i < 10; i++) {
list.innerHTML += "<li>Item " + i + "</li>"; //避免!!!
}
这段代码的问题在于每次循环都要调用 innerHTML,这是极其低效的。调用 innerHTML 实际上就是一次现场更新,所以也要如此对待。构建好一个字符串然后一次性调用 innerHTML 要比调用innerHTML 多次快得多
使用事件代理
大多数 Web 应用在用户交互上大量用到事件处理程序。页面上的事件处理程序的数量和页面响应用户交互的速度之间有个负相关。为了减轻这种惩罚,最好使用事件代理。
任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理。使用这个知识,就可以将事件处理程序附加到更高层的地方负责多个目标的事件处理。如果可能,在文档级别附加事件处理程序,这样可以处理整个页面的事件。
注意HTMLCOllection
任何时候要访问 HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进行一个查询,这个查询开销很昂贵。最小化访问 HTMLCollection 的次数可以极大地改进脚本的性能。
也许优化 HTMLCollection 访问最重要的地方就是循环了。前面提到过将长度计算移入 for 循环的初始化部分。现在看一下这个例子:
var images = document.getElementsByTagName("img"),
i, len;
for (i=0, len=images.length; i < len; i++){
//处理
}
这里的关键在于长度 length 存入了 len 变量,而不是每次都去访问 HTMLCollection 的 length属性。当在循环中使用 HTMLCollection 的时候,下一步应该是获取要使用的项目的引用,如下所示,以便避免在循环体内多次调用 HTMLCollection。
var images = document.getElementsByTagName("img"),
image,
i, len;
for (i=0, len=images.length; i < len; i++){
image = images[i];
//处理
}
这段代码添加了 image 变量,保存了当前的图像。这之后,在循环内就没有理由再访问 images 的HTMLCollection 了。
ps:为什么加个中间变量就能够加快性能?感觉就是个指针,指向了内存,调用的方法之类应该都是同一个对象吧?就很神奇。
完备 JavaScript 代码可以用于部署的一件很重要的事情,就是给它开发某些类型的构建过程。软件开发的典型模式是写代码—编译—测试,即首先书写好代码,将其编译通过,然后运行并确保其正常工作。由于 JavaScript 并非一个编译型语言,模式变成了写代码—测试,这里你写的代码就是你要在浏览器中测试的代码。这个方法的问题在于它不是最优的,你写的代码不应该原封不动地放入浏览器中,理由如下所示。
记住将代码分离成多个文件只是为了提高可维护性,并非为了部署。要进行部署的时候,需要将这些源代码合并为一个或几个归并文件。推荐 Web 应用中尽可能使用最少的 JavaScript 文件,是因为 HTTP请求是 Web 中的主要性能瓶颈之一。记住通过