JavaScript 是因特网上最流行的脚本语言,它存在于全世界所有 Web 浏览器中,能够增强用户与 Web 站点和 Web 应用程序之间的交互。
JavaScript 由 Brendan Eich 发明。它于 1995 年出现在 Netscape 中(该浏览器已停止更新),并于 1997 年被 ECMA(一个标准协会)采纳。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript,这个版本就是1.0版。
尽管 ECMAScript 是一个重要的标准,但它并不是 JavaScript 唯一的部分,当然,也不是唯一被标准化的部分。实际上,一个完整的 JavaScript 实现是由以下 3 个不同部分组成的:
简单地说,ECMAScript 描述了以下内容:
ECMAScript 是一个标准的描述,定义了脚本语言的所有属性、方法和对象。而 JavaScript 是一种遵循改标准实现的语言:
DOM(文档对象模型)是 HTML 和 XML 的应用程序接口(API)。DOM 将把整个页面规划成由节点层级构成的文档。HTML 或 XML 页面的每个部分都是一个节点的衍生物。请考虑下面的 HTML 页面:
1 | <html> |
这段代码可以用 DOM 绘制成一个节点层次图:
DOM 通过创建树来表示文档,从而使开发者对文档的内容和结构具有空前的控制力。用 DOM API 可以轻松地删除、添加和替换节点。
IE 3.0 和 Netscape Navigator 3.0 提供了一种特性 - BOM(浏览器对象模型),可以对浏览器窗口进行访问和操作。使用 BOM,开发者可以移动窗口、改变状态栏中的文本以及执行其他与页面内容不直接相关的动作。使 BOM 独树一帜且又常常令人怀疑的地方在于,它只是 JavaScript 的一个部分,没有任何相关的标准。
BOM 主要处理浏览器窗口和框架,不过通常浏览器特定的 JavaScript 扩展都被看做 BOM 的一部分。这些扩展包括:
IE 扩展了 BOM,加入了 ActiveXObject 类,可以通过 JavaScript 实例化 ActiveX 对象 由于没有相关的 BOM 标准,每种浏览器都有自己的 BOM 实现。有一些事实上的标准,如具有一个窗口对象和一个导航对象,不过每种浏览器可以为这些对象或其他对象定义自己的属性和方法。
比如变量 test
与变量 TEST
是不同的。
与 Java 和 C 不同,ECMAScript 中的变量无特定的类型,定义变量时只用 var 运算符,可以将它初始化为任意值。
因此,可以随时改变变量所存数据的类型(尽量避免这样做)。
例子:
1 | var color = "red"; |
Java、C 和 Perl 都要求每行代码以分号(;)结束才符合语法。
ECMAScript 则允许开发者自行决定是否以分号结束一行代码。如果没有分号,ECMAScript 就把折行代码的结尾看做该语句的结尾(与 Visual Basic 和 VBScript 相似),前提是这样没有破坏代码的语义。
最好的代码编写习惯是总加入分号,因为没有分号,有些浏览器就不能正确运行,不过根据 ECMAScript 标准,下面两行代码都是正确的:
1 | var test1 = "red" |
ECMAScript 借用了 Java 、C 这些语言的注释语法。
有两种类型的注释:
1 | //this is a single-line comment |
从 Java 中借鉴的另一个概念是代码块。
代码块表示一系列应该按顺序执行的语句,这些语句被封装在左括号({)和右括号(})之间。
例如:
1 | if (test1 == "red") { |
请使用 var 运算符声明变量。变量名需要遵守一些简单的规则。
在上一节中我们讲解过,ECMAScript 中的变量是用 var 运算符(variable 的缩写)加变量名定义的。例如:
1 | var test = "hi"; |
在这个例子中,声明了变量 test,并把它的值初始化为 “hi”(字符串)。由于 ECMAScript 是弱类型的,所以解释程序会为 test 自动创建一个字符串值,无需明确的类型声明。
还可以用一个 var 语句定义两个或多个变量:
1 | var test1 = "hi", test2 = "hello"; |
不过用同一个 var 语句定义的变量不必具有相同的类型,如下所示:
1 | var test = "hi", age = 25; |
这个例子除了(再次)定义 test 外,还定义了 age,并把它初始化为 25。即使 test 和 age 属于两种不同的数据类型,在 ECMAScript 中这样定义也是完全合法的。
与 Java 不同,ECMAScript 中的变量并不一定要初始化。因此,下面这一行代码也是有效的:
1 | var sTest = "hello "; |
此外,与 Java 不同的还有变量可以存放不同类型的值。这是弱类型变量的优势。例如,可以把变量初始化为字符串类型的值,之后把它设置为数字值,如下所示:
1 | var test = "hi"; |
这段代码将毫无问题地输出字符串值和数字值。但是,如前所述,使用变量时,好的编码习惯是始终存放相同类型的值。
变量名需要遵守两条简单的规则:
_
)或美元符号($
)下面的变量都是合法的:
1 | var test; |
W3CSchool 用的是匈牙利类型标记法命名,在 Pascal 标记法命名的变量前附加一个小写字母(或小写字母序列),说明该变量的类型。例如,i 表示整数,s 表示字符串,如下所示:
类型 | 前缀 | 示例 |
---|---|---|
数组 | a | aValues |
布尔型 | b | bFound |
浮点型(数字) | f | fValue |
函数 | fn | fnMethod |
整型(数字) | i | iValue |
对象 | o | oType |
正则表达式 | re | rePattern |
字符串 | s | sValue |
变型(可以是任何类型) | v | vValue |
在 ECMAScript 中,变量可以存在两种类型的值,即原始值和引用值。
为变量赋值时,ECMAScript 的解释程序必须判断该值是原始类型,还是引用类型。要实现这一点,解释程序则需尝试判断该值是否为 ECMAScript 的原始类型之一,即 Undefined、Null、Boolean、Number 和 String 型。由于这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 - 栈中。这样存储便于迅速查寻变量的值。
如果一个值是引用类型的,那么它的存储空间将从堆中分配。由于引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。如下图所示:
ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String。
typeof 运算符有一个参数,即要检查的变量或值。例如:
1 | var sTemp = "test string"; |
对变量或值调用 typeof 运算符将返回下列值之一:
如前所述,Undefined 类型只有一个值,即 undefined。当声明的变量未初始化时,该变量的默认值是 undefined。
1 | var oTemp; |
前面一行代码声明变量 oTemp,没有初始值。该变量将被赋予值 undefined,即 undefined 类型的字面量。可以用下面的代码段测试该变量的值是否等于 undefined:
1 | var oTemp; |
这段代码将显示 “true”,说明这两个值确实相等。还可以用 typeof 运算符显示该变量的值是 undefined:
1 | var oTemp; |
1 | var oTemp; |
前面的代码对两个变量输出的都是 “undefined”,即使只有变量 oTemp2 从未被声明过。如果对 oTemp2 使用除 typeof 之外的其他运算符的话,会引起错误,因为其他运算符只能用于已声明的变量上。
例如,下面的代码将引发错误:
1 | var oTemp; |
当函数无明确返回值时,返回的也是值 “undefined”,如下所示:
1 | function testFunc() { |
另一种只有一个值的类型是 Null,它只有一个专用值 null,即它的字面量。值 undefined 实际上是从值 null 派生来的,因此 ECMAScript 把它们定义为相等的。
1 | alert(null == undefined); //输出 "true" |
尽管这两个值相等,但它们的含义不同。undefined 是声明了变量但未对其初始化时赋予该变量的值,null 则用于表示尚未存在的对象(在讨论 typeof 运算符时,简单地介绍过这一点)。如果函数或方法要返回的是对象,那么找不到该对象时,返回的通常是 null。
Boolean 类型是 ECMAScript 中最常用的类型之一。它有两个值 true 和 false (即两个 Boolean 字面量)。
即使 false 不等于 0,0 也可以在必要时被转换成 false,这样在 Boolean 语句中使用两者都是安全的。
1 | var bFound = true; |
ECMA-262 中定义的最特殊的类型是 Number 类型。这种类型既可以表示 32 位的整数,还可以表示 64 位的浮点数。
直接输入的(而不是从另一个变量访问的)任何数字都被看做 Number 类型的字面量。例如,下面的代码声明了存放整数值的变量,它的值由字面量 86 定义:
1 | var iNum = 86; |
整数也可以被表示为八进制(以 8 为底)或十六进制(以 16 为底)的字面量。八进制字面量的首数字必须是 0,其后的数字可以是任何八进制数字(0-7),如下面的代码所示:
1 | var iNum = 070; //070 等于十进制的 56 |
要创建十六进制的字面量,首位数字必须为 0,后面接字母 x,然后是任意的十六进制数字(0 到 9 和 A 到 F)。这些字母可以是大写的,也可以是小写的。例如:
1 | var iNum = 0x1f; //0x1f 等于十进制的 31 |
要定义浮点值,必须包括小数点和小数点后的一位数字(例如,用 1.0 而不是 1)。这被看作浮点数字面量。例如:
1 | var fNum = 5.0; |
对于浮点字面量的有趣之处在于,用它进行计算前,真正存储的是字符串。
对于非常大或非常小的数,可以用科学计数法表示浮点数,可以把一个数表示为数字(包括十进制数字)加 e(或 E),后面加乘以 10 的倍数。例如:
1 | var fNum = 5.618e7 |
该符号表示的是数 56180000。把科学计数法转化成计算式就可以得到该值:5.618 x 107。
也可以用科学计数法表示非常小的数,例如 0.00000000000000008 可以表示为 8-e17(这里,10 被升到 -17 次冥,意味着需要被 10 除 17 次)。ECMAScript 默认把具有 6 个或 6 个以上前导 0 的浮点数转换成科学计数法。
几个特殊值也被定义为 Number 类型。前两个是 Number.MAX_VALUE
和 Number.MIN_VALUE
,它们定义了 Number 值集合的外边界。所有 ECMAScript 数都必须在这两个值之间。不过计算生成的数值结果可以不落在这两个值之间。
当计算生成的数大于 Number.MAX_VALUE
时,它将被赋予值 Number.POSITIVE_INFINITY
,意味着不再有数字值。同样,生成的数值小于 Number.MIN_VALUE
的计算也会被赋予值 Number.NEGATIVE_INFINITY
,也意味着不再有数字值。如果计算返回的是无穷大值,那么生成的结果不能再用于其他计算。
事实上,有专门的值表示无穷大,(如你猜到的)即 Infinity。Number.POSITIVE_INFINITY
的值为 Infinity
。Number.NEGATIVE_INFINITY
的值为 -Infinity
。
由于无穷大数可以是正数也可以是负数,所以可用一个方法判断一个数是否是有穷的(而不是单独测试每个无穷数)。可以对任何数调用 isFinite() 方法,以确保该数不是无穷大。例如:
1 | var iResult = iNum * some_really_large_number; |
最后一个特殊值是 NaN
,表示非数(Not a Number)。NaN
是个奇怪的特殊值。一般说来,这种情况发生在类型(String、Boolean 等)转换失败时。例如,要把单词 blue 转换成数值就会失败,因为没有与之等价的数值。与无穷大一样,NaN
也不能用于算术计算。NaN
的另一个奇特之处在于,它与自身不相等,这意味着下面的代码将返回 false:
1 | alert(NaN == NaN); //输出 "false" |
出于这个原因,不推荐使用 NaN 值本身。函数 isNaN()
会做得相当好:
1 | alert(isNaN("blue")); //输出 "true" |
String 类型的独特之处在于,它是唯一没有固定大小的原始类型。可以用字符串存储 0 或更多的 Unicode 字符,有 16 位整数表示(Unicode 是一种国际字符集,本教程后面将讨论它)。
字符串中每个字符都有特定的位置,首字符从位置 0 开始,第二个字符在位置 1,依此类推。这意味着字符串中的最后一个字符的位置一定是字符串的长度减 1:
字符串字面量是由双引号("
)或单引号('
)声明的。而 Java 则是用双引号声明字符串,用单引号声明字符。但是由于 ECMAScript 没有字符类型,所以可使用这两种表示法中的任何一种。例如,下面的两行代码都有效:
1 | var sColor1 = "red"; |
String 类型还包括几种字符字面量,Java、C 和 Perl 的开发者应该对此非常熟悉。
ECMAScript 给开发者提供了大量简单的类型转换方法。
3 种主要的原始类型 Boolean 值、数字和字符串都有 toString()
方法,可以把它们的值转换成字符串。
Boolean 类型的 toString()
方法只是输出 “true” 或 “false”,结果由变量的值决定:
1 | var bFound = false; |
Number 类型的 toString()
方法比较特殊,它有两种模式,即默认模式和基模式。采用默认模式,toString() 方法只是用相应的字符串输出数字值(无论是整数、浮点数还是科学计数法),如下所示:
1 | var iNum1 = 10; |
注释:在默认模式中,无论最初采用什么表示法声明数字,Number 类型的 toString()
方法返回的都是数字的十进制表示。因此,以八进制或十六进制字面量形式声明的数字输出的都是十进制形式的。
采用 Number 类型的 toString()
方法的基模式,可以用不同的基输出数字,例如二进制的基是 2,八进制的基是 8,十六进制的基是 16。
基只是要转换成的基数的另一种加法而已,它是 toString()
方法的参数:
1 | var iNum = 10; |
在前面的示例中,以 3 种不同的形式输出了数字 10,即二进制形式、八进制形式和十六进制形式。HTML 采用十六进制表示每种颜色,在 HTML 中处理数字时这种功能非常有用。
toString(10)
与调用 toString()
相同,它们返回的都是该数字的十进制形式。
ECMAScript 提供了两种把非数字的原始值转换成数字的方法,即 parseInt()
和 parseFloat()
。
正如您可能想到的,前者把值转换成整数,后者把值转换成浮点数。只有对 String 类型调用这些方法,它们才能正确运行;对其他类型返回的都是 NaN。
在判断字符串是否是数字值前,parseInt()
和 parseFloat()
都会仔细分析该字符串。
parseInt() 方法首先查看位置 0 处的字符,判断它是否是个有效数字;如果不是,该方法将返回 NaN,不再继续执行其他操作。但如果该字符是有效数字,该方法将查看位置 1 处的字符,进行同样的测试。这一过程将持续到发现非有效数字的字符为止,此时 parseInt()
将把该字符之前的字符串转换成数字。
例如,如果要把字符串 “12345red” 转换成整数,那么 parseInt()
将返回 12345,因为当它检查到字符 r 时,就会停止检测过程。
字符串中包含的数字字面量会被正确转换为数字,比如 “0xA” 会被正确转换为数字 10。不过,字符串 “22.5” 将被转换成 22,因为对于整数来说,小数点是无效字符。
一些示例如下:
1 | var iNum1 = parseInt("12345red"); //返回 12345 |
parseInt()
方法还有基模式,可以把二进制、八进制、十六进制或其他任何进制的字符串转换成整数。基是由 parseInt()
方法的第二个参数指定的,所以要解析十六进制的值,需如下调用 parseInt()
方法:
1 | var iNum1 = parseInt("AF", 16); //返回 175 |
当然,对二进制、八进制甚至十进制(默认模式),都可以这样调用 parseInt()
方法:
1 | var iNum1 = parseInt("10", 2); //返回 2 |
如果十进制数包含前导 0,那么最好采用基数 10,这样才不会意外地得到八进制的值。例如:
1 | var iNum1 = parseInt("010"); //返回 8 |
在这段代码中,两行代码都把字符 “010” 解析成一个数字。第一行代码把这个字符串看作八进制的值,解析它的方式与第二行代码(声明基数为 8)相同。最后一行代码声明基数为 10,所以 iNum3 最后等于 10。
parseFloat()
方法与 parseInt()
方法的处理方式相似,从位置 0 开始查看每个字符,直到找到第一个非有效的字符为止,然后把该字符之前的字符串转换成整数。
不过,对于这个方法来说,第一个出现的小数点是有效字符。如果有两个小数点,第二个小数点将被看作无效的。parseFloat()
会把这个小数点之前的字符转换成数字。这意味着字符串 “11.22.33” 将被解析成 11.22。
使用 parseFloat()
方法的另一不同之处在于,字符串必须以十进制形式表示浮点数,而不是用八进制或十六进制。该方法会忽略前导 0,所以八进制数 0102 将被解析为 102。对于十六进制数 0xA,该方法将返回 NaN,因为在浮点数中,x 不是有效字符。(注释:经测试,具体的浏览器实现会返回 0,而不是 NaN。)
此外,parseFloat()
方法也没有基模式。
下面是使用 parseFloat()
方法的一些示例:
1 | var fNum1 = parseFloat("12345red"); //返回 12345 |
您还可以使用强制类型转换(type casting)来处理转换值的类型。使用强制类型转换可以访问特定的值,即使它是另一种类型的。
ECMAScript 中可用的 3 种强制类型转换如下:
1 | Boolean(value) - 把给定的值转换成 Boolean 型; |
用这三个函数之一转换值,将创建一个新值,存放由原始值直接转换成的值。这会造成意想不到的后果。
当要转换的值是至少有一个字符的字符串、非 0 数字或对象时,Boolean()
函数将返回 true。如果该值是空字符串、数字 0、undefined 或 null,它将返回 false。
可以用下面的代码测试 Boolean 型的强制类型转换:
1 | var b1 = Boolean(""); //false - 空字符串 |
Number()
函数的强制类型转换与 parseInt()
和 parseFloat()
方法的处理方式相似,只是它转换的是整个值,而不是部分值。
还记得吗,parseInt()
和 parseFloat()
方法只转换第一个无效字符之前的字符串,因此 “1.2.3” 将分别被转换为 “1” 和 “1.2”。
用 Number()
进行强制类型转换,“1.2.3” 将返回 NaN,因为整个字符串值不能转换成数字。如果字符串值能被完整地转换,Number()
将判断是调用 parseInt()
方法还是 parseFloat()
方法。
下表说明了对不同的值调用 Number()
方法会发生的情况:
用法 | 结果 |
---|---|
Number(false) | 0 |
Number(true) | 1 |
Number(undefined) | NaN |
Number(null) | 0 |
Number(“1.2”) | 1.2 |
Number(“12”) | 12 |
Number(“1.2.3”) | NaN |
Number(new object()) | NaN |
Number(50) | 50 |
最后一种强制类型转换方法 String()
是最简单的,因为它可把任何值转换成字符串。
要执行这种强制类型转换,只需要调用作为参数传递进来的值的 toString()
方法,即把 12 转换成 “12”,把 true 转换成 “true”,把 false 转换成 “false”,以此类推。
强制转换成字符串和调用 toString() 方法的唯一不同之处在于,对 null 和 undefined 值强制类型转换可以生成字符串而不引发错误:
1 | var s1 = String(null); //"null" |
在处理 ECMAScript 这样的弱类型语言时,强制类型转换非常有用,不过应该确保使用值的正确。
引用类型通常叫做类(class)。也就是说,遇到引用值,所处理的就是对象。
对象是由 new 运算符加上要实例化的对象的名字创建的。例如,下面的代码创建 Object 对象的实例:
1 | var o = new Object(); |
这种语法与 Java 语言很相似,不过当有不止一个参数时,ECMAScript 要求使用括号。如果没有参数,如以下代码所示,括号可以省略:
1 | var o = new Object; |
这一节的重点是具有等价的原始类型的引用类型。
Object 对象自身用处不大,不过在了解其他类之前,还是应该了解它。因为 ECMAScript 中的 Object 对象与 Java 中的 java.lang.Object 相似,ECMAScript 中的所有对象都由这个对象继承而来,Object 对象中的所有属性和方法都会出现在其他对象中,所以理解了 Object 对象,就可以更好地理解其他对象。
Object 对象具有下列属性:
Object 对象还具有几个方法:
o.hasOwnProperty("name")
)for...in
语句进行枚举。Boolean 对象是 Boolean 原始类型的引用类型。
要创建 Boolean 对象,只需要传递 Boolean 值作为参数:
1 | var oBooleanObject = new Boolean(true); |
Boolean 对象将覆盖 Object 对象的 ValueOf() 方法,返回原始值,即 true 和 false。ToString() 方法也会被覆盖,返回字符串 “true” 或 “false”。
遗憾的是,在 ECMAScript 中很少使用 Boolean 对象,即使使用,也不易理解。
问题通常出现在 Boolean 表达式中使用 Boolean 对象时。例如:
1 | var oFalseObject = new Boolean(false); |
在这段代码中,用 false 值创建 Boolean 对象。然后用这个值与原始值 true 进行 AND 操作。在 Boolean 运算中,false 和 true 进行 AND 操作的结果是 false。不过,在这行代码中,计算的是 oFalseObject,而不是它的值 false。在 Boolean 表达式中,所有对象都会被自动转换为 true,所以 oFalseObject 的值是 true。然后 true 再与 true 进行 AND 操作,结果为 true。
正如你可能想到的,Number 对象是 Number 原始类型的引用类型。要创建 Number 对象,采用下列代码:
1 | var oNumberObject = new Number(68); |
您应该已认出本章前面小节中讨论特殊值(如 Number.MAX_VALUE
)时提到的 Number 对象。所有特殊值都是 Number 对象的静态属性。
要得到数字对象的 Number 原始值,只需要使用 valueOf() 方法:
1 | var iNumber = oNumberObject.valueOf(); |
当然,Number 类也有 toString()
方法,在讨论类型转换的小节中已经详细讨论过该方法。
除了从 Object 对象继承的标准方法外,Number 对象还有几个处理数值的专用方法。
toFixed() 方法返回的是具有指定位数小数的数字的字符串表示。例如:
1 | var oNumberObject = new Number(68); |
在这里,toFixed() 方法的参数是 2,说明应该显示两位小数。该方法返回 “68.00”,空的字符串位由 0 来补充。对于处理货币的应用程序,该方法非常有用。toFixed() 方法能表示具有 0 到 20 位小数的数字,超过这个范围的值会引发错误。
与格式化数字相关的另一个方法是 toExponential(),它返回的是用科学计数法表示的数字的字符串形式。
与 toFixed() 方法相似,toExponential() 方法也有一个参数,指定要输出的小数的位数。例如:
1 | var oNumberObject = new Number(68); |
这段代码的结果是 “6.8e+1”,前面解释过,它表示 6.8x101。问题是,如果不知道要用哪种形式(预定形式或指数形式)表示数字怎么办?可以用 toPrecision() 方法。
toPrecision() 方法根据最有意义的形式来返回数字的预定形式或指数形式。它有一个参数,即用于表示数的数字总数(不包括指数)。例如,
1 | var oNumberObject = new Number(68); |
这段代码的任务是用一位数字表示数字 68,结果为 “7e+1”,以另外的形式表示即 70。的确,toPrecision() 方法会对数进行舍入。不过,如果用 2 位数字表示 68,就容易多了:
1 | var oNumberObject = new Number(68); |
当然,输出的是 “68”,因为这正是该数的准确表示。不过,如果指定的位数多于需要的位数又如何呢?
1 | var oNumberObject = new Number(68); |
在这种情况下,toPrecision(3) 等价于 toFixed(1),输出的是 “68.0”。
toFixed()、toExponential() 和 toPrecision() 方法都会进行舍入操作,以便用正确的小数位数正确地表示一个数。
String 对象是 String 原始类型的对象表示法,它是以下方式创建的:
1 | var oStringObject = new String("hello world"); |
如果运行这段代码,输出是 “true”,说明这些值真的相等。
String 对象具有属性 length,它是字符串中的字符个数:
1 | var oStringObject = new String("hello world"); |
这个例子输出的是 “11”,即 “hello world” 中的字符个数。注意,即使字符串包含双字节的字符(与 ASCII 字符相对,ASCII 字符只占用一个字节),每个字符也只算一个字符。
String 对象还拥有大量的方法。
首先,两个方法 charAt() 和 charCodeAt() 访问的是字符串中的单个字符。这两个方法都有一个参数,即要操作的字符的位置。
charAt() 方法返回的是包含指定位置处的字符的字符串:
1 | var oStringObject = new String("hello world"); |
在字符串 “hello world” 中,位置 1 处的字符是 “e”。在“ECMAScript 原始类型”这一节中我们讲过,第一个字符的位置是 0,第二个字符的位置是 1,依此类推。因此,调用 charAt(1) 返回的是 “e”。
如果想得到的不是字符,而是字符代码,那么可以调用 charCodeAt() 方法:
1 | var oStringObject = new String("hello world"); |
这个例子输出 “101”,即小写字母 “e” 的字符代码。
接下来是 concat() 方法,用于把一个或多个字符串连接到 String 对象的原始值上。该方法返回的是 String 原始值,保持原始的 String 对象不变:
1 | var oStringObject = new String("hello "); |
在上面这段代码中,调用 concat() 方法返回的是 “hello world”,而 String 对象存放的仍然是 "hello "。出于这种原因,较常见的是用加号(+)连接字符串,因为这种形式从逻辑上表明了真正的行为:
1 | var oStringObject = new String("hello "); |
迄今为止,已讨论过连接字符串的方法,访问字符串中的单个字符的方法。不过如果无法确定在某个字符串中是否确实存在一个字符,应该调用什么方法呢?这时,可调用 indexOf() 和 lastIndexOf() 方法。
indexOf() 和 lastIndexOf() 方法返回的都是指定的子串在另一个字符串中的位置,如果没有找不到子串,则返回 -1。
这两个方法的不同之处在于,indexOf() 方法是从字符串的开头(位置 0)开始检索字符串,而 lastIndexOf() 方法则是从字符串的结尾开始检索子串。例如:
1 | var oStringObject = new String("hello world!"); |
在这里,第一个 “o” 字符串出现在位置 4,即 “hello” 中的 “o”;最后一个 “o” 出现在位置 7,即 “world” 中的 “o”。如果该字符串中只有一个 “o” 字符串,那么 indexOf() 和 lastIndexOf() 方法返回的位置相同。
下一个方法是 localeCompare(),对字符串进行排序。该方法有一个参数 - 要进行比较的字符串,返回的是下列三个值之一:
示例如下:
1 | var oStringObject = new String("yellow"); |
在这段代码中,字符串 “yellow” 与 3 个值进行了对比,即 “brick”、“yellow” 和 “zoo”。由于按照字母顺序排列,“yellow” 位于 “brick” 之后,所以 localeCompare() 返回 1;“yellow” 等于 “yellow”,所以 localeCompare() 返回 0;“zoo” 位于 “yellow” 之后,localeCompare() 返回 -1。再强调一次,由于返回的值是由实现决定的,所以最好以下面的方式调用 localeCompare() 方法:
1 | var oStringObject1 = new String("yellow"); |
采用这种结构,可以确保这段代码在所有实现中都能正确运行。
localeCompare() 方法的独特之处在于,实现所处的区域(locale,兼指国家/地区和语言)确切说明了这种方法运行的方式。在美国,英语是 ECMAScript 实现的标准语言,localeCompare() 是区分大小写的,大写字母在字母顺序上排在小写字母之后。不过,在其他区域,情况可能并非如此。
ECMAScript 提供了两种方法从子串创建字符串值,即 slice() 和 substring()。这两种方法返回的都是要处理的字符串的子串,都接受一个或两个参数。第一个参数是要获取的子串的起始位置,第二个参数(如果使用的话)是要获取子串终止前的位置(也就是说,获取终止位置处的字符不包括在返回的值内)。如果省略第二个参数,终止位就默认为字符串的长度。
与 concat() 方法一样,slice() 和 substring() 方法都不改变 String 对象自身的值。它们只返回原始的 String 值,保持 String 对象不变。
1 | var oStringObject = new String("hello world"); |
在这个例子中,slice() 和 substring() 的用法相同,返回值也一样。当只有参数 3 时,两个方法返回的都是 “lo world”,因为 “hello” 中的第二个 “l” 位于位置 3 上。当有两个参数 “3” 和 “7” 时,两个方法返回的值都是 “lo w”(“world” 中的字母 “o” 位于位置 7 上,所以它不包括在结果中)。
为什么有两个功能完全相同的方法呢?事实上,这两个方法并不完全相同,不过只在参数为负数时,它们处理参数的方式才稍有不同。
对于负数参数,slice() 方法会用字符串的长度加上参数,substring() 方法则将其作为 0 处理(也就是说将忽略它)。例如:
1 | var oStringObject = new String("hello world"); |
这样即可看出 slice() 和 substring() 方法的主要不同。
当只有参数 -3 时,slice() 返回 “rld”,substring() 则返回 “hello world”。这是因为对于字符串 “hello world”,slice("-3") 将被转换成 slice(“8”),而 substring("-3") 将被转换成 substring(“0”)。
同样,使用参数 3 和 -4 时,差别也很明显。slice() 将被转换成 slice(3, 7),与前面的例子相同,返回 “lo w”。而 substring() 方法则将两个参数解释为 substring(3, 0),实际上即 substring(0, 3),因为 substring() 总把较小的数字作为起始位,较大的数字作为终止位。因此,substring(“3, -4”) 返回的是 “hel”。这里的最后一行代码用来说明如何使用这些方法。
最后一套要讨论的方法涉及大小写转换。有 4 种方法用于执行大小写转换,即
1 | toLowerCase() |
从名字上可以看出它们的用途,前两种方法用于把字符串转换成全小写的,后两种方法用于把字符串转换成全大写的。
toLowerCase() 和 toUpperCase() 方法是原始的,是以 java.lang.String 中相同方法为原型实现的。
toLocaleLowerCase() 和 toLocaleUpperCase() 方法是基于特定的区域实现的(与 localeCompare() 方法相同)。在许多区域中,区域特定的方法都与通用的方法完全相同。不过,有几种语言对 Unicode 大小写转换应用了特定的规则(例如土耳其语),因此必须使用区域特定的方法才能进行正确的转换。
1 | var oStringObject = new String("Hello World"); |
这段代码中,toUpperCase() 和 toLocaleUpperCase() 输出的都是 “HELLO WORLD”,toLowerCase() 和 toLocaleLowerCase() 输出的都是 “hello world”。一般来说,如果不知道在以哪种编码运行一种语言,则使用区域特定的方法比较安全。
在使用 typeof 运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象,它都返回 “object”。ECMAScript 引入了另一个 Java 运算符 instanceof 来解决这个问题。
instanceof 运算符与 typeof 运算符相似,用于识别正在处理的对象的类型。与 typeof 方法不同的是,instanceof 方法要求开发者明确地确认对象为某特定类型。例如:
1 | var oStringObject = new String("hello world"); |
这段代码问的是“变量 oStringObject 是否为 String 对象的实例?”oStringObject 的确是 String 对象的实例,因此结果是 “true”。尽管不像 typeof 方法那样灵活,但是在 typeof 方法返回 “object” 的情况下,instanceof 方法还是很有用的。
delete 运算符删除对以前定义的对象属性或方法的引用。例如:
1 | var o = new Object; |
在这个例子中,删除了 name 属性,意味着强制解除对它的引用,将其设置为 undefined(即创建的未初始化的变量的值)。
delete 运算符不能删除开发者未定义的属性和方法。例如,下面的代码将引发错误:
1 | delete o.toString; |
即使 toString 是有效的方法名,这行代码也会引发错误,因为 toString() 方法是原始的 ECMAScript 方法,不是开发者定义的。
void 运算符对任何值返回 undefined。该运算符通常用于避免输出不应该输出的值,例如,从 HTML 的 <a>
元素调用 JavaScript 函数时。要正确做到这一点,函数不能返回有效值,否则浏览器将清空页面,只显示函数的结果。例如:
1 | <a href="javascript:window.open('about:blank')">Click me</a> |
如果把这行代码放入 HTML 页面,点击其中的链接,即可看到屏幕上显示 “[object]”。
这是因为 window.open() 方法返回了新打开的窗口的引用。然后该对象将被转换成要显示的字符串。要避免这种效果,可以用 void 运算符调用 window.open() 函数:
1 | <a href="javascript:void(window.open('about:blank'))">Click me</a> |
这使 window.open() 调用返回 undefined,它不是有效值,不会显示在浏览器窗口中。
提示:请记住,没有返回值的函数真正返回的都是 undefined。
直接从 C(和 Java)借用的两个运算符是前增量运算符和前减量运算符。所谓前增量运算符,就是数值上加 1,形式是在变量前放两个加号(++):
1 | var iNum = 10; |
第二行代码把 iNum 增加到了 11,它实质上等价于:
1 | var iNum = 10; |
同样,前减量运算符是从数值上减 1,形式是在变量前放两个减号(–):
1 | var iNum = 10; |
在这个例子中,第二行代码把 iNum 的值减到 9。
在使用前缀式运算符时,注意增量和减量运算符都发生在计算表达式之前。考虑下面的例子:
1 | var iNum = 10; |
第二行代码对 iNum 进行减量运算,第三行代码显示的结果是(“9”)。第四行代码又对 iNum 进行减量运算,不过这次前减量运算和输出操作出现在同一个语句中,显示的结果是 “8”。为了证明已实现了所有的减量操作,第五行代码又输出一次"8"。
在算术表达式中,前增量和前减量运算符的优先级是相同的,因此要按照从左到右的顺序计算之。例如:
1 | var iNum1 = 2; |
在前面的代码中,iNum3 等于 22,因为表达式要计算的是 1 + 21。变量 iNum4 也等于 22,也是 1 + 21。
还有两个直接从 C(和 Java)借用的运算符,即后增量运算符和后减量运算符。后增量运算符也是给数值上加 1,形式是在变量后放两个加号(++):
1 | var iNum = 10; |
不出所料,后减量运算符也是从数值上减 1,形式为在变量后加两个减号(–):
1 | var iNum = 10; |
第二行代码把 iNum 的 值减到 9。与前缀式运算符不同的是,后缀式运算符是在计算过包含它们的表达式后才进行增量或减量运算的。考虑以下的例子:
1 | var iNum = 10; |
与前缀式运算符的例子相似,第二行代码对 iNum 进行减量运算,第三行代码显示结果(“9”)。第四行代码继续显示 iNum 的值,不过这次是在同一语句中应用减量运算符。由于减量运算发生在计算过表达式之后,所以这条语句显示的数是 “9”。执行了第五行代码后,alert 函数显示的是 “8”,因为在执行第四行代码之后和执行第五行代码之前,执行了后减量运算。
在算术表达式中,后增量和后减量运算符的优先级是相同的,因此要按照从左到右的顺序计算之。例如:
1 | var iNum1 = 2; |
在前面的代码中,iNum3 等于 22,因为表达式要计算的是 2 + 20。变量 iNum4 也等于 22,不过它计算的是 1 + 21,因为增量和减量运算都在给 iNum3 赋值后才发生。
大多数人都熟悉一元加法和一元减法,它们在 ECMAScript 中的用法与您高中数学中学到的用法相同。
一元加法本质上对数字无任何影响:
1 | var iNum = 20; |
这段代码对数字 20 应用了一元加法,返回的还是 20。尽管一元加法对数字无作用,但对字符串却有有趣的效果,会把字符串转换成数字。
1 | var sNum = "20"; |
这段代码把字符串 “20” 转换成真正的数字。当一元加法运算符对字符串进行操作时,它计算字符串的方式与 parseInt() 相似,主要的不同是只有对以 “0x” 开头的字符串(表示十六进制数字),一元运算符才能把它转换成十进制的值。因此,用一元加法转换 “010”,得到的总是 10,而 “0xB” 将被转换成 11。
另一方面,一元减法就是对数值求负(例如把 20 转换成 -20):
1 | var iNum = 20; |
与一元加法运算符相似,一元减法运算符也会把字符串转换成近似的数字,此外还会对该值求负。例如:
1 | var sNum = "20"; |
在上面的代码中,一元减法运算符将把字符串 “-20” 转换成 -20(一元减法运算符对十六进制和十进制的处理方式与一元加法运算符相似,只是它还会对该值求负)。
位运算 NOT 由否定号(~)表示,它是 ECMAScript 中为数不多的与二进制算术有关的运算符之一。
位运算 NOT 是三步的处理过程:
例如:
1 | var iNum1 = 25; //25 等于 00000000000000000000000000011001 |
位运算 NOT 实质上是对数字求负,然后减 1,因此 25 变 -26。用下面的方法也可以得到同样的方法:
1 | var iNum1 = 25; |
位运算 AND 由和号(&)表示,直接对数字的二进制形式进行运算。它把每个数字中的数位对齐,然后用下面的规则对同一位置上的两个数位进行 AND 运算:
第一个数字中的数位 | 第二个数字中的数位 | 结果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
例如,要对数字 25 和 3 进行 AND 运算,代码如下所示:
1 | var iResult = 25 & 3; |
25 和 3 进行 AND 运算的结果是 1。为什么?分析如下:
1 | 25 = 0000 0000 0000 0000 0000 0000 0001 1001 |
可以看出,在 25 和 3 中,只有一个数位(位 0)存放的都是 1,因此,其他数位生成的都是 0,所以结果为 1。
位运算 OR 由符号(|)表示,也是直接对数字的二进制形式进行运算。在计算每位时,OR 运算符采用下列规则:
第一个数字中的数位 | 第二个数字中的数位 | 结果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
仍然使用 AND 运算符所用的例子,对 25 和 3 进行 OR 运算,代码如下:
1 | var iResult = 25 | 3; |
25 和 3 进行 OR 运算的结果是 27:
1 | 25 = 0000 0000 0000 0000 0000 0000 0001 1001 |
可以看出,在两个数字中,共有 4 个数位存放的是 1,这些数位被传递给结果。二进制代码 11011 等于 27。
位运算 XOR 由符号(^)表示,当然,也是直接对二进制形式进行运算。XOR 不同于 OR,当只有一个数位存放的是 1 时,它才返回 1。真值表如下:
第一个数字中的数位 | 第二个数字中的数位 | 结果 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
对 25 和 3 进行 XOR 运算,代码如下:
1 | var iResult = 25 ^ 3; |
25 和 3 进行 XOR 运算的结果是 26:
1 | 25 = 0000 0000 0000 0000 0000 0000 0001 1001 |
可以看出,在两个数字中,共有 4 个数位存放的是 1,这些数位被传递给结果。二进制代码 11010 等于 26。
左移运算由两个小于号表示(<<)。它把数字中的所有数位向左移动指定的数量。例如,把数字 2(等于二进制中的 10)左移 5 位,结果为 64(等于二进制中的 1000000):
1 | var iOld = 2; //等于二进制 10 |
注意:左移运算保留数字的符号位。例如,如果把 -2 左移 5 位,得到的是 -64,而不是 64。“符号仍然存储在第 32 位中吗?”是的,不过这在 ECMAScript 后台进行,开发者不能直接访问第 32 个数位。即使输出二进制字符串形式的负数,显示的也是负号形式(例如,-2 将显示 -10。)
有符号右移运算符由两个大于号表示(>>)。它把 32 位数字中的所有数位整体右移,同时保留该数的符号(正号或负号)。有符号右移运算符恰好与左移运算相反。例如,把 64 右移 5 位,将变为 2:
1 | var iOld = 64; //等于二进制 1000000 |
同样,移动数位后会造成空位。这次,空位位于数字的左侧,但位于符号位之后。ECMAScript 用符号位的值填充这些空位,创建完整的数字,如下图所示:
无符号右移运算符由三个大于号(>>>)表示,它将无符号 32 位数的所有数位整体右移。对于正数,无符号右移运算的结果与有符号右移运算一样。
用有符号右移运算中的例子,把 64 右移 5 位,将变为 2:
1 | var iOld = 64; //等于二进制 1000000 |
对于负数,情况就不同了。
无符号右移运算用 0 填充所有空位。对于正数,这与有符号右移运算的操作一样,而负数则被作为正数来处理。
由于无符号右移运算的结果是一个 32 位的正数,所以负数的无符号右移运算得到的总是一个非常大的数字。例如,如果把 -64 右移 5 位,将得到 134217726。如何得到这种结果的呢?
要实现这一点,需要把这个数字转换成无符号的等价形式(尽管该数字本身还是有符号的),可以通过以下代码获得这种形式:
1 | var iUnsigned64 = -64 >>> 0; |
然后,用 Number 类型的 toString() 获取它的真正的位表示,采用的基为 2:
1 | alert(iUnsigned64.toString(2)); |
这将生成 11111111111111111111111111000000,即有符号整数 -64 的二进制补码表示,不过它等于无符号整数 4294967232。
出于这种原因,使用无符号右移运算符要小心。
逻辑运算符有三种:NOT、AND 和 OR。
在学习各种逻辑运算符之前,让我们先了解一下 ECMAScript-262 v5 规范中描述的 ToBoolean 操作。
抽象操作 ToBoolean 将其参数按照下表中的规则转换为逻辑值:
参数类型 | 结果 |
---|---|
Undefined | false |
Null | false |
Boolean | 结果等于输入的参数(不转换) |
Number | 如果参数为 +0, -0 或 NaN,则结果为 false;否则为 true。 |
String | 如果参数为空字符串,则结果为 false;否则为 true。 |
Object | true |
在 ECMAScript 中,逻辑 NOT 运算符与 C 和 Java 中的逻辑 NOT 运算符相同,都由感叹号(!)表示。
与逻辑 OR 和逻辑 AND 运算符不同的是,逻辑 NOT 运算符返回的一定是 Boolean 值。
逻辑 NOT 运算符的行为如下:
通常,该运算符用于控制循环:
1 | var bFound = false; |
在这个例子中,Boolean 变量(bFound)用于记录检索是否成功。找到问题中的数据项时,bFound 将被设置为 true,!bFound 将等于 false,意味着运行将跳出 while 循环。
判断 ECMAScript 变量的 Boolean 值时,也可以使用逻辑 NOT 运算符。这样做需要在一行代码中使用两个 NOT 运算符。无论运算数是什么类型,第一个 NOT 运算符返回 Boolean 值。第二个 NOT 将对该 Boolean 值求负,从而给出变量真正的 Boolean 值。
1 | var bFalse = false; |
结果:
在 ECMAScript 中,逻辑 AND 运算符用双和号(&&)表示:
例如:
1 | var bTrue = true; |
下面的真值表描述了逻辑 AND 运算符的行为:
运算数 1 | 运算数 2 | 结果 |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
逻辑 AND 运算的运算数可以是任何类型的,不止是 Boolean 值。
如果某个运算数不是原始的 Boolean 型值,逻辑 AND 运算并不一定返回 Boolean 值:
与 Java 中的逻辑 AND 运算相似,ECMAScript 中的逻辑 AND 运算也是简便运算,即如果第一个运算数决定了结果,就不再计算第二个运算数。对于逻辑 AND 运算来说,如果第一个运算数是 false,那么无论第二个运算数的值是什么,结果都不可能等于 true。
考虑下面的例子:
1 | var bTrue = true; |
这段代码在进行逻辑 AND 运算时将引发错误,因为变量 bUnknown 是未定义的。变量 bTrue 的值为 true,因为逻辑 AND 运算将继续计算变量 bUnknown。这样做就会引发错误,因为 bUnknown 的值是 undefined,不能用于逻辑 AND 运算。
如果修改这个例子,把第一个数设为 false,那么就不会发生错误:
1 | var bFalse = false; |
在这段代码中,脚本将输出逻辑 AND 运算返回的值,即字符串 “false”。即使变量 bUnknown 的值为 undefined,它也不会被计算,因为第一个运算数的值是 false。
ECMAScript 中的逻辑 OR 运算符与 Java 中的相同,都由双竖线(||)表示:
1 | var bTrue = true; |
下面的真值表描述了逻辑 OR 运算符的行为:
运算数 1 | 运算数 2 | 结果 |
---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
与逻辑 AND 运算符相似,如果某个运算数不是 Boolean 值,逻辑 OR 运算并不一定返回 Boolean 值:
与逻辑 AND 运算符一样,逻辑 OR 运算也是简便运算。对于逻辑 OR 运算符来说,如果第一个运算数值为 true,就不再计算第二个运算数。
例如:
1 | var bTrue = true; |
与前面的例子相同,变量 bUnknown 是未定义的。不过,由于变量 bTrue 的值为 true,bUnknown 不会被计算,因此输出的是 “true”。
如果把 bTrue 改为 false,将发生错误:
1 | var bFalse = false; |
加法运算符由加号(+)表示:
1 | var iResult = 1 + 2 |
在处理特殊值时,ECMAScript 中的加法有一些特殊行为:
不过,如果某个运算数是字符串,那么采用下列规则:
例如:
1 | var result = 5 + 5; //两个数字 |
这段代码说明了加法运算符的两种模式之间的差别。正常情况下,5+5 等于 10(原始数值),如上述代码中前两行所示。不过,如果把一个运算数改为字符串 “5”,那么结果将变为 “55”(原始的字符串值),因为另一个运算数也会被转换为字符串。
减法运算符(-),也是一个常用的运算符:
1 | var iResult = 2 - 1; |
与加法运算符一样,在处理特殊值时,减法运算符也有一些特殊行为:
乘法运算符由星号(*)表示,用于两数相乘。
ECMAScript 中的乘法语法与 C 语言中的相同:
1 | var iResult = 12 * 34 |
不过,在处理特殊值时,ECMAScript 中的乘法还有一些特殊行为:
除法运算符由斜杠(/)表示,用第二个运算数除第一个运算数:
1 | var iResult = 88 /11; |
与乘法运算符相似,在处理特殊值时,除法运算符也有一些特殊行为:
除法(余数)运算符由百分号(%)表示,使用方法如下:
1 | var iResult = 26%5; //等于 1 |
与其他乘性运算符相似,对于特殊值,取模运算符也有特殊的行为:
关系运算符执行的是比较运算。每个关系运算符都返回一个布尔值。
关系运算符小于、大于、小于等于和大于等于执行的是两个数的比较运算,比较方式与算术比较运算相同。
每个关系运算符都返回一个布尔值:
1 | var bResult1 = 2 > 1 //true |
不过,对两个字符串应用关系运算符,它们的行为则不同。许多人认为小于表示“在字母顺序上靠前”,大于表示“在字母顺序上靠后”,但事实并非如此。对于字符串,第一个字符串中每个字符的代码都与会第二个字符串中对应位置的字符的代码进行数值比较。完成这种比较操作后,返回一个 Boolean 值。问题在于大写字母的代码都小于小写字母的代码,这意味这着可能会遇到下列情况:
1 | var bResult = "Blue" < "alpha"; |
在上面的例子中,字符串 “Blue” 小于 “alpha”,因为字母 B 的字符代码是 66,字母 a 的字符代码是 97。要强制性得到按照真正的字母顺序比较的结果,必须把两个数转换成相同的大小写形式(全大写或全小写的),然后再进行比较:
1 | var bResult = "Blue".toLowerCase() < "alpha".toLowerCase(); |
把两个运算数都转换成小写,确保了正确识别出 “alpha” 在字母顺序上位于 “Blue” 之前。
另一种棘手的状况发生在比较两个字符串形式的数字时,比如:
1 | var bResult = "25" < "3"; |
上面这段代码比较的是字符串 “25” 和 “3”。两个运算数都是字符串,所以比较的是它们的字符代码(“2” 的字符代码是 50,“3” 的字符代码是 51)。
不过,如果把某个运算数改为数字,那么结果就有趣了:
1 | var bResult = "25" < 3; |
这里,字符串 “25” 将被转换成数字 25,然后与数字 3 进行比较,结果不出所料。
无论何时比较一个数字和一个字符串,ECMAScript 都会把字符串转换成数字,然后按照数字顺序比较它们。
不过,如果字符串不能转换成数字又该如何呢?考虑下面的例子:
1 | var bResult = "a" < 3; |
你能预料到这段代码输出什么吗?字母 “a” 不能转换成有意义的数字。不过,如果对它调用 parseInt() 方法,返回的是 NaN。根据规则,任何包含 NaN 的关系运算符都要返回 false,因此这段代码也输出 false:
1 | var bResult = "a" >= 3; |
通常,如果小于运算的两个值返回 false,那么大于等于运算必须返回 true,不过如果某个数字是 NaN,情况则非如此。
判断两个变量是否相等是程序设计中非常重要的运算。在处理原始值时,这种运算相当简单,但涉及对象,任务就稍有点复杂。 ECMAScript 提供了两套等性运算符:等号和非等号用于处理原始值,全等号和非全等号用于处理对象。
在 ECMAScript 中,等号由双等号(==)表示,当且仅当两个运算数相等时,它返回 true。非等号由感叹号加等号(!=)表示,当且仅当两个运算数不相等时,它返回 true。为确定两个运算数是否相等,这两个运算符都会进行类型转换。
执行类型转换的规则如下:
在比较时,该运算符还遵守下列规则:
下表列出了一些特殊情况,以及它们的结果:
表达式 | 值 |
---|---|
null == undefined | true |
“NaN” == NaN | false |
5 == NaN | false |
NaN == NaN | false |
NaN != NaN | true |
false == 0 | true |
true == 1 | true |
true == 2 | false |
undefined == 0 | false |
null == 0 | false |
“5” == 5 | true |
等号和非等号的同类运算符是全等号和非全等号。这两个运算符所做的与等号和非等号相同,只是它们在检查相等性前,不执行类型转换。
全等号由三个等号表示(===),只有在无需类型转换运算数就相等的情况下,才返回 true。
例如:
1 | var sNum = "66"; |
在这段代码中,第一个 alert 使用等号来比较字符串 “66” 和数字 66,输出 “true”。如前所述,这是因为字符串 “66” 将被转换成数字 66,,然后才与另一个数字 66 进行比较。第二个 alert 使用全等号在没有类型转换的情况下比较字符串和数字,当然,字符串不等于数字,所以输出 “false”。
非全等号由感叹号加两个等号(!==)表示,只有在无需类型转换运算数不相等的情况下,才返回 true。
例如:
1 | var sNum = "66"; |
这里,第一个 alert 使用非等号,把字符串 “66” 转换成数字 66,使得它与第二个运算数 66 相等。因此,计算结果为 “false”,因为两个运算数是相等的。第二个 alert 使用的非全等号。该运算是在问:“sNum” 与 “iNum” 不同吗?这个问题的答案是:是的(true),因为 sNum 是字符串,而 iNum 是数字,它们当然不同。
条件运算符是 ECMAScript 中功能最多的运算符,它的形式与 Java 中的相同。
1 | variable = boolean_expression ? true_value : false_value; |
该表达式主要是根据 boolean_expression 的计算结果有条件地为变量赋值。如果 Boolean_expression 为 true,就把 true_value 赋给变量;如果它是 false,就把 false_value 赋给变量。
例如:
1 | var iMax = (iNum1 > iNum2) ? iNum1 : iNum2; |
在这里例子中,iMax 将被赋予数字中的最大值。表达式声明如果 iNum1 大于 iNum2,则把 iNum1 赋予 iMax。但如果表达式为 false(即 iNum2 大于或等于 iNum1),则把 iNum2 赋予 iMax。
简单的赋值运算符由等号(=)实现,只是把等号右边的值赋予等号左边的变量。
例如:
1 | var iNum = 10; |
复合赋值运算是由乘性运算符、加性运算符或位移运算符加等号(=)实现的。这些赋值运算符是下列这些常见情况的缩写形式:
1 | var iNum = 10; |
可以用一个复合赋值运算符改写第二行代码:
1 | var iNum = 10; |
每种主要的算术运算以及其他几个运算都有复合赋值运算符:
用逗号运算符可以在一条语句中执行多个运算。
例如:
1 | var iNum1 = 1, iNum = 2, iNum3 = 3; |
逗号运算符常用变量声明中。
if 语句是 ECMAScript 中最常用的语句之一,事实上在许多计算机语言中都是如此。
if 语句的语法:
1 | if (condition) statement1 else statement2 |
其中 condition 可以是任何表达式,计算的结果甚至不必是真正的 boolean 值,ECMAScript 会把它转换成 boolean 值。
如果条件计算结果为 true,则执行 statement1;如果条件计算结果为 false,则执行 statement2。
每个语句都可以是单行代码,也可以是代码块。
例如:
1 | if (i > 30) |
提示:使用代码块被认为是一种最佳的编程实践,即使要执行的代码只有一行。这样做可以使每个条件要执行什么一目了然。
还可以串联多个 if 语句。就像这样:
1 | if (condition1) statement1 else if (condition2) statement2 else statement3 |
例如:
1 | if (i > 30) { |
switch 语句是 if 语句的兄弟语句。
开发者可以用 switch 语句为表达式提供一系列的情况(case)。
switch 语句的语法:
1 | switch (expression) |
每个情况(case)都是表示“如果 expression 等于 value,就执行 statement”。
关键字 break 会使代码跳出 switch 语句。如果没有关键字 break,代码执行就会继续进入下一个 case。
关键字 default 说明了表达式的结果不等于任何一种情况时的操作(事实上,它相对于 else 从句)。
switch 语句主要是为避免让开发者编写下面的代码:
1 | if (i == 20) |
等价的 switch 语句是这样的:
1 | switch (i) { |
ECMAScript 和 Java 中的 switch 语句有两点不同。在 ECMAScript 中,switch 语句可以用于字符串,而且能用不是常量的值说明情况:
1 | var BLUE = "blue", RED = "red", GREEN = "green"; |
这里,switch 语句用于字符串 sColor,声明 case 使用的是变量 BLUE、RED 和 GREEN,这在 ECMAScript 中是完全有效的。
do-while 语句是后测试循环,即退出条件在执行循环内部的代码之后计算。这意味着在计算表达式之前,至少会执行循环主体一次。
它的语法如下:
1 | do {statement} while (expression); |
例子:
1 | var i = 0; |
while 语句是前测试循环。这意味着退出条件是在执行循环内部的代码之前计算的。因此,循环主体可能根本不被执行。
它的语法如下:
1 | while (expression) statement |
例子:
1 | var i = 0; |
for 语句是前测试循环,而且在进入循环之前,能够初始化变量,并定义循环后要执行的代码。
它的语法如下:
1 | for (initialization; expression; post-loop-expression) statement |
注意:post-loop-expression 之后不能写分号,否则无法运行。
例子:
1 | iCount = 6; |
这段代码定义了初始值为 0 的变量 i。只有当条件表达式(i < iCount)的值为 true 时,才进入 for 循环,这样循环主体可能不被执行。如果执行了循环主体,那么将执行循环后表达式,并迭代变量 i。
for 语句是严格的迭代语句,用于枚举对象的属性。
它的语法如下:
1 | for (property in expression) statement |
例子:
1 | for (sProp in window) { |
这里,for-in 语句用于显示 window 对象的所有属性。前面讨论过的 PropertyIsEnumerable() 是 ECMAScript 中专门用于说明属性是否可以用 for-in 语句访问的方法。
可以用下列语句给语句加标签,以便以后调用(有点类似 C 语言的 goto):
1 | label : statement |
例如:
1 | start : i = 5; |
在这个例子中,标签 start 可以被之后的 break 或 continue 语句 语句引用。
break 语句可以立即退出循环,阻止再次反复执行任何代码。而 continue 语句只是退出当前循环,根据控制表达式还允许继续进行下一次循环。
break 语句和 continue 语句都可以与有标签的语句联合使用,返回代码中的特定位置。
通常,当循环内部还有循环时,会这样做,例如:
1 | var iNum = 0; |
在上面的例子中,标签 outermost 表示的是第一个 for 语句。正常情况下,每个 for 语句执行 10 次代码块,这意味着 iNum++ 正常情况下将被执行 100 次,在执行完成时,iNum 应该等于 100。这里的 break 语句有一个参数,即停止循环后要跳转到的语句的标签。这样 break 语句不止能跳出内部 for 语句(即使用变量 j 的语句),还能跳出外部 for 语句(即使用变量 i 的语句)。因此,iNum 最后的值是 55,因为当 i 和 j 的值都等于 5 时,循环将终止。
可以以相同的方式使用 continue 语句:
1 | var iNum = 0; |
在上例中,continue 语句会迫使循环继续,不止是内部循环,外部循环也如此。当 j 等于 5 时出现这种情况,意味着内部循环将减少 5 次迭代,致使 iNum 的值为 95。
with 语句用于设置代码在特定对象中的作用域。
它的语法:
1 | with (expression) statement |
例如:
1 | var sMessage = "hello"; |
在这个例子中,with 语句用于字符串,所以在调用 toUpperCase() 方法时,解释程序将检查该方法是否是本地函数。如果不是,它将检查伪对象 sMessage,看它是否为该对象的方法。然后,alert 输出 “HELLO”,因为解释程序找到了字符串 “hello” 的 toUpperCase() 方法。
在 ECMAScript 5 的严格模式中,with 已经被禁用。
函数是由这样的方式进行声明的:关键字 function、函数名、一组参数,以及置于括号中的待执行代码。
函数的基本语法是这样的:
1 | function functionName(arg0, arg1, ... argN) { |
例如:
1 | function sayHi(sName, sMessage) { |
如果您想调用上例中的那个函数,可以使用如下的代码:
1 | sayHi("David", " Nice to meet you!") |
函数 sayHi() 未返回值,不过不必专门声明它(像在 Java 中使用 void 那样)。
即使函数确实有值,也不必明确地声明它。该函数只需要使用 return 运算符后跟要返回的值即可。
1 | function sum(iNum1, iNum2) { |
下面的代码把 sum 函数返回的值赋予一个变量:
1 | var iResult = sum(1,1); |
另一个重要概念是,与在 Java 中一样,函数在执行过 return 语句后立即停止代码。因此,return 语句后的代码都不会被执行。
例如,在下面的代码中,alert 窗口就不会显示出来:
1 | function sum(iNum1, iNum2) { |
一个函数中可以有多个 return 语句,如下所示:
1 | function diff(iNum1, iNum2) { |
上面的函数用于返回两个数的差。要实现这一点,必须用较大的数减去较小的数,因此用 if 语句决定执行哪个 return 语句。
如果函数无返回值,那么可以调用没有参数的 return 运算符,随时退出函数。
例如:
1 | function sayHi(sMessage) { |
这段代码中,如果 sMessage 等于 “bye”,就永远不显示警告框。
在函数代码中,使用特殊对象 arguments,开发者无需明确指出参数名,就能访问它们。
例如,在函数 sayHi() 中,第一个参数是 message。用 arguments[0] 也可以访问这个值,即第一个参数的值(第一个参数位于位置 0,第二个参数位于位置 1,依此类推)。
因此,无需明确命名参数,就可以重写函数:
1 | function sayHi() { |
还可以用 arguments 对象检测函数的参数个数,引用属性 arguments.length 即可。
下面的代码将输出每次调用函数使用的参数个数:
1 | function howManyArgs() { |
上面这段代码将依次显示 “2”、“0” 和 “1”。
用 arguments 对象判断传递给函数的参数个数,即可模拟函数重载:
1 | function doAdd() { |
当只有一个参数时,doAdd() 函数给参数加 5。如果有两个参数,则会把两个参数相加,返回它们的和。所以,doAdd(10) 输出的是 “15”,而 doAdd(40, 20) 输出的是 “60”。
虽然不如重载那么好,不过已足以避开 ECMAScript 的这种限制。
Function 类可以表示开发者定义的任何函数。用 Function 类直接创建函数的语法如下:
1 | var function_name = new function(arg1, arg2, ..., argN, function_body) |
在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串。
记得下面这个函数吗?
1 | function sayHi(sName, sMessage) { |
还可以这样定义它:
1 | var sayHi |
虽然由于字符串的关系,这种形式写起来有些困难,但有助于理解函数只不过是一种引用类型,它们的行为与用 Function 类明确创建的函数行为是相同的。
请看下面这个例子:
1 | function doAdd(iNum) { |
如你所知,第二个函数重写了第一个函数,使 doAdd(10) 输出了 “20”,而不是 “30”。
如果以下面的形式重写该代码块,这个概念就清楚了:
1 | var doAdd = new Function("iNum", "alert(iNum + 20)"); |
请观察这段代码,很显然,doAdd 的值被改成了指向不同对象的指针。函数名只是指向函数对象的引用值,行为就像其他对象一样。甚至可以使两个变量指向同一个函数:
1 | var doAdd = new Function("iNum", "alert(iNum + 10)"); |
在这里,变量 doAdd 被定义为函数,然后 alsodoAdd 被声明为指向同一个函数的指针。用这两个变量都可以执行该函数的代码,并输出相同的结果 - “20”。因此,如果函数名只是指向函数的变量,那么可以把函数作为参数传递给另一个函数吗?回答是肯定的!
1 | function callAnotherFunc(fnFunction, vArgument) { |
在上面的例子中,callAnotherFunc() 有两个参数 - 要调用的函数和传递给该函数的参数。这段代码把 doAdd() 传递给 callAnotherFunc() 函数,参数是 10,输出 “20”。
如前所述,函数属于引用类型,所以它们也有属性和方法。
ECMAScript 定义的属性 length 声明了函数期望的参数个数。例如:
1 | function doAdd(iNum) { |
函数 doAdd() 定义了一个参数,因此它的 length 是 1;sayHi() 没有定义参数,所以 length 是 0。
记住,无论定义了几个参数,ECMAScript 可以接受任意多个参数(最多 25 个),这一点在《函数概述》这一章中讲解过。属性 length 只是为查看默认情况下预期的参数个数提供了一种简便方式。
Function 对象也有与所有对象共享的 valueOf() 方法和 toString() 方法。这两个方法返回的都是函数的源代码,在调试时尤其有用。例如:
1 | function doAdd(iNum) { |
上面这段代码输出了 doAdd() 函数的文本。
在 ECMAScript 中使用全局变量是一个简单的闭包实例。请思考下面这段代码:
1 | var sMessage = "hello world"; |
在上面这段代码中,脚本被载入内存后,并没有为函数 sayHelloWorld() 计算变量 sMessage 的值。该函数捕获 sMessage 的值只是为了以后的使用,也就是说,解释程序知道在调用该函数时要检查 sMessage 的值。sMessage 将在函数调用 sayHelloWorld() 时(最后一行)被赋值,显示消息 “hello world”。
在一个函数中定义另一个会使闭包变得更加复杂。例如:
1 | var iBaseNum = 10; |
这里,函数 addNum() 包括函数 doAdd() (闭包)。内部函数是一个闭包,因为它将获取外部函数的参数 iNum1 和 iNum2 以及全局变量 iBaseNum 的值。 addNum() 的最后一步调用了 doAdd(),把两个参数和全局变量相加,并返回它们的和。
这里要掌握的重要概念是,doAdd() 函数根本不接受参数,它使用的值是从执行环境中获取的。
可以看到,闭包是 ECMAScript 中非常强大多用的一部分,可用于执行复杂的计算。
ECMA-262 把对象(object)定义为“属性的无序集合,每个属性存放一个原始值、对象或函数”。严格来说,这意味着对象是无特定顺序的值的数组。
尽管 ECMAScript 如此定义对象,但它更通用的定义是基于代码的名词(人、地点或事物)的表示。
每个对象都由类定义,可以把类看做对象的配方。类不仅要定义对象的接口(interface)(开发者访问的属性和方法),还要定义对象的内部工作(使属性和方法发挥作用的代码)。编译器和解释程序都根据类的说明构建对象。
程序使用类创建对象时,生成的对象叫作类的实例(instance)。对类生成的对象的个数的唯一限制来自于运行代码的机器的物理内存。每个实例的行为相同,但实例处理一组独立的数据。由类创建对象实例的过程叫做实例化(instantiation)。
在前面的章节我们提到过,ECMAScript 并没有正式的类。相反,ECMA-262 把对象定义描述为对象的配方。这是 ECMAScript 逻辑上的一种折中方案,因为对象定义实际上是对象自身。即使类并不真正存在,我们也把对象定义叫做类,因为大多数开发者对此术语更熟悉,而且从功能上说,两者是等价的。
一种面向对象语言需要向开发者提供四种基本能力:
ECMAScript 支持这些要求,因此可被是看做面向对象的。
在 ECMAScript 中,对象由特性(attribute)构成,特性可以是原始值,也可以是引用值。如果特性存放的是函数,它将被看作对象的方法(method),否则该特性被看作对象的属性(property)。
对象的创建方式是用关键字 new 后面跟上实例化的类的名字:
1 | var oObject = new Object(); |
第一行代码创建了 Object 类的一个实例,并把它存储到变量 oObject 中。第二行代码创建了 String 类的一个实例,把它存储在变量 oStringObject 中。如果构造函数无参数,括号则不是必需的。因此可以采用下面的形式重写上面的两行代码:
1 | var oObject = new Object; |
在前面的章节中,我们介绍了引用类型的概念。在 ECMAScript 中,不能访问对象的物理表示,只能访问对象的引用。每次创建对象,存储在变量中的都是该对象的引用,而不是对象本身。
ECMAScript 拥有无用存储单元收集程序(garbage collection routine),意味着不必专门销毁对象来释放内存。当再没有对对象的引用时,称该对象被废除(dereference)了。运行无用存储单元收集程序时,所有废除的对象都被销毁。每当函数执行完它的代码,无用存储单元收集程序都会运行,释放所有的局部变量,还有在一些其他不可预知的情况下,无用存储单元收集程序也会运行。 把对象的所有引用都设置为 null,可以强制性地废除对象。例如:
1 | var oObject = new Object; |
当变量 oObject 设置为 null 后,对第一个创建的对象的引用就不存在了。这意味着下次运行无用存储单元收集程序时,该对象将被销毁。
每用完一个对象后,就将其废除,来释放内存,这是个好习惯。这样还确保不再使用已经不能访问的对象,从而防止程序设计错误的出现。此外,旧的浏览器(如 IE/MAC)没有全面的无用存储单元收集程序,所以在卸载页面时,对象可能不能被正确销毁。废除对象和它的所有特性是确保内存使用正确的最好方法。
所谓绑定(binding),即把对象的接口与对象实例结合在一起的方法。
早绑定(early binding)是指在实例化对象之前定义它的属性和方法,这样编译器或解释程序就能够提前转换机器代码。在 Java 和 Visual Basic 这样的语言中,有了早绑定,就可以在开发环境中使用 IntelliSense(即给开发者提供对象中属性和方法列表的功能)。ECMAScript 不是强类型语言,所以不支持早绑定。
另一方面,晚绑定(late binding)指的是编译器或解释程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需检查对象是否支持属性和方法即可。ECMAScript 中的所有变量都采用晚绑定方法。这样就允许执行大量的对象操作,而无任何惩罚。
在 ECMAScript 中,所有对象并非同等创建的。
一般来说,可以创建并使用的对象有三种:本地对象、内置对象和宿主对象。
ECMA-262 把本地对象(native object)定义为“独立于宿主环境的 ECMAScript 实现提供的对象”。简单来说,本地对象就是 ECMA-262 定义的类(引用类型)。它们包括:
ECMA-262 把内置对象(built-in object)定义为“由 ECMAScript 实现提供的、独立于宿主环境的所有对象,在 ECMAScript 程序开始执行时出现”。这意味着开发者不必明确实例化内置对象,它已被实例化了。ECMA-262 只定义了两个内置对象,即 Global
和 Math
(它们也是本地对象,根据定义,每个内置对象都是本地对象)。
所有非本地对象都是宿主对象(host object),即由 ECMAScript 实现的宿主环境提供的对象。
所有 BOM 和 DOM 对象都是宿主对象。
在传统的面向对象程序设计中,主要关注于公用和私有作用域。公用作用域中的对象属性可以从对象外部访问,即开发者创建对象的实例后,就可使用它的公用属性。而私有作用域中的属性只能在对象内部访问,即对于外部世界来说,这些属性并不存在。这意味着如果类定义了私有属性和方法,则它的子类也不能访问这些属性和方法。
受保护作用域也是用于定义私有的属性和方法,只是这些属性和方法还能被其子类访问。
对 ECMAScript 讨论上面这些作用域几乎毫无意义,因为 ECMAScript 中只存在一种作用域 - 公用作用域。ECMAScript 中的所有对象的所有属性和方法都是公用的。因此,定义自己的类和对象时,必须格外小心。记住,所有属性和方法默认都是公用的!
许多开发者都在网上提出了有效的属性作用域模式,解决了 ECMAScript 的这种问题。
由于缺少私有作用域,开发者确定了一个规约,说明哪些属性和方法应该被看做私有的。这种规约规定在属性前后加下划线:
1 | obj._color_ = "blue"; |
这段代码中,属性 color 是私有的。注意,下划线并不改变属性是公用属性的事实,它只是告诉其他开发者,应该把该属性看作私有的。
有些开发者还喜欢用单下划线说明私有成员,例如:obj._color
。
静态作用域定义的属性和方法任何时候都能从同一位置访问。在 Java 中,类可具有属性和方法,无需实例化该类的对象,即可访问这些属性和方法,例如 java.net.URLEncoder 类,它的函数 encode() 就是静态方法。
严格来说,ECMAScript 并没有静态作用域。不过,它可以给构造函数提供属性和方法。还记得吗,构造函数只是函数。函数是对象,对象可以有属性和方法。例如:
1 | function sayHello() { |
这里,方法 alternate() 实际上是函数 sayHello 的方法。可以像调用常规函数一样调用 sayHello() 输出 “hello”,也可以调用 sayHello.alternate()
输出 “hi”。即使如此,alternate() 也是 sayHello() 公用作用域中的方法,而不是静态方法。
在 ECMAScript 中,要掌握的最重要的概念之一是关键字 this 的用法,它用在对象的方法中。关键字 this 总是指向调用该方法的对象,例如:
1 | var oCar = new Object; |
在上面的代码中,关键字 this 用在对象的 showColor() 方法中。在此环境中,this 等于 oCar。下面的代码与上面的代码的功能相同:
1 | var oCar = new Object; |
为什么使用 this 呢?因为在实例化对象时,总是不能确定开发者会使用什么样的变量名。使用 this,即可在任何多个地方重用同一个函数。请思考下面的例子:
1 | function showColor() { |
在上面的代码中,首先用 this 定义函数 showColor(),然后创建两个对象(oCar1 和 oCar2),一个对象的 color 属性被设置为 “red”,另一个对象的 color 属性被设置为 “blue”。两个对象都被赋予了属性 showColor,指向原始的 showColor () 函数(注意这里不存在命名问题,因为一个是全局函数,而另一个是对象的属性)。调用每个对象的 showColor(),oCar1 输出是 “red”,而 oCar2 的输出是 “blue”。这是因为调用 oCar1.showColor() 时,函数中的 this 关键字等于 oCar1。调用 oCar2.showColor() 时,函数中的 this 关键字等于 oCar2。
function showColor() { alert(color); };
如果不用对象或 this 关键字引用变量,ECMAScript 就会把它看作局部变量或全局变量。然后该函数将查找名为 color 的局部或全局变量,但是不会找到。结果如何呢?该函数将在警告中显示 “null”。
ECMAScript 拥有很多创建对象或类的方法。
因为对象的属性可以在对象创建后动态定义,所有许多开发者都在 JavaScript 最初引入时编写类似下面的代码:
1 | var oCar = new Object; |
在上面的代码中,创建对象 car。然后给它设置几个属性:它的颜色是蓝色,有四个门,每加仑油可以跑 25 英里。最后一个属性实际上是指向函数的指针,意味着该属性是个方法。执行这段代码后,就可以使用对象 car。 不过这里有一个问题,就是可能需要创建多个 car 的实例。
要解决该问题,开发者创造了能创建并返回特定类型的对象的工厂函数(factory function)。
例如,函数 createCar() 可用于封装前面列出的创建 car 对象的操作:
1 | function createCar() { |
在这里,第一个例子中的所有代码都包含在 createCar() 函数中。此外,还有一行额外的代码,返回 car 对象(oTempCar)作为函数值。调用此函数,将创建新对象,并赋予它所有必要的属性,复制出一个我们在前面说明过的 car 对象。因此,通过这种方法,我们可以很容易地创建 car 对象的两个版本(oCar1 和 oCar2),它们的属性完全一样。
我们还可以修改 createCar() 函数,给它传递各个属性的默认值,而不是简单地赋予属性默认值:
1 | function createCar(sColor,iDoors,iMpg) { |
给 createCar() 函数加上参数,即可为要创建的 car 对象的 color、doors 和 mpg 属性赋值。这使两个对象具有相同的属性,却有不同的属性值。
虽然 ECMAScript 越来越正式化,但创建对象的方法却被置之不理,且其规范化至今还遭人反对。一部分是语义上的原因(它看起来不像使用带有构造函数 new 运算符那么正规),一部分是功能上的原因。功能原因在于用这种方式必须创建对象的方法。前面的例子中,每次调用函数 createCar(),都要创建新函数 showColor(),意味着每个对象都有自己的 showColor() 版本。而事实上,每个对象都共享同一个函数。
有些开发者在工厂函数外定义对象的方法,然后通过属性指向该方法,从而避免这个问题:
1 | function showColor() { |
在上面这段重写的代码中,在函数 createCar() 之前定义了函数 showColor()。在 createCar() 内部,赋予对象一个指向已经存在的 showColor() 函数的指针。从功能上讲,这样解决了重复创建函数对象的问题;但是从语义上讲,该函数不太像是对象的方法。
所有这些问题都引发了开发者定义的构造函数的出现。
创建构造函数就像创建工厂函数一样容易。第一步选择类名,即构造函数的名字。根据惯例,这个名字的首字母大写,以使它与首字母通常是小写的变量名分开。除了这点不同,构造函数看起来很像工厂函数。请考虑下面的例子:
1 | function Car(sColor,iDoors,iMpg) { |
下面为您解释上面的代码与工厂方式的差别。首先在构造函数内没有创建对象,而是使用 this 关键字。使用 new 运算符构造函数时,在执行第一行代码前先创建一个对象,只有用 this 才能访问该对象。然后可以直接赋予 this 属性,默认情况下是构造函数的返回值(不必明确使用 return 运算符)。
现在,用 new 运算符和类名 Car 创建对象,就更像 ECMAScript 中一般对象的创建方式了。
你也许会问,这种方式在管理函数方面是否存在于前一种方式相同的问题呢?是的。
就像工厂函数,构造函数会重复生成函数,为每个对象都创建独立的函数版本。不过,与工厂函数相似,也可以用外部函数重写构造函数,同样地,这么做语义上无任何意义。这正是下面要讲的原型方式的优势所在。
该方式利用了对象的 prototype 属性,可以把它看成创建新对象所依赖的原型。
这里,首先用空构造函数来设置类名。然后所有的属性和方法都被直接赋予 prototype 属性。我们重写了前面的例子,代码如下:
1 | function Car() { |
在这段代码中,首先定义构造函数(Car),其中无任何代码。接下来的几行代码,通过给 Car 的 prototype 属性添加属性去定义 Car 对象的属性。调用 new Car() 时,原型的所有属性都被立即赋予要创建的对象,意味着所有 Car 实例存放的都是指向 showColor() 函数的指针。从语义上讲,所有属性看起来都属于一个对象,因此解决了前面两种方式存在的问题。
此外,使用这种方式,还能用 instanceof 运算符检查给定变量指向的对象的类型。因此,下面的代码将输出 TRUE:
1 | alert(oCar1 instanceof Car); //输出 "true" |
原型方式看起来是个不错的解决方案。遗憾的是,它并不尽如人意。
首先,这个构造函数没有参数。使用原型方式,不能通过给构造函数传递参数来初始化属性的值,因为 Car1 和 Car2 的 color 属性都等于 “blue”,doors 属性都等于 4,mpg 属性都等于 25。这意味着必须在对象创建后才能改变属性的默认值,这点很令人讨厌,但还没完。真正的问题出现在属性指向的是对象,而不是函数时。函数共享不会造成问题,但对象却很少被多个实例共享。请思考下面的例子:
1 | function Car() { |
上面的代码中,属性 drivers 是指向 Array 对象的指针,该数组中包含两个名字 “Mike” 和 “John”。由于 drivers 是引用值,Car 的两个实例都指向同一个数组。这意味着给 oCar1.drivers 添加值 “Bill”,在 oCar2.drivers 中也能看到。输出这两个指针中的任何一个,结果都是显示字符串 “Mike,John,Bill”。
由于创建对象时有这么多问题,你一定会想,是否有种合理的创建对象的方法呢?答案是有,需要联合使用构造函数和原型方式。
联合使用构造函数和原型方式,就可像用其他程序设计语言一样创建对象。这种概念非常简单,即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果是,所有函数都只创建一次,而每个对象都具有自己的对象属性实例。
我们重写了前面的例子,代码如下:
1 | function Car(sColor,iDoors,iMpg) { |
现在就更像创建一般对象了。所有的非函数属性都在构造函数中创建,意味着又能够用构造函数的参数赋予属性默认值了。因为只创建 showColor() 函数的一个实例,所以没有内存浪费。此外,给 oCar1 的 drivers 数组添加 “Bill” 值,不会影响到 oCar2 的数组,所以输出这些数组的值时,oCar1.drivers 显示的是 “Mike,John,Bill”,而 oCar2.drivers 显示的是 “Mike,John”。因为使用了原型方式,所以仍然能利用 instanceof 运算符来判断对象的类型。
这种方式是 ECMAScript 采用的主要方式,它具有其他方式的特性,却没有他们的副作用。不过,有些开发者仍觉得这种方法不够完美。
批评混合的构造函数/原型方式的人认为,在构造函数内部定义属性,在其外部定义方法的做法不合逻辑。因此,他们设计了动态原型方法,以提供更友好的编码风格。
动态原型方法的基本想法与混合的构造函数/原型方式相同,即在构造函数内定义非函数属性,而函数属性则利用原型属性定义。唯一的区别是赋予对象方法的位置。下面是用动态原型方法重写的 Car 类:
1 | function Car(sColor,iDoors,iMpg) { |
直到检查 typeof Car._initialized 是否等于 “undefined” 之前,这个构造函数都未发生变化。这行代码是动态原型方法中最重要的部分。如果这个值未定义,构造函数将用原型方式继续定义对象的方法,然后把 Car._initialized 设置为 true。如果这个值定义了(它的值为 true 时,typeof 的值为 Boolean),那么就不再创建该方法。简而言之,该方法使用标志(_initialized)来判断是否已给原型赋予了任何方法。该方法只创建并赋值一次,传统的 OOP 开发者会高兴地发现,这段代码看起来更像其他语言中的类定义了。
这种方式通常是在不能应用前一种方式时的变通方法。它的目的是创建假构造函数,只返回另一种对象的新实例。
这段代码看起来与工厂函数非常相似:
1 | function Car() { |
与经典方式不同,这种方式使用 new 运算符,使它看起来像真正的构造函数:
1 | var car = new Car(); |
由于在 Car() 构造函数内部调用了 new 运算符,所以将忽略第二个 new 运算符(位于构造函数之外),在构造函数内部创建的对象被传递回变量 car。
这种方式在对象方法的内部管理方面与经典方式有着相同的问题。强烈建议:除非万不得已,还是避免使用这种方式。
如前所述,目前使用最广泛的是混合的构造函数/原型方式。此外,动态原始方法也很流行,在功能上与构造函数/原型方式等价。可以采用这两种方式中的任何一种。不过不要单独使用经典的构造函数或原型方式,因为这样会给代码引入问题。
通过使用 ECMAScript,不仅可以创建对象,还可以修改已有对象的行为。
prototype 属性不仅可以定义构造函数的属性和方法,还可以为本地对象添加属性和方法。
可以用 prototype 属性为任何已有的类定义新方法,就像处理自己的类一样。例如,还记得 Number 类的 toString() 方法吗?如果给它传递参数 16,它将输出十六进制的字符串。如果这个方法的参数是 2,那么它将输出二进制的字符串。我们可以创建一个方法,可以把数字对象直接转换为十六进制字符串。创建这个方法非常简单:
1 | Number.prototype.toHexString = function() { |
在此环境中,关键字 this 指向 Number 的实例,因此可完全访问 Number 的所有方法。有了这段代码,可实现下面的操作:
1 | var iNum = 15; |
由于数字 15 等于十六进制中的 F,因此警告将显示 “F”。
我们还可以为已有的方法命名更易懂的名称。例如,可以给 Array 类添加两个方法 enqueue() 和 dequeue(),只让它们反复调用已有的 push() 和 shift() 方法即可:
1 | Array.prototype.enqueue = function(vItem) { |
当然,还可以添加与已有方法无关的方法。例如,假设要判断某个项在数组中的位置,没有本地方法可以做这种事情。我们可以轻松地创建下面的方法:
1 | Array.prototype.indexOf = function (vItem) { |
该方法 indexOf() 与 String 类的同名方法保持一致,在数组中检索每个项,直到发现与传进来的项相同的项目为止。如果找到相同的项,则返回该项的位置,否则,返回 -1。有了这种定义,我们可以编写下面的代码:
1 | var aColors = new Array("red","green","blue"); |
最后,如果想给 ECMAScript 中每个本地对象添加新方法,必须在 Object 对象的 prototype 属性上定义它。前面的章节我们讲过,所有本地对象都继承了 Object 对象,所以对 Object 对象做任何改变,都会反应在所有本地对象上。例如,如果想添加一个用警告输出对象的当前值的方法,可以采用下面的代码:
1 | Object.prototype.showValue = function () { |
这里,String 和 Number 对象都从 Object 对象继承了 showValue() 方法,分别在它们的对象上调用该方法,将显示 “hello” 和 “25”。
就像能给已有的类定义新方法一样,也可重定义已有的方法。如前面的章节所述,函数名只是指向函数的指针,因此可以轻松地指向其他函数。如果修改了本地方法,如 toString(),会出现什么情况呢?
1 | Function.prototype.toString = function() { |
前面的代码完全合法,运行结果完全符合预期:
1 | function sayHi() { |
也许你还记得,Function 对象这一章中介绍过 Function 的 toString() 方法通常输出的是函数的源代码。覆盖该方法,可以返回另一个字符串(在这个例子中,可以返回 “Function code hidden”)。不过,toString() 指向的原始函数怎么了呢?它将被无用存储单元回收程序回收,因为它被完全废弃了。没有能够恢复原始函数的方法,所以在覆盖原始方法前,比较安全的做法是存储它的指针,以便以后的使用。有时你甚至可能在新方法中调用原始方法:
1 | Function.prototype.originalToString = Function.prototype.toString; |
在这段代码中,第一行代码把对当前 toString() 方法的引用保存在属性 originalToString 中。然后用定制的方法覆盖了 toString() 方法。新方法将检查该函数源代码的长度是否大于 100。如果是,就返回错误信息,说明该函数代码太长,否则调用 originalToString() 方法,返回函数的源代码。
从技术上讲,根本不存在极晚绑定。本书采用该术语描述 ECMAScript 中的一种现象,即能够在对象实例化后再定义它的方法。例如:
1 | var o = new Object(); |
在大多数程序设计语言中,必须在实例化对象之前定义对象的方法。这里,方法 sayHi() 是在创建 Object 类的一个实例之后来添加进来的。在传统语言中不仅没听说过这种操作,也没听说过该方法还会自动赋予 Object 对象的实例并能立即使用(接下来的一行)。
ECMAScript 实现继承的方式不止一种。这是因为 JavaScript 中的继承机制并不是明确规定的,而是通过模仿实现的。下面为您介绍几种具体的继承方式。
构想原始的 ECMAScript 时,根本没打算设计对象冒充(object masquerading)。它是在开发者开始理解函数的工作方式,尤其是如何在函数环境中使用 this 关键字后才发展出来。
其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数,所以可使 ClassA 构造函数成为 ClassB 的方法,然后调用它。ClassB 就会收到 ClassA 的构造函数中定义的属性和方法。例如,用下面的方式定义 ClassA 和 ClassB:
1 | function ClassA(sColor) { |
还记得吗?关键字 this 引用的是构造函数当前创建的对象。不过在这个方法中,this 指向的所属的对象。这个原理是把 ClassA 作为常规函数来建立继承机制,而不是作为构造函数。如下使用构造函数 ClassB 可以实现继承机制:
1 | function ClassB(sColor) { |
在这段代码中,为 ClassA 赋予了方法 newMethod(请记住,函数名只是指向它的指针)。然后调用该方法,传递给它的是 ClassB 构造函数的参数 sColor。最后一行代码删除了对 ClassA 的引用,这样以后就不能再调用它。
所有新属性和新方法都必须在删除了新方法的代码行后定义。否则,可能会覆盖超类的相关属性和方法:
1 | function ClassB(sColor, sName) { |
为证明前面的代码有效,可以运行下面的例子:
1 | var objA = new ClassA("blue"); |
有趣的是,对象冒充可以支持多重继承。也就是说,一个类可以继承多个超类。用 UML 表示的多重继承机制如下图所示:
例如,如果存在两个类 ClassX 和 ClassY,ClassZ 想继承这两个类,可以使用下面的代码:
1 | function ClassZ() { |
这里存在一个弊端,如果存在两个类 ClassX 和 ClassY 具有同名的属性或方法,ClassY 具有高优先级。因为它从后面的类继承。除这点小问题之外,用对象冒充实现多重继承机制轻而易举。
由于这种继承方法的流行,ECMAScript 的第三版为 Function 对象加入了两个方法,即 call() 和 apply()。
call() 方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作 this 的对象。其他参数都直接传递给函数自身。例如:
1 | function sayColor(sPrefix,sSuffix) { |
在这个例子中,函数 sayColor() 在对象外定义,即使它不属于任何对象,也可以引用关键字 this。对象 obj 的 color 属性等于 blue。调用 call() 方法时,第一个参数是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个和第三个参数是字符串。它们与 sayColor() 函数中的参数 sPrefix 和 sSuffix 匹配,最后生成的消息 “The color is blue, a very nice color indeed.” 将被显示出来。
要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:
1 | function ClassB(sColor, sName) { |
这里,我们需要让 ClassA 中的关键字 this 等于新创建的 ClassB 对象,因此 this 是第一个参数。第二个参数 sColor 对两个类来说都是唯一的参数。
apply() 方法有两个参数,用作 this 的对象和要传递给函数的参数的数组。例如:
1 | function sayColor(sPrefix,sSuffix) { |
这个例子与前面的例子相同,只是现在调用的是 apply() 方法。调用 apply() 方法时,第一个参数仍是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个参数是由两个字符串构成的数组,与 sayColor() 函数中的参数 sPrefix 和 sSuffix 匹配,最后生成的消息仍是 “The color is blue, a very nice color indeed.”,将被显示出来。
该方法也用于替换前三行的赋值、调用和删除新方法的代码:
1 | function ClassB(sColor, sName) { |
同样的,第一个参数仍是 this,第二个参数是只有一个值 color 的数组。可以把 ClassB 的整个 arguments 对象作为第二个参数传递给 apply() 方法:
1 | function ClassB(sColor, sName) { |
当然,只有超类中的参数顺序与子类中的参数顺序完全一致时才可以传递参数对象。如果不是,就必须创建一个单独的数组,按照正确的顺序放置参数。此外,还可使用 call() 方法。
继承这种形式在 ECMAScript 中原本是用于原型链的。上一章介绍了定义类的原型方式。原型链扩展了这种方式,以一种有趣的方式实现继承机制。
在上一章学过,prototype 对象是个模板,要实例化的对象都以这个模板为基础。总而言之,prototype 对象的任何属性和方法都被传递给那个类的所有实例。原型链利用这种功能来实现继承机制。
如果用原型方式重定义前面例子中的类,它们将变为下列形式:
1 | function ClassA() { |
原型方式的神奇之处在于最后一行。这里,把 ClassB 的 prototype 属性设置成 ClassA 的实例。这很有意思,因为想要 ClassA 的所有属性和方法,但又不想逐个将它们 ClassB 的 prototype 属性。还有比把 ClassA 的实例赋予 prototype 属性更好的方法吗?
与对象冒充相似,子类的所有属性和方法都必须出现在 prototype 属性被赋值后,因为在它之前赋值的所有方法都会被删除。为什么?因为 prototype 属性被替换成了新对象,添加了新方法的原始对象将被销毁。所以,为 ClassB 类添加 name 属性和 sayName() 方法的代码如下:
1 | function ClassB() { |
可通过运行下面的例子测试这段代码:
1 | var objA = new ClassA(); |
此外,在原型链中,instanceof 运算符的运行方式也很独特。对 ClassB 的所有实例,instanceof 为 ClassA 和 ClassB 都返回 true。例如:
1 | var objB = new ClassB(); |
在 ECMAScript 的弱类型世界中,这是极其有用的工具,不过使用对象冒充时不能使用它。
原型链的弊端是不支持多重继承。记住,原型链会用另一类型的对象重写类的 prototype 属性。
这种继承方式使用构造函数定义类,并非使用任何原型。对象冒充的主要问题是必须使用构造函数方式,这不是最好的选择。不过如果使用原型链,就无法使用带参数的构造函数了。开发者如何选择呢?答案很简单,两者都用。
在前一章,我们曾经讲解过创建类的最好方式是用构造函数定义属性,用原型定义方法。这种方式同样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承 prototype 对象的方法。用这两种方式重写前面的例子,代码如下:
1 |
|
在此例子中,继承机制由两行突出显示的蓝色代码实现。在第一行突出显示的代码中,在 ClassB 构造函数中,用对象冒充继承 ClassA 类的 sColor 属性。在第二行突出显示的代码中,用原型链继承 ClassA 类的方法。由于这种混合方式使用了原型链,所以 instanceof 运算符仍能正确运行。
下面的例子测试了这段代码:
1 | var objA = new ClassA("blue"); |