Javascript 高级程序设计

Nicholas C. Zakas

第 1 章 Javascript 简介

Javascript 简史

1997 年,以 Javascript 1.1 为蓝本的建议被提交给了欧洲计算机制造商协会(ECMA, European Computer Manufacturers Association)。该协会指定 39 号技术委员会(TC39, Technical Committee #39) 负责“标准化一种通用、跨平台、供应商中立的脚本语言的语法和语义”。TC39 经过数月的努力完成了 ECMA-262 —— 定义了一种名为 ECMAScript 的新脚本语言的标准。

ECMAscript 的版本

ECMA-262 第 3 版才是对该标准第一次真正的修改。修改的内容涉及字符串处理、错误定义和数值输出。这一版本还增加了对正则表达式、新控制语句、try-catch 异常处理的支持,并围绕标准的国际化做出了一些小的修改。从各方面综合来看,第三版标志着 ECMAScript 成为了一门真正的编程语言。

ECMAScript 第 4 版出台的标准几乎在第 3 版基础上完全定义了一门新语言。第 4 版不仅包含了强类型变量、新语句和新数据结构、真正的类和经典继承,还定义了与数据交互的新方式。

与此同时,TC39 下属的一个小组也提出了一个名为 ECMAscript 3.1 的替代性建议,该建议只对这门语言进行了较少的改进。这个小组认为第 4 版给这门语言带来的跨越太大了。最终,ES3.1 附属委员会获得的支持朝贡了 TC39,ECMA-262 第 4 版在正式发布前被放弃。

ECMAScript 3.1 成为 ECMA-262 第 5 版,并于 2009 年 12 月 3 日正式发布。第 5 版力求澄清第 3 版中已知的歧义并增添了新的功能。新功能包括原生 JSON 对象,继承的方法和高级属性定义,另外还包括了一种严格模式。

第 2 章 在 HTML 中使用 Javascript

<script> 元素

按照传统的做法,所有 <script> 元素都应该放在页面的 <head> 元素中,这种做法的目的是把所有外部文件(包括 CSS 文件和 Javascript 文件)的引用都放在相同的地方。可是,在文档的 <head> 元素中包含所有 Javascript 文件,意味着必须等到全部 Javascript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 <body> 标签才开始呈现内容)。对于那些需要很多 Javascript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口将是一片空白。为了避免这个问题,现代 Web 应用程序一般把全部 Javascript 引用放在 <body> 元素中页面内容的后面。

第 3 章 基本概念

标识符

所谓标识符,就是指变量、函数、属性的名字,或者函数的参数。标识符可以是按照下列格式规则组合起来的一个或多个字符:

  • 第一个字符必须是一个字母、下划线或一个美元符号($);
  • 其它字符可以是字母、下划线、美元符号或数字。

变量

用 var 操作符定义的变量将成为定义该变量的作用域的局部变量;如果省略 var 操作符,则创建了一个全局变量。

数据类型

ECMAScript 中有 5 种基本数据类型:Undefined、Null、Boolean、Number 和 String。还有 1 种复杂数据类型 —— Object。

typeof 操作符

对一个值使用 typeof 操作符可能返回下列某个字符串:

  • “undefined” —— 如果这个值未定义或未初始化;
  • “boolean” —— 如果这个值是布尔值;
  • “string” —— 如果这个值是字符串;
  • “number” —— 如果这个值是数值;
  • “object” —— 如果这个值是对象或 null;
  • “function” —— 如果这个值是函数。

Undefined 类型

Undefined 类型只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其初始化时,这个变量的值就是 undefined

一般而言,不存在需要显式地把一个变量设置为 undefined 值得情况。字面值 undefined 的主要目的是用于比较,为了正式区分空对象指针与未经初始化的变量。

包含 undefined 值的变量和尚未定义的变量是不一样的,使用尚未定义的变量会导致一个错误。对于尚未声明过的变量,只能执行一项操作,即使用 typeof 操作符检测其数据类型。

对未初始化的变量执行 typeof 操作符会返回 undefined 值,对未声明的变量执行 typeof 操作符同样也会返回 undefined 值。

即使未初始化的变量会自动获得 undefined 值,但显式地初始化变量依然是明智的选择。如果能做到这一点,那么当 typeof 操作符返回 “undefined” 值时,我们就知道被检测的变量还没有被声明,而不是尚未初始化。

Null 类型

Null 类型是第二个只有一个值的数据类型,这个特殊的值是 null。从逻辑角度看,null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回 “object” 的原因。

如果定义的变量准备在将来用于保存对象,那么最好应该将变量初始化为 null 而不是其他值。这样一来,只要直接检查 null 值就可以知道相应的变量是否已经保存了一个对象的引用。

Boolean 类型

Boolean 类型只有两个字面值:truefalse

需要注意的是,Boolean 类型的字面值 true 和 false 是区分大小写的。也就是说,True 和 False(以及其他的混合大小写形式)都不是 Boolean 值,而只是标识符。

虽然 Boolean 类型的字面值只有两个,但 ECMAScript 中所有类型的值都有与这两个 Boolean 值等价的值。要将一个值转换为其对应的 Boolean 值,可以调用转型函数 Boolean()

var message = "Hello world!"
var messageAsBoolean = Boolean(message)

可以对任何数据类型的值调用 Boolean() 函数,而且总会返回一个 Boolean 值。至于返回的这个值是 true 还是 false,却决于要转换值得数据类型及其实际值。

数据类型        转换为 true 的值       转换为 false 的值

Boolean        true                   false
String         任何非空字符串          ""
Number         任何非0数值             0 和 NaN
Object         任何对象                null
Undefined      N/A                    undefined

Number 类型

为支持各种数值类型,ECMAScript-262 定义了不同的数值字面量格式。

最基本的数值字面量格式是十进制整数。

var intNum = 55     // 整数

除了以十进制表示外,整数还可以通过八进制或十六进制的字面值表示。其中,八进制字面值的第一位必须是 0,然后是八进制数字序列(0~7)。如果字面值中的数值超出了范围,那么前导零将被忽略,后面的数值将被当作十进制值解析。

var octalNum1 = 070     // 八进制的 56
var octalNum2 = 079     // 无效的八进制数值 —— 解析为 79

十六进制字面值的前两位必须是 0x,后跟任何十六进制数值(0~9 以及 A~F)。

在进行算术计算时,所有以八进制和十六进制表示的数值最终都被转换为十进制数值。

数值转换

有 3 个函数可以把非数值转换为数值:Number()、parseInt() 和 parseFloat()。第一个函数即转型函数 Number() 可以用于任何数据类型,而另外两个函数则专门用于把字符串转换成数值。

Number() 函数的转换规则如下:

  • 如果是 Boolean 值,true 和 false 将分别被转换为 1 和 0。
  • 如果是数字值,只是简单的传入和返回。
  • 如果是 null 值,返回 0.
  • 如果是 undefined,返回 NaN。
  • 如果是字符串,遵循下列规则
    • 如果字符串中只包含数字,则将其转换为十进制数值,即 “1” 会变成 1,“123” 会变成 123,而 “011” 会变成 11(注意:前导的零被忽略了);
    • 如果字符串中包含有效的浮点格式,如 “1.1”,则将其转换为对应的浮点数值(同样,也会忽略前导零);
    • 如果字符串中包含有效的十六进制格式,例如 “0xf”,则将其转换为相同大小的十进制整数数值;
    • 如果字符串是空的,则将其转换为 0;
    • 如果字符串包含除上述格式之外的字符,则将其转换为 NaN。
  • 如果是对象,则调用对象的 valueOf() 方法,然后依照前面的规则转换返回的值。如果转换的结果是 NaN,则调用对象的 toString() 方法,然后再次依照前面的规则转换返回的字符串值。

由于 Numnber() 函数在转换字符串时比较复杂而且不够合理,因此在处理整数的时候常用的是 parseInt() 函数。

使用 parseInt() 时应该为函数提供第二个参数:转换时使用过的基数:

var num1 = parseInt("10", 2)    // 2
var num2 = parseInt("10", 8)    // 8
var num3 = parseInt("10", 10)   // 10
var num4 = parseInt("10", 16)   // 16

由于 parseFloat() 只解析十进制数值,因此它没有用第二个参数制定基数的用法。

var num1 = parseFloat("1234blue")   // 1234 (整数)
var num2 = parseFloat("0xA")        // 0
var num3 = parseFloat("22.5")       // 22.5

String 类型

String 类型用于表示由零或多个 16 位 Unicode 字符组成的字符串序列,即字符串。字符串是不可变的。

字符串转换

要把一个值转换成字符串有两种方式。第一种是使用几乎每个值都有的 toString() 方法。但 null 和 undefined 值没有这个方法。

在不知道要转换的值是不是 null 或 undefined 的情况下,还可以使用转型函数 String(),这个函数能够将任何类型的值转换为字符串。String() 函数遵循下列转换规则: (验证失败)

  • 如果值有 toString() 方法,则调用该方法并返回相应的结果;
  • 如果值是 null,则返回 “null”;
  • 如果值是 undefined,则返回 “undefined”.

Object 类型

Object 对象是所有对象的基础。Object 的每个实例都具有下列属性和方法:

  • constructor: 保存着用于创建当前对象的函数。
  • hasOwnPropert(propertyName): 用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。
  • isPrototypeOf(object): 用于检查传入的对象是否是当前对象的原型。
  • propertyIsEnumerabl(propertyName): 用于检查给定的属性是否能够使用 for-in 语句来枚举。
  • toLocaleString(): 返回对象的字符串表示,该字符串与执行环境的地区对应。
  • toString(): 返回对象的字符串表示。
  • valueOf(): 返回对象的字符串、数值或布尔值表示。通常与 toString() 方法的返回值相同。

操作符

ECMA-262 描述了一组用于操作数据值的操作符,包括算术操作符、位操作符、关系操作符和相等操作符。ECMAScript 操作符的与众不同之处在于,他们能够使用于很多值,例如字符串、数字、布尔值,甚至对象。不过,在应用对象是,相应的操作符通常都会调用对象的 valueOf() 和(或) toStrin() 方法,以便取得可以操作的值。

一元加和减操作符

一元加操作符以一个加号(+)表示,放在数值前面,对数值不会产生任何影响。

不过,对非数值应用一元加操作符时,该操作会像 Number() 转型函数一样对这个值进行转换。换句话说,布尔值 false 和 true 将被转换为 0 和 1,字符串会按照一组特殊的规则进行解析,而对象是先条用他们的 valueOf() 和(或) toStrin() 方法,在转换得到的值。

逻辑非操作符

逻辑非操作符可以应用于任何值。无论这个值是什么数据类型,这个操作符都会返回一个布尔值。逻辑非操作符首先会将它的操作数转换为一个布尔值(Boolean()),然后再对其求反。

逻辑与操作符

逻辑与操作可以应用于任何类型的操作数,而不仅仅是布尔值。在有一个操作数不是布尔值的情况下,逻辑与操作就不一定返回布尔值;此时,它遵循下列规则:

  • 如果第一个操作数是的求值结果为 true,则返回第二个操作数;
  • 如果有一个操作数为 null,则返回null;
  • 如果有一个操作数为 undefined,则返回 undefined;
  • 如果有一个操作数为 NaN,则返回 NaN;

逻辑与操作属于短路操作,即如果第一个操作数能够决定结果,那么就不会对第二个操作数求值。

逻辑或操作符

与逻辑与相似,如果有一个操作数不是布尔值,逻辑或也不一定返回布尔值。此时,它遵循下列规则:

  • 如果第一个操作数的求值结果为 true,则返回第一个操作数;
  • 如果第一个操作数的求值结果为 false,则返回第二个操作数;
  • 如果两个操作数都是 null,则返回 null;
  • 如果两个操作数都是 undefined,则返回 undefined;
  • 如果两个操作数都是 NaN,则返回 NaN;

逻辑或操作符也是短路操作。

关系操作符

与 ECMAScript 中的其他操作符一样,当关系操作符的操作数使用了非数值时,也要进行数据转换或完成某些奇怪的操作。一下就是相应的规则:

  • 如果两个操作数都是数值,则执行数值比较;
  • 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值;
  • 如果一个操作数是数值,则将另一个操作数转换为一个数值(Number()),然后执行数值比较;
  • 如果一个操作数是对象,则调用这个对象的 valueOf() 方法,用得到的结果按照前面的规则执行比较。如果对象没有 valueOf() 方法,则调用 toString() 方法,用得到的结果按照前面的规则执行比较;
  • 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较;
  • 如果一个操作数是 NaN,结果是 false。

举例:

"23" < "3"      // true, "3" 字符编码在"2"之前
"23" < 3        // false, "23" 转为 23
"a" < 3         // false, "a" 转为 NaN

相等操作符

相等和不相等,转换后再比较;全等和不全等,仅比较而不转换。

if 语句

以下是 if 语句的用法:

if (condition) statement1 else statement2

其中的 condition 可以是任意表达式;而且对这个表达式求值的结果不一定是布尔值。ECMAScript 会自动调用 Boolean() 转换函数将这个表达式的结果转换为一个布尔值。

switch 语句

break 关键字会导致代码执行流跳出 switch 语句。如果省略 break 关键字,就会导致执行完当前 case 后,继续执行下一个 case。最后的 default 关键字用于在表达式不匹配前面任何一种情形的时候,执行机动代码。

通过为每个 case 后面添加一个 break 语句,就可以避免同时执行多个 case 代码的情况。如果确实需要混合几种情形,不要忘了在代码中添加注释,说明你是有意省略了 break 关键字:

switch (i) {
    // 合并两种情形
    case 25:
    case 35:
        console.log("25 or 35")
        break
    case 45:
        console.log("45")
        break;
}

switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换。

函数

ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数,可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。之所以会这样,原因是 ECMAScript 中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。实际上,在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。

其实,arguments 对象只是与数组类似(它并不是 Array 的实例),因为可以用方括号语法访问它的每一个元素,使用 length 属性来确定传进来多少个参数。

第四章 变量、作用域和内存问题

基本类型和引用类型的值

Javascript 的变量类型是松散类型的,一个变量只是在特定时间用于保存特定值的一个名字而已,它的值和数据类型可以在脚本的生命周期内改变。

ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段,共有五种基本数据类型:Undefined、Null、Boolean、Number 和 String。引用类型的值是保存在内存中的对象。

对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。但是,我们不能给基本类型的值添加属性,尽管这样做不会导致错误。

如果从一个变量向另一个变量复制基本类型的值,复制的是其值;当从一个变量向另一个变量复制引用类型的值时,复制的是其引用。

传递参数

所有函数的参数都是按值传递的。基本变量值的传递如同基本类型变量的复制一样,而引用类型值的传递,如同引用类型变量的复制一样。

没有块级作用域

Javascript 没有块级作用域。在其他类 C 的语言中,由花括号封闭的代码块都有自己的作用域(也就是它们自己的执行环境),因而支持根据条件来定义变量。例如,下面的代码在 Javascript 中并不会得到想象中的结果:

if (true) {
    var color = "blue"
}

console.log(color)  // "blue"

如果是在 C、C++ 或 Java 中, color 会在 if 语句执行完毕后销毁。但在 Javascript 中,if 语句中的变量声明会将变量添加到当前的执行环境中。在使用 for 语句时要牢记这一差异,例如:

for (var i = 0; i < 10; i++) {
    doSomething(i)
}

console.log(i)  // 10