javascript高级程序设计第三版书摘
在HTML 中使用JavaScript
<script>元素
在使用<script>元素嵌入 JavaScript 代码时,只须为<script>指定 type 属性。然后,像下面这样把 JavaScript 代码直接放在元素内部即可:
<script type="text/javascript">
function sayHi(){
alert("Hi!");
}
</script>
包含在<script>元素内部的 JavaScript 代码将被从上至下依次解释。就拿前面这个例子来说,解释器会解释一个函数的定义,然后将该定义保存在自己的环境当中。在解释器对<script>元素内部的所有代码求值完毕以前,页面中的其余内容都不会被浏览器加载或显示。
如果要通过<script>元素来包含外部 JavaScript 文件,那么 src 属性就是必需的。这个属性的值
是一个指向外部 JavaScript 文件的链接,例如:
<script type="text/javascript" src="example.js"></script>
在这个例子中,外部文件 example.js 将被加载到当前页面中。外部文件只须包含通常要放在开始的<script>和结束的</script>之间的那些 JavaScript 代码即可。与解析嵌入式 JavaScript 代码一样,在解析外部 JavaScript 文件(包括下载该文件)时,页面的处理也会暂时停止。如果是在 XHTML 文档中,也可以省略前面示例代码中结束的标签,例如:
<script type="text/javascript" src="example.js" />
需要注意的是,带有 src 属性的<script>元素不应该在其<script>和</script>标签之间再包含额外的 JavaScript 代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。
另外,通过<script>元素的 src 属性还可以包含来自外部域的 JavaScript 文件。这一点既让
<script>元素倍显强大,又让它备受争议。在这一点上, <script>与<img>元素非常相似, 即它的 src
属性可以是指向当前 HTML 页面所在域之外的某个域中的完整 URL,例如:
<script type="text/javascript" src="http://www.somewhere.com/afile.js"></script>
这样,位于外部域中的代码也会被加载和解析,就像这些代码位于加载它们的页面中一样。利用这一点就可以在必要时通过不同的域来提供 JavaScript 文件。不过,在访问自己不能控制的服务器上的JavaScript 文件时则要多加小心。如果不幸遇到了怀有恶意的程序员,那他们随时都可能替换该文件中的代码。因此,如果想包含来自不同域的代码,则要么你是那个域的所有者,要么那个域的所有者值得信赖。
标签的位置
按照传统的做法,所有<script>元素都应该放在页面的<head>元素中。
这种做法的目的就是把所有外部文件(包括 CSS 文件和 JavaScript 文件)的引用都放在相同的地方。可是,在文档的<head>元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到<body>标签时才开始呈现内容)。对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题,现代 Web 应用程序一般都把全部JavaScript 引用放在<body>元素中页面内容的后面
延迟脚本
HTML 4.01 为<script>标签定义了 defer 属性。这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素中设置defer 属性,相当于告诉浏览器立即下载,但延迟执行。
但是这个脚本并不是所有的浏览器都支持,为此,把延迟脚本放在页面底部仍然是最佳选择。
异步脚本
HTML5 为<script>元素定义了 async 属性。这个属性与 defer 属性类似,都用于改变处理脚本的行为。同样与 defer 类似, async 只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为 async 的脚本并不保证按照指定它们的先后顺序执行。
基本概念
语法
区分大小写
ECMAScript 中的一切(变量、函数名和操作符)都区分大小写。这也就意味着,变量名 test 和变量名 Test 分别表示两个不同的变量,而函数名不能使用 typeof,因它是一个关键字(3.2 节介绍关键字),但 typeOf 则完全可以是一个有效的函数名。
标志符
所谓标识符,就是指变量、函数、属性的名字,或者函数的参数。标识符可以是按照下列格式规则
组合起来的一或多个字符:
- 第一个字符必须是一个字母、下划线(_)或一个美元符号($);
- 其他字符可以是字母、下划线、美元符号或数字。
按照惯例, ECMAScript 标识符采用驼峰大小写格式,也就是第一个字母小写,剩下的每个单词的首字母大写
注释
ECMAScript 使用 C 风格的注释,包括单行注释和块级注释。
// 单行注释
/*
* 这是一个多行
* (块级)注释
*/
严格模式
ECMAScript 5 引入了严格模式(strict mode)的概念。严格模式是为 JavaScript 定义了一种不同的解析与执行模型。
要在整个脚本中启用严格模式,可以在顶部添加如下代码:
"use strict";
在函数内部的上方包含这条编译指示,也可以指定函数在严格模式下执行:
function doSomething(){
"use strict";
//函数体
}
语句
ECMAScript 中的语句以一个分号结尾;如果省略分号,则由解析器确定语句的结尾,如下例所示:
var sum = a + b // 即使没有分号也是有效的语句——不推荐
var diff = a - b; // 有效的语句——推荐
虽然语句结尾的分号不是必需的,但我们建议任何时候都不要省略它。因为加上这个分号可以避免很多错误(例如不完整的输入),开发人员也可以放心地通过删除多余的空格来压缩 ECMAScript 代码(代码行结尾处没有分号会导致压缩错误)。另外,加上分号也会在某些情况下增进代码的性能,为这样解析器就不必再花时间推测应该在哪里插入分号了。
虽然条件控制语句(如 if 语句)只在执行多条语句的情况下才要求使用代码块,但最佳实践是始
终在控制语句中使用代码块——即使代码块中只有一条语句,例如:
if (test)
alert(test); // 有效但容易出错,不要使用
if (test){ // 推荐使用
alert(test);
}
关键字和保留字
ECMA-262 描述了一组具有特定用途的关键字,这些关键字可用于表示控制语句的开始或结束,或
者用于执行特定操作等。按照规则,关键字也是语言保留的,不能用作标识符。以下就是 ECMAScript的全部关键字(带*号上标的是第 5 版新增的关键字):
break do instanceof typeof
case else new var
catch finally return void
continue for switch while
debugger* function this with
default if throw
delete in try
ECMA-262 还描述了另外一组不能用作标识符的保留字。尽管保留字在这门语言中还没有任何特定的用途,但它们有可能在将来被用作关键字。以下是 ECMA-262 第 3 版定义的全部保留字:
abstract enum int short
boolean export interface static
byte extends long super
char final native synchronized
class float package throws
const goto private transient
debugger implements protected volatile
double import public
变量
有一点必须注意,即用 var 操作符定义的变量将成为定义该变量的作用域中的局部变量。也就是说,如果在函数中使用 var 定义一个变量,那么这个变量在函数退出后就会被销毁,例如:
function test(){
var message = "hi"; // 局部变量
}
test();
alert(message); // 错误!
这里,变量 message 是在函数中使用 var 定义的。当函数被调用时,就会创建该变量并为其赋值。而在此之后,这个变量又会立即被销毁,因此例子中的下一行代码就会导致错误。不过,可以下面这样省略 var 操作符,从而创建一个全局变量:
function test(){
message = "hi"; // 全局变量
}
test();
alert(message); // "hi
虽然省略 var 操作符可以定义全局变量,但这也不是我们推荐的做法。因为在局部作用域中定义的全局变量很难维护,而且如果有意地忽略了 var 操作符,也会由于相应变量不会马上就有定义而导致不必要的混乱。给未经声明的变量赋值在严格模式下会导致抛出 ReferenceError 错误。
数据类型
ECMAScript 中有 5 种简单数据类型(也称为基本数据类型):Undefined、Null、Boolean、Number和 String。还有 1 种复杂数据类型——Object,Object 本质上是由一组无序的名值对组成的。
typeof操作符
鉴于 ECMAScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型——typeof 就
是负责提供这方面信息的操作符。对一个值使用 typeof 操作符可能返回下列某个字符串:
- "undefined"——如果这个值未定义;
- "boolean"——如果这个值是布尔值;
- "string"——如果这个值是字符串;
- "number"——如果这个值是数值;
- "object"——如果这个值是对象或 null;
- "function"——如果这个值是函数。
undefined 类型
Undefined 类型只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined
Null 类型
Null 类型是第二个只有一个值的数据类型,这个特殊的值是 null。从逻辑角度来看, null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回"object"的原因。
下表给出了各种数据类型及其对应的转换规则。
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | “”(空字符串) |
Number | 任何非零数字值(包括无穷大) | 0和NaN |
Object | 任何对象 | null |
Undefined | n/a | undefined |
数值
NaN
NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)
NaN 本身有两个非同寻常的特点。首先,任何涉及 NaN 的操作(例如 NaN/10)都会返回 NaN,这个特点在多步计算中有可能导致问题。其次, NaN 与任何值都不相等,包括 NaN 本身。
布尔操作符
逻辑与
逻辑与操作可以应用于任何类型的操作数,而不仅仅是布尔值。在有一个操作数不是布尔值的情况下,逻辑与操作就不一定返回布尔值;此时,它遵循下列规则:
- 如果第一个操作数是对象,则返回第二个操作数;
- 如果第二个操作数是对象,则只有在第一个操作数的求值结果为 true 的情况下才会返回该对象;
- 如果两个操作数都是对象,则返回第二个操作数;
- 如果有一个操作数是 null,则返回 null;
- 如果有一个操作数是 NaN,则返回 NaN;
- 如果有一个操作数是 undefined,则返回 undefined。
逻辑与操作属于短路操作,即如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值。对于逻辑与操作而言,如果第一个操作数是 false,则无论第二个操作数是什么值,结果都不可能是true 了。
逻辑或
与逻辑与操作相似,如果有一个操作数不是布尔值,逻辑或也不一定返回布尔值;此时,它遵循下列规则:
- 如果第一个操作数是对象,则返回第一个操作数;
- 如果第一个操作数的求值结果为 false,则返回第二个操作数;
- 如果两个操作数都是对象,则返回第一个操作数;
- 如果两个操作数都是 null,则返回 null;
- 如果两个操作数都是 NaN,则返回 NaN;
- 如果两个操作数都是 undefined,则返回 undefined。
与逻辑与操作符相似,逻辑或操作符也是短路操作符。也就是说,如果第一个操作数的求值结果为true,就不会对第二个操作数求值了。
乘性操作符
乘法
乘法操作符由一个星号(*)表示,用于计算两个数值的乘积。其语法类似于 C,如下面的例子所示:
var result = 34 * 56;
在处理特殊值的情况下,乘法操作符遵循下列特殊的规则:
- 如果操作数都是数值,执行常规的乘法计算,即两个正数或两个负数相乘的结果还是正数,而如果只有一个操作数有符号,那么结果就是负数。如果乘积超过了 ECMAScript 数值的表示范围,则返回 Infinity 或-Infinity;
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果是 Infinity 与 0 相乘,则结果是 NaN;
- 如果是 Infinity 与非 0 数值相乘,则结果是 Infinity 或-Infinity,取决于有符号操作数
- 号;
- 如果是 Infinity 与 Infinity 相乘,则结果是 Infinity;
- 如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值,然后再应用上面的
规则
除法
除法操作符由一个斜线符号(/)表示,执行第二个操作数除第一个操作数的计算,如下面的例子所示:
var result = 66 / 11;
与乘法操作符类似,除法操作符对特殊的值也有特殊的处理规则。这些规则如下:
- 如果操作数都是数值,执行常规的除法计算,即两个正数或两个负数相除的结果还是正数,而如果只有一个操作数有符号,那么结果就是负数。如果商超过了 ECMAScript 数值的表示范围,则返回 Infinity 或-Infinity;
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果是 Infinity 被 Infinity 除,则结果是 NaN;
- 如果是零被零除,则结果是 NaN;
- 如果是非零的有限数被零除,则结果是 Infinity 或-Infinity,取决于有符号操作数的符号;
- 如果是 Infinity 被任何非零数值除,则结果是 Infinity 或-Infinity,取决于有符号操作数的符号;
- 如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值,然后再应用上面的规则
求模
求模(余数)操作符由一个百分号(%)表示,用法如下:
var result = 26 % 5; // 等于 1
与另外两个乘性操作符类似,求模操作符会遵循下列特殊规则来处理特殊的值:
- 如果操作数都是数值,执行常规的除法计算,返回除得的余数;
- 如果被除数是无穷大值而除数是有限大的数值,则结果是 NaN;
- 如果被除数是有限大的数值而除数是零,则结果是 NaN;
- 如果是 Infinity 被 Infinity 除,则结果是 NaN;
- 如果被除数是有限大的数值而除数是无穷大的数值,则结果是被除数;
- 如果被除数是零,则结果是零;
- 如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值,然后再应用上面的规则
加性操作符
加法
加法操作符(+)的用法如下所示:
var result = 1 + 2;
如果两个操作符都是数值,执行常规的加法计算,然后根据下列规则返回结果:
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果是 Infinity 加 Infinity,则结果是 Infinity;
- 如果是-Infinity 加-Infinity,则结果是-Infinity;
- 如果是 Infinity 加-Infinity,则结果是 NaN;
- 如果是+0 加+0,则结果是+0;
- 如果是-0 加-0,则结果是-0;
- 如果是+0 加-0,则结果是+0。
不过,如果有一个操作数是字符串,那么就要应用如下规则: - 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来;
- 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接
起来。 - 如果有一个操作数是对象、数值或布尔值,则调用它们的 toString()方法取得相应的字符串值,然后再应用前面关于字符串的规则。对于 undefined 和 null,则分别调用 String()函数并取得字符串"undefined"和"null"。
减法
减法操作符(-)是另一个极为常用的操作符,其用法如下所示:
var result = 2 - 1;
与加法操作符类似, ECMAScript 中的减法操作符在处理各种数据类型转换时,同样需要遵循一些特殊规则,如下所示:
- 如果两个操作符都是数值,则执行常规的算术减法操作并返回结果;
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果是 Infinity 减 Infinity,则结果是 NaN;
- 如果是-Infinity 减-Infinity,则结果是 NaN;
- 如果是 Infinity 减-Infinity,则结果是 Infinity;
- 如果是-Infinity 减 Infinity,则结果是-Infinity;
- 如果是+0 减+0,则结果是+0;
- 如果是+0 减-0,则结果是-0;
- 如果是-0 减-0,则结果是+0;
- 如果有一个操作数是字符串、布尔值、 null 或 undefined,则先在后台调用 Number()函数将其转换为数值,然后再根据前面的规则执行减法计算。如果转换的结果是 NaN,则减法的结果就是 NaN;
- 如果有一个操作数是对象,则调用对象的 valueOf()方法以取得表示该对象的数值。如果得到的值是 NaN,则减法的结果就是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法并将得到的字符串转换为数值
关系操作符
小于(<)、大于(>)、小于等于(<=)和大于等于(>=)这几个关系操作符用于对两个值进行比较,比较的规则与我们在数学课上所学的一样。这几个操作符都返回一个布尔值,如下面的例子所示:
var result1 = 5 > 3; //true
var result2 = 5 < 3; //false
RelationalOperatorsExample01.htm 中包含本节所有的代码片段
与 ECMAScript 中的其他操作符一样,当关系操作符的操作数使用了非数值时,也要进行数据转换或完成某些奇怪的操作。以下就是相应的规则。
- 如果两个操作数都是数值,则执行数值比较。
- 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值。
- 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较。
- 如果一个操作数是对象,则调用这个对象的 valueOf()方法,用得到的结果按照前面的规则执行比较。如果对象没有 valueOf()方法,则调用 toString()方法,并用得到的结果根据前面的规则执行比较。
- 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较
var result = "a" < 3; // false,因为"a"被转换成了 NaN
由于字母"a"不能转换成合理的数值,因此就被转换成了 NaN。根据规则,任何操作数与 NaN 进行关系比较,结果都是 false。于是,就出现了下面这个有意思的现象:
var result1 = NaN < 3; //false
var result2 = NaN >= 3; //false
按照常理,如果一个值不小于另一个值,则一定是大于或等于那个值。然而,在与 NaN 进行比较时,这两个比较操作的结果都返回了 false。
相等操作符
相等和不相等
在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:
- 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false 转换为 0,而true 转换为 1;
- 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值;
- 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法,用得到的基本类
型值按照前面的规则进行比较;
这两个操作符在进行比较时则要遵循下列规则。
- null 和 undefined 是相等的。
- 要比较相等性之前,不能将 null 和 undefined 转换成其他任何值。
- 如果有一个操作数是 NaN,则相等操作符返回 false,而不相等操作符返回 true。重要提示:即使两个操作数都是 NaN,相等操作符也返回 false;因为按照规则, NaN 不等于 NaN。
- 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true;否则,返回 false。
除了在比较之前不转换操作数之外,全等和不全等操作符与相等和不相等操作符没有什么区别。全等操作符由 3 个等于号(===)表示,它只在两个操作数未经转换就相等的情况下返回 true,如下面的例子所示:
var result1 = ("55" == 55); //true,因为转换后相等
var result2 = ("55" === 55); //false,因为不同的数据类型不相等
break和continue语句
break 和 continue 语句都可以与 label 语句联合使用,从而返回代码中特定的位置。这种联合使用的情况多发生在循环嵌套的情况下,如下面的例子所示:
var num = 0;
outermost:
for (var i=0; i < 10; i++) {
for (var j=0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost;
}
num++;
}
}
alert(num); //55
在这个例子中, outermost 标签表示外部的 for 语句。如果每个循环正常执行 10 次,则 num++语句就会正常执行 100 次。换句话说,如果两个循环都自然结束, num 的值应该是 100。但内部循环中的 break 语句带了一个参数:要返回到的标签。添加这个标签的结果将导致 break 语句不仅会退出内部的 for 语句(即使用变量 j 的循环),而且也会退出外部的 for 语句(即使用变量 i 的循环)。为此,当变量 i 和 j 都等于 5 时, num 的值正好是 55。
同样, continue 语句也可以像这样与 label 语句联用,如下面的例子所示:
var num = 0;
outermost:
for (var i=0; i < 10; i++) {
for (var j=0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost;
}
num++;
}
}
alert(num); //95
在这种情况下, continue 语句会强制继续执行循环——退出内部循环,执行外部循环。当 j 是 5时, continue 语句执行,而这也就意味着内部循环少执行了 5 次,因此 num 的结果是 95。
ps:continue+label跳出一个循环,这和break的作用感觉是一样的?,这种标签显示编码中还是比较少用,表示看不懂。另外如果有>2层的玄幻,contine+label会出现什么情况,不知道......
swith
var num = 25;
switch (true) {
case num < 0:
alert("Less than 0.");
break;
case num >= 0 && num <= 10:
alert("Between 0 and 10.");
break;
case num > 10 && num <= 20:
alert("Between 10 and 20.");
break;
default:
alert("More than 20.");
}
switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换(例如,字符串"10"不等于数值 10)。
ps:这语法糖厉害,还可以在case上做运算的,牛逼
函数
理解参数
ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。 ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。之所以会这样,原因是 ECMAScript 中的参数在内部是用一个数组来表示的函数接收到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。如果这个数组中不包含任何元素,无所谓;如果包含多个元素,也没有问题。实际上,在函数体内可以通过 argument对象来访问这个参数数组,从而获取传递给函数的每一个参数。
通过访问 arguments 对象的 length 属性可以获知有多少个参数传递给了函数。下面这个函数会在每次被调用时,输出传入其中的参数个数:
function howManyArgs() {
alert(arguments.length);
}
howManyArgs("string", 45); //2
howManyArgs(); //0
howManyArgs(12); //1
另一个与参数相关的重要方面,就是 arguments 对象可以与命名参数一起使用,如下面的例子所示:
function doAdd(num1, num2) {
if(arguments.length == 1) {
alert(num1 + 10);
} else if (arguments.length == 2) {
alert(arguments[0] + num2);
}
}
在重写后的这个 doAdd()函数中,两个命名参数都与 arguments 对象一起使用。由于 num1 的值与 arguments[0]的值相同,因此它们可以互换使用(当然, num2 和 arguments[1]也是如此)。
没有重载
ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。如前所述, ECMAScirpt函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。如果在 ECMAScript 中定义了两个名字相同的函数,则该名字只属于后定义的函数。请看下面的例子:
function addSomeNumber(num){
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
var result = addSomeNumber(100); //300
ps:这两个分明是相同的函数定义,在java中,是会抛异常的,只是在js中会覆盖而已。另外如果我定义一个 addSomeNuber(num1,num2)的函数,按照作者的意思也不算是重载,因为对于js来说入参都是arguments数组,但是从使用者来看,就是重载?
变量、作用域和内存问题
基本类型和引用类型
ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。 基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。
动态的属性
对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。
var person = new Object();
person.name = "Nicholas";
alert(person.name); //"Nicholas"
但是,我们不能给基本类型的值添加属性,尽管这样做不会导致任何错误。比如:
var name = "Nicholas";
name.age = 27;
alert(name.age); //undefined
在这个例子中,我们为字符串 name 定义了一个名为 age 的属性,并为该属性赋值 27。但在下一行访问这个属性时,发现该属性不见了。这说明只能给引用类型值动态地添加属性。
复制变量值
复制基本变量是两个相互独立的内存,操作互不影响。
当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。
传递参数
ECMAScript 中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。
function addTen(num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
alert(count); //20,没有变化
alert(result); //30
这里的函数 addTen()有一个参数 num,而参数实际上是函数的局部变量。在调用这个函数时,变量 count 作为参数被传递给函数,这个变量的值是 20。于是,数值 20 被复制给参数 num 以便在 addTen()中使用。在函数内部,参数 num 的值被加上了 10,但这一变化不会影响函数外部的 count 变量。参数num 与变量 count 互不相识,它们仅仅是具有相同的值。
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
以上代码中创建一个对象,并将其保存在了变量 person 中。然后,这个变量被传递到 setName()函数中之后就被复制给了 obj。在这个函数内部, obj 和 person 引用的是同一个对象。换句话说,即使这个变量是按值传递的, obj 也会按引用来访问同一个对象。于是,当在函数内部为 obj 添加 name属性后,函数外部的 person 也将有所反映;因为 person 指向的对象在堆内存中只有一个,而且是全局对象。
有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明
参数是按引用传递的。为了证明对象是按值传递的,我们再看一看下面这个经过修改的例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
这个例子与前一个例子的唯一区别,就是在 setName()函数中添加了两行代码:一行代码为 obj重新定义了一个对象,另一行代码为该对象定义了一个带有不同值的 name 属性。在把 person 传递给setName()后,其 name 属性被设置为"Nicholas"。然后,又将一个新对象赋给变量 obj,同时将其 name属性设置为"Greg"。如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值为"Greg"的新对象。但是,当接下来再访问 person.name 时,显示的值仍然是"Nicholas"。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。
执行环境和作用域
JavaScript 没有块级作用域经常会导致理解上的困惑。在其他类 C 的语言中,由花括号封闭的代码块都有自己的作用域(如果用 ECMAScript 的话来讲,就是它们自己的执行环境),因而支持根据条件来定义变量。
for (var i=0; i < 10; i++){
doSomething(i);
}
alert(i); //10
对于有块级作用域的语言来说, for 语句初始化变量的表达式所定义的变量,只会存在于循环的环境之中。而对于 JavaScript 来说,由 for 语句创建的变量 i 即使在 for 循环执行结束后,也依旧会存在于循环外部的执行环境中。
声明变量
使用 var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
var result = add(10, 20); //30
alert(sum); //由于 sum 不是有效的变量,因此会导致错误
垃圾收集
JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存
引用类型
Object
创建 Object 实例的方式有两种。第一种是使用 new 操作符后跟 Object 构造函数,如下所示:
var person = new Object();
person.name = "Nicholas";
person.age = 29;
另一种方式是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。下面这个例子就使用了对象字面量语法定义了与前面那个例子中相同的person 对象:
var person = {
name : "Nicholas",
age : 29
};
一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言中通用的语法。不过,在 JavaScript 也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要问的属性以字符串的形式放在方括号中,如下面的例子所示。
alert(person["name"]); //"Nicholas"
alert(person.name); //"Nicholas"
从功能上看,这两种访问对象属性的方法没有任何区别。但方括号语法的主要优点是可以通过变量来访问属性,例如:
var propertyName = "name";
alert(person[propertyName]); //"Nicholas"
如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法。例如:
person["first name"] = "Nicholas";
由于"first name"中包含一个空格,所以不能使用点表示法来访问它。然而,属性名中是可以包含非字母非数字的,这时候就可以使用方括号表示法来访问它们。通常,除非必须使用变量来访问属性,否则我们建议使用点表示法
Array类型
数组的 length 属性很有特点——它不是只读的。因此,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。请看下面的例子:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors.length = 2;
alert(colors[2]); //undefined
这个例子中的数组 colors 一开始有 3 个值。将其 length 属性设置为 2 会移除最后一项(位置为2 的那一项),结果再访问 colors[2]就会显示 undefined 了。如果将其 length 属性设置为大于数组项数的值,则新增的每一项都会取得 undefined 值,如下所示:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors.length = 4;
alert(colors[3]); //undefined
利用 length 属性也可以方便地在数组末尾添加新项,如下所示:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors[colors.length] = "black"; //(在位置 3)添加一种颜色
colors[colors.length] = "brown"; //(在位置 4)再添加一种颜色
当把一个值放在超出当前数组大小的位置上时,数组就会重新计算其长度值,即长度值等于最后一项的索引加 1,如下面的例子所示:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors[99] = "black"; // (在位置 99)添加一种颜色
alert(colors.length); // 100
栈方法
ECMAScript 数组也提供了一种让数组的行为类似于其他数据结构的方法。具体说来,数组可以表现得就像栈一样,后者是一种可以限制插入和删除项的数据结构。栈是一种 LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部。 ECMAScript 为数组专门提供了 push()和 pop()方法,以便实现类似栈的行为
var colors = new Array(); // 创建一个数组
var count = colors.push("red", "green"); // 推入两项
alert(count); //2
count = colors.push("black"); // 推入另一项
alert(count); //3
var item = colors.pop(); // 取得最后一项
alert(item); //"black"
alert(colors.length); //2
队列方法
栈数据结构的访问规则是 LIFO(后进先出),而队列数据结构的访问规则是 FIFO(First-In-First-Out,先进先出)。队列在列表的末端添加项,从列表的前端移除项。由于 push()是向数组末端添加项的方法,因此要模拟队列只需一个从数组前端取得项的方法。实现这一操作的数组方法就是 shift(),它能够移除数组中的第一个项并返回该项,同时将数组长度减 1。结合使用 shift()和 push()方法,可以像使用队列一样使用数组。
var colors = new Array(); //创建一个数组
var count = colors.push("red", "green"); //推入两项
alert(count); //2
count = colors.push("black"); //推入另一项
alert(count); //3
var item = colors.shift(); //取得第一项
alert(item); //"red"
alert(colors.length); //2
ECMAScript 还为数组提供了一个 unshift()方法。顾名思义,unshift()与 shift()的用途相反:它能在数组前端添加任意个项并返回新数组的长度。因此,同时使用 unshift()和 pop()方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项,如下面的例子所示。
var colors = new Array(); //创建一个数组
var count = colors.unshift("red", "green"); //推入两项
alert(count); //2
count = colors.unshift("black"); //推入另一项
alert(count); //3
var item = colors.pop(); //取得最后一项
alert(item); //"green"
alert(colors.length); //2
重排序方法
数组中已经存在两个可以直接用来重排序的方法: reverse()和 sort()。
这个比较函数可以适用于大多数数据类型,只要将其作为参数传递给 sort()方法即可,如下面这
个例子所示。
var values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); //0,1,5,10,15
迭代方法
以下是数组的 5 个迭代方法的作用。
- every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
- filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
- forEach():对数组中的每一项运行给定函数。这个方法没有返回值。
- map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
- some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。
Function类型
说起来 ECMAScript 中什么最有意思,我想那莫过于函数了——而有意思的根源,则在于函数实际上是对象。每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。函数通常是使用函
数声明语法定义的,如下面的例子所示。
function sum (num1, num2) {
return num1 + num2;
}
这与下面使用函数表达式定义函数的方式几乎相差无几。
var sum = function(num1, num2){
return num1 + num2;
};
函数声明与函数表达式
解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。请看下面的例子。
alert(sum(10,10));
function sum(num1, num2){
return num1 + num2;
}
alert(sum(10,10));
var sum = function(num1, num2){
return num1 + num2;
};
以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,由于第一行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。
作为值的函数
因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
函数内部属性
虽然 arguments 的主要用途是保存函数参数,但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。
请看下面这个非常经典的阶乘函数。
function factorial(num){
if (num <=1) {
return 1;
} else {
return num * factorial(num-1)
}
}
定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用 arguments.callee。
function factorial(num){
if (num <=1) {
return 1;
} else {
return num * arguments.callee(num-1)
}
}
函数属性和方法
前面曾经提到过, ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性: length 和 prototype。其中, length 属性表示函数希望接收的命名参数的个数,如下面的例子所示。
function sayName(name){
alert(name);
}
function sum(num1, num2){
return num1 + num2;
}
function sayHi(){
alert("hi");
}
alert(sayName.length); //1
alert(sum.length); //2
alert(sayHi.length); //0
在 ECMAScript 核心所定义的全部属性中,最耐人寻味的就要数 prototype 属性了。对于ECMAScript 中的引用类型而言, prototype 是保存它们所有实例方法的真正所在。换句话说,诸如toString()和 valueOf()等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时, prototype 属性的作用是极为重要的
每个函数都包含两个非继承而来的方法: apply()和 call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。首先, apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是arguments 对象。例如:
function sum(num1, num2){
return num1 + num2;
}
function callSum1(num1, num2){
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2){
return sum.apply(this, [num1, num2]); // 传入数组
}
alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20
call()方法与 apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call()方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来,如下面的例子所示。
function sum(num1, num2){
return num1 + num2;
}
function callSum(num1, num2){
return sum.call(this, num1, num2);
}
alert(callSum(10,10)); //20
事实上,传递参数并非 apply()和 call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。下面来看一个例子。
window.color = "red";
var o = { color: "blue" };
function sayColor(){
alert(this.color);
}
sayColor(); //red
sayColor.call(this); //red
sayColor.call(window); //red
sayColor.call(o); //blue
ECMAScript 5 还定义了一个方法: bind()。这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind()函数的值。例如:
window.color = "red";
var o = { color: "blue" };
function sayColor(){
alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); //blue
Global对象
Global(全局)对象可以说是 ECMAScript 中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。 ECMAScript 中的 Global 对象在某种意义上是作为一个终极的“兜底儿对象”来定义的。换句话说,不属于任何其他对象的属性和方法,最终都是它的属性和方法。事实上,没有全局变量或全局函数;所有在全局作用域中定义的属性和函数,都是 Global 对象的属性。本书前面介绍过的那些函数,诸如 isNaN()、isFinite()、parseInt()以及 parseFloat(),实际上全都是 Global对象方法。
**URI 编码方法 **
encodeURI()主要用于整个 URI(例如, http://www.wrox.com/illegal value.htm),而encodeURIComponent()主要用于对 URI 中的某一段(例如前面 URI 中的 illegal value.htm)进行编码。它们的主要区别在于, encodeURI()不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而 encodeURIComponent()则会对它发现的任何非标准字符进行编码。
eval()方法
eval("alert('hi')");
这行代码的作用等价于下面这行代码:
alert("hi");
当解析器发现代码中调用 eval()方法时,它会将传入的参数当作实际的 ECMAScript 语句来解析,然后把执行结果插入到原位置。通过 eval()执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过 eval()执行的代码可以引用在包含环境中定义的变量,举个例子:
``js
var msg = "hello world";
eval("alert(msg)"); //"hello world"
可见,变量 msg 是在 eval()调用的环境之外定义的,但其中调用的 alert()仍然能够显示"helloworld"。这是因为上面第二行代码最终被替换成了一行真正的代码。同样地,我们也可以在 eval()调用中定义一个函数,然后再在该调用的外部代码中引用这个函数:
```js
eval("function sayHi() { alert('hi'); }");
sayHi();
window对象
ECMAScript 虽然没有指出如何直接访问 Global 对象,但 Web 浏览器都是将这个全局对象作为window 对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了 window对象的属性。
JavaScript中的 window 对象除了扮演 ECMAScript规定的 Global 对象的角色外,
还承担了很多别的任务
面向对象的程序设计
理解对象
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
};
这个例子中的 person 对象与前面例子中的 person 对象是一样的,都有相同的属性和方法。
属性类型
数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。
- [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
- [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
- [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
- [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是: configurable、 enumerable、 writable 和 value。设置其中的一或多个值,可以修改对应的特性值。例如:
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
把 configurable 设置为 false,表示不能从对象中删除属性。如果对这个属性调用 delete,则在非严格模式下什么也不会发生,而在严格模式下会导致错误。而且,一旦把属性定义为不可配置的,就不能再把它变回可配置了。
访问器属性
访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。
- [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特
性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为
true。 - [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这
个特性的默认值为 true。 - [[Get]]:在读取属性时调用的函数。默认值为 undefined。
- [[Set]]:在写入属性时调用的函数。默认值为 undefined。
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
定义多个属性
ECMAScript 5 又定义了一个 Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
以上代码在 book 对象上定义了两个数据属性(_year 和 edition)和一个访问器属性(year)。最终的对象与上一节中定义的对象相同。唯一的区别是这里的属性都是在同一时间创建的。
创建对象
虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的种变体。
工厂模式
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
函数 createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式
例如,可以使用构造函数模式将前面的例子重写如下。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在这个例子中, Person()函数取代了 createPerson()函数。我们注意到, Person()中的代码
除了与 createPerson()中相同的部分外,还存在以下不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句。
此外,还应该注意到函数名 Person 使用的是大写字母 P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他 OO 语言,主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
- 一个新对象;
- 造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 构造函数中的代码(为这个新对象添加属性);
- 新对象。
在前面例子的最后, person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都有一个 constructor(构造函数)属性,该属性指向 Person,如下所示。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
函数的问题
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中, person1 和 person2 都有一个名为 sayName()的方法,但那两个方法不是同一个 Function 的实例。不要忘了——ECMAScript 中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
}
然而,创建两个完成同样任务的 Function 实例的确没有必要;况且有 this 对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
** 理解原型对象**
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示。
使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。来看下面这个例子。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //"Greg"—— 来自实例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"—— 来自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //"Nicholas"—— 来自原型
alert(person1.hasOwnProperty("name")); //false
通过使用 hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了。调用 person1.hasOwnProperty( "name")时,只有当 person1 重写 name 属性后才会返回 true,因为只有这时候 name 才是一个实例属性,而非原型属性。
原型对象的问题
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子。
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
在此, Person.prototype 对象有一个名为 friends 的属性,该属性包含一个字符串数组。然后,创建了 Person 的两个实例。接着,修改了 person1.friends 引用的数组,向数组中添加了一个字符串。由于 friends 数组存在于 Person.prototype 而非 person1 中,所以刚刚提到的修改也会通过person2.friends(与 person1.friends 指向同一个数组)反映出来。假如我们的初衷就是像这样在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。下面的代码重写了前面的例子。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
动态原型模式
function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
lert(this.name);
};
}
}
这里只在 sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中, if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if 语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。
妥构造函数模式
道格拉斯·克罗克福德(Douglas Crockford)发明了 JavaScript 中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的 Person 构造函数重写如下。
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name);
};
//返回对象
return o;
}
这样,变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境——例如, ADsafe(www.adsafe.org)和 Caja(http://code.google.com/p/google-caja/)提供的环境——下使用。
ps:一般场景下是用不到的吧
继承
原型链
简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
以上代码定义了两个类型: SuperType 和 SubType。每个类型分别有一个属性和一个方法。它们的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。在确立了继承关系之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 SuperType 的属性和方法的基础上又添加了一个新方法。
原型链的问题
最主要的问题来自包含引用类型值的原型。想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
借用构造函数
开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//继承了 SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"
代码中加粗的那一行代码“借调”了超类型的构造函数。通过使用 call()方法(或 apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会具有自己的 colors 属性的副本了。
构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使的。
组合继承
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
//继承属性
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且, instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。
第7章 函数表达式
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。虽然像 V8 等优化后的 JavaScript 引擎会尝试回收被闭包占用的内存,但请大家还是要慎重使用闭包
闭包的副作用
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个函数都返回 10。因为每个函数的作用域链中都 保 存 着 createFunctions() 函 数 的 活 动 对 象 , 所 以 它 们 引 用 的 都 是 同 一 个 变 量 i 。 当createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是 10。
我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数 num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量 i。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建并返回了一个访问 num 的闭包。这样一来, result 数组中的每个函数都有自己num 变量的一个副本,因此就可以返回各自不同的数值了。
关于this对象
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
每个函数在被调用时都会自动取得两个特殊变量: this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量
ps:object.getNameFunc()()
,认真一看就知道是先获得了一个function,紧接着又在全局环境调用了该方法,所以this,肯定是指向window
不过,把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
代码中突出的行展示了这个例子与前一个例子之间的不同之处。在定义匿名函数之前,我们把 this对象赋值给了一个名叫 that 的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声名的一个变量。即使在函数返回之后, that 也仍然引用着 object,所以调用object.getNameFunc()()就返回了"My Object"。
模仿块级作用域
用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示。
(function(){
//这里是块级作用域
})();
以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。
ps:专业名称是即时运行函数?
模块函数
模块模式(modulepattern)则是为单例创建私有变量和特权方法。如下就是一个单例的例子
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return privateVariable;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
第8章 BOM
window 对象
BOM 的核心对象是 window,它表示浏览器的一个实例。在浏览器中, window 对象有双重角色,它既是通过 JavaScript 访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象。这意味着在网页中定义的任何一个对象、变量和函数,都以 window 作为其 Global 对象,因此有权访问parseInt()等方法。
全局作用域
由于 window 对象同时扮演着 ECMAScript 中 Global 对象的角色,因此所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。
抛开全局变量会成为 window 对象的属性不谈,定义全局变量与在 window 对象上直接定义属性还是有一点差别:全局变量不能通过 delete 操作符删除,而直接在 window 对象上的定义的属性可以。例如:
var age = 29;
window.color = "red";
//在 IE < 9 时抛出错误,在其他所有浏览器中都返回 false
delete window.age;
//在 IE < 9 时抛出错误,在其他所有浏览器中都返回 true
delete window.color; //returns true
alert(window.age); //29
alert(window.color); //undefined
使用 var 语句添加的 window 属性有一个名为[[Configurable]]的特性,这个特性的值被设置为false,因此这样定义的属性不可以通过delete 操作符删除。
窗口关系及框架
如果页面中包含框架,则每个框架都拥有自己的 window 对象,并且保存在 frames 集合中。在 frames集合中,可以通过数值索引(从 0 开始,从左至右,从上到下)或者框架名称来访问相应的 window 对象。每个 window 对象都有一个 name 属性,其中包含框架的名称。
<html>
<head>
<title>Frameset Example</title>
</head>
<frameset rows="160,*">
<frame src="frame.htm" name="topFrame">
<frameset cols="50%,50%">
<frame src="anotherframe.htm" name="leftFrame">
<frame src="yetanotherframe.htm" name="rightFrame">
</frameset>
</frameset>
</html>
以上代码创建了一个框架集,其中一个框架居上,两个框架居下。对这个例子而言,可以通过window.frames[0]或者 window.frames["topFrame"]来引用上方的框架。不过,恐怕你最好使用top 而非 window 来引用这些框架(例如,通过 top.frames[0])。
top 对象始终指向最高(最外)层的框架,也就是浏览器窗口。使用它可以确保在一个框架中正确地访问另一个框架。因为对于在一个框架中编写的任何代码来说,其中的 window 对象指向的都是那个框架的特定实例,而非最高层的框架。
与 top 相对的另一个 window 对象是 parent。顾名思义, parent(父)对象始终指向当前框架的直接上层框架。在某些情况下, parent 有可能等于 top;但在没有框架的情况下, parent 一定等于top(此时它们都等于 window)。
导航和打开窗口
//等同于< a href="http://www.wrox.com" target="topFrame"></a>
window.open("http://www.wrox.com/", "topFrame");
调用这行代码,就如同用户单击了 href 属性为 http://www.wrox.com/,target 属性为"topFrame"
的链接。如果有一个名叫"topFrame"的窗口或者框架,就会在该窗口或框架加载这个 URL;否则,就会创建一个新窗口并将其命名为"topFrame"。此外,第二个参数也可以是下列任何一个特殊的窗口名称: _self、 _parent、 _top 或_blank。
弹出窗口
如果给 window.open()传递的第二个参数并不是一个已经存在的窗口或框架,那么该方法就会根据在第三个参数位置上传入的字符串创建一个新窗口或新标签页。如果没有传入第三个参数,那么就会打开一个带有全部默认设置(工具栏、地址栏和状态栏等)的新浏览器窗口(或者打开一个新标签页——根据浏览器设置)。在不打开新窗口的情况下,会忽略第三个参数。
window.open("http://www.wrox.com/","wroxWindow",
"height=400,width=400,top=10,left=10,resizable=yes");
这行代码会打开一个新的可以调整大小的窗口,窗口初始大小为 400×400 像素,并且距屏幕上沿和左边各 10 像素。
间歇调用与超时调用
//推荐的调用方式
setTimeout(function() {
alert("Hello world!");
}, 1000);
第二个参数是一个表示等待多长时间的毫秒数,但经过该时间后指定的代码不一定会执行。JavaScript 是一个单线程序的解释器,因此一定时间内只能执行一段代码。为了控制要执行的码,就有一个 JavaScript 任务队列。这些任务会按照将它们添加到队列的顺序执行。 setTimeout()的第二个参数告诉 JavaScript 再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。
调用 setTimeout()之后,该方法会返回一个数值 ID,表示超时调用。这个超时调用 ID 是计划执行代码的唯一标识符,可以通过它来取消超时调用。要取消尚未执行的超时调用计划,可以调用clearTimeout()方法并将相应的超时调用 ID 作为参数传递给它,如下所示。
//设置超时调用
var timeoutId = setTimeout(function() {
alert("Hello world!");
}, 1000);
//注意:把它取消
clearTimeout(timeoutId);
间歇调用与超时调用类似,只不过它会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。
//推荐的调用方式
setInterval (function() {
alert("Hello world!");
}, 10000);
一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。而像前面示例中那样使用超时调用,则完全可以避免这一点。所以,最好不要使用间歇调用。
var num = 0;
var max = 10;
function incrementNumber() {
num++;
//如果执行次数未达到 max 设定的值,则设置另一次超时调用
if (num < max) {
setTimeout(incrementNumber, 500);
} else {
alert("Done");
}
}
setTimeout(incrementNumber, 500);
系统对话框
浏览器通过 alert()、 confirm()和 prompt()方法可以调用系统对话框向用户显示消息。系统对话框与在浏览器中显示的网页没有关系,也不包含 HTML。它们的外观由操作系统及(或)浏览器设置决定,而不是由 CSS 决定。此外,通过这几个方法打开的对话框都是同步和模态的。也就是说,显示这些对话框的时候代码会停止执行,而关掉这些对话框后代码又会恢复执行。
location 对象
location 是最有用的 BOM 对象之一,它提供了与当前窗口中加载的文档有关的信息,还提供了一些导航功能。事实上, location 对象是很特别的一个对象,因为它既是 window 对象的属性,也是document 对象的属性;换句话说, window.location 和 document.location 引用的是同一个对象。location 对象的用处不只表现在它保存着当前文档的信息,还表现在它将 URL 解析为独立的片段,让开发人员可以通过不同的属性访问这些片段。
使用 location 对象可以通过很多方式来改变浏览器的位置。首先,也是最常用的方式,就是使用assign()方法并为其传递一个 URL,如下所示。
location.assign("http://www.wrox.com");
这样,就可以立即打开新 URL 并在浏览器的历史记录中生成一条记录。如果是将 location.href或 window.location 设置为一个 URL 值,也会以该值调用 assign()方法。例如,下列两行代码与显式调用 assign()方法的效果完全一样。
window.location = "http://www.wrox.com";
location.href = "http://www.wrox.com";
在这些改变浏览器位置的方法中,最常用的是设置 location.href 属性。
每次修改 location 的属性hash、search、hostname、pathname、port等(hash 除外),页面都会以新 URL 重新加载。
与位置有关的最后一个方法是 reload(),作用是重新加载当前显示的页面。
location.reload(); //重新加载(有可能从缓存中加载)
location.reload(true); //重新加载(从服务器重新加载)
navigator 对象
最早由 Netscape Navigator 2.0 引入的 navigator 对象,现在已经成为识别客户端浏览器的事实标准。虽然其他浏览器也通过其他方式提供了相同或相似的信息(例如, IE 中的 window.clientInformation 和 Opera 中的 window.opera),但 navigator 对象却是所有支持 JavaScript 的浏览器所共有的。
screen 对象
JavaScript 中有几个对象在编程中用处不大,而 screen 对象就是其中之一。 screen 对象基本上只用来表明客户端的能力,其中包括浏览器窗口外部的显示器的信息,如像素宽度和高度等。
history
history 对象保存着用户上网的历史记录,从窗口被打开的那一刻算起。因为 history 是 window
对象的属性,因此每个浏览器窗口、每个标签页乃至每个框架,都有自己的 history 对象与特定的window 对象关联。出于安全方面的考虑,开发人员无法得知用户浏览过的 URL。
使用 go()方法可以在用户的历史记录中任意跳转,可以向后也可以向前。
//后退一页
history.go(-1);
//前进一页
history.go(1);
//前进两页
history.go(2);
//跳转到最近的 wrox.com 页面
history.go("wrox.com");
//跳转到最近的 nczonline.net 页面
history.go("nczonline.net");
//后退一页
history.back();
//前进一页
history.forward();
客户端检测
检测 Web 客户端的手段很多,而且各有利弊。但最重要的还是要知道,不到万不得已,就不要使用客户端检测。只要能找到更通用的方法,就应该优先采用更通用的方法。一言以蔽之,先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。
能力检测
能力检测的基本模式如下:
if (object.propertyInQuestion){
//使用 object.propertyInQuestion
}
举例来说, IE5.0 之前的版本不支持 document.getElementById()这个 DOM 方法。尽管可以使用非标准的 document.all 属性实现相同的目的,但 IE 的早期版本中确实不存在 document.getElementById()。于是,也就有了类似下面的能力检测代码:
function getElement(id){
if (document.getElementById){
return document.getElementById(id);
} else if (document.all){
return document.all[id];
} else {
throw new Error("No way to retrieve element!");
}
}
第二个重要的概念就是必须测试实际要用到的特性。一个特性存在,不一定意味着另一个特性也存在。来看一个例子:
function getWindowWidth(){
if (document.all){ //假设是 IE
return document.documentElement.clientWidth; //错误的用法!!!
} else {
return window.innerWidth;
}
}
更可靠的能力检测
利用类型转换来确定某个对象成员是否存在,但这样你还是不知道该成员是不是你想要的。来看下面的函数,它用来确定一个对象是否支持排序。
//不要这样做!这不是能力检测——只检测了是否存在相应的方法
function isSortable(object){
return !!object.sort;
}
这个函数通过检测对象是否存在 sort()方法,来确定对象是否支持排序。问题是,任何包含 sort
属性的对象也会返回 true。
var result = isSortable({ sort: true });
检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是检测 sort 是不是一个函数。
//这样更好:检查 sort 是不是函数
function isSortable(object){
`return typeof object.sort == "function";
}
在浏览器环境下测试任何对象的某个特性是否存在,要使用下面这个函数。
//作者: Peter Michaux
function isHostMethod(object, property) {
var t = typeof object[property];
return t=='function' ||
(!!(t=='object' && object[property])) ||
t=='unknown';
}
可以像下面这样使用这个函数:
result = isHostMethod(xhr, "open"); //true
result = isHostMethod(xhr, "foo"); //false
能力检测,不是浏览器检测
检测某个或某几个特性并不能够确定浏览器。下面给出的这段代码(或与之差不多的代码)可以在许多网站中看到,这种“浏览器检测”代码就是错误地依赖能力检测的典型示例。
//错误!还不够具体
var isFirefox = !!(navigator.vendor && navigator.vendorSub);
//错误!假设过头了
var isIE = !!(document.all && document.uniqueID);
实际上,根据浏览器不同将能力组合起来是更可取的方式。如果你知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所有相关特性,而不要分别检测。看下面的例子。
//确定浏览器是否支持 Netscape 风格的插件
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
//确定浏览器是否具有 DOM1 级规定的能力
var hasDOM1 = !!(document.getElementById && document.createElement &&
document.getElementsByTagName);
怪癖检测
与能力检测类似, 怪癖检测(quirks detection)的目标是识别浏览器的特殊行为。但与能力检测确认浏览器支持什么能力不同,怪癖检测是想要知道浏览器存在什么缺陷(“怪癖”也就是 bug)。这通常需要运行一小段代码,以确定某一特性不能正常工作。
一般来说,“怪癖”都是个别浏览器所独有的,而且通常被归为 bug。在相关浏览器的新版本中,这些问题可能会也可能不会被修复。由于检测“怪癖”涉及运行代码,因此我们建议仅检测那些对你有直接影响的“怪癖”,而且最好在脚本一开始就执行此类检测。
用户代理检测
第三种,也是争议最大的一种客户端检测技术叫做用户代理检测。用户代理检测通过检测用户代理字符串来确定实际使用的浏览器。在每一次 HTTP 请求过程中,用户代理字符串是作为响应首部发送的,而且该字符串可以通过 JavaScript 的 navigator.userAgent 属性访问。在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用而且广为接受的做法。而在客户端,用户代理检测一般被当作一种万不得已才用的做法,其优先级排在能力检测和(或)怪癖检测之后。
用户代理字符串检测技术
考虑到历史原因以及现代浏览器中用户代理字符串的使用方式,通过用户代理字符串来检测特定的浏览器并不是一件轻松的事。因此,首先要确定的往往是你需要多么具体的浏览器信息。一般情况下,知道呈现引擎和最低限度的版本就足以决定正确的操作方法了。例如,我们不推荐使用下列代码:
if (isIE6 || isIE7) { //不推荐!!!
//代码
}
这个例子是想要在浏览器为 IE6 或 IE7 时执行相应代码。这种代码其实是很脆弱的,因为它要依据特定的版本来决定做什么。如果是 IE8 怎么办呢?只要 IE 有新版本出来,就必须更新这些代码。不过,像下面这样使用相对版本号则可以避免此问题:
if (ieVer >=6){
//代码
}
这个例子首先检测 IE 的版本号是否至少等于 6,如果是则执行相应操作。这样就可以确保相应的代码将来照样能够起作用
** 识别呈现引擎**
如前所述,确切知道浏览器的名字和版本号不如确切知道它使用的是什么呈现引擎。如果 Firefox、Camino 和 Netscape 都使用相同版本的 Gecko,那它们一定支持相同的特性。类似地,不管是什么浏览器,只要它跟 Safari 3 使用的是同一个版本的 WebKit,那么该浏览器也就跟 Safari 3 具备同样的功能。因此,我们要编写的脚本将主要检测五大呈现引擎: IE、 Gecko、 WebKit、 KHTML 和 Opera。
这个网上应该已经有现成的完整代码了。不过如果要适配各大平台,各大系统,各级这开发量不少啊,很可能公司都是值适配某一特定的浏览器。
第10章 DOM
DOM(文档对象模型)是针对 HTML 和 XML 文档的一个 API(应用程序编程接口)。 DOM 描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。
节点层次
DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。节点分为几种不同的类型,每种类型分别表示文档中不同的信息及(或)标记。每个节点都拥有各自的特点、数据和方法,另外也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点为根节点的树形结构。以下面的 HTML 为例:
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
Node 类型
DOM1 级定义了一个 Node 接口,该接口将由 DOM 中的所有节点类型实现。这个 Node 接口在JavaScript 中是作为 Node 类型实现的;除了 IE 之外,在其他所有浏览器中都可以访问到这个类型。JavaScript 中的所有节点类型都继承自 Node 类型,因此所有节点类型都共享着相同的基本属性和方法。
每个节点都有一个 nodeType 属性,用于表明节点的类型。节点类型由在 Node 类型中定义的下列
12 个数值常量来表示,任何节点类型必居其一:
- Node.ELEMENT_NODE(1);
- Node.ATTRIBUTE_NODE(2);
- Node.TEXT_NODE(3);
- Node.CDATA_SECTION_NODE(4);
- Node.ENTITY_REFERENCE_NODE(5);
- Node.ENTITY_NODE(6);
- Node.PROCESSING_INSTRUCTION_NODE(7);
- Node.COMMENT_NODE(8);
- Node.DOCUMENT_NODE(9);
- Node.DOCUMENT_TYPE_NODE(10);
- Node.DOCUMENT_FRAGMENT_NODE(11);
- Node.NOTATION_NODE(12)。
文档写入
这个能力体现在下列 4 个方法中: write()、 writeln()、 open()和 close()。其中, write()和 writeln()方法都接受一个字符串参数,即要写入到输出流中的文本。 write()会原样写入,而 writeln()则会在字符串的末尾添加一个换行符(\n)。如果在文档加载结束后再调用 document.write(),那么输出的内容将会重写整个页面。
Element类型
创建元素
使用 document.createElement()方法可以创建新元素。这个方法只接受一个参数,即要创建元素的标签名。使用下面的代码可以创建一个<div>元素。
var div = document.createElement("div");
由于新元素尚未被添加到文档树中,因此设置这些特性不会影响浏览器的显示。要把新元素添加到文档树,可以使用 appendChild()、 insertBefore()或 replaceChild()方法。下面的代码会把新创建的元素添加到文档的<body>元素中。
document.body.appendChild(div);
Text类型
文本节点由 Text 类型表示,包含的是可以照字面解释的纯文本内容。纯文本中可以包含转义后的HTML 字符,但不能包含 HTML 代码。
在默认情况下,每个可以包含内容的元素最多只能有一个文本节点,而且必须确实有内容存在。来看几个例子。
<!-- 没有内容,也就没有文本节点 -->
<div></div>
<!-- 有空格,因而有一个文本节点 -->
<div> </div>
<!-- 有内容,因而有一个文本节点 -->
<div>Hello World!</div>
可以使用 document.createTextNode()创建新文本节点,这个方法接受一个参数——要插入节点中的文本。与设置已有文本节点的值一样,作为参数的文本也将按照 HTML 或 XML 的格式进行编码。
var textNode = document.createTextNode("\<strong>Hello\</strong> world!");
在创建新文本节点的同时,也会为其设置 ownerDocument 属性。不过,除非把新节点添加到文档树中已经存在的节点中,否则我们不会在浏览器窗口中看到新节点。下面的代码会创建一个、<div>元素并向其中添加一条消息。
var element = document.createElement("div");
element.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element);
规范化文本节点
DOM 文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。另外, DOM 文档中出现相邻文本节点的情况也不在少数,于是就催生了一个能够将相邻文本节点合并的方法。这个方法是由 Node 类型定义的(因而在所有节点类型中都存在),名叫 normalize()。如果在一个包含两个或多个文本节点的父元素上调用 normalize()方法,则会将所有文本节点合并成一个节点,结果节点的 nodeValue 等于将合并前每个文本节点的 nodeValue 值拼接起来的值。
分割文本节点
Text 类型提供了一个作用与 normalize()相反的方法: splitText()。这个方法会将一个文本节点分成两个文本节点,即按照指定的位置分割 nodeValue 值。原来的文本节点将包含从开始到指定位置之前的内容,新文本节点将包含剩下的文本。这个方法会返回一个新文本节点,该节点与原节点的parentNode 相同。
var element = document.createElement("div");
element.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element);
var newNode = element.firstChild.splitText(5);
alert(element.firstChild.nodeValue); //"Hello"
alert(newNode.nodeValue); //" world!"
alert(element.childNodes.length); //2
CDATASection类型
CDATASection 类型只针对基于 XML 的文档,表示的是 CDATA 区域。与 Comment 类似,CDATASection 类型继承自 Text 类型,因此拥有除 splitText()之外的所有字符串操作方法。
DocumentType类型
DocumentType 类型在 Web 浏览器中并不常用,仅有 Firefox、 Safari 和 Opera 支持它。 Document-Type 包含着与文档的 doctype 有关的所有信息。
DocumentFragment类型
在所有节点类型中,只有 DocumentFragment 在文档中没有对应的标记。 DOM 规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。
虽然不能把文档片段直接添加到文档中,但可以将它作为一个“仓库”来使用,即可以在里面保存将来可能会添加到文档中的节点。
来看下面的 HTML 示例代码:
<ul id="myList"></ul>
假设我们想为这个<ul>元素添加 3 个列表项。如果逐个地添加列表项,将会导致浏览器反复渲染(呈现)新信息。为避免这个问题,可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性将它们添加到文档中。
var fragment = document.createDocumentFragment();
var ul = document.getElementById("myList");
var li = null;
for (var i=0; i < 3; i++){
li = document.createElement("li");
li.appendChild(document.createTextNode("Item " + (i+1)));
fragment.appendChild(li);
}
ul.appendChild(fragment);
循环结束后,再调用appendChild()并传入文档片段,将所有列表项添加到\ul>元素中。此时,文档片段的所有子节点都被删除并转移到了\ul>元素中。
attr类型
元素的特性在 DOM 中以 Attr 类型来表示。在所有浏览器中(包括 IE8),都可以访问 Attr 类型
的构造函数和原型。从技术角度讲,特性就是存在于元素的 attributes 属性中的节点。
我们并不建议直接访问特性节点。实际上,使用 getAttribute()、setAttribute()和 removeAttribute()方法远比操作特性节点更为方便。
使用 document.createAttribute()并传入特性的名称可以创建新的特性节点。例如,要为元素添加 align 特性,可以使用下列代码:
var attr = document.createAttribute("align");
attr.value = "left";
element.setAttributeNode(attr);
alert(element.attributes["align"].value); //"left"
alert(element.getAttributeNode("align").value); //"left"
alert(element.getAttribute("align")); //"left"
由于在调用 createAttribute()时已经为 name 属性赋了值,所以后面就不必给它赋值了。之后,又把 value 属性的值设置为"left"。为了将新创建的特性添加到元素中,必须使用元素的 setAttributeNode()方法。添加特性之后,可以通过下列任何方式访问该特性: attributes 属性、 getAttributeNode()方法以及 getAttribute()方法。其中, attributes和 getAttributeNode()都会返回对应特性的 Attr 节点,而 getAttribute()则只返回特性的值。
DOM 操作技术
动态加载的外部 JavaScript 文件能够立即运行,比如下面的<script>元素:
<script type="text/javascript" src="client.js"></script>
这个<script>元素包含了第 9 章的客户端检测脚本。而创建这个节点的 DOM 代码如下所示:
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);
显然,这里的 DOM 代码如实反映了相应的 HTML 代码。不过,在执行最后一行代码把<script>元素添加到页面中之前,是不会下载外部文件的。加载完成后,就可以在页面中的其他地方使用这个脚本了。问题只有一个:怎么知道脚本加载完成呢?遗憾的是,并没有什么标准方式来探知这一点。
function loadScriptString(code){
var script = document.createElement("script");
script.type = "text/javascript";
try {
script.appendChild(document.createTextNode(code));
} catch (ex){
script.text = code;
}
document.body.appendChild(script);
}
下面是调用这个函数的示例:
loadScriptString("function sayHi(){alert('hi');}");
以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后将立即可用。实际上,这样执行代码与在全局作用域中把相同的字符串传递给 eval()是一样的。
动态样式
能够把 CSS 样式包含到 HTML 页面中的元素有两个。其中, <link>元素用于包含来自外部的文件,而<style>元素用于指定嵌入的样式。与动态脚本类似,所谓动态样式是指在页面刚加载时不存在的样式;动态样式是在页面加载完成后动态添加到页面中的。
我们以下面这个典型的<link>元素为例:
<link rel="stylesheet" type="text/css" href="styles.css">
使用 DOM 代码可以很容易地动态创建出这个元素:
var link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "style.css";
var head = document.getElementsByTagName("head")[0];
head.appendChild(link);
需要注意的是,必须将<link>元素添加到<head>而不是<body>元素,才能保证在所有浏览器中的行为一致。整个过程可以用以下函数来表示:
function loadStyles(url){
var link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = url;
var head = document.getElementsByTagName("head")[0];
head.appendChild(link);
}
调用 loadStyles()函数的代码如下所示:
loadStyles("styles.css");
加载外部样式文件的过程是异步的,也就是加载样式与执行 JavaScript 代码的过程没有固定的次序。一般来说,知不知道样式已经加载完成并不重要;
function loadStyleString(css){
var style = document.createElement("style");
style.type = "text/css";
try{
style.appendChild(document.createTextNode(css));
} catch (ex){
style.styleSheet.cssText = css;
}
var head = document.getElementsByTagName("head")[0];
head.appendChild(style);
}
调用这个函数的示例如下:
loadStyleString("body{background-color:red}");
这种方式会实时地向页面中添加样式,因此能够马上看到变化。
<table>元素是 HTML 中最复杂的结构之一。要想创建表格,一般都必须涉及表示表格行、 单元格、
表头等方面的标签。由于涉及的标签多,因而使用核心 DOM 方法创建和修改表格往往都免不了要编写
大量的代码。假设我们要使用 DOM 来创建下面的 HTML 表格。
<table border="1" width="100%">
<tbody>
<tr>
<td>Cell 1,1</td>
<td>Cell 2,1</td>
</tr>
<tr>
<td>Cell 1,2</td>
<td>Cell 2,2</td>
</tr>
</tbody>
</table>
要使用核心 DOM 方法创建这些元素,得需要像下面这么多的代码:
//创建 table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";
//创建 tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);
//创建第一行
var row1 = document.createElement("tr");
tbody.appendChild(row1);
var cell1_1 = document.createElement("td");
cell1_1.appendChild(document.createTextNode("Cell 1,1"));
row1.appendChild(cell1_1);
var cell2_1 = document.createElement("td");
cell2_1.appendChild(document.createTextNode("Cell 2,1"));
row1.appendChild(cell2_1);
//创建第二行
var row2 = document.createElement("tr");
tbody.appendChild(row2);
var cell1_2 = document.createElement("td");
cell1_2.appendChild(document.createTextNode("Cell 1,2"));
row2.appendChild(cell1_2);
var cell2_2= document.createElement("td");
cell2_2.appendChild(document.createTextNode("Cell 2,2"));
row2.appendChild(cell2_2);
//将表格添加到文档主体中
document.body.appendChild(table);
使用这些属性和方法,可以极大地减少创建表格所需的代码数量。例如,使用这些属性和方法可以将前面的代码重写如下。
//创建 table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";
//创建 tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);
//创建第一行
tbody.insertRow(0);
tbody.rows[0].insertCell(0);
tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1"));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1"));
//创建第二行
tbody.insertRow(1);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2"));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2"));
//将表格添加到文档主体中
document.body.appendChild(table)
理解 NodeList 及其“近亲” NamedNodeMap 和 HTMLCollection,是从整体上透彻理解 DOM 的关键所在。这三个集合都是“动态的”;换句话说,每当文档结构发生变化时,它们都会得到更新。因此,它们始终都会保存着最新、最准确的信息。从本质上说,所有 NodeList 对象都是在访问 DOM 文档时实时运行的查询。例如,下列代码会导致无限循环:
var divs = document.getElementsByTagName("div"),
i,
div;
for (i=0; i < divs.length; i++){
div = document.createElement("div");
document.body.appendChild(div);
}
浏览器不会将创建的所有集合都保存在一个列表中,而是在下一次访问集合时再更新集合。
ps:意味着 divs.length 始终保存着最新的值。
如果想要迭代一个 NodeList,最好是使用 length 属性初始化第二个变量,然后将迭代器与该变
量进行比较,如下面的例子所示:
var divs = document.getElementsByTagName("div"),
i,
len,
div;
for (i=0, len=divs.length; i < len; i++){
div = document.createElement("div");
document.body.appendChild(div);
}
第11章 DOM扩展
选择符API
Selectors API(www.w3.org/TR/selectors-api/)是由 W3C 发起制定的一个标准,致力于让浏览器原生支持 CSS 查询。所有实现这一功能的 JavaScript 库都会写一个基础的 CSS 解析器,然后再使用已有的DOM 方法查询文档并找到匹配的节点。尽管库开发人员在不知疲倦地改进这一过程的性能,但到头来都只能通过运行 JavaScript 代码来完成查询操作。而把这个功能变成原生 API 之后,解析和树查询操作可以在浏览器内部通过编译后的代码来完成,极大地改善了性能。
querySelector()方法
querySelector()方法接收一个 CSS 选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回 null。
//取得 body 元素
var body = document.querySelector("body");
//取得 ID 为"myDiv"的元素
var myDiv = document.querySelector("#myDiv");
//取得类为"selected"的第一个元素
var selected = document.querySelector(".selected");
//取得类为"button"的第一个图像元素
var img = document.body.querySelector("img.button");
querySelectorAll 方法
querySelectorAll()方法接收的参数与 querySelector()方法一样,都是一个 CSS 选择符,但返回的是所有匹配的元素而不仅仅是一个元素。这个方法返回的是一个 NodeList 的实例。具体来说,返回的值实际上是带有所有属性和方法的 NodeList,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询。这样实现可以避免使用 NodeList 对象通常会引起的大多数性能问题。
//取得某<div>中的所有<em>元素(类似于 getElementsByTagName("em"))
var ems = document.getElementById("myDiv").querySelectorAll("em");
//取得类为"selected"的所有元素
var selecteds = document.querySelectorAll(".selected");
//取得所有<p>元素中的所有<strong>元素
var strongs = document.querySelectorAll("p strong");
matchesSelector 方法
Selectors API Level 2 规范为 Element 类型新增了一个方法 matchesSelector()。这个方法接收一个参数,即 CSS 选择符,如果调用元素与该选择符匹配,返回 true;否则,返回 false。看例子。
if (document.body.matchesSelector("body.page1")){
//true
}
在取得某个元素引用的情况下,使用这个方法能够方便地检测它是否会被 querySelector()或querySelectorAll()方法返回。
元素遍历
Element Traversal API 为 DOM 元素添加了以下 5 个属性。
- childElementCount:返回子元素(不包括文本节点和注释)的个数。
- firstElementChild:指向第一个子元素; firstChild 的元素版。
- lastElementChild:指向最后一个子元素; lastChild 的元素版。
- previousElementSibling:指向前一个同辈元素; previousSibling 的元素版。
- nextElementSibling:指向后一个同辈元素; nextSibling 的元素版。
下面来看一个例子。过去,要跨浏览器遍历某元素的所有子元素,需要像下面这样写代码。
var i,
len,
child = element.firstChild;
while(child != element.lastChild){
if (child.nodeType == 1){ //检查是不是元素
processChild(child);
}
child = child.nextSibling;
}
而使用 Element Traversal 新增的元素,代码会更简洁。
var i,
len,
child = element.firstElementChild;
while(child != element.lastElementChild){
processChild(child); //已知其是元素
child = child.nextElementSibling;
}
HTML5
对于传统 HTML 而言, HTML5 是一个叛逆。所有之前的版本对 JavaScript 接口的描述都不过三言两语,主要篇幅都用于定义标记,与 JavaScript 相关的内容一概交由 DOM 规范去定义。而 HTML5 规范则围绕如何使用新增标记定义了大量 JavaScript API。其中一些 API 与 DOM 重叠,定义了浏览器应该支持的 DOM 扩展。
与类相关的扩充
HTML5 新增了很多 API,致力于简化 CSS 类的用法。
getElementsByClassName方法
//取得所有类中包含"username"和"current"的元素,类名的先后顺序无所谓
var allCurrentUsernames = document.getElementsByClassName("username current");
//取得 ID 为"myDiv"的元素中带有类名"selected"的所有元素
var selected = document.getElementById("myDiv").getElementsByClassName("selected");
classList属性
因为 className 中是一个字符串,所以即使只修改字符串一部分,也必须每次都设置整个字符串的值。比如,以下面的 HTML 代码为例。
<div class="bd user disabled">...</div>
这个<div>元素一共有三个类名。要从中删除一个类名,需要把这三个类名拆开,删除不想要的那个,然后再把其他类名拼成一个新字符串。请看下面的例子。
//删除"user"类
//首先,取得类名字符串并拆分成数组
var classNames = div.className.split(/\s+/);
//找到要删的类名
var pos = -1,
i,
len;
for (i=0, len=classNames.length; i < len; i++){
if (classNames[i] == "user"){
pos = i;
break;
}
}
//删除类名
classNames.splice(i,1);
//把剩下的类名拼成字符串并重新设置
div.className = classNames.join(" ");
HTML5 新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加classList 属性。这个 classList 属性是新集合类型 DOMTokenList 的实例。与其他 DOM 集合似,DOMTokenList 有一个表示自己包含多少元素的 length 属性,而要取得每个元素可以使用 item()方法,也可以使用方括号语法。
//删除"disabled"类
div.classList.remove("disabled");
//添加"current"类
div.classList.add("current");
//切换"user"类
div.classList.toggle("user");
//确定元素中是否包含既定的类名
if (div.classList.contains("bd") && !div.classList.contains("disabled")){
//执行操作
)
//迭代类名
for (var i=0, len=div.classList.length; i < len; i++){
doSomething(div.classList[i]);
}
焦点管理
HTML5 也添加了辅助管理 DOM 焦点的功能。首先就是 document.activeElement 属性,这个属性始终会引用 DOM 中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按 Tab 键)和在代码中调用 focus()方法。
var button = document.getElementById("myButton");
button.focus();
alert(document.activeElement === button); //true
另外就是新增了 document.hasFocus()方法,这个方法用于确定文档是否获得了焦点。
var button = document.getElementById("myButton");
button.focus();
alert(document.hasFocus()); //true
通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。查询文档获知哪个元素获得了焦点,以及确定文档是否获得了焦点,这两个功能最重要的用途是提高 Web 应用的无障碍性。无障碍 Web 应用的一个主要标志就是恰当的焦点管理,而确切地知道哪个元素获得了焦点是一个极大的进步,至少我们不用再像过去那样靠猜测了。
HTMLDocument的变化
HTML5 扩展了 HTMLDocument,增加了新的功能。与 HTML5 中新增的其他 DOM 扩展类似,这些变化同样基于那些已经得到很多浏览器完美支持的专有扩展。所以,尽管这些扩展被写入标准的时间相对不长,但很多浏览器很早就已经支持这些功能了。
readyState属性
Document 的 readyState 属性有两个可能的值:
- loading,正在加载文档;
- complete,已经加载完文档。
使用 document.readyState 的最恰当方式,就是通过它来实现一个指示文档已经加载完成的指示器。
兼容模式
在标准模式下, document.compatMode 的值等于"CSS1Compat",而在混杂模式下, document.compatMode 的值等于"BackCompat"。
if (document.compatMode == "CSS1Compat"){
alert("Standards mode");
} else {
alert("Quirks mode");
}
head属性
作为对 document.body 引用文档的<body>元素的补充, HTML5 新增了 document.head 属性,引用文档的<head>元素。要引用文档的<head>元素,可以结合使用这个属性和另一种后备方法。
var head = document.head || document.getElementsByTagName("head")[0];
字符集属性
HTML5 新增了几个与文档字符集有关的属性。其中, charset 属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为"UTF-16",但可以通过元素、响应头部或直接设置 charset 属性修改这个值。来看一个例子。
alert(document.charset); //"UTF-16"
document.charset = "UTF-8";
另一个属性是 defaultCharset,表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那 charset 和 defaultCharset 属性的值可能会不一样,例如:
if (document.charset != document.defaultCharset){
alert("Custom character set being used.");
}
通过这两个属性可以得到文档使用的字符编码的具体信息,也能对字符编码进行准确地控制。运行适当的情况下,可以保证用户正常查看页面或使用应用。
自定义数据属性
HTML5 规定可以为元素添加非标准的属性,但要添加前缀 data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便命名,只要以 data-开头即可。来看一个例子。
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
如果需要给元素添加一些不可见的数据以便进行其他处理,那就要用到自定义数据属性。在跟踪链接或混搭应用中,通过自定义数据属性能方便地知道点击来自页面中的哪个部分。
//本例中使用的方法仅用于演示
var div = document.getElementById("myDiv");
//取得自定义属性的值
var appId = div.dataset.appId;
var myName = div.dataset.myname;
//设置值
div.dataset.appId = 23456;
div.dataset.myname = "Michael";
//有没有"myname"值呢?
if (div.dataset.myname){
alert("Hello, " + div.dataset.myname);
}
插入标记
虽然 DOM 为操作节点提供了细致入微的控制手段,但在需要给文档插入大量新 HTML 标记的情况下,通过 DOM 操作仍然非常麻烦,因为不仅要创建一系列 DOM 节点,而且还要小心地按照正确的顺序把它们连接起来。相对而言,使用插入标记的技术,直接插入 HTML 字符串不仅更简单,速度也更快。以下与插入标记相关的 DOM 扩展已经纳入了 HTML5 规范。
innerHTML属性
在读模式下, innerHTML 属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的 HTML 标记。在写模式下, innerHTML 会根据指定的值创建新的 DOM 树,然后用这个DOM 树完全替换调用元素原先的所有子节点。下面是一个例子。
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
对于上面的<div>元素来说,它的 innerHTML 属性会返回如下字符串。
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
在写模式下, innerHTML 的值会被解析为 DOM 子树,替换调用元素原来的所有子节点。因为它的值被认为是 HTML,所以其中的所有标签都会按照浏览器处理 HTML 的标准方式转换为元素(同样,这里的转换结果也因浏览器而异)。
用 innerHTML 属性也有一些限制。比如,在大多数浏览器中,通过 innerHTML 插入<script>元素并不会执行其中的脚本。
outerHTML模式
在读模式下, outerHTML 返回调用它的元素及所有子节点的 HTML 标签。在写模式下, outerHTML会根据指定的 HTML 字符串创建新的 DOM 子树,然后用这个 DOM 子树完全替换调用元素。下面是一个例子。
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
如果在<div>元素上调用 outerHTML,会返回与上面相同的代码,包括<div>本身。
使用 outerHTML 属性以下面这种方式设置值:
div.outerHTML = "<p>This is a paragraph.</p>";
这行代码完成的操作与下面这些 DOM 脚本代码一样:
var p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);
结果,就是新创建的<p>元素会取代 DOM 树中的<div>元素。
insertAdjacentHTML方法
- "beforebegin",在当前元素之前插入一个紧邻的同辈元素;
- "afterbegin",在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;
- "beforeend",在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素;
- "afterend",在当前元素之后插入一个紧邻的同辈元素。
//作为前一个同辈元素插入
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
//作为第一个子元素插入
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
//作为最后一个子元素插入
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
//作为后一个同辈元素插入
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>");
内存与性能问题
使用本节介绍的方法替换子节点可能会导致浏览器的内存占用问题,尤其是在 IE 中,问题更加明显。在删除带有事件处理程序或引用了其他 JavaScript 对象子树时,就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个 JavaScript 对象作为属性),在使用前述某个属性将该元素从文档树中删除后,元素与事件处理程序(或 JavaScript 对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用 innerHTML、outerHTML 属性和 insertAdjacentHTML()方法时,最好先手工删除要被替换的元素的所有事件处理程序和 JavaScript 对象属性。
scrollIntoView方法
如何滚动页面也是 DOM 规范没有解决的一个问题。为了解决这个问题,浏览器实现了一些方法,以方便开发人员更好地控制页面滚动。在各种专有方法中, HTML5 最终选择了 scrollIntoView()作为标准方法。
scrollIntoView()可以在所有 HTML 元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入 true 作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入 false 作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口顶部平齐。)不过顶部不一定平齐,例如:
//让元素可见
document.forms[0].scrollIntoView();
当页面发生变化时,一般会用这个方法来吸引用户的注意力。实际上,为某个元素设置焦点也会导致浏览器滚动并显示出获得焦点的元素
专有扩展
虽然所有浏览器开发商都知晓坚持标准的重要性,但在发现某项功能缺失时,这些开发商都会一如既往地向 DOM 中添加专有扩展,以弥补功能上的不足。表面上看,这种各行其事的做法似乎不太好,但实际上专有扩展为 Web 开发领域提供了很多重要的功能,这些功能最终都在 HTML5 规范中得到了标准化。
文档模式
E8 引入了一个新的概念叫“文档模式”(document mode)。页面的文档模式决定了可以使用什么功能。换句话说,文档模式决定了你可以使用哪个级别的 CSS,可以在 JavaScript 中使用哪些 API,以及如何对待文档类型(doctype)。
第12章 DOM2和DOM3
DOM1 级主要定义的是 HTML 和 XML 文档的底层结构。 DOM2 和 DOM3 级则在这个结构的基础上引入了更多的交互能力,也支持了更高级的 XML 特性。
DOM变化
类似地,“DOM2 级视图”和“DOM2 级 HTML”模块也增强了 DOM 接口,提供了新的属性和方法。由于这两个模块很小,因此我们将把它们与“DOM2 级核心”放在一起,讨论基本 JavaScript 对象的变化。可以通过下列代码来确定浏览器是否支持这些 DOM 模块。
var supportsDOM2Core = document.implementation.hasFeature("Core", "2.0");
var supportsDOM3Core = document.implementation.hasFeature("Core", "3.0");
var supportsDOM2HTML = document.implementation.hasFeature("HTML", "2.0");
var supportsDOM2Views = document.implementation.hasFeature("Views", "2.0");
var supportsDOM2XML = document.implementation.hasFeature("XML", "2.0");
元素大小
本节介绍的属性和方法并不属于“DOM2 级样式”规范,但却与 HTML 元素的样式息息相关。 DOM中没有规定如何确定页面中元素的大小。 IE 为此率先引入了一些属性,以便开发人员使用。目前,所有主要的浏览器都已经支持这些属性。
偏移量
首先要介绍的属性涉及偏移量(offset dimension),包括元素在屏幕上占用的所有可见的空间。元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列 4 个属性可以取得元素的偏移量。
- offsetHeight:元素在垂直方向上占用的空间大小,以像素计。包括元素的高度、(可见的)水平滚动条的高度、上边框高度和下边框高度。
- offsetWidth:元素在水平方向上占用的空间大小,以像素计。包括元素的宽度、(可见的)垂直滚动条的宽度、左边框宽度和右边框宽度。
- offsetLeft:元素的左外边框至包含元素的左内边框之间的像素距离。
- offsetTop:元素的上外边框至包含元素的上内边框之间的像素距离。
要想知道某个元素在页面上的偏移量,将这个元素的 offsetLeft 和 offsetTop 与其 offsetParent的相同属性相加,如此循环直至根元素,就可以得到一个基本准确的值。以下两个函数就可以用于分别取得元素的左和上偏移量。
function getElementLeft(element){
var actualLeft = element.offsetLeft;
var current = element.offsetParent;
while (current !== null){
actualLeft += current.offsetLeft;
current = current.offsetParent;
}
return actualLeft;
}
function getElementTop(element){
var actualTop = element.offsetTop;
var current = element.offsetParent;
while (current !== null){
actualTop += current. offsetTop;
current = current.offsetParent;
}
return actualTop;
}
客户区大小
元素的客户区大小(client dimension),指的是元素内容及其内边距所占据的空间大小。有关客户区大小的属性有两个: clientWidth 和 clientHeight。其中, clientWidth 属性是元素内容区宽度加上左右内边距宽度; clientHeight 属性是元素内容区高度加上上下内边距高度。
滚动大小
滚动大小(scroll dimension),指的是包含滚动内容的元素的大小。有些元素(例如<html>元素),即使没有执行任何代码也能自动地添加滚动条;但另外一些元素,则需要通过 CSS 的overflow 属性进行设置才能滚动。以下是 4 个与滚动大小相关的属性。
- scrollHeight:在没有滚动条的情况下,元素内容的总高度。
- scrollWidth:在没有滚动条的情况下,元素内容的总宽度。
- scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。
- scrollTop:被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。
遍历
如前所述, DOM 遍历是深度优先的 DOM 结构遍历,也就是说,移动的方向至少有两个(取决于使用的遍历类型)。遍历以给定节点为根,不可能向上超出 DOM 树的根节点。以下面的 HTML 页面为例。
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<p><b>Hello</b> world!</p>
</body>
</html>
任何节点都可以作为遍历的根节点。如果假设<body>元素为根节点,那么遍历的第一步就是访问<p>元素,然后再访问同为<body>元素后代的两个文本节点。不过,这次遍历永远不会到达<html>、<head>元素,也不会到达不属于<body>元素子树的任何节点。而以 document 为根节点的遍历则可以访问到文档中的全部节点。
从 document 开始依序向前,访问的第一个节点是 document,访问的最后一个节点是包含"world!"的文本节点。从文档最后的文本节点开始,遍历可以反向移动到 DOM 树的顶端。此时,访问的第一个节点是包含"Hello"的文本节点,访问的最后一个节点是 document 节点。 NodeIterator和 TreeWalker 都以这种方式执行遍历。
范围
为了让开发人员更方便地控制页面,“DOM2 级遍历和范围”模块定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的 DOM 操作不能更有效地修改文档时,使用范围往往可以达到目的。
第13章 事件
JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预订事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式的模型,支持页面的行为(JavaScript 代码)与页面的外观(HTML 和 CSS 代码)之间的松散耦合。
事件流
当浏览器发展到第四代时(IE4 及 Netscape Communicator 4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,可以想象画在一张纸上的一组同心圆。如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的所有圆。两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。如果你单击了某个按钮,他们都认为单击事件不仅仅发生在按钮上。换句话说,在单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。
事件流描述的是从页面中接收事件的顺序。但有意思的是, IE 和 Netscape 开发团队居然提出了差不多是完全相反的事件流的概念。 IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。
事件冒泡
IE 的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 HTML 页面为例:
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
如果你单击了页面中的<div>元素,那么这个 click 事件会按照如下顺序传播:
(1) <div>
(2) <body>
(3) <html>
(4) document
事件捕获
Netscape Communicator 团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。如果仍以前面的 HTML 页面作为演示事件捕获的例子,那么单击<div>元素就会以下列顺序触发 click 事件。
(1) document
(2) <html>
(3) <body>
(4) <div>
在事件捕获过程中, document 对象首先接收到 click 事件,然后事件沿 DOM 树依次向下,一直传播到事件的实际目标,即<div>元素。
DOM事件流
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
在 DOM 事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从 document 到<html>再到<body>后就停止了。下一个阶段是“处于目标”阶段,于是事件在<div>上发生,并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回文档。
时间处理程序
事件就是用户或浏览器自身执行的某种动作。诸如 click、 load 和 mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以"on"开头,因此click 事件的事件处理程序就是 onclick, load 事件的事件处理程序就是 onload。为事件指定处理程序的方式有好几种。
HTML事件处理程序
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。这个特性的值应该是能够执行的 JavaScript 代码。例如,要在按钮被单击时执行一些 JavaScript,可以像下面这样编写代码:
<input type="button" value="Click Me" onclick="alert('Clicked')" />
在 HTML 中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本,如下面的例子所示:
<script type="text/javascript">
function showMessage(){
alert("Hello world!");
}
</script>
<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 指定事件处理程序的原因所在。
DOM0级事件处理程序
通过 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级事件处理程序
“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事件处理程序
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中的事件对象
兼容 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);
IE中的事件对象
与访问 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(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;
- 焦点事件,当元素获得或失去焦点时触发;
- 鼠标事件,当用户通过鼠标在页面上执行操作时触发;
- 滚轮事件,当使用鼠标滚轮(或类似设备)时触发;
- 文本事件,当在文档中输入文本时触发;
- 键盘事件,当用户通过键盘在页面上执行操作时触发;
- 合成事件,当为 IME(Input Method Editor,输入法编辑器)输入字符时触发;
- 变动(mutation)事件,当底层 DOM 结构发生变化时触发。
- 变动名称事件,当元素或属性名变动时触发。此类事件已经被废弃,没有任何浏览器实现它们
UI事件
UI 事件指的是那些不一定与用户操作有关的事件。这些事件在 DOM 规范出现之前,都是以这种或那种形式存在的,而在 DOM 规范中保留是为了向后兼容。现有的 UI 事件如下。
- DOMActivate:表示元素已经被用户操作(通过鼠标或键盘)激活。这个事件在 DOM3 级事件中被废弃,但 Firefox 2+和 Chrome 支持它。考虑到不同浏览器实现的差异,不建议使用这个事件。
- load:当页面完全加载后在 window 上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>元素上面触发,或者当嵌入的内容加载完毕时在<object>元素上面触发。
- unload:当页面完全卸载后在 window 上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后在<object>元素上面触发。
- abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上面触发。
- error:当发生 JavaScript 错误时在 window 上面触发,当无法加载图像时在<img>元素上面触发,当无法加载嵌入内容时在<object>元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。第 17 章将继续讨论这个事件。
- select:当用户选择文本框(<input>或<texterea>)中的一或多个字符时触发。第 14 章将继续讨论这个事件。
- resize:当窗口或框架的大小变化时在 window 或框架上面触发。
- scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。 <body>元素中包含所加载页面的滚动条。
load事件
JavaScript 中最常用的一个事件就是 load。当页面完全加载后(包括所有图像、 JavaScript 文件、CSS 文件等外部资源),就会触发 window 上面的 load 事件。
unload事件
与 load 事件对应的是 unload 事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生 unload 事件。而利用这个事件最多的情况是清除引用,以避免内存泄漏。
resize事件
当浏览器窗口被调整到一个新的高度或宽度时,就会触发 resize 事件。这个事件在 window(窗口)上面触发,因此可以通过 JavaScript 或者<body>元素中的 onresize 特性来指定事件处理程序。
scroll事件
虽然 scroll 事件是在 window 对象上发生的,但它实际表示的则是页面中相应元素的变化。在混杂模式下,可以通过<body>元素的 scrollLeft 和 scrollTop 来监控到这一变化;而在标准模式下,除 Safari 之外的所有浏览器都会通过<html>元素来反映这一变化
焦点事件
焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与 document.hasFocus()方法及
document.activeElement 属性配合,可以知晓用户在页面上的行踪。有以下 6 个焦点事件。
- 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
当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:
(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开发时,要记住以下几点。
- 不支持 dblclick 事件。双击浏览器窗口会放大画面,而且没有办法改变该行为。
- 轻击可单击元素会触发 mousemove 事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生 mousedown、 mouseup 和 click 事件。轻击不可单击的元素不会触发任何事件。可单击的元素是指那些单击可产生默认操作的元素(如链接),或者那些已经被指定了 onclick 事件处理程序的元素。
- mousemove 事件也会触发 mouseover 和 mouseout 事件。
- 两个手指放在屏幕上且页面随手指移动而滚动时会触发 mousewheel 和 scroll 事件
键盘与文本事件
用户在使用键盘时会触发键盘事件。有 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 级定义了如下变动事件。
- DOMSubtreeModified:在 DOM 结构中发生任何变化时触发。这个事件在其他任何事件触发后都会触发。
- DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发。
- DOMNodeRemoved:在节点从其父节点中被移除时触发。
- DOMNodeInsertedIntoDocument:在一个节点被直接插入文档或通过子树间接插入文档之后触发。这个事件在 DOMNodeInserted 之后触发。
- DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发。这个事件在 DOMNodeRemoved 之后触发。
- DOMAttrModified:在特性被修改之后触发。
- DOMCharacterDataModified:在文本节点的值发生变化时触发。
HTML5事件
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 个值中的一个。
- uninitialized(未初始化):对象存在但尚未初始化。
- loading(正在加载):对象正在加载数据。
- loaded(加载完毕):对象加载数据完成。
- interactive(交互):可以操作对象了,但还没有完全加载。
- complete(完成):对象已经加载完毕。
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>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function(){
//先执行某些操作
document.getElementById("myDiv").innerHTML = "Processing..."; //麻烦了!
};
</script>
在<div>元素上设置 innerHTML 可以把按钮移走,但事件处理程序仍然与按钮保持着引用关系。
<div id="myDiv">
<input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function(){
//先执行某些操作
btn.onclick = null; //移除事件处理程序
document.getElementById("myDiv").innerHTML = "Processing...";
};
</script>
模拟事件
在测试 Web 应用程序,模拟触发事件是一种极其有用的技术。
DOM中的事件模拟
可以在 document 对象上使用 createEvent()方法创建 event 对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在 DOM2 级中,所有这些字符串都使用英文复数形式,而在 DOM3级中都变成了单数。这个字符串可以是下列几字符串之一。
- UIEvents:一般化的 UI 事件。 鼠标事件和键盘事件都继承自 UI 事件。 DOM3 级中是 UIEvent。
- MouseEvents:一般化的鼠标事件。 DOM3 级中是 MouseEvent。
- MutationEvents:一般化的 DOM 变动事件。 DOM3 级中是 MutationEvent。
- HTMLEvents:一般化的 HTML 事件。没有对应的 DOM3 级事件(HTML 事件被分散到其他类别中)
第14章 表单脚本
JavaScript 最初的一个应用,就是分担服务器处理表单的责任,打破处处依赖服务器的局面。尽管目前的 Web 和 JavaScript 已经有了长足的发展,但 Web 表单的变化并不明显。由于 Web 表单没有为许多常见任务提供现成的解决手段,很多开发人员不仅会在验证表单时使用 JavaScirpt,而且还增强了一些标准表单控件的默认行为。
表单的基础知识
在 HTML 中,表单是由<form>元素来表示的,而在 JavaScript 中,表单对应的则是 HTMLFormElement 类型。 HTMLFormElement 继承了 HTMLElement,因而与其他 HTML 元素具有相同的默认属性。不过, HTMLFormElement 也有它自己下列独有的属性和方法。
- acceptCharset:服务器能够处理的字符集;等价于 HTML 中的 accept-charset 特性。
- action:接受请求的 URL;等价于 HTML 中的 action 特性。
- elements:表单中所有控件的集合(HTMLCollection)。
- enctype:请求的编码类型;等价于 HTML 中的 enctype 特性。
- length:表单中控件的数量。
- method:要发送的 HTTP 请求类型,通常是"get"或"post";等价于 HTML 的 method 特性。
- name:表单的名称;等价于 HTML 的 name 特性。
- reset():将所有表单域重置为默认值。
- submit():提交表单。
- target:用于发送请求和接收响应的窗口名称;等价于 HTML 的 target 特性。
取得<form>元素引用的方式有好几种。其中最常见的方式就是将它看成与其他元素一样,并为其添加 id 特性,然后再像下面这样使用 getElementById()方法找到它
取得<form>元素引用的方式有好几种。其中最常见的方式就是将它看成与其他元素一样,并为其添加 id 特性,然后再像下面这样使用 getElementById()方法找到它。
var form = document.getElementById("form1");
其次,通过 document.forms 可以取得页面中所有的表单。在这个集合中,可以通过数值索引或name 值来取得特定的表单,如下面的例子所示。
var firstForm = document.forms[0]; //取得页面中的第一个表单
var myForm = document.forms["form2"]; //取得页面中名称为"form2"的表单
用户单击提交按钮或图像按钮时,就会提交表单。使用<input>或<button>都可以定义提交按钮,只要将其 type 特性的值设置为"submit"即可,而图像按钮则是通过将<input>的 type 特性值设置为"image"来定义的。因此,只要我们单击以下代码生成的按钮,就可以提交表单。
<!-- 通用提交按钮 -->
<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>或<button>都可以创建重置按钮,如下面的例子所示。
<!-- 通用重置按钮 -->
<input type="reset" value="Reset Form">
<!-- 自定义重置按钮 -->
<button type="reset">Reset Form</button>
这两个按钮都可以用来重置表单。在重置表单时,所有表单字段都会恢复到页面刚加载完毕时的初始值。如果某个字段的初始值为空,就会恢复为空;而带有默认值的字段,也会恢复为默认值。
表单字段
共有表单字段属性
表单字段共有的属性如下。
- disabled:布尔值,表示当前字段是否被禁用。
- form:指向当前字段所属表单的指针;只读。
- name:当前字段的名称。
- readOnly:布尔值,表示当前字段是否只读。
- tabIndex:表示当前字段的切换(tab)序号。
- type:当前字段的类型,如"checkbox"、 "radio",等等。
- value:当前字段将被提交给服务器的值。对文件字段来说,这个属性是只读的,包含着文件在计算机中的路径。
在涉及信用卡消费时,这就是个问题:因为会导致费用翻番。为此,最常见的解决方案,就是在第一次单击后就禁用提交按钮。只要侦听 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 个事件。
- blur:当前字段失去焦点时触发。
- change:对于<input>和<textarea>元素,在它们失去焦点且 value 值改变时触发;对于<select>元素,在其选项改变时触发。
- focus:当前字段获得焦点时触发
对于<input>和<textarea>元素,当它们从获得焦点到失去焦点且 value 值改变时,
才会触发 change 事件。对于<select>元素,只要用户选择了不同的选项,就会触发 change 事件;换句话说,不失去焦点也会触发 change 事件。
文本框脚本
在 HTML 中,有两种方式来表现文本框:一种是使用元素的单行文本框,另一种是使用<textarea>的多行文本框。
要表现文本框,必须将<input>元素的 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 个剪贴板事件。
- beforecopy:在发生复制操作前触发。
- copy:在发生复制操作时触发。
- beforecut:在发生剪切操作前触发。
- cut:在发生剪切操作时触发。
- beforepaste:在发生粘贴操作前触发。
- paste:在发生粘贴操作时触发
自动切换焦点
使用 JavaScript 可以从多个方面增强表单字段的易用性。其中,最常见的一种方式就是在用户填写完当前字段时,自动将焦点切换到下一个字段。通常,在自动切换焦点之前,必须知道用户已经输入了既定长度的数据(例如电话号码)。
开始的 tabForward()函数是实现“自动切换焦点”的关键所在。这个函数通过比较用户输入的值与文本框的 maxlength 特性,可以确定是否已经达到最大长度。如果这两个值相等(因为浏览器最终会强制它们相等,因此用户绝不会多输入字符),则需要查找表单字段集合,直至找到下一个文本框。找到下一个文本框之后,则将焦点切换到该文本框。然后,我们把这个函数指定为每个文本框的 onkeyup事件处理程序。由于 keyup 事件会在用户输入了新字符之后触发,所以此时是检测文本框中内容长度的最佳时机。这样一来,用户在填写这个简单的表单时,就不必再通过按制表键切换表单字段和提交表单了。
HTML5约束验证API
为了在将表单提交到服务器之前验证数据,HTML5 新增了一些功能。
必填字段
第一种情况是在表单字段中指定了 required 属性,如下面的例子所示:
<input type="text" name="username" required>
任何标注有 required 的字段,在提交表单时都不能空着。这个属性适用于<input>、 <textarea>和<select>字段(Opera 11 及之前版本还不支持<select>的 required 属性)。在 JavaScript 中,通过对应的 required 属性,可以检查某个表单字段是否为必填字段
其他输入类型
HTML5 为<input>元素的 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>
选择框脚本
选择框是通过<select>和<option>元素创建的。为了方便与这个控件交互,除了所有表单字段共有的属性和方法外, HTMLSelectElement 类型还提供了下列属性和方法。
- add(newOption, relOption):向控件中插入新<option>元素,其位置在相关项(relOption)之前。
- multiple:布尔值,表示是否允许多项选择;等价于 HTML 中的 multiple 特性。
- options:控件中所有<option>元素的 HTMLCollection。
- remove(index):移除给定位置的选项。
- selectedIndex:基于 0 的选中项的索引,如果没有选中项,则值为-1。对于支持多选的控件,只保存选中项中第一项的索引。
- size:选择框中可见的行数;等价于 HTML 中的 size 特性。
选择框的 type 属性不是"select-one",就是"select-multiple",这取决于 HTML 代码中有没有 multiple 特性。选择框的 value 属性由当前选中项决定,相应规则如下。
- 如果没有选中的项,则选择框的 value 属性保存空字符串。
- 如果有一个选中项,而且该项的 value 特性已经在 HTML 中指定,则选择框的 value 属性等于选中项的 value 特性。即使 value 特性的值是空字符串,也同样遵循此条规则。
- 如果有一个选中项,但该项的 value 特性在 HTML 中未指定,则选择框的 value 属性等于该项的文本。
- 如果有多个选中项,则选择框的 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 属性一起实现对表单的序列化。在编写代码之前,有必须先搞清楚在表单提交期间,浏览器是怎样将数据发送给服务器的。
- 对表单字段的名称和值进行 URL 编码,使用和号(&)分隔。
- 不发送禁用的表单字段。
- 只发送勾选的复选框和单选按钮。
- 不发送 type 为"reset"和"button"的按钮。
- 多选选择框中的每个选中的值单独一个条目。
- 在单击提交按钮提交表单的情况下,也会发送提交按钮;否则,不发送提交按钮。也包括 type为"image"的<input>元素。
- <select>元素的值,就是选中的<option>元素的 value 特性的值。如果<option>元素没有value 特性,则是<option>元素的文本值。
富文本编辑
富文本编辑,又称为 WYSIWYG(What You See Is What You Get,所见即所得)。在网页中编辑富文本内容,是人们对 Web 应用程序最大的期待之一。虽然也没有规范,但在 IE 最早引入的这一功能基础上,已经出现了事实标准。而且, Opera、 Safari、 Chrome 和 Firefox 都已经支持这一功能。这一技术的本质,就是在页面中嵌入一个包含空 HTML 页面的 iframe。通过设置 designMode 属性,这个空白的 HTML 页面可以被编辑,而编辑对象则是该页面<body>元素的 HTML 代码。 designMode 属性有两个可能的值: "off"(默认值)和"on"。在设置为"on"时,整个文档都会变得可以编辑(显示插入符号),然后就可以像使用字处理软件一样,通过键盘将文本内容加粗、变成斜体,等等。
第15章使用Canvas绘图
HTML5 添加的最受欢迎的功能就是<canvas>元素。这个元素负责在页面中设定一个区域,然后就可以通过 JavaScript 动态地在这个区域中绘制图形。 <canvas>元素最早是由苹果公司推出的,当时主要用在其 Dashboard 微件中。很快, HTML5 加入了这个元素。
基本用法
要使用<canvas>元素,必须先设置其 width 和 height 属性,指定可以绘图的区域大小。出现在开始和结束标签中的内容是后备信息,如果浏览器不支持<canvas>元素,就会显示这些信息。下面就是<canvas>元素的例子。
<canvas id="drawing" width=" 200" height="200">A drawing of something.</canvas>
与其他元素一样, <canvas>元素对应的 DOM 元素对象也有 width 和 height 属性,可以随意修改。而且,也能通过 CSS 为该元素添加样式,如果不添加任何样式或者不绘制任何图形,在页面中是看不到该元素的。
ps:牛逼的技术,感觉用不到,跳过跳过
第16章 HTML5脚本编程
跨文档消息传递
跨文档消息传送(cross-document messaging),有时候简称为 XDM,指的是在来自不同域的页面间传递消息。例如, www.wrox.com 域中的页面与位于一个内嵌框架中的 p2p.wrox.com 域中的页面通信。在 XDM 机制出现之前,要稳妥地实现这种通信需要花很多工夫。 XDM 把这种机制规范化,让我们能既稳妥又简单地实现跨文档通信。
XDM 的核心是 postMessage()方法。在 HTML5 规范中,除了 XDM 部分之外的其他部分也会提到这个方法名,但都是为了同一个目的:向另一个地方传递数据。对于 XDM 而言, “另一个地方”指的是包含在当前页面中的<iframe>元素,或者由当前页面弹出的窗口。
原生拖放
最早在网页中引入 JavaScript 拖放功能的是 IE4。当时,网页中只有两种对象可以拖放:图像和某些文本。拖动图像时,把鼠标放在图像上,按住鼠标不放就可以拖动它。拖动文本时,要先选中文本,然后可以像拖动图像一样拖动被选中的文本。在 IE 4 中,唯一有效的放置目标是文本框。到了 IE5,拖放功能得到扩展,添加了新的事件,而且几乎网页中的任何元素都可以作为放置目标。 IE5.5 更进一步,让网页中的任何元素都可以拖放。(IE6 同样也支持这些功能。) HTML5 以 IE 的实例为基础制定了拖放规范。 Firefox 3.5、 Safari 3+和 Chrome 也根据 HTML5 规范实现了原生拖放功能。
媒体元素
随着音频和视频在 Web 上的迅速流行,大多数提供富媒体内容的站点为了保证跨浏览器兼容性,不得不选择使用 Flash。 HTML5 新增了两个与媒体相关的标签,让开发人员不必依赖任何插件就能在网页中嵌入跨浏览器的音频和视频内容。这两个标签就是<audio>和<video>。
历史状态管理
历史状态管理是现代 Web 应用开发中的一个难点。在现代 Web 应用中,用户的每次操作不一定会打开一个全新的页面,因此“后退”和“前进”按钮也就失去了作用,导致用户很难在不同状态间切换。要解决这个问题,首选使用 hashchange 事件(第 13 章曾讨论过)。 HTML5 通过更新 history 对象为管理历史状态提供了方便。
第17章 错误处理与调试
浏览器报告的错误
IE、 Firefox、 Safari、 Chrome 和 Opera 等主流浏览器,都具有某种向用户报告 JavaScript 错误的机制。默认情况下,所有浏览器都会隐藏此类信息,毕竟除了开发人员之外,很少有人关心这些内容。因此,在基于浏览器编写 JavaScript 脚本时,别忘了启用浏览器的 JavaScript 报告功能,以便及时收到错误通知。
错误处理
try-catch语句
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 种错误类型:
- Error
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误作出恰当的处理。要想知道错误的类型,可以像下面这样在 try-catch 语句的 catch 语句中使用 instanceof 操作符。
try {
someFunction();
} catch (error){
if (error instanceof TypeError){
//处理类型错误
} else if (error instanceof ReferenceError){
//处理引用错误
} else {
//处理其他类型的错误
}
}
合理使用 try-catch
使用 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 库,甚至只编写一个可能会在应用程序内部多个地方使用的辅助函数,我都强烈建议你在抛出错误时提供详尽的信息。然后,即可在应用程序中捕获并适当地处理这些错误。
说到抛出错误与捕获错误,我们认为只应该捕获那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息
错误(error)事件
任何没有通过 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 错误也回写到服务器。换句话说,也要将这些错误写入到保存服务器端错误的地方,只不过要标明它们来自前端。把前后端的错误集中起来,能够极大地方便对数据的分析。
第18章 JavaScript与XML
浏览器对XML DOM的支持
DOMParser类型
这个方法接受两个参数:要解析的 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 对象,但这个对象的文档元素是<parsererror>,而文档元素的内容是对解析错误的描述。
XMLSerializer类型
在引入 DOMParser 的同时, Firefox 还引入了 XMLSerializer 类型,提供了相反的功能:将 DOM文档序列化为 XML 字符串。
var serializer = new XMLSerializer();
var xml = serializer.serializeToString(xmldom);
alert(xml);
XMLSerializer 可以序列化任何有效的 DOM 对象,不仅包括个别的节点,也包括 HTML 文档。将 HTML 文档传入 serializeToString()以后, HTML 文档将被视为 XML 文档,因此得到的代码也将是格式良好的。
浏览器对XPath的支持
XPath 是设计用来在 DOM 文档中查找节点的一种手段,因而对 XML 处理也很重要。但是, DOM3级以前的标准并没有就 XPath 的 API 作出规定; XPath 是在 DOM3 级 XPath 模块中首次跻身推荐标准行列的。
浏览器对XSLT的支持
XSLT 是与 XML 相关的一种技术,它利用 XPath 将文档从一种表现形式转换成另一种表现形式。与XML 和 XPath 不同, XSLT 没有正式的 API,在正式的 DOM 规范中也没有它的位置。结果,只能依靠浏览器开发商以自己的方式来实现它。
第20章 JSON
关于 JSON,最重要的是要理解它是一种数据格式,不是一种编程语言。虽然具有相同的语法形式,但 JSON 并不从属于 JavaScript。而且,并不是只有 JavaScript 才使用 JSON,毕竟 JSON 只是一种数据格式。很多编程语言都有针对 JSON 的解析器和序列化器。
JSON 的语法可以表示以下三种类型的值。
- 简单值:使用与 JavaScript 相同的语法,可以在 JSON 中表示字符串、数值、布尔值和 null。但 JSON 不支持 JavaScript 中的特殊值 undefined。
- 对象:对象作为一种复杂数据类型,表示的是一组无序的键值对儿。而每个键值对儿中的值可以是简单值,也可以是复杂数据类型的值。
- 数组:数组也是一种复杂数据类型,表示一组有序的值的列表,可以通过数值索引来访问其中的值。数组的值也可以是任意类型——简单值、对象或数组。
解析与序列化
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(),序列化该对象的顺序如下。
- 如果存在 toJSON()方法而且能通过它取得有效的值,则调用该方法。否则,返回对象本身。
- 如果提供了第二个参数,应用这个函数过滤器。传入函数过滤器的值是第(1)步返回的值。
- 对第(2)步返回的每个值进行相应的序列化。
- 如果提供了第三个参数,执行相应的格式化。
解析选项
JSON.parse()方法也可以接收另一个参数,该参数是一个函数,将在每个键值对儿上调用。为了区别 JSON.stringify()接收的替换(过滤)函数(replacer),这个函数被称为还原函数(reviver),但实际上这两个函数的签名是相同的——它们都接收两个参数,一个键和一个值,而且都需要返回一个值。
第21章 Ajax 与 Comet
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 数据。
XMLHttpRequest 对象
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的用法
在使用 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 对象的属性,相关的属性简介如下。
- responseText:作为响应主体被返回的文本。
- responseXML:如果响应的内容类型是"text/xml"或"application/xml",这个属性中将保存包含着响应数据的 XML DOM 文档。
- status:响应的 HTTP 状态。
- statusText: HTTP 状态的说明。
在接收到响应后,第一步是检查 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 对象。
- 0:未初始化。尚未调用 open()方法。
- 1:启动。已经调用 open()方法,但尚未调用 send()方法。
- 2:发送。已经调用 send()方法,但尚未接收到响应。
- 3:接收。已经接收到部分响应数据。
- 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
只要 readyState 属性的值由一个值变成另一个值,都会触发一次 readystatechange 事件。可以利用这个事件来检测每次状态变化后 readyState 的值。通常,我们只对 readyState 值为 4 的阶
段感兴趣,因为这时所有数据都已经就绪。不过,必须在调用 open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性。
HTTP 头部信息
默认情况下,在发送 XHR 请求的同时,还会发送下列头部信息。
- Accept:浏览器能够处理的内容类型。
- Accept-Charset:浏览器能够显示的字符集。
- Accept-Encoding:浏览器能够处理的压缩编码。
- Accept-Language:浏览器当前设置的语言。
- Connection:浏览器与服务器之间连接的类型。
- Cookie:当前页面设置的任何 Cookie。
- Host:发出请求的页面所在的域 。
- Referer:发出请求的页面的 URI。注意, HTTP 规范将这个头部字段拼写错了,而为保证与规范一致,也只能将错就错了。(这个英文单词的正确拼法应该是 referrer。)
- User-Agent:浏览器的用户代理字符串。
使用 setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。要成功发送请求头部信息,必须在调用 open()方法之后且调用 send()方法之前调用 setRequestHeader()。
调用 XHR 对象的 getResponseHeader()方法并传入头部字段名称,可以取得相应的响应头部信息。而调用 getAllResponseHeaders()方法则可以取得一个包含所有头部信息的长字符串。
GET请求
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请求
默认情况下,服务器对 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));
}
XMLHttpRequest 2级
鉴于 XHR 已经得到广泛接受,成为了事实标准, W3C 也着手制定相应的标准以规范其行为。XMLHttpRequest 1 级只是把已有的 XHR 对象的实现细节描述了出来。而 XMLHttpRequest 2 级则进一步发展了 XHR。并非所有浏览器都完整地实现了 XMLHttpRequest 2 级规范,但所有浏览器都实现了它规定的部分内容。
FormData
下面的代码创建了一个 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);
overrideMimeType()方法
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:在因为调用 abort()方法而终止连接时触发。
- load:在接收到完整的响应数据时触发。
- loadend:在通信完成或者触发 error、 abort 或 load 事件后触发。
每个请求都从触发 loadstart 事件开始,接下来是一或多个 progress 事件,然后触发 error、abort 或 load 事件中的一个,最后以触发 loadend 事件结束。目前还没有浏览器支持 loadend 事件。
load事件
只要浏览器接收到服务器的响应,不管其状态如何,都会触发 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);
progress 事件
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 信息
IE对CORS的实现
微软在 IE8 中引入了 XDR(XDomainRequest)类型。这个对象与 XHR 类似,但能实现安全可靠的跨域通信。 XDR 对象的安全机制部分实现了 W3C 的 CORS 规范。以下是 XDR 与 XHR 的一些不同之处。
- cookie 不会随请求发送,也不会随响应返回。
- 只能设置请求头部信息中的 Content-Type 字段。
- 不能访问响应头部信息。
- 只支持 GET 和 POST 请求。
这些变化使 CSRF(Cross-Site Request Forgery,跨站点请求伪造)和 XSS(Cross-Site Scripting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)来决定是否设置 Access-Control- Allow-Origin 头部。作为请求的一部分, Origin 头部的值表示请求的来源域,以便远程资源明确地识别 XDR 请求。
其他浏览器对CORS的实现
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);
Preflighted Requests
CORS 通过一种叫做 Preflighted Requests 的透明服务器验证机制支持开发人员使用自定义的头部、GET 或 POST 之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个 Preflight 请求。这种请求使用 OPTIONS 方法,发送下列头部。
- Origin:与简单的请求相同。
- Access-Control-Request-Method:请求自身使用的方法。
- Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。以下是一个带有自定义头部 NCZ 的使用 POST 方法发送的请求。
带凭据的请求
默 认 情 况 下, 跨 源 请 求不 提 供 凭 据(cookie 、 HTTP 认 证 及 客户 端 SSL 证明 等 )。 通 过 将withCredentials 属性设置为 true,可以指定某个请求应该发送凭据。
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是, responseText 中将是空字符串, status 的值为 0,而且会调用 onerror()事件处理程序)。
跨浏览器的CORS
即使浏览器对 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。图像 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
JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种新方法,在后来的 Web 服务中非常流行。 JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON
JSONP 之所以在开发人员中极为流行,主要原因是它非常简单易用。与图像 Ping 相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过, JSONP 也有两点不足。
首先, JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。因此在使用不是你自己运维的 Web 服务时,一定得保证它安全可靠。
其次,要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给<script>元素新增了一个 onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指时间内是否接收到了响应。但就算这样也不能尽如人意,毕竟不是每个用户上网的速度和带宽都一样。
Comet
Comet 是 Alex Russell①发明的一个词儿,指的是一种更高级的 Ajax 技术(经常也有人称为“服务器推送”)。 Ajax 是一种从页面向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技术。 Comet 能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。
有两种实现 Comet 的方式: 长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。
长轮询把短轮询颠倒了一下。页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。
无论是短轮询还是长轮询,浏览器都要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用 XHR 对象和 setTimeout()就能实现。而你要做的就是决定什么时候发送请求。
第二种流行的 Comet 实现是 HTTP 流。流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个 HTTP 连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。
服务器发送事件
SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet 交互推出的 API 或者模式。 SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的 MIME类型必须是 text/event-stream,而且是浏览器中的 JavaScript API 能解析格式输出。 SSE 支持短轮询、长轮询和 HTTP 流,而且能在断开连接时自动确定何时重新连接。有了这么简单实用的 API,再实现 Comet 就容易多了。
Web Sockets
要说最令人津津乐道的新浏览器 API,就得数 Web Sockets 了。 Web Sockets 的目标是在一个单独的持久连接上提供全双工、双向通信。在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 WebSocket 协议。也就是说,使用标准的 HTTP 服务器无法实现 Web Sockets,只有支持这种协议的专门服务器才能正常工作。
由于 Web Sockets 使用了自定义的协议,所以 URL 模式也略有不同。未加密的连接不再是 http://,而是 ws://;加密的连接也不是 https://,而是 wss://。在使用 Web Socket URL 时,必须带着这个模式,因为将来还有可能支持其他模式。
使用自定义协议而非 HTTP 协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不必担心 HTTP 那样字节级的开销。由于传递的数据包很小,因此 Web Sockets 非常适合移动应用。毕竟对移动应用而言,带宽和网络延迟都是关键问题。使用自定义协议的缺点在于,制定协议的时间比制定JavaScript API 的时间还要长。 Web Sockets 曾几度搁浅,就因为不断有人发现这个新协议存在一致性和安全性的问题。
同源策略对 Web Sockets 不适用,因此可以通过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器。
SSE与Web Sockets
面对某个具体的用例,在考虑是使用 SSE 还是使用 Web Sockets 时,可以考虑如下几个因素。首先,你是否有自由度建立和维护 Web Sockets 服务器?因为 Web Socket 协议不同于 HTTP,所以现有服务器不能用于 Web Socket 通信。 SSE 倒是通过常规 HTTP 通信,因此现有服务器就可以满足需求。
第二个要考虑的问题是到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩),那么 SSE 比较容易实现。如果用例必须双向通信(如聊天室),那么 Web Sockets 显然更好。别忘了,在不能选择 Web Sockets 的情况下,组合 XHR 和 SSE 也是能实现双向通信的。
第22章 高级技巧
高级函数
安全的类型检测
avaScript 内置的类型检测机制并非完全可靠。事实上,发生错误否定及错误肯定的情况也不在少数。比如说 typeof 操作符吧,由于它有一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果。 Safari(直至第 4 版)在对正则表达式应用 typeof 操作符时会返回"function",因此很难确定某个值到底是不是函数。
再比如, instanceof 操作符在存在多个全局作用域(像一个页面包含多个 frame)的情况下,也是问题多多。
大家知道,在任何值上调用 Object 原生的 toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性中就指定了上述字符串中的构造函数名。
alert(Object.prototype.toString.call(value)); //"[object Array]"
由于原生数组的构造函数名与全局作用域无关,因此使用 toString()就能保证返回一致的值。利用这一点,可以创建如下函数:
function isArray(value){
return Object.prototype.toString.call(value) == "[object Array]";
}
同样,也可以基于这一思路来测试某个值是不是原生函数或正则表达式:
function isFunction(value){
return Object.prototype.toString.call(value) == "[object Function]";
}
function isRegExp(value){
return Object.prototype.toString.call(value) == "[object RegExp]";
}
不过要注意,对于在 IE 中以 COM 对象形式实现的任何函数, isFunction()都将返回 false,因为它们并非原生的 JavaScript 函数。
请注意, Object.prototpye.toString()本身也可能会被修改。本节讨论的技巧假设 Object.prototpye.toString()是未被修改过的原生版本。
作用域安全的构造函数
构造函数其实就是一个使用 new 操作符调用的函数。当使用 new 调用时,构造函数内用到的 this 对象会指向新创建的对象实例,如下面的例子所示:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
var person = new Person("Nicholas", 29, "Software Engineer");
问题出在当没有使用 new 操作符来调用该构造函数的情况上。由于该 this 对象是在运行时绑定的,所以直接调用 Person(),this 会映射到全局对象 window 上,导致错误对象属性的意外增加。例如:
var person = Person("Nicholas", 29, "Software Engineer");
alert(window.name); //"Nicholas"
alert(window.age); //29
alert(window.job); //"Software Engineer"
这里,原本针对 Person 实例的三个属性被加到 window 对象上,因为构造函数是作为普通函数调用的, 忽略了 new 操作符。这个问题是由 this 对象的晚绑定造成的,在这里 this 被解析成了 window对象。由于 window 的 name 属性是用于识别链接目标和 frame 的,所以这里对该属性的偶然覆盖可能会导致该页面上出现其他错误。这个问题的解决方法就是创建一个作用域安全的构造函数。
function Person(name, age, job){
if (this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
} else {
return new Person(name, age, job);
}
}
var person1 = Person("Nicholas", 29, "Software Engineer");
alert(window.name); //""
alert(person1.name); //"Nicholas"
var person2 = new Person("Shelby", 34, "Ergonomist");
alert(person2.name); //"Shelby"
实现这个模式后,你就锁定了可以调用构造函数的环境。如果你使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。这里有个例子:
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
var rect = new Rectangle(5, 10);
alert(rect.sides); //undefined
在这段代码中, Polygon 构造函数是作用域安全的,然而 Rectangle 构造函数则不是。新创建一个 Rectangle 实例之后,这个实例应该通过 Polygon.call()来继承 Polygon 的 sides 属性。但是,由于 Polygon 构造函数是作用域安全的, this 对象并非 Polygon 的实例,所以会创建并返回一个新的 Polygon 对象。 Rectangle 构造函数中的 this 对象并没有得到增长,同时 Polygon.call()返回的值也没有用到,所以 Rectangle 实例中就不会有 sides 属性。
如果构造函数窃取结合使用原型链或者寄生组合则可以解决这个问题。考虑以下例子:
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
Rectangle.prototype = new Polygon();
var rect = new Rectangle(5, 10);
alert(rect.sides); //2
惰性载入函数
ps:不理解,跳过
函数绑定
另一个日益流行的高级技巧叫做函数绑定。函数绑定要创建一个函数,可以在特定的 this 环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this.message);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick);
在上面这个例子中,创建了一个叫做 handler 的对象。 handler.handleClick()方法被分配为一个 DOM 按钮的事件处理程序。当按下该按钮时,就调用该函数,显示一个警告框。虽然貌似警告框应 该 显 示 Event handled , 然 而 实 际 上 显 示 的 是 undefiend 。 这 个 问 题 在 于 没 有 保 存handler.handleClick()的环境,所以 this 对象最后是指向了 DOM 按钮而非 handler(在 IE8 中,this 指向 window。)可以如下面例子所示,使用一个闭包来修正这个问题。
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this.message);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", function(event){
handler.handleClick(event);
});
这个解决方案在 onclick 事件处理程序内使用了一个闭包直接调用 handler.handleClick()。当然,这是特定于这段代码的解决方案。创建多个闭包可能会令代码变得难于理解和调试。因此,很多JavaScript 库实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫 bind()。一个简单的 bind()函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。语法如下:
function bind(fn, context){
return function(){
return fn.apply(context, arguments);
};
}
这个函数似乎简单,但其功能是非常强大的。在 bind()中创建了一个闭包,闭包使用 apply()调用传入的函数,并给 apply()传递 context 对象和参数。注意这里使用的 arguments 对象是内部函数的,而非 bind()的。当调用返回的函数时,它会在给定环境中执行被传入的函数并给出所有参数。bind()函数按如下方式使用:
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this.message);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));
ECMAScript 5 为所有函数定义了一个原生的 bind()方法,进一步简单了操作。换句话说,你不用再自己定义 bind()函数了,而是可以直接在函数上调用这个方法。
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this.message + ":" + event.type);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));
科里化函数
ps:看不懂系列
防篡改对象
JavaScript 共享的本质一直是开发人员心头的痛。因为任何对象都可以被在同一环境中运行的代码修改。开发人员很可能会意外地修改别人的代码,甚至更糟糕地,用不兼容的功能重写原生对象。ECMAScript 5 致力于解决这个问题,可以让开发人员定义防篡改对象(tamper-proof object)。
不过请注意:一旦把对象定义为防篡改,就无法撤销了
不可扩展对象
默认情况下,所有对象都是可以扩展的。
使用
Object.preventExtensions()方法可以改变这个行为,让你不能再给对象添加属性和方法。例如:
var person = { name: "Nicholas" };
Object.preventExtensions(person);
person.age = 29;
alert(person.age); //undefined
密封的对象
ECMAScript 5 为对象定义的第二个保护级别是密封对象(sealed object)。密封对象不可扩展,而且已有成员的[[Configurable]]特性将被设置为 false。这就意味着不能删除属性和方法,因为不能使用 Object.defineProperty()把数据属性修改为访问器属性,或者相反。属性值是可以修改的。要密封对象,可以使用 Object.seal()方法。
var person = { name: "Nicholas" };
Object.seal(person);
person.age = 29;
alert(person.age); //undefined
delete person.name;
alert(person.name); //"Nicholas"
冻结的对象
最严格的防篡改级别是冻结对象(frozen object)。冻结的对象既不可扩展,又是密封的,而且对象数据属性的[[Writable]]特性会被设置为 false。如果定义[[Set]]函数,访问器属性仍然是可写的。ECMAScript 5 定义的 Object.freeze()方法可以用来冻结对象。
var person = { name: "Nicholas" };
Object.freeze(person);
person.age = 29;
alert(person.age); //undefined
delete person.name;
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
高级定时器
使用 setTimeout()和 setInterval()创建的定时器可以用于实现有趣且有用的功能。虽然人们对 JavaScript 的定时器存在普遍的误解,认为它们是线程,其实 JavaScript 是运行于单线程的环境中的,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机是不能保证的,因为在页面的生命周期中,不同时间可能有其他代码在控制 JavaScript 进程。在页面下载完后的代码运行、事件处理程序、 Ajax 回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
可以把 JavaScript 想象成在时间线上运行的。当页面载入时,首先执行是任何包含在<script>元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,不过有时候也包含一些初始数据的处理。在这之后, JavaScript 进程将等待更多代码执行。当进程空闲的时候,下一个代码会被触发并立刻执行。例如,当点击某个按钮时, onclick 事件处理程序会立刻执行,只要 JavaScript 进程处于空闲状态。
除了主 JavaScript 执行进程外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序添加入队列。例如,当某个按钮被按下时,它的事件处理程序代码就会被添加到队列中,并在下一个可能的时间里执行。当接收到某个 Ajax 响应时,回调函数的代码会被添加到队列。在 JavaScript 中没有任何代码是立刻执行的,但一旦进程空闲则尽快执行。
定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立刻执行,而只能表示它会尽快执行。设定一个 150ms 后执行的定时器不代表到了 150ms 代码就立刻执行,它表示代码会在 150ms 后被加入到队列中。如果在这个时间点上,队列中没有其他东西,那么这段代码就会被执行,表面上看上去好像代码就在精确指定的时间点上执行了。其他情况下,代码可能明显地等待更长时间才执行。
关于定时器要记住的最重要的事情是,指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。
重复的定时器
使用 setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个方式的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。幸好, JavaScript 引擎够聪明,能避免这个问题。当使用 setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码入到队列中的最小时间间隔为指定间隔。
这种重复定时器的规则有两个问题: (1) 某些间隔会被跳过; (2) 多个定时器的代码执行之间的间隔可能会比预期的小。假设,某个 onclick 事件处理程序使用 setInterval()设置了一个 200ms 间隔的重复定时器。如果事件处理程序花了 300ms 多一点的时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。
为了避免 setInterval()的重复定时器的这 2个缺点,你可以用如下模式使用链式 setTimeout()调用。
setTimeout(function(){
//处理中
setTimeout(arguments.callee, interval);
}, interval);
这个模式链式调用了 setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用使用了 arguments.callee 来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。这个模式主要用于重复定时器,如下例所示
setTimeout(function(){
var div = document.getElementById("myDiv");
left = parseInt(div.style.left) + 5;
div.style.left = left + "px";
if (left < 200){
setTimeout(arguments.callee, 50);
}
}, 50);
Yielding Processes
运行在浏览器中的 JavaScript 都被分配了一个确定数量的资源。不同于桌面应用往往能够随意控制他们要的内存大小和处理器时间, JavaScript 被严格限制了,以防止恶意的 Web 程序员把用户的计算机搞挂了。其中一个限制是长时间运行脚本的制约,如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的时间执行,询问是允许其继续执行还是停止它。所有 JavaScript 开发人员的目标就是,确保用户永远不会在浏览器中看到这个令人费解的对话框。定时器是绕开此限制的方法之一。
脚本长时间运行的问题通常是由两个原因之一造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环。这两者中,后者是较为容易解决的问题。长时间运行的循环通常遵循以下模式
for (var i=0, len=data.length; i < len; i++){
process(data[i]);
}
这个模式的问题在于要处理的项目的数量在运行前是不可知的。如果完成 process()要花 100ms,只有 2 个项目的数组可能不会造成影响,但是 10 个的数组可能会导致脚本要运行一秒钟才能完成。数组中的项目数量直接关系到执行完该循环的时间长度。同时由于 JavaScript 的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越久。在展开该循环之前,你需要回答以下两个重要的问题。
- 该处理是否必须同步完成?如果这个数据的处理会造成其他运行的阻塞,那么最好不要改动它。不过,如果你对这个问题的回答确定为“否”,那么将某些处理推迟到以后是个不错的备选项。
- 数据是否必须按顺序完成?通常,数组只是对项目的组合和迭代的一种简便的方法而无所谓顺序。如果项目的顺序不是非常重要,那么可能可以将某些处理推迟到以后
当你发现某个循环占用了大量时间,同时对于上述两个问题,你的回答都是“否”,那么你就可以使用定时器分割这个循环。这是一种叫做数组分块(array chunking)的技术,小块小块地处理数组,通常每次一小块。基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。
setTimeout(function(){
//取出下一个条目并处理
var item = array.shift();
process(item);
//若还有条目,再设置另一个定时器
if(array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
在数组分块模式中, array 变量本质上就是一个“待办事宜”列表,它包含了要处理的项目。使用shift()方法可以获取队列中下一个要处理的项目,然后将其传递给某个函数。如果在队列中还有其他项目,则设置另一个定时器,并通过 arguments.callee 调用同一个匿名函数。要实现数组分块非常简单,可以使用以下函数。
function chunk(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context, item);
if (array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
函数节流
浏览器中某些计算和处理要比其他的昂贵很多。例如, DOM 操作比起非 DOM 交互需要更多的内存和 CPU 时间。连续尝试进行过多的 DOM 相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在 IE 中使用 onresize 事件处理程序的时候容易发生,当调整浏览器大小的时候,该事件会连续触发。在 onresize 事件处理程序内部如果尝试进行 DOM 操作,其高频率的更改可能会让浏览器崩溃。为了绕开这个问题,你可以使用定时器对该函数进行节流。
函数节流背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。以下是该模式的基本形式:
var processor = {
timeoutId: null,
//实际进行处理的方法
performProcessing: function(){
//实际执行的代码
},
//初始处理调用的方法
process: function(){
clearTimeout(this.timeoutId);
var that = this;
this.timeoutId = setTimeout(function(){
that.performProcessing();
}, 100);
}
};
//尝试开始执行
processor.process();
时间间隔设为了 100ms,这表示最后一次调用 process()之后至少 100ms 后才会调用 performProcessing()。所以如果 100ms 之内调用了 process()共 20 次, performanceProcessing()仍只会被调用一次。
这个模式可以使用 throttle()函数来简化,这个函数可以自动进行定时器的设置和清除,如下例所示:
function throttle(method, context) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(context);
}, 100);
}
假设有一个<div/>元素需要保持它的高度始终等同于宽度。那么实现这一功能的 JavaScript 可以如下编写:
window.onresize = function(){
var div = document.getElementById("myDiv");
div.style.height = div. offsetWidth + "px";
};
这段非常简单的例子有两个问题可能会造成浏览器运行缓慢。首先,要计算 offsetWidth 属性,如果该元素或者页面上其他元素有非常复杂的 CSS 样式,那么这个过程将会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的 CSS 的话,这又需要很多计算。这就可以用到 throttle()函数,如下例所示:
function resizeDiv(){
var div = document.getElementById("myDiv");
div.style.height = div.offsetWidth + "px";
}
window.onresize = function(){
throttle(resizeDiv);
};
自定义事件
ps:观察者模式的js实现,跳过
拖放
ps:暂时用不到,跳过
第23章 离线应用于客户端存储
支持离线 Web 应用开发是 HTML5 的另一个重点。所谓离线 Web 应用,就是在设备不能上网的情况下仍然可以运行的应用。 HTML5 把离线应用作为重点,主要是基于开发人员的心愿。前端开发人员一直希望 Web 应用能够与传统的客户端应用同场竞技,起码做到只要设备有电就能使用。
离线检测
开发离线应用的第一步是要知道设备是在线还是离线,HTML5为此定义了一个navigator.onLine属性,这个属性值为 true 表示设备能上网,值为 false 表示设备离线。这个属性的关键是浏览器必须知道设备能否访问网络,从而返回正确的值。实际应用中, navigator.onLine 在不同浏览器间还有些小的差异。
除 navigator.onLine 属性之外,为了更好地确定网络是否可用, HTML5 还定义了两个事件:online 和 offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件在 window 对象上触发。
EventUtil.addHandler(window, "online", function(){
alert("Online");
});
EventUtil.addHandler(window, "offline", function(){
alert("Offline");
});
应用缓存
HTML5 的应用缓存(application cache),或者简称为 appcache,是专门为开发离线 Web 应用而设计的。 Appcache 就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可以使用一个描述文件(manifest file),列出要下载和缓存的资源。下面是一个简单的描述文件示例。
CACHE MANIFEST
#Comment
file.js
file.css
在最简单的情况下,描述文件中列出的都是需要下载的资源,以备离线时使用。
虽然应用缓存的意图是确保离线时资源可用,但也有相应的 JavaScript API 让你知道它都在做什么。
这个 API 的核心是 applicationCache 对象,这个对象有一个 status 属性,属性的值是常量,表示
应用缓存的如下当前状态。
- 0:无缓存,即没有与页面相关的应用缓存。
- 1:闲置,即应用缓存未得到更新。
- 2:检查中,即正在下载描述文件并检查更新。
- 3:下载中,即应用缓存正在下载描述文件中指定的资源。
- 4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过 swapCache()来使用了。
- 5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存。应用缓存还有很多相关的事件,表示其状态的改变。以下是这些事件。
- checking:在浏览器为应用缓存查找更新时触发。
- error:在检查更新或下载资源期间发生错误时触发。
- noupdate:在检查描述文件发现文件无变化时触发。
- downloading:在开始下载应用缓存资源时触发。
- progress:在文件下载应用缓存的过程中持续不断地触发。
- updateready:在页面新的应用缓存下载完毕且可以通过 swapCache()使用时触发。
- cached:在应用缓存完整可用时触发。
一般来讲,这些事件会随着页面加载按上述顺序依次触发。不过,通过调用 update()方法也可以手工干预,让应用缓存为检查更新而触发上述事件。
applicationCache.update();
update()一经调用,应用缓存就会去检查描述文件是否更新(触发 checking 事件),然后就像页面刚刚加载一样,继续执行后续操作。如果触发了 cached 事件,就说明应用缓存已经准备就绪,不会再发生其他操作了。如果触发了 updateready 事件,则说明新版本的应用缓存已经可用,而此时你需要调用 swapCache()来启用新应用缓存。
EventUtil.addHandler(applicationCache, "updateready", function(){
applicationCache.swapCache();
});
数据存储
随着 Web 应用程序的出现,也产生了对于能够直接在客户端上存储用户信息能力的要求。想法很合乎逻辑,属于某个特定用户的信息应该存在该用户的机器上。无论是登录信息、偏好设定或其他数据,Web 应用提供者发现他们在找各种方式将数据存在客户端上。这个问题的第一个方案是以 cookie 的形式出现的, cookie 是原来的网景公司创造的。
Cookie
HTTP Cookie,通常直接叫做 cookie,最初是在客户端用于存储会话信息的。该标准要求服务器对任意 HTTP 请求发送 Set-Cookie HTTP 头作为响应的一部分,其中包含会话信息。
限制
cookie 在性质上是绑定在特定的域名下的。当设定了一个 cookie 后,再给创建它的域名发送请求时,都会包含这个 cookie。这个限制确保了储存在 cookie 中的信息只能让批准的接受者访问,而无法被其他域访问。
由于 cookie 是存在客户端计算机上的,还加入了一些限制确保 cookie 不会被恶意使用,同时不会占据太多磁盘空间。每个域的 cookie 总数是有限的,不过浏览器之间各有不同。
** cookie的构成**
- 名称:一个唯一确定 cookie 的名称。cookie 名称是不区分大小写的,所以 myCookie 和 MyCookie被认为是同一个 cookie。然而,实践中最好将 cookie 名称看作是区分大小写的,因为某些服务器会这样处理 cookie。 cookie 的名称必须是经过 URL 编码的。
- 值:储存在 cookie 中的字符串值。值必须被 URL 编码
- 域: cookie 对于哪个域是有效的。所有向该域发送的请求中都会包含这个 cookie 信息。这个值可以包含子域(subdomain,如 www.wrox.com),也可以不包含它(如.wrox.com,则对于 wrox.com的所有子域都有效)。如果没有明确设定,那么这个域会被认作来自设置 cookie 的那个域。
- 路径:对于指定域中的那个路径,应该向服务器发送 cookie。例如,你可以指定 cookie 只有从http://www.wrox.com/books/ 中才能访问,那么 http://www.wrox.com 的页面就不会发送 cookie 信息,即使请求都是来自同一个域的
- 失效时间:表示 cookie 何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有 cookie 删除;不过也可以自己设置删除时间。这个值是个 GMT 格式的日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定应该删除cookie 的准确时间。因此, cookie 可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则 cookie 会被立刻删除。
- 安全标志:指定后, cookie 只有在使用 SSL 连接的时候才发送到服务器。例如, cookie 信息只能发送给 https://www.wrox.com,而 http://www.wrox.com 的请求则不能发送 cookie
每一段信息都作为 Set-Cookie 头的一部分,使用分号加空格分隔每一段,如下例所示。
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
Other-header: other-header-value
secure 标志是 cookie 中唯一一个非名值对儿的部分,直接包含一个 secure 单词。如下:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value
JavaScript中的cookie
在 JavaScript中处理 cookie有些复杂,因为其众所周知的蹩脚的接口,即 BOM的document. cookie属性。这个属性的独特之处在于它会因为使用它的方式不同而表现出不同的行为。当用来获取属性值时,document.cookie 返回当前页面可用的(根据 cookie 的域、路径、失效时间和安全设置)所有 cookie的字符串,一系列由分号隔开的名值对儿,如下例所示。
name1=value1;name2=value2;name3=value3
关于cookie的思考
还有一类 cookie 被称为“HTTP 专有 cookie”。 HTTP 专有 cookie 可以从浏览器或者服务器设置,但是只能从服务器端读取,因为 JavaScript 无法获取 HTTP 专有 cookie 的值。由于所有的 cookie 都会由浏览器作为请求头发送,所以在 cookie 中存储大量信息会影响到特定域的请求性能。 cookie 信息越大,完成对服务器请求的时间也就越长。尽管浏览器对 cookie 进行了大小限制,不过最好还是尽可能在 cookie 中少存储信息,以避免影响性能。
一定不要在 cookie 中存储重要和敏感的数据。 cookie 数据并非存储在一个安全环境中,其中包含的任何数据都可以被他人访问。所以不要在 cookie 中存储诸如信用卡号或者个人地址之类的数据。
IE用户数据
在 IE5.0 中,微软通过一个自定义行为引入了持久化用户数据的概念。用户数据允许每个文档最多128KB 数据,每个域名最多 1MB 数据。要使用持久化用户数据,首先必须如下所示,使用 CSS 在某个元素上指定 userData 行为:
<div style="behavior:url(#default#userData)" id="dataStore"></div>
一旦该元素使用了 userData 行为,那么就可以使用 setAttribute()方法在上面保存数据了。为了将数据提交到浏览器缓存中,还必须调用 save()方法并告诉它要保存到的数据空间的名字。数据空间名字可以完全任意,仅用于区分不同的数据集。请看以下例子。
var dataStore = document.getElementById("dataStore");
dataStore.setAttribute("name", "Nicholas");
dataStore.setAttribute("book", "Professional JavaScript");
dataStore.save("BookInfo");
在这段代码中, <div>元素上存入了两部分信息。在用 setAttribute()存储了数据之后,调用了
save()方法,指定了数据空间的名称为 BookInfo。下一次页面载入之后,可以使用 load()方法指定同样的数据空间名称来获取数据,如下所示。
dataStore.load("BookInfo");
alert(dataStore.getAttribute("name")); //"Nicholas"
alert(dataStore.getAttribute("book")); //"Professional JavaScript"
对 IE 用户数据的访问限制和对 cookie 的限制类似。要访问某个数据空间,脚本运行的页面必须来自同一个域名,在同一个路径下,并使用与进行存储的脚本同样的协议。和 cookie 不同的是,你无法将用户数据访问限制扩展到更多的客户。还有一点不同,用户数据默认是可以跨越会话持久存在的,同时也不会过期;数据需要通过 removeAttribute()方法专门进行删除以释放空间。
和 cookie 一样, IE 用户数据并非安全的,所以不能存放敏感信息。
Web存储机制
Web Storage 的目的是克服由 cookie 带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。 Web Storage 的两个主要目标是:
- 提供一种在 cookie 之外存储会话数据的途径;
- 提供一种存储大量可以跨会话存在的数据的机制。
最初的 Web Storage 规范包含了两种对象的定义: sessionStorage 和 globalStorage。这两个对象在支持的浏览器中都是以 windows 对象属性的形式存在的
Storage类型
Storage 类型提供最大的存储空间(因浏览器而异)来存储名值对儿。 Storage 的实例与其他对
象类似,有如下方法。
- clear(): 删除所有值; Firefox 中没有实现 。
- getItem(name):根据指定的名字 name 获取对应的值。
- key(index):获得 index 位置处的值的名字。
- removeItem(name):删除由 name 指定的名值对儿。
- setItem(name, value):为指定的 name 设置一个对应的值。
sessionStroage对象
sessionStorage 对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。这个对象就像会话 cookie,也会在浏览器关闭后消失。存储在 sessionStorage 中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器崩溃并重启之后依然可用。
因为 seesionStorage 对象绑定于某个服务器会话,所以当文件在本地运行的时候是不可用的。存储在 sessionStorage 中的数据只能由最初给对象存储数据的页面访问到,所以对多页面应用有限制。
由于 sessionStorage 对象其实是 Storage 的一个实例,所以可以使用 setItem()或者直接设置新的属性来存储数据。下面是这两种方法的例子。
//使用方法存储数据
sessionStorage.setItem("name", "Nicholas");
//使用属性存储数据
sessionStorage.book = "Professional JavaScript";
globalStorage对象
作为最初的 Web Storage 规范的一部分,这个对象的目的是跨越会话存储数据,但有特定的访问限制。要使用 globalStorage,首先要指定哪些域可以访问该数据。可以通过方括号标记使用属性来实现,如以下例子所示。
//保存数据
globalStorage["wrox.com"].name = "Nicholas";
//获取数据
var name = globalStorage["wrox.com"].name;
localStorage对象
localStorage 对象在修订过的 HTML 5 规范中作为持久保存客户端数据的方案取代了globalStorage。与 globalStorage 不同,不能给 localStorage 指定任何访问规则;规则事先就设定好了。要访问同一个 localStorage 对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于 globalStorage[location.host]。
storage事件
对 Storage 对象进行任何修改,都会在文档上触发 storage 事件。当通过属性或 setItem()方法保存数据,使用 delete 操作符或 removeItem()删除数据,或者调用 clear()方法时,都会发生该事件。这个事件的 event 对象有以下属性。
- domain:发生变化的存储空间的域名。
- key:设置或者删除的键名。
- newValue:如果是设置值,则是新值;如果是删除键,则是 null。
- oldValue:键被更改之前的值。
无论对 sessionStorage、 globalStorage 还是 localStorage 进行操作,都会触发 storage事件,但不作区分。
限制
与其他客户端数据存储方案类似, Web Storage 同样也有限制。这些限制因浏览器而异。一般来说,对存储空间大小的限制都是以每个来源(协议、域和端口)为单位的。
IndexedDB
Indexed Database API,或者简称为 IndexedDB,是在浏览器中保存结构化数据的一种数据库。IndexedDB 是为了替代目前已被废弃的 Web SQL Database API(因为已废弃,所以本书未介绍)而出现的。 IndexedDB 的思想是创建一套 API,方便保存和读取 JavaScript 对象,同时还支持查询及搜索。
IndexedDB 设计的操作完全是异步进行的。因此,大多数操作会以请求方式进行,但这些操作会在后期执行,然后如果成功则返回结果,如果失败则返回错误。差不多每一次 IndexedDB 操作,都需要你注册 onerror 或 onsuccess 事件处理程序,以确保适当地处理结果。
数据库
IndexedDB 就是一个数据库,与 MySQL 或 Web SQL Database 等这些你以前可能用过的数据库类似。IndexedDB 最大的特色是使用对象保存数据,而不是使用表来保存数据。一个 IndexedDB 数据库,就是一组位于相同命名空间下的对象的集合。
使用 IndexedDB 的第一步是打开它,即把要打开的数据库名传给 indexDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求。总之,调用 indexDB.open()会返回一个 IDBRequest 对象,在这个对象上可以添加 onerror和 onsuccess 事件处理程序。先来看一个例子。
var request, database;
request = indexedDB.open("admin");
request.onerror = function(event){
alert("Something bad happened while trying to open: " +
event.target.errorCode);
};
request.onsuccess = function(event){
database = event.target.result;
};
对象存储空间
在建立了与数据库的连接之后,下一步就是使用对象存储空间①。如果数据库的版本与你传入的版本不匹配,那可能就需要创建一个新的对象存储空间。在创建对象存储空间之前,必须要想清楚你想要保存什么数据类型。
假设你要保存的用户记录由用户名、密码等组成,那么保存一条记录的对象应该类似如下所示:
var user = {
username: "007",
firstName: "James",
lastName: "Bond",
password: "foo"
};
事务
在数据库对象上调用 transaction()方法可以创建事务。任何时候,只要想读取或修改数据,都要通过事务来组织所有操作。
使用游标查询
使用事务可以直接通过已知的键检索单个对象。而在需要检索多个对象的情况下,则需要在事务内部创建游标。游标就是一指向结果集的指针。与传统数据库查询不同,游标并不提前收集结果。游标指针会先指向结果中的第一项,在接到查找下一项的指令时,才会指向下一项。
键范围
使用游标总让人觉得不那么理想,因为通过游标查找数据的方式太有限了。键范围(key range)为使用游标增添了一些灵活性。
设定游标方向
正常情况下,游标都是从存储空间的第一项开始,调用 continue()或 advance()前进到最后一项。游标的默认方向值是 IDBCursor.NEXT。如果对象存储空间中有重复的项,而你想让游标跳过那些重复的项,可以为 openCursor 传入 IDBCursor.NEXT_NO_DUPLICATE 作为第二个参数:
var store = db.transaction("users").objectStore("users"),
request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE);
当然,也可以创建一个游标,让它在对象存储空间中向后移动,即从最后一个对象开始,逐个迭代,直至第一个对象。
var store = db.transaction("users").objectStore("users"),
request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE);
当然,也可以创建一个游标,让它在对象存储空间中向后移动,即从最后一个对象开始,逐个迭代,直至第一个对象。
索引
对于某些数据,可能需要为一个对象存储空间指定多个键。比如,若要通过用户 ID 和用户名两种方式来保存用户资料,就需要通过这两个键来存取记录。为此,可以考虑将用户 ID 作为主键,然后为用户名创建索引。
并发问题
虽然网页中的 IndexedDB 提供的是异步 API,但仍然存在并发操作的问题。如果浏览器的两个不同的标签页打开了同一个页面,那么一个页面试图更新另一个页面尚未准备就绪的数据库的问题就有可能发生。把数据库设置为新版本有可能导致这个问题。因此,只有当浏览器中仅有一个标签页使用数据库的情况下,调用 setVersion()才能完成操作。
刚打开数据库时,要记着指定 onversionchange 事件处理程序。当同一个来源的另一个标签页调用 setVersion()时,就会执行这个回调函数。处理这个事件的最佳方式是立即关闭数据库,从而保证版本更新顺利完成。
限制
对 IndexedDB 的限制很多都与对 Web Storage 的类似。首先, IndexedDB 数据库只能由同源(相同协议、域名和端口)页面操作,因此不能跨域共享信息。换句话说, www.wrox.com 与 p2p.wrox.com的数据库是完全独立的。
其次,每个来源的数据库占用的磁盘空间也有限制。
第24章 最佳时间
可维护性
在早期的网站中, JavaScript 主要是用于小特效或者是表单验证。而今天的 Web 应用则会有成千上万行 JavaScript 代码,执行各种复杂的过程。这种演化让开发者必须得考虑到可维护性。
什么是可维护的代码
可维护的代码有一些特征。一般来说,如果说代码是可维护的,它需要遵循以下特点。
- 可理解性——其他人可以接手代码并理解它的意图和一般途径,而无需原开发人员的完整解释。
- 直观性——代码中的东西一看就能明白,不管其操作过程多么复杂。
- 可适应性——代码以一种数据上的变化不要求完全重写的方法撰写。
- 可扩展性——在代码架构上已考虑到在未来允许对核心功能进行扩展。
- 可调试性——当有地方出错时,代码可以给予你足够的信息来尽可能直接地确定问题所在
代码约定
可读性
要让代码可维护,首先它必须可读。可读性与代码作为文本文件的格式化方式有关。可读性的大部分内容都是和代码的缩进相关的。当所有人都使用一样的缩进方式时,整个项目中的代码都会更加易于阅读。
可读性的另一方面是注释。一般而言,有如下一些地方需要进行注释。
函数和方法——每个函数或方法都应该包含一个注释,描述其目的和用于完成任务所可能使用
的算法。陈述事先的假设也非常重要,如参数代表什么,函数是否有返回值(因为这不能从函
数定义中推断出来)。
- 大段代码——用于完成单个任务的多行代码应该在前面放一个描述任务的注释。
- 复杂的算法——如果使用了一种独特的方式解决某个问题,则要在注释中解释你是如何做的。这不仅仅可以帮助其他浏览你代码的人,也能在下次你自己查阅代码的时候帮助理解。
- Hack——因为存在浏览器差异, JavaScript 代码一般会包含一些 hack。不要假设其他人在看代码的时候能够理解 hack 所要应付的浏览器问题。如果因为某种浏览器无法使用普通的方法,所以你需要用一些不同的方法,那么请将这些信息放在注释中。这样可以减少出现这种情况的可能性:有人偶然看到你的 hack,然后“修正”了它,最后重新引入了你本来修正了的错误。
变量和函数命名
适当给变量和函数起名字对于增加代码可理解性和可维护性是非常重要的。
- 变量名应为名词如 car 或 person。
- 函数名应该以动词开始,如 getName()。返回布尔类型值的函数一般以 is 开头,如isEnable()。
- 变量和函数都应使用合乎逻辑的名字,不要担心长度。长度问题可以通过后处理和压缩(本章后面会讲到)来缓解
变量类型透明
由于在 JavaScript 中变量是松散类型的,很容易就忘记变量所应包含的数据类型。合适的命名方式可以一定程度上缓解这个问题。
第一种方式是初始化。当定义了一个变量后,它应该被初始化为一个值,来暗示它将来应该如何应用。例如,将来保存布尔类型值的变量应该初始化为 true 或者 false,将来保存数字的变量就应该初始化为一个数字,如以下例子所示:
//通过初始化指定变量类型
var found = false; //布尔型
var count = -1; //数字
var name = ""; //字符串
var person = null; //对象
第二种方法是使用匈牙利标记法来指定变量类型。匈牙利标记法在变量名之前加上一个或多个字符来表示数据类型。这个标记法在脚本语言中很流行,曾经很长时间也是 JavaScript 所推崇的方式。JavaScript 中最传统的匈牙利标记法是用单个字符表示基本类型: "o"代表对象, "s"代表字符串, "i"代表整数, "f"代表浮点数, "b"代表布尔型。如下所示:
//用于指定数据类型的匈牙利标记法
var bFound; //布尔型
var iCount; //整数
var sName; //字符串
var oPerson; //对象
JavaScript 中用匈牙利标记法的好处是函数参数一样可以使用。但它的缺点是让代码某种程度上难以阅读,阻碍了没有用它时代码的直观性和句子式的特质。因此,匈牙利标记法失去了一些开发者的宠爱。
最后一种指定变量类型的方式是使用类型注释。类型注释放在变量名右边,但是在初始化前面。这种方式是在变量旁边放一段指定类型的注释,如下所示:
//用于指定类型的类型注释
var found /*:Boolean*/ = false;
var count /*:int*/ = 10;
var name /*:String*/ = "Nicholas";
var person /*:Object*/ = null;
松散耦合
只要应用的某个部分过分依赖于另一部分,代码就是耦合过紧,难于维护。典型的问题如:对象直接引用另一个对象,并且当修改其中一个的同时需要修改另外一个。紧密耦合的软件难于维护并且需要经常重写。
解耦HTML/JavaScript
一种最常见的耦合类型是 HTML/JavaScript 耦合。在 Web 上, HTML 和 JavaScript 各自代表了解决方案中的不同层次: HTML 是数据, JavaScript 是行为。因为它们天生就需要交互,所以有多种不同的方法将这两个技术关联起来。但是,有一些方法会将 HTML 和 JavaScript 过于紧密地耦合在一起。直接写在 HTML 中的 JavaScript,使用包含内联代码的<script>元素或者是使用 HTML 属性来分配事件处理程序,都是过于紧密的耦合。请看以下代码。
<!-- 使用了 <script> 的紧密耦合的 HTML/JavaScript -->
<script type="text/javascript">
document.write("Hello world!");
</script>
<!-- 使用事件处理程序属性值的紧密耦合的 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() 函数可以实现复用了
以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:
- 勿将 event 对象传给其他方法;只传来自 event 对象中所需的数据;
- 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行;
- 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。
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。一个循环的基本优化步骤如下所示。
- 减值迭代——大多数循环使用一个从 0 开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
- 简化终止条件——由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或其他 O(n)的操作。
- 简化循环体——循环体是执行最多的,所以要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
- 使用后测试循环——最常用 for 循环和 while 循环都是前测试循环。而如 do-while 这种后测试循环,可以避免最初终止条件的计算,因此运行更快。
展开循环
当循环的次数是确定的,消除循环并使用多次函数调用往往更快 。请看一下前面的例子。如果数组的长度总是一样的,对每个元素都调用 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 重写一个。原生方法是用诸如 C/C++之类的编译型语言写出来的,所以要比 JavaScript 的快很多很多。 JavaScript 中最容易被忘记的就是可以在 Math 对象中找到的复杂的数学运算;这些方法要比任何用 JavaScript 写的同样方法如正弦、余弦快的多。
- itch 语句较快 —— 如果有一系列复杂的 if-else 语句,可以转换成单个 switch 语句则可以得到更快的代码。还可以通过将 case 语句按照最可能的到最不可能的顺序进行组织,来进一步优化 switch 语句。
- 算符较快 —— 当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能。诸如取模,逻辑与和逻辑或都可以考虑用位运算来替换
最小化语句数
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 的交互可以极大得提高脚本完成速度。
最小化现场更新
一旦你需要访问的 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 个现场更新:一个添加<li>元素,另一个给它添加文本节点。这样添加 10 个项目,这个操作总共要完成 20 个现场更新。要修正这个性能瓶颈,需要减少现场更新的数量。一般有 2 种方法。第一种是将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置。这个方法不是非常理想,因为在每次页面更新的时候它会不必要的闪烁。第二个方法是使用文档片段来构建 DOM 结构,接着将其添加到 List 元素中。这个方式避免了现场更新和页面闪烁问题。请看下面内容:
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 中的主要性能瓶颈之一。记住通过<script>标记引用 JavaScript 文件是一个阻塞操作,当代码下载并运行的时候会停止其他所有的下载。因此,尽量从逻辑上将 JavaScript 代码分组成部署文件。
验证
首先,验证过程难以自动化或者在不同系统间直接移植。其次,除了语法错误外,很多问题只有在执行代码的时候才会遇到,这给错误留下了空间;有些工具可以帮助确定 JavaScript 代码中潜在的问题,其中最著名的就是 Douglas Crockford 的 JSLint (www.jslint.com)。
压缩
当谈及 JavaScript 文件压缩,其实在讨论两个东西:代码长度和配重(Wire weight)。代码长度指的是浏览器所需解析的字节数,配重指的是实际从服务器传送到浏览器的字节数。在 Web 开发的早期,这两个数字几乎是一样的,因为从服务器端到客户端原封不动地传递了源文件。而在今天的 Web 上,这两者很少相等,实际上也不应相等。
文件压缩
因为 JavaScript 并非编译为字节码,而是按照源代码传送的,代码文件通常包含浏览器执行所不需要的额外的信息和格式。注释,额外的空白,以及长长的变量名和函数名虽然提高了可读性,但却是传送给浏览器时不必要的字节。不过,我们可以使用压缩工具减少文件的大小。压缩器一般进行如下一些步骤:
- 删除额外的空白(包括换行);
- 删除所有注释;
- 缩短变量名。
HTTP压缩
一个指定了文件使用了给定格式进行了压缩的 HTTP 头包含在了服务器响应中。接着浏览器会查看该 HTTP 头确定文件是否已被压缩,然后使用合适的格式进行解压缩。结果是和原来的代码量相比在网络中传递的字节数量大大减少了。
第25章 新兴的API
requestAnimationFrame()
这个方法会告诉浏览器:有一个动画开始了。进而浏览器就可以确定重绘的最佳方式。
Page Visibility API
不知道用户是不是正在与页面交互,这是困扰广大 Web 开发人员的一个主要问题。如果页面最小化了或者隐藏在了其他标签页后面,那么有些功能是可以停下来的,比如轮询服务器或者某些动画效果。而 Page Visibility API(页面可见性 API)就是为了让开发人员知道页面是否对用户可见而推出的。
Geolocation API
地理定位(geolocation)是最令人兴奋,而且得到了广泛支持的一个新 API。 通过这套 API, JavaScript代码能够访问到用户的当前位置信息。当然,访问之前必须得到用户的明确许可,即同意在页面中共享其位置信息。如果页面尝试访问地理定位信息,浏览器就会显示一个对话框,请求用户许可共享其位置信息。
File API
不能直接访问用户计算机中的文件,一直都是 Web 应用开发中的一大障碍。 2000 年以前,处理文件的唯一方式就是在表单中加入<input type="file">字段,仅此而已。 File API(文件 API)的宗旨是为 Web 开发人员提供一种安全的方式,以便在客户端访问用户计算机中的文件,并更好地对这些文件执行操作。支持 File API 的浏览器有 IE10+、 Firefox 4+、 Safari 5.0.5+、 Opera 11.1+和 Chrome。
Web计时
页面性能一直都是 Web 开发人员最关注的领域。但直到最近,度量页面性能指标的唯一方式,就是提高代码复杂程度和巧妙地使用 JavaScript 的 Date 对象。 Web Timing API 改变了这个局面,让开发人员通过 JavaScript 就能使用浏览器内部的度量结果,通过直接读取这些信息可以做任何想做的分析。
Web Workers
随着 Web 应用复杂性的与日俱增,越来越复杂的计算在所难免。长时间运行的 JavaScript 进程会导致浏览器冻结用户界面,让人感觉屏幕“冻结”了。 Web Workers 规范通过让 JavaScript 在后台运行解决了这个问题。浏览器实现 Web Workers 规范的方式有很多种,可以使用线程、后台进程或者运行在其他处理器核心上的进程,等等。具体的实现细节其实没有那么重要,重要的是开发人员现在可以放心地运行 JavaScript,而不必担心会影响用户体验了。
附录C JavaScript库
通用库
YUI
它是一个开源 JavaScript 与 CSS 库,以一种组件方式设计的。这个库不只有一个文件;它包含了很多文件,提供各种不同的配置,让你可以按需载入。 YUI(Yahoo!User Interface Library,雅虎用户界面库)涵盖了 JavaScript 的所有方面,从基本的工具及帮助函数到完善的浏览器部件。
Prototype
它是一个提供了常见任务 API的开源库。最初是针对 Ruby on Rails框架中的使用而开发的,Prototype是类驱动的,旨在为 JavaScript 提供类定义和继承。因此, Prototype 提供了很多类,用于将常见或复杂功能封装为简单的 API 调用。 Prototype 只有一个单独的文件,可以很容易地放入任意页面。
Dojo Toolkit
Dojo Toolkit 开源库基于一种包系统建模,一组功能组成一个包,可以按需载入。 Dojo 提供了范围广泛的选项和配置,几乎涵盖了你要用 JavaScript 做的任何事情。 Dojo Toolkit 由 Alex Russell 创建,并由 Dojo 基金会的雇员和志愿者维护。
MooTools
MooTools 是一个为了精简和优化而设计的开源库,它为内置 JavaScript 对象添加了各种方法,以通过接近的接口提供新功能,或者直接提供新的对象。 MooTools 的短小精悍受到一些 Web 开发者的青睐。
JQuery
jQuery 是一个给 JavaScript 提供了函数式编程接口的开源库。它是一个完整的库,其核心是构建于CSS 选择器上的,用来操作 DOM 元素。通过链式调用, jQuery 代码看上去更像是对于应该发生什么的描述而不是 JavaScript 代码。这种代码风格在设计师和原型制作人中非常流行。
MochiKit
MochKit 是一个由一些小工具组成的开源库,它以完善的文档和完整的测试见长,拥有大量 API 及相关范例文档以及数百个测试来确保质量。
Underscore.js
虽然严格来讲 Underscore.js 并不是一个通用的库,但它的确为 JavaScript 中的功能性编程提供了很多额外的功能。其文档称 Underscore.js 是对 jQuery 的补充,提供了操作对象、数组、函数和其他 JavaScript数据类型的更多的低级功能。
互联网应用
互联网应用库是针对于简化完整的 Web 应用开发而设计的。它们并不提供应用问题的小块组件,而是提供了快速应用开发的整个概念框架。虽然这些库也可能提供一些底层功能,但他们的目标是帮助用户快速开发 Web 应用。
Backbone.js
Backbone.js 是构建于 Underscore.js 基础之上的一个迷你 MVC 开源库,它针对单页应用进行优化,让你能够随着应用状态变化方便地更新页面的任意部分。
Rico
Rico 是一个开源库,旨在让行为丰富的互联网应用的开发更加简单。它提供了 Ajax、动画、样式以及部件的工具。
qooxdoo
它是一个旨在为整个互联网应用开发周期提供帮助的开源库。 qooxdoo 实现了它自己的类和接口,用于创建类似于传统面向对象语言的编程模型。这个库包含了一个完整的 GUI 工具包以及用于简化前端构建过程的编译器。
动画和特效
动画和其他视觉特效也成为了 Web 开发的重要部分。在网页上做出流畅的动画是一个很重要的任务,一些开发者已经做出了易用的库,提供流畅的动画和特效。前面提到的很多通用 JavaScript 库也有动画功能。
script.aculo.us
cript.aculo.us 是 Prototype 的“同伴”,它提供了出色特效的简单使用方式,使用的东西不超过是 CSS和 DOM。 Prototype 必须在使用 script.aculo.us 之前载入。 script.aculo.us 是最流行的特效库之一,世界上很多网站和 Web 应用都在使用它。
moo.fx
moo.fx 开源动画库是设计在 Prototype 或者 MooTools 之上运行的。它的目标是尽可能小(最新的版本是 3KB),并支持开发人员用尽可能少的代码创建动画。 MooTools 是默认包含 moo.fx 的,但也可以单独下载用于 Prototype 中。
Lightbox
Lightbox 是一个用于在任意页面上创建图像浮动层的 JavaScript 库,依赖于 Prototype 和script.aculo.us 来实现它的视觉特效。基本的理念是让用户在一个浮动层中浏览一个或者一系列图像,而不必离开当前页面。 Lightbox 浮动层无论是外观还是过渡效果都可以自定义。
加密
随着 Ajax 应用的流行,对于浏览器端加密以确保通讯安全的需求也越来越多。幸好,一些人已经在 JavaScript 中实现了常用的安全算法。这些库大部分并没有其作者的正式支持,但还是被广泛应用着。
JavaScript MD5
该开源库实现了 MD4、 MD5 以及 SHA-1 安全散列函数。作者 Paul Johnston 和其他一些贡献者将每个算法作为一个文件,创建了这个用于 Web 应用的丰富的库。主页上提供了散列算法的概述、对于其弱点的讨论以及适当的使用方法。
JavaScrypt
该 JavaScript 库实现了 MD5 和 AES(256 位)加密算法。 JavaScrypt 的网站提供了很多关于密码学历史及其在计算机中应用的信息。但是缺乏关于如何将该库集成到 Web 应用中的基本文档, JavaScrypt的代码里面全都是深奥的数学处理和计算。
附录D JavaScript工具
校验器
JavaScript 调试有一个问题,很多 IDE 并不能在输入的时候自动指出语法错误。大多数开发者写了一部分代码之后要将其载入到浏览器中查找错误。你可以通过在部署之前校验 JavaScript 代码,以便显著地减少此类错误。校验器提供了基本的语法检查,并给出某些风格的警告。
JSLint
JSLint 是一个由 Douglas Crockford 撰写的 JavaScript 校验器。它通过跨浏览器问题的最小共同点检查,能够从核心层次上检查语法错误。(它遵循最严格的规则来确保代码到处都能运行。)你可以启用Crockford 对于代码风格的警告,包括代码格式、未声明的全局变量的使用以及其他更多警告。尽管 JSLint是用 JavaScript 写的,但是通过基于 Java 的 Rhino 解释器,它可以在命令行中运行,或者通过 WScript或者其他 JavaScript 解释器。网站上提供了针对各种命令行解释器的自定义版本。
JSHint
JSHint 是 JSLint 的一个分支,为应用规则提供了更多的自定义功能。与 JSLint 类似,它首先检查语法错误,然后检查有问题的编码模式。 JSLint 的每一项检查 JSHint 都有,但开发人员可以更好地控制应用什么规则。与 JSLint 一样, JSHint 也能使用 Rhino 在命令行中运行。
JavaScript Lint
它和 JSLint 完全不相干, JavaScript Lint 是 Matthias Miller 写的一个基于 C 的 JavaScript 校验器。它使用了 SpiderMonkey(即 Firefox 所用的 JavaScript 解释器)来分析代码并查找语法错误。这个工具包含大量选项,可以启用额外关于编码风格的警告,以及未声明的变量和不可到达的代码警告。
压缩器
JavaScript 构建过程中很重要的一部分,是压缩输出并移除多余的字符。这样做可以确保传送到浏览器的字节数最少,最终加速了用户体验。有几种压缩比率不同的工具可以选择。
JSMin
JSMin 是由 Douglas Crockford 写的一个基于 C 的压缩器,进行最基本的 JavaScript 压缩。它主要是移除空白和注释,确保最终的代码依然可以被顺利执行。
Dojo ShrinkSafe
负责 Dojo Toolkit 的同一批人开发了一个叫做 ShrinkSafe 的工具,它使用了 Rhino JavaScript 解释器首先将 JavaScript 代码解析为记号流,然后用它们来安全压缩代码。和 JSMin 一样, ShrinkSafe 移除多余的空白符(不包括换行)和注释,但是还更进一步将局部变量替换为两个字符长的变量名。
YUI Compressor
YUI 小组有一个叫做 YUI Compressor 的压缩器。和 ShrinkSafe 类似, YUI Compressor 利用了 Rhino解释器将 JavaScript 代码解析为记号流,并移除注释和空白字符并替换变量名。与 ShrinkSafe 不同, YUICompressor 还移除换行并进行一些细微的优化进一步节省字节数。
单元测试
JsUnit
最早的 JavaScript 单元测试框架,不绑定于任何特定的 JavaScript 库。 JsUnit 是 Java 知名的 JUnit 测试框架的移植。测试在页面中运行,并可以设置为自动测试并将结果提交到服务器。
YUI Test
作为 YUI 的一部分, YUI Test 不仅可以用于测试使用 YUI 的代码,也可以测试网站或者应用中的任何代码。 YUI Test 包含了简单和复杂的断言,以及一种模拟简单的鼠标和键盘事件的方法。该框架在Yahoo! Developer Network 上有完整的文档描述,包含了例子、 API 文档和更多内容。
DOH
DOH(Dojo Object Harness)在发布给大家使用之前,最初是作为 Dojo 内部的单元测试工具出现的。和其他框架一样,单元测试是在浏览器中运行的。
qUnit
qUnit 是为测试 jQuery 而开发的一个单元测试框架。 jQuery 本身的确使用 qUnit 进行各项测试。除此之外, qUnit 与 jQuery 并没有绑定关系,也可以用它来测试所有 JavaScript 代码。
文档生成器
JsSoc Toolkit
JsDoc Toolkit 是最早出现的 JavaScript文档生成器之一。它要求你在代码中输入类似 Javadoc的注释,然后处理这些注释并输出为 HTML 文件。你可以自定义 HTML 的格式,这需使用预定义的 JsDoc 模板或者创建自己的模版。 JsDoc Toolkit 可以以 Java 包的形式获得。
YUI Doc
YUI Doc 是 YUI 的文档生成器。该生成器以 Python 书写,所以它要求安装有 Python 运行时环境。YUI Doc 可以输出集成了属性和方法搜索(用 YUI 的自动完成挂件实现的)的 HTML 文件。和 JsDoc一样, YUI Doc 要求源代码中使用类似 Javadoc 的注释。默认的 HTML 可以通过修改默认的 HTML 模板文件和相关的样式表来更改。
AjaxDoc
AjaxDoc 的目标和前面提到的生成器有些差异。它不为 JavaScript 文档生成 HTML 文件,而是创建与针对.NET 语言(如 C#和 Visual Basic. NET)所创建文件相同格式的 XML 文件。这样做就可以由标准的.NET 文档生成器创建 HTML 文件形式的文档。 AjaxDoc 使用类似于所有.NET 语言用到的文档注释格式。创建 AjaxDoc 是针对 ASP.NET 的 Ajax 解决方案,但是它也可以用于单独的项目。
安全执行环境
随着 mashup 应用越来越流行,对于允许来自外界的 JavaScript 存在于同一个页面上并执行有着越来越多的需求。这导致了一些访问受限功能的安全问题。以下工具旨在创建安全的执行环境,其中不同来源的 JavaScript 可以共存,而不会互相影响。
ADsafe
由 Douglas Crockford 创建, ADsafe 是 JavaScript 的子集,这个子集被认为可以被第三方脚本安全访问。对于用 ADsafe 运行的代码,页面必须包含 ADsafe JavaScript 库并标记为 ADsafe 挂件格式。因此,代码可以在任何页面上安全执行。
Caja
Caja 用一种独特的方式来确保 JavaScript 的安全执行。类似于 ADsafe, Caja 定义了 JavaScript 的一个可以用安全方式使用的子集。 Caja 继而可以清理 JavaScript 代码并验证它只按照预期的方式运行。作为该项目的一部分,有一种叫做 Cajita 的语言,它是 JavaScript 功能的一种更小的子集。 Caja 还处于幼年期,但是已经展示了很多前景,允许多个脚本在同一个页面执行而没有恶意活动的可能。