JavaScript第5 章 基本引用类型

本章内容
 理解对象
 基本JavaScript 数据类型
 原始值与原始值包装类型
引用值(或者对象)是某个特定引用类型的实例。在ECMAScript 中,引用类型是把数据和功能组
织到一起的结构,经常被人错误地称作“类”。虽然从技术上讲JavaScript 是一门面向对象语言,但
ECMAScript 缺少传统的面向对象编程语言所具备的某些基本结构,包括类和接口。引用类型有时候也
被称为对象定义,因为它们描述了自己的对象应有的属性和方法。
注意 引用类型虽然有点像类,但跟类并不是一个概念。为避免混淆,本章后面不会使用
术语“类”。
对象被认为是某个特定引用类型的实例。新对象通过使用new 操作符后跟一个构造函数(constructor)
来创建。构造函数就是用来创建新对象的函数,比如下面这行代码:
let now = new Date();
这行代码创建了引用类型Date 的一个新实例,并将它保存在变量now 中。Date()在这里就是构
造函数,它负责创建一个只有默认属性和方法的简单对象。ECMAScript 提供了很多像Date 这样的原
生引用类型,帮助开发者实现常见的任务。
注意 函数也是一种引用类型,但有关函数的内容太多了,一章放不下,所以本书专门用
第10 章来介绍函数。


5.1 Date


ECMAScript 的Date 类型参考了Java 早期版本中的java.util.Date。为此,Date 类型将日期
保存为自协调世界时(UTC,Universal Time Coordinated)时间1970 年1 月1 日午夜(零时)至今所
经过的毫秒数。使用这种存储格式,Date 类型可以精确表示1970 年1 月1 日之前及之后285 616 年的
日期。
要创建日期对象,就使用new 操作符来调用Date 构造函数:
let now = new Date();
在不给Date 构造函数传参数的情况下,创建的对象将保存当前日期和时间。要基于其他日期和时间创建日期对象,必须传入其毫秒表示(UNIX 纪元1970 年1 月1 日午夜之后的毫秒数)。ECMAScript为此提供了两个辅助方法:Date.parse()和Date.UTC()。
Date.parse()方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。ECMA-262 第5 版定义了Date.parse()应该支持的日期格式,填充了第3 版遗留的空白。所有实现都必须支持下列日期格式:
“月/日/年”,如"5/23/2019";
“月名 日, 年”,如"May 23, 2019";
“周几 月名 日 年 时:分:秒 时区”,如"Tue May 23 2019 00:00:00 GMT-0700";
ISO 8601 扩展格式“YYYY-MM-DDTHH:mm:ss.sssZ”,如2019-05-23T00:00:00(只适用于兼容ES5 的实现)。
比如,要创建一个表示“2019 年5 月23 日”的日期对象,可以使用以下代码:
let someDate = new Date(Date.parse("May 23, 2019"));
如果传给Date.parse()的字符串并不表示日期,则该方法会返回NaN。如果直接把表示日期的字符串传给Date 构造函数,那么Date 会在后台调用Date.parse()。换句话说,下面这行代码跟前面那行代码是等价的:let someDate = new Date("May 23, 2019");这两行代码得到的日期对象相同。
注意 不同的浏览器对Date 类型的实现有很多问题。比如,很多浏览器会选择用当前日期替代越界的日期,因此有些浏览器会将"January 32, 2019"解释为"February 1,2019"。Opera 则会插入当前月的当前日,返回"January 当前日, 2019"。就是说,如果是在9 月21 日运行代码,会返回"January 21, 2019"。
Date.UTC()方法也返回日期的毫秒表示,但使用的是跟Date.parse()不同的信息来生成这个值。传给Date.UTC()的参数是年、零起点月数(1 月是0,2 月是1,以此类推)、日(1~31)、时(0~23)、分、秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为1 日。其他参数的默认值都是0。下面是使用Date.UTC()的两个例子:
这个例子创建了两个日期 。第一个日期是2000 年1 月1 日零点(GMT),2000 代表年,0 代表月(1 月)。因为没有其他参数(日取1,其他取0),所以结果就是该月第1 天零点。第二个日期表示2005年5 月5 日下午5 点55 分55 秒(GMT)。虽然日期里面涉及的都是5,但月数必须用4,因为月数是零起点的。小时也必须是17,因为这里采用的是24 小时制,即取值范围是0~23。其他参数就都很直观了。
与Date.parse()一样,Date.UTC()也会被Date 构造函数隐式调用,但有一个区别:这种情况下创建的是本地日期,不是GMT 日期。不过Date 构造函数跟Date.UTC()接收的参数是一样的。因此,如果第一个参数是数值,则构造函数假设它是日期中的年,第二个参数就是月,以此类推。前面的例子也可以这样来写:
以上代码创建了与前面例子中相同的两个日期,但这次的两个日期是(由于系统设置决定的)本地
时区的日期。
ECMAScript 还提供了Date.now()方法,返回表示方法执行时日期和时间的毫秒数。这个方法可
以方便地用在代码分析中:  
5.1.1 继承的方法
与其他类型一样,Date 类型重写了toLocaleString()、toString()和valueOf()方法。但与
其他类型不同,重写后这些方法的返回值不一样。Date 类型的toLocaleString()方法返回与浏览器
运行的本地环境一致的日期和时间。这通常意味着格式中包含针对时间的AM(上午)或PM(下午),
但不包含时区信息(具体格式可能因浏览器而不同)。toString()方法通常返回带时区信息的日期和时
间,而时间也是以24 小时制(0~23)表示的。下面给出了toLocaleString()和toString()返回的
2019 年2 月1 日零点的示例(地区为"en-US"的PST,即Pacific Standard Time,太平洋标准时间):
现代浏览器在这两个方法的输出上已经趋于一致。在比较老的浏览器上,每个方法返回的结果可能
在每个浏览器上都是不同的。这些差异意味着toLocaleString()和toString()可能只对调试有用,
不能用于显示。
Date 类型的valueOf()方法根本就不返回字符串,这个方法被重写后返回的是日期的毫秒表示。
因此,操作符(如小于号和大于号)可以直接使用它返回的值。比如下面的例子:
日期2019 年1 月1 日在2019 年2 月1 日之前,所以说前者小于后者没问题。因为2019 年1 月1 日
的毫秒表示小于2019 年2 月1 日的毫秒表示,所以用小于号比较这两个日期时会返回true。这也是确
保日期先后的一个简单方式。
5.1.2 日期格式化方法
Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串:
这些方法的输出与toLocaleString()和toString()一样,会因浏览器而异。因此不能用于在
用户界面上一致地显示日期。
注意 还有一个方法叫toGMTString(),这个方法跟toUTCString()是一样的,目的
是为了向后兼容。不过,规范建议新代码使用toUTCString()。
5.1.3 日期/时间组件方法
Date 类型剩下的方法(见下表)直接涉及取得或设置日期值的特定部分。注意表中“UTC 日期”,
指的是没有时区偏移(将日期转换为GMT)时的日期。
方 法 说 明
getTime() 返回日期的毫秒表示;与valueOf()相同
setTime(milliseconds) 设置日期的毫秒表示,从而修改整个日期
getFullYear() 返回4 位数年(即2019 而不是19)
getUTCFullYear() 返回UTC 日期的4 位数年
setFullYear(year) 设置日期的年(year 必须是4 位数)
setUTCFullYear(year) 设置UTC 日期的年(year 必须是4 位数)
getMonth() 返回日期的月(0 表示1 月,11 表示12 月)
getUTCMonth() 返回UTC 日期的月(0 表示1 月,11 表示12 月)
setMonth(month) 设置日期的月(month 为大于0 的数值,大于11 加年)
setUTCMonth(month) 设置UTC 日期的月(month 为大于0 的数值,大于11 加年)
getDate() 返回日期中的日(1~31)
getUTCDate() 返回UTC 日期中的日(1~31)
setDate(date) 设置日期中的日(如果date 大于该月天数,则加月)
setUTCDate(date) 设置UTC 日期中的日(如果date 大于该月天数,则加月)
getDay() 返回日期中表示周几的数值(0 表示周日,6 表示周六)
getUTCDay() 返回UTC 日期中表示周几的数值(0 表示周日,6 表示周六)
getHours() 返回日期中的时(0~23)
getUTCHours() 返回UTC 日期中的时(0~23)
setHours(hours) 设置日期中的时(如果hours 大于23,则加日)
setUTCHours(hours) 设置UTC 日期中的时(如果hours 大于23,则加日)
getMinutes() 返回日期中的分(0~59)
getUTCMinutes() 返回UTC 日期中的分(0~59)
setMinutes(minutes) 设置日期中的分(如果minutes 大于59,则加时)
setUTCMinutes(minutes) 设置UTC 日期中的分(如果minutes 大于59,则加时)
getSeconds() 返回日期中的秒(0~59)
getUTCSeconds() 返回UTC 日期中的秒(0~59)
setSeconds(seconds) 设置日期中的秒(如果seconds 大于59,则加分)
setUTCSeconds(seconds) 设置UTC 日期中的秒(如果seconds 大于59,则加分)
getMilliseconds() 返回日期中的毫秒
getUTCMilliseconds() 返回UTC 日期中的毫秒
setMilliseconds(milliseconds) 设置日期中的毫秒
setUTCMilliseconds(milliseconds) 设置UTC 日期中的毫秒
getTimezoneOffset() 返回以分钟计的UTC 与本地时区的偏移量(如美国EST 即“东部标准时间”
返回300,进入夏令时的地区可能有所差异)
5.2 RegExp
ECMAScript 通过RegExp 类型支持正则表达式。正则表达式使用类似Perl 的简洁语法来创建:
let expression = /pattern/flags;
这个正则表达式的pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、
分组、向前查找和反向引用。每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式
的行为。下面给出了表示匹配模式的标记。
 g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。
 i:不区分大小写,表示在查找匹配时忽略pattern 和字符串的大小写。
 m:多行模式,表示查找到一行文本末尾时会继续查找。
 y:粘附模式,表示只查找从lastIndex 开始及之后的字符串。
 u:Unicode 模式,启用Unicode 匹配。
 s:dotAll 模式,表示元字符.匹配任何字符(包括\n 或\r)。
使用不同模式和标记可以创建出各种正则表达式,比如:
与其他语言中的正则表达式类似,所有元字符在模式中也必须转义,包括:
元字符在正则表达式中都有一种或多种特殊功能,所以要匹配上面这些字符本身,就必须使用反斜
杠来转义。下面是几个例子:
这里的pattern1 匹配"bat"或"cat",不区分大小写。要直接匹配"[bc]at",左右中括号都必
须像pattern2 中那样使用反斜杠转义。在pattern3 中,点号表示"at"前面的任意字符都可以匹配。
如果想匹配".at",那么要像pattern4 中那样对点号进行转义。
前面例子中的正则表达式都是使用字面量形式定义的。正则表达式也可以使用RegExp 构造函数来
创建,它接收两个参数:模式字符串和(可选的)标记字符串。任何使用字面量定义的正则表达式也可
以通过构造函数来创建,比如:
这里的pattern1 和pattern2 是等效的正则表达式。注意,RegExp 构造函数的两个参数都是字
符串。因为RegExp 的模式参数是字符串,所以在某些情况下需要二次转义。所有元字符都必须二次转
义,包括转义字符序列,如\n(\转义后的字符串是\\,在正则表达式字符串中则要写成\\\\)。下表
展示了几个正则表达式的字面量形式,以及使用RegExp 构造函数创建时对应的模式字符串。
字面量模式 对应的字符串
/\[bc\]at/ "\\[bc\\]at"
/\.at/ "\\.at"
/name\/age/ "name\\/age"
/\d.\d{1,2}/ "\\d.\\d{1,2}"
/\w\\hello\\123/ "\\w\\\\hello\\\\123"
此外,使用RegExp 也可以基于已有的正则表达式实例,并可选择性地修改它们的标记:
5.2.1 RegExp 实例属性
每个RegExp 实例都有下列属性,提供有关模式的各方面信息。
 global:布尔值,表示是否设置了g 标记。
 ignoreCase:布尔值,表示是否设置了i 标记。
 unicode:布尔值,表示是否设置了u 标记。
 sticky:布尔值,表示是否设置了y 标记。
 lastIndex:整数,表示在源字符串中下一次搜索的开始位置,始终从0 开始。
 multiline:布尔值,表示是否设置了m 标记。
 dotAll:布尔值,表示是否设置了s 标记。
 source:正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的
斜杠。
 flags:正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回(没
有前后斜杠)。
通过这些属性可以全面了解正则表达式的信息,不过实际开发中用得并不多,因为模式声明中包含
这些信息。下面是一个例子:
注意,虽然第一个模式是通过字面量创建的,第二个模式是通过RegExp 构造函数创建的,但两个
模式的source 和flags 属性是相同的。source 和flags 属性返回的是规范化之后可以在字面量中
使用的形式。
5.2.2 RegExp 实例方法
RegExp 实例的主要方法是exec(),主要用于配合捕获组使用。这个方法只接收一个参数,即要应
用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回
null。返回的数组虽然是Array 的实例,但包含两个额外的属性:index 和input。index 是字符串
中匹配模式的起始位置,input 是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串,
其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。来看
下面的例子:
在这个例子中,模式包含两个捕获组:最内部的匹配项" and baby",以及外部的匹配项" and dad"
或" and dad and baby"。调用exec()后找到了一个匹配项。因为整个字符串匹配模式,所以matchs
数组的index 属性就是0。数组的第一个元素是匹配的整个字符串,第二个元素是匹配第一个捕获组的
字符串,第三个元素是匹配第二个捕获组的字符串。
如果模式设置了全局标记,则每次调用exec()方法会返回一个匹配的信息。如果没有设置全局标
记,则无论对同一个字符串调用多少次exec(),也只会返回第一个匹配的信息。
上面例子中的模式没有设置全局标记,因此调用exec()只返回第一个匹配项("cat")。lastIndex
在非全局模式下始终不变。
如果在这个模式上设置了g 标记,则每次调用exec()都会在字符串中向前搜索下一个匹配项,如
下面的例子所示:
这次模式设置了全局标记,因此每次调用exec()都会返回字符串中的下一个匹配项,直到搜索到
字符串末尾。注意模式的lastIndex 属性每次都会变化。在全局匹配模式下,每次调用exec()都会
更新lastIndex 值,以反映上次匹配的最后一个字符的索引。
如果模式设置了粘附标记y,则每次调用exec()就只会在lastIndex 的位置上寻找匹配项。粘附
标记覆盖全局标记。

正则表达式的另一个方法是test(),接收一个字符串参数。如果输入的文本与模式匹配,则参数
返回true,否则返回false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。
test()经常用在if 语句中:
在这个例子中,正则表达式用于测试特定的数值序列。如果输入的文本与模式匹配,则显示匹配成
功的消息。这个用法常用于验证用户输入,此时我们只在乎输入是否有效,不关心为什么无效。
无论正则表达式是怎么创建的,继承的方法toLocaleString()和toString()都返回正则表达
式的字面量表示。比如:
这里的模式是通过RegExp 构造函数创建的,但toLocaleString()和toString()返回的都是其
字面量的形式。
注意 正则表达式的valueOf()方法返回正则表达式本身。
5.2.3 RegExp 构造函数属性
RegExp 构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用
于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,
112 第5 章 基本引用类型
就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了
RegExp 构造函数的属性。
全 名 简 写 说 明
input $ _ 最后搜索的字符串(非标准特性)
lastMatch $& 最后匹配的文本
lastParen $+ 最后匹配的捕获组(非标准特性)
leftContext $` input 字符串中出现在lastMatch 前面的文本
rightContext $' input 字符串中出现在lastMatch 后面的文本
通过这些属性可以提取出与exec()和test()执行的操作相关的信息。来看下面的例子:
以上代码创建了一个模式,用于搜索任何后跟"hort"的字符,并把第一个字符放在了捕获组中。
不同属性包含的内容如下。
 input 属性中包含原始的字符串。
 leftConext 属性包含原始字符串中"short"之前的内容,rightContext 属性包含"short"
之后的内容。
 lastMatch 属性包含匹配整个正则表达式的上一个字符串,即"short"。
 lastParen 属性包含捕获组的上一次匹配,即"s"。
这些属性名也可以替换成简写形式,只不过要使用中括号语法来访问,如下面的例子所示,因为大
多数简写形式都不是合法的ECMAScript 标识符:
RegExp 还有其他几个构造函数属性,可以存储最多9 个捕获组的匹配项。这些属性通过RegExp.
$1~RegExp.$9 来访问,分别包含第1~9 个捕获组的匹配项。在调用exec()或test()时,这些属性
就会被填充,然后就可以像下面这样使用它们:
在这个例子中,模式包含两个捕获组。调用test()搜索字符串之后,因为找到了匹配项所以返回
true,而且可以打印出通过RegExp 构造函数的$1 和$2 属性取得的两个捕获组匹配的内容。
注意 RegExp 构造函数的所有属性都没有任何Web 标准出处,因此不要在生产环境中使
用它们。
5.2.4 模式局限
虽然ECMAScript 对正则表达式的支持有了长足的进步,但仍然缺少Perl 语言中的一些高级特性。
下列特性目前还没有得到ECMAScript 的支持(想要了解更多信息,可以参考Regular-Expressions.info
网站):
 \A 和\Z 锚(分别匹配字符串的开始和末尾)
 联合及交叉类
 原子组
 x(忽略空格)匹配模式
 条件式匹配
 正则表达式注释
虽然还有这些局限,但ECMAScript 的正则表达式已经非常强大,可以用于大多数模式匹配任务。
5.3 原始值包装类型
为了方便操作原始值,ECMAScript 提供了3 种特殊的引用类型:Boolean、Number 和String。
这些类型具有本章介绍的其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。每当用
到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的
各种方法。来看下面的例子:
let s1 = "some text";
let s2 = s1.substring(2);
在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在s1 上调用了substring()
方法,并把结果保存在s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上
这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当
第二行访问s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串
值的任何时候,后台都会执行以下3 步:
(1) 创建一个String 类型的实例;
(2) 调用实例上的特定方法;
(3) 销毁实例。
可以把这3 步想象成执行了如下3 行ECMAScript 代码:
这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上3 步也会在后台发生,只不过
使用的是Boolean 和Number 包装类型而已。
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过new 实例化引用类型后,得到
的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期
间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:
这里的第二行代码尝试给字符串s1 添加了一个color 属性。可是,第三行代码访问color 属性时,
它却不见了。原因就是第二行代码运行时会临时创建一个String 对象,而当第三行代码执行时,这个对
象已经被销毁了。实际上,第三行代码在这里创建了自己的String 对象,但这个对象没有color 属性。
可以显式地使用Boolean、Number 和String 构造函数创建原始值包装对象。不过应该在确实必
要时再这么做,否则容易让开发者疑惑,分不清它们到底是原始值还是引用值。在原始值包装类型的实
例上调用typeof 会返回"object",所有原始值包装对象都会转换为布尔值true。
另外,Object 构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实
例。比如:
如果传给Object 的是字符串,则会创建一个String 的实例。如果是数值,则会创建Number 的
实例。布尔值则会得到Boolean 的实例。
注意,使用new 调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:
在这个例子中,变量number 中保存的是一个值为25 的原始数值,而变量obj 中保存的是一个
Number 的实例。
虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。每个原始
值包装类型都有相应的一套方法来方便数据操作。
5.3.1 Boolean
Boolean 是对应布尔值的引用类型。要创建一个Boolean 对象,就使用Boolean 构造函数并传入
true 或false,如下例所示:
Boolean 的实例会重写valueOf()方法,返回一个原始值true 或false。toString()方法被调
用时也会被覆盖,返回字符串"true"或"false"。不过,Boolean 对象在ECMAScript 中用得很少。
不仅如此,它们还容易引起误会,尤其是在布尔表达式中使用Boolean 对象时,比如:
在这段代码中,我们创建一个值为false 的Boolean 对象。然后,在一个布尔表达式中通过&&操
作将这个对象与一个原始值true 组合起来。在布尔算术中,false && true 等于false。可是,这
个表达式是对falseObject 对象而不是对它表示的值(false)求值。前面刚刚说过,所有对象在布
尔表达式中都会自动转换为true,因此falseObject 在这个表达式里实际上表示一个true 值。那么
true && true 当然是true。
除此之外,原始值和引用值(Boolean 对象)还有几个区别。首先,typeof 操作符对原始值返回
"boolean",但对引用值返回"object"。同样,Boolean 对象是Boolean 类型的实例,在使用
instaceof 操作符时返回true,但对原始值则返回false,如下所示:
理解原始布尔值和Boolean 对象之间的区别非常重要,强烈建议永远不要使用后者。
5.3.2 Number
Number 是对应数值的引用类型。要创建一个Number 对象,就使用Number 构造函数并传入一个
数值,如下例所示:
与Boolean 类型一样,Number 类型重写了valueOf()、toLocaleString()和toString()方
法。valueOf()方法返回Number 对象表示的原始数值,另外两个方法返回数值字符串。toString()
方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:
除了继承的方法,Number 类型还提供了几个用于将数值格式化为字符串的方法。
toFixed()方法返回包含指定小数点位数的数值字符串,如:
这里的toFixed()方法接收了参数2,表示返回的数值字符串要包含两位小数。结果返回值为
"10.00",小数位填充了0。如果数值本身的小数位超过了参数指定的位数,则四舍五入到最接近的
小数位:
toFixed()自动舍入的特点可以用于处理货币。不过要注意的是,多个浮点数值的数学计算不一定
得到精确的结果。比如,0.1 + 0.2 = 0.30000000000000004。
注意 toFixed()方法可以表示有0~20 个小数位的数值。某些浏览器可能支持更大的范
围,但这是通常被支持的范围。
另一个用于格式化数值的方法是toExponential(),返回以科学记数法(也称为指数记数法)表
示的数值字符串。与toFixed()一样,toExponential()也接收一个参数,表示结果中小数的位数。
来看下面的例子:
这段代码的输出为"1.0e+1"。一般来说,这么小的数不用表示为科学记数法形式。如果想得到数
值最适当的形式,那么可以使用toPrecision()。
toPrecision()方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法
形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:
在这个例子中,首先要用1 位数字表示数值99,得到"1e+2",也就是100。因为99 不能只用1 位
数字来精确表示,所以这个方法就将它舍入为100,这样就可以只用1 位数字(及其科学记数法形式)
来表示了。用2 位数字表示99 得到"99",用3 位数字则是"99.0"。本质上,toPrecision()方法会
根据数值和精度来决定调用toFixed()还是toExponential()。为了以正确的小数位精确表示数值,
这3 个方法都会向上或向下舍入。
注意 toPrecision()方法可以表示带1~21 个小数位的数值。某些浏览器可能支持更大
的范围,但这是通常被支持的范围。
与Boolean 对象类似,Number 对象也为数值提供了重要能力。但是,考虑到两者存在同样的潜在
问题,因此并不建议直接实例化Number 对象。在处理原始数值和引用数值时,typeof 和instacnceof
操作符会返回不同的结果,如下所示:
原始数值在调用typeof 时始终返回"number",而Number 对象则返回"object"。类似地,Number
对象是Number 类型的实例,而原始数值不是。
isInteger()方法与安全整数
ES6 新增了Number.isInteger()方法,用于辨别一个数值是否保存为整数。有时候,小数位的0
可能会让人误以为数值是一个浮点值:
IEEE 754 数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个数值
范围从Number.MIN_SAFE_INTEGER(253 + 1)到Number.MAX_SAFE_INTEGER(253  1)。对超出这
个范围的数值,即使尝试保存为整数,IEEE 754 编码格式也意味着二进制值可能会表示一个完全不同的
数值。为了鉴别整数是否在这个范围内,可以使用Number.isSafeInteger()方法:
5.3.3 String
String 是对应字符串的引用类型。要创建一个String 对象,使用String 构造函数并传入一个
数值,如下例所示:
String 对象的方法可以在所有字符串原始值上调用。3 个继承的方法valueOf()、toLocaleString()
和toString()都返回对象的原始字符串值。
每个String 对象都有一个length 属性,表示字符串中字符的数量。来看下面的例子:
这个例子输出了字符串"hello world"中包含的字符数量:11。注意,即使字符串中包含双字节
字符(而不是单字节的ASCII 字符),也仍然会按单字符来计数。
String 类型提供了很多方法来解析和操作字符串。
1. JavaScript 字符
JavaScript 字符串由16 位码元(code unit)组成。对多数字符来说,每16 位码元对应一个字符。换
句话说,字符串的length 属性表示字符串包含多少16 位码元:
此外,charAt()方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方
法查找指定索引位置的16 位码元,并返回该码元对应的字符:
JavaScript 字符串使用了两种Unicode 编码混合的策略:UCS-2 和UTF-16。对于可以采用16 位编码
的字符(U+0000~U+FFFF),这两种编码实际上是一样的。
注意 要深入了解关于字符编码的内容,推荐Joel Spolsky 写的博客文章:“The Absolute
Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and
Character Sets (No Excuses!)”。
另一个有用的资源是Mathias Bynens 的博文:“JavaScript’s Internal Character Encoding:
UCS-2 or UTF-16?”。
使用charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索
引以整数指定。比如:
fromCharCode()方法用于根据给定的UTF-16 码元创建字符串中的字符。这个方法可以接受任意
多个数值,并返回将所有数值对应的字符拼接起来的字符串:
对于U+0000~U+FFFF 范围内的字符,length、charAt()、charCodeAt()和fromCharCode()
返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16 位表示的,而这几个方法
也都基于16 位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。
这个对应关系在扩展到Unicode 增补字符平面时就不成立了。问题很简单,即16 位只能唯一表示
65 536 个字符。这对于大多数语言字符集是足够了,在Unicode 中称为基本多语言平面(BMP)。为了
表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外16 位去选择一个增补平面。这种每个
字符使用两个16 位码元的策略称为代理对。
在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。比如,下面的例子中使用了一个笑
脸表情符号,也就是一个使用代理对编码的字符:
这些方法仍然将16 位码元当作一个字符,事实上索引2 和索引3 对应的码元应该被看成一个代理
对,只对应一个字符。fromCharCode()方法仍然返回正确的结果,因为它实际上是基于提供的二进制
表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个
Unicode 笑脸字符。
为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用codePointAt()来代替
charCodeAt()。跟使用charCodeAt()时类似,codePointAt()接收16 位码元的索引并返回该索引
位置上的码点(code point)。码点是Unicode 中一个字符的完整标识。比如,"c"的码点是0x0063,而
"☺"的码点是0x1F60A。码点可能是16 位,也可能是32 位,而codePointAt()方法可以从指定码元
位置识别完整的码点。
注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符
的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代
理对的码点:
与charCodeAt()有对应的codePointAt()一样,fromCharCode()也有一个对应的fromCodePoint()。
这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:
2. normalize()方法
某些Unicode 字符可以有多种编码方式。有的字符既可以通过一个BMP 字符表示,也可以通过一
个代理对表示。比如:
比较操作符不在乎字符看起来是什么样的,因此这3 个字符互不相等。
为解决这个问题,Unicode 提供了4 种规范化形式,可以将类似上面的字符规范化为一致的格式,无论
底层字符的代码是什么。这4 种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、
NFKD(Normalization Form KD)和NFKC(Normalization Form KC)。可以使用normalize()方法对字
符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:"NFD"、"NFC"、"NFKD"或"NFKC"。
注意 这4 种规范化形式的具体细节超出了本书范围,有兴趣的读者可以自行参考UAX
15#: Unicode Normalization Forms 中的1.2 节“Normalization Forms”。
通过比较字符串与其调用normalize()的返回值,就可以知道该字符串是否已经规范化了:
选择同一种规范化形式可以让比较操作符返回正确的结果:
3. 字符串操作方法
本节介绍几个操作字符串值的方法。首先是concat(),用于将一个或多个字符串拼接成一个新字符串。来看下面的例子:
在这个例子中,对stringValue 调用concat()方法的结果是得到"hello world",但
stringValue 的值保持不变。concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串,
如下所示:
这个修改后的例子将字符串"world"和"!"追加到了"hello "后面。虽然concat()方法可以拼接
字符串,但更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加
号更方便。
ECMAScript 提供了3 个从字符串中提取子字符串的方法:slice()、substr()和substring()。这
3 个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开
始的位置,第二个参数表示子字符串结束的位置。对slice()和substring()而言,第二个参数是提取结
束的位置(即该位置之前的字符会被提取出来)。对substr()而言,第二个参数表示返回的子字符串数量。
任何情况下,省略第二个参数都意味着提取到字符串末尾。与concat()方法一样,slice()、substr()
和substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。来看下面的例子:
在这个例子中,slice()、substr()和substring()是以相同方式被调用的,而且多数情况下返
回的值也相同。如果只传一个参数3,则所有方法都将返回"lo world",因为"hello"中"l"位置为3。
如果传入两个参数3 和7,则slice()和substring()返回"lo w"(因为"world"中"o"在位置7,
不包含),而substr()返回"lo worl",因为第二个参数对它而言表示返回的字符数。
当某个参数是负值时,这3 个方法的行为又有不同。比如,slice()方法将所有负值参数都当成字
符串长度加上负参数值。
而substr()方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为0。
substring()方法会将所有负参数值都转换为0。看下面的例子:
这个例子明确演示了3 个方法的差异。在给slice()和substr()传入负参数时,它们的返回结果
相同。这是因为-3 会被转换为8(长度加上负参数),实际上调用的是slice(8)和substr(8)。而
substring()方法返回整个字符串,因为-3 会转换为0。
在第二个参数是负值时,这3 个方法各不相同。slice()方法将第二个参数转换为7,实际上相当
于调用slice(3, 7),因此返回"lo w"。而substring()方法会将第二个参数转换为0,相当于调用
substring(3, 0),等价于substring(0, 3),这是因为这个方法会将较小的参数作为起点,将较
大的参数作为终点。对substr()来说,第二个参数会被转换为0,意味着返回的字符串包含零个字符,
因而会返回一个空字符串。
4. 字符串位置方法
有两个方法用于在字符串中定位子字符串:indexOf()和lastIndexOf()。这两个方法从字符
串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()方法
从字符串开头开始查找子字符串,而lastIndexOf()方法从字符串末尾开始查找子字符串。来看下面
的例子:
这里,字符串中第一个"o"的位置是4,即"hello"中的"o"。最后一个"o"的位置是7,即"world"
中的"o"。如果字符串中只有一个"o",则indexOf()和lastIndexOf()返回同一个位置。
这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,indexOf()会从这个
参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;lastIndexOf()则会从这个参数指
定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。下面看一个例子:
在传入第二个参数6 以后,结果跟前面的例子恰好相反。这一次,indexOf()返回7,因为它从位
置6(字符"w")开始向后搜索字符串,在位置7 找到了"o"。而lastIndexOf()返回4,因为它从位
置6 开始反向搜索至字符串开头,因此找到了"hello"中的"o"。像这样使用第二个参数并循环调用
indexOf()或lastIndexOf(),就可以在字符串中找到所有的目标子字符串,如下所示:
这个例子逐步增大开始搜索的位置,通过indexOf()遍历了整个字符串。首先取得第一个"e"的位
置,然后进入循环,将上一次的位置加1 再传给indexOf(),确保搜索到最后一个子字符串实例之后。
每个位置都保存在positions 数组中,可供以后使用。
5. 字符串包含方法
ECMAScript 6 增加了3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()、
endsWith()和includes()。这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含
的布尔值。它们的区别在于,startsWith()检查开始于索引0 的匹配项,endsWith()检查开始于索
引(string.length - substring.length)的匹配项,而includes()检查整个字符串:
startsWith()和includes()方法接收可选的第二个参数,表示开始搜索的位置。如果传入第二
个参数,则意味着这两个方法会从指定位置向着字符串末尾搜索,忽略该位置之前的所有字符。下面是
一个例子:
endsWith()方法接收可选的第二个参数,表示应该当作字符串末尾的位置。如果不提供这个参数,
那么默认就是字符串长度。如果提供这个参数,那么就好像字符串只有那么多字符一样:
6. trim()方法
ECMAScript 在所有字符串上都提供了trim()方法。这个方法会创建字符串的一个副本,删除前、
后所有空格符,再返回结果。比如:
由于trim()返回的是字符串的副本,因此原始字符串不受影响,即原本的前、后空格符都会保留。
另外,trimLeft()和trimRight()方法分别用于从字符串开始和末尾清理空格符。
7. repeat()方法
ECMAScript 在所有字符串上都提供了repeat()方法。这个方法接收一个整数参数,表示要将字
符串复制多少次,然后返回拼接所有副本后的结果。
8. padStart()和padEnd()方法
padStart()和padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至
满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格
(U+0020)。
可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配
指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。
9. 字符串迭代与解构
字符串的原型上暴露了一个@@iterator 方法,表示可以迭代字符串的每个字符。可以像下面这样
手动使用迭代器:
在for-of 循环中可以通过这个迭代器按序访问每个字符:
有了这个迭代器之后,字符串就可以通过解构操作符来解构了。比如,可以更方便地把字符串分割
为字符数组:
10. 字符串大小写转换
下一组方法涉及大小写转换,包括4 个方法:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。toLowerCase()和toUpperCase()方法是原来就有的方法,
与java.lang.String 中的方法同名。toLocaleLowerCase()和toLocaleUpperCase()方法旨在基于
特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语),
Unicode 大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。下面是几个例子:
这里,toLowerCase()和toLocaleLowerCase()都返回hello world,而toUpperCase()和
toLocaleUpperCase()都返回HELLO WORLD。通常,如果不知道代码涉及什么语言,则最好使用地
区特定的转换方法。
11. 字符串模式匹配方法
String 类型专门为在字符串中实现模式匹配设计了几个方法。第一个就是match()方法,这个方
法本质上跟RegExp 对象的exec()方法相同。match()方法接收一个参数,可以是一个正则表达式字
符串,也可以是一个RegExp 对象。来看下面的例子:
match()方法返回的数组与RegExp 对象的exec()方法返回的数组是一样的:第一个元素是与整
个模式匹配的字符串,其余元素则是与表达式中的捕获组匹配的字符串(如果有的话)。
另一个查找模式的字符串方法是search()。这个方法唯一的参数与match()方法一样:正则表达
式字符串或RegExp 对象。这个方法返回模式第一个匹配的位置索引,如果没找到则返回1。search()
始终从字符串开头向后匹配模式。看下面的例子:
这里,search(/at/)返回1,即"at"的第一个字符在字符串中的位置。
为简化子字符串替换操作,ECMAScript 提供了replace()方法。这个方法接收两个参数,第一个
参数可以是一个RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是
一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字
符串,第一个参数必须为正则表达式并且带全局标记,如下面的例子所示:
在这个例子中,字符串"at"先传给replace()函数,而替换文本是"ond"。结果是"cat"被修改为"cond",而字符串的剩余部分保持不变。通过将第一个参数改为带全局标记的正则表达式,字符串
中的所有"at"都被替换成了"ond"。
第二个参数是字符串的情况下,有几个特殊的字符序列,可以用来插入正则表达式操作的值。
ECMA-262 中规定了下表中的值。
字符序列 替换文本
$$ $
$& 匹配整个模式的子字符串。与RegExp.lastMatch 相同
$' 匹配的子字符串之前的字符串。与RegExp.rightContext 相同
$` 匹配的子字符串之后的字符串。与RegExp.leftContext 相同
$n 匹配第n 个捕获组的字符串,其中n 是0~9。比如,$1 是匹配第一个捕获组的字符串,$2 是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串
$nn 匹配第nn 个捕获组字符串,其中nn 是01~99。比如,$01 是匹配第一个捕获组的字符串,$02 是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串
使用这些特殊的序列,可以在替换文本中使用之前匹配的内容,如下面的例子所示:
这里,每个以"at"结尾的词都会被替换成"word"后跟一对小括号,其中包含捕获组匹配的内容$1。
replace()的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到3 个参数:与整
个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串。在有多个捕获组的情况下,每
个匹配捕获组的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和
原始字符串。这个函数应该返回一个字符串,表示应该把匹配项替换成什么。使用函数作为第二个参数
可以更细致地控制替换过程,如下所示:
这里,函数htmlEscape()用于将一段HTML 中的4 个字符替换成对应的实体:小于号、大于号、
和号,还有双引号(都必须经过转义)。实现这个任务最简单的办法就是用一个正则表达式查找这些字
符,然后定义一个函数,根据匹配的每个字符分别返回特定的HTML 实体。
最后一个与模式匹配相关的字符串方法是split()。这个方法会根据传入的分隔符将字符串拆分成
数组。作为分隔符的参数可以是字符串,也可以是RegExp 对象。(字符串分隔符不会被这个方法当成
正则表达式。)还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。来看下面的
例子:
在这里,字符串colorText 是一个逗号分隔的颜色名称符串。调用split(",")会得到包含这些
颜色名的数组,基于逗号进行拆分。要把数组元素限制为2 个,传入第二个参数2 即可。最后,使用正
则表达式可以得到一个包含逗号的数组。注意在最后一次调用split()时,返回的数组前后包含两个空
字符串。这是因为正则表达式指定的分隔符出现在了字符串开头("red")和末尾("yellow")。
12. localeCompare()方法
最后一个方法是localeCompare(),这个方法比较两个字符串,返回如下3 个值中的一个。
 如果按照字母表顺序,字符串应该排在字符串参数前头,则返回负值。(通常是-1,具体还要看
与实际值相关的实现。)
 如果字符串与字符串参数相等,则返回0。
 如果按照字母表顺序,字符串应该排在字符串参数后头,则返回正值。(通常是1,具体还要看
与实际值相关的实现。)
下面是一个例子:
在这里,字符串"yellow"与3 个不同的值进行了比较:"brick"、"yellow"和"zoo"。"brick"
按字母表顺序应该排在"yellow"前头,因此localeCompare()返回1。"yellow"等于"yellow",因
此"localeCompare()"返回0。最后,"zoo"在"yellow"后面,因此localeCompare()返回-1。强调
一下,因为返回的具体值可能因具体实现而异,所以最好像下面的示例中一样使用localeCompare():
这样一来,就可以保证在所有实现中都能正确判断字符串的顺序了。
localeCompare()的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字
符串。在美国,英语是ECMAScript 实现的标准语言,localeCompare()区分大小写,大写字母排在小
写字母前面。但其他地区未必是这种情况。
13. HTML 方法
早期的浏览器开发商认为使用JavaScript 动态生成HTML 标签是一个需求。因此,早期浏览器扩展
了规范,增加了辅助生成HTML 标签的方法。下表总结了这些HTML 方法。不过,这些方法基本上已
经没有人使用了,因为结果通常不是语义化的标记。
方 法 输 出
anchor(name) <a name="name">string</a>
big() <big>string</big>
bold() <b>string</b>
fixed() <tt>string</tt>
fontcolor(color) <font color="color">string</font>
fontsize(size) <font size="size">string</font>
italics() <i>string</i>
link(url) <a href="url">string</a>
small() <small>string</small>
strike() <strike>string</strike>
sub() <sub>string</sub>
sup() <sup>string</sup>
5.4 单例内置对象
ECMA-262 对内置对象的定义是“任何由ECMAScript 实现提供、与宿主环境无关,并在ECMAScript
程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例
化好了。前面我们已经接触了大部分内置对象,包括Object、Array 和String。本节介绍ECMA-262
定义的另外两个单例内置对象:Global 和Math。

 


5.4.1 Global
Global 对象是ECMAScript 中最特别的对象,因为代码不会显式地访问它。ECMA-262 规定Global
对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函
数这种东西。在全局作用域中定义的变量和函数都会变成Global 对象的属性 。本书前面介绍的函数,
包括isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是Global 对象的方法。除
了这些,Global 对象上还有另外一些方法。
1. URL 编码方法
encodeURI()和encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器。
有效的URI 不能包含某些字符,比如空格。使用URI 编码方法来编码URI 可以让浏览器能够理解它们,
同时又以特殊的UTF-8 编码替换掉所有无效字符。
ecnodeURI()方法用于对整个URI 进行编码,比如"www.wrox.com/illegal value.js"。而
encodeURIComponent()方法用于编码URI 中单独的组件,比如前面URL 中的"illegal value.js"。
这两个方法的主要区别是,encodeURI()不会编码属于URL 组件的特殊字符,比如冒号、斜杠、问号、
井号,而encodeURIComponent()会编码它发现的所有非标准字符。来看下面的例子:
这里使用encodeURI()编码后,除空格被替换为%20 之外,没有任何变化。而encodeURIComponent()
方法将所有非字母字符都替换成了相应的编码形式。这就是使用encodeURI()编码整个
URI,但只使用encodeURIComponent()编码那些会追加到已有URI 后面的字符串的原因。
注意 一般来说,使用encodeURIComponent()应该比使用encodeURI()的频率更高,
这是因为编码查询字符串参数比编码基准URI 的次数更多。
与encodeURI()和encodeURIComponent()相对的是decodeURI()和decodeURIComponent()。
decodeURI()只对使用encodeURI()编码过的字符解码。例如,%20 会被替换为空格,但%23 不会被
替换为井号(#),因为井号不是由encodeURI()替换的。类似地,decodeURIComponent()解码所有
被encodeURIComponent()编码的字符,基本上就是解码所有特殊值。来看下面的例子:
这里,uri 变量中包含一个使用encodeURIComponent()编码过的字符串。首先输出的是使用
decodeURI()解码的结果,可以看到只用空格替换了%20。然后是使用decodeURIComponent()解码的
结果,其中替换了所有特殊字符,并输出了没有包含任何转义的字符串。(这个字符串不是有效的URL。)
注意 URI 方法encodeURI()、encodeURIComponent()、decodeURI()和decodeURIComponent()
取代了escape()和unescape()方法,后者在ECMA-262 第3 版中就已经
废弃了。URI 方法始终是首选方法,因为它们对所有Unicode 字符进行编码,而原来的方
法只能正确编码ASCII 字符。不要在生产环境中使用escape()和unescape()。
2. eval()方法
最后一个方法可能是整个ECMAScript 语言中最强大的了,它就是eval()。这个方法就是一个完
整的ECMAScript 解释器,它接收一个参数,即一个要执行的ECMAScript(JavaScript)字符串。来看
一个例子:
eval("console.log('hi')");
上面这行代码的功能与下一行等价:
console.log("hi");
当解释器发现eval()调用时,会将参数解释为实际的ECMAScript 语句,然后将其插入到该位置。
通过eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意
味着定义在包含上下文中的变量可以在eval()调用内部被引用,比如下面这个例子:
这里,变量msg 是在eval()调用的外部上下文中定义的,而console.log()显示了文本"hello
world"。这是因为第二行代码会被替换成一行真正的函数调用代码。类似地,可以在eval()内部定义
一个函数或变量,然后在外部代码中引用,如下所示:
这里,函数sayHi()是在eval()内部定义的。因为该调用会被替换为真正的函数定义,所以才可
能在下一行代码中调用sayHi()。对于变量也是一样的:
通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在
一个字符串中的。它们只是在eval()执行的时候才会被创建。
在严格模式下,在eval()内部创建的变量和函数无法被外部访问。换句话说,最后两个例子会报
错。同样,在严格模式下,赋值给eval 也会导致错误:
注意 解释代码字符串的能力是非常强大的,但也非常危险。在使用eval()的时候必须
极为慎重,特别是在解释用户输入的内容时。因为这个方法会对XSS 利用暴露出很大的
攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。
3. Global 对象属性
Global 对象有很多属性,其中一些前面已经提到过了。像undefined、NaN 和Infinity 等特殊
值都是Global 对象的属性。此外,所有原生引用类型构造函数,比如Object 和Function,也都是
Global 对象的属性。下表列出了所有这些属性。
属 性 说 明
undefined 特殊值undefined
NaN 特殊值NaN
Infinity 特殊值Infinity
Object Object 的构造函数
Array Array 的构造函数
Function Function 的构造函数
Boolean Boolean 的构造函数
String String 的构造函数
Number Number 的构造函数
Date Date 的构造函数
RegExp RegExp 的构造函数
Symbol Symbol 的伪构造函数
Error Error 的构造函数
EvalError EvalError 的构造函数
RangeError RangeError 的构造函数
ReferenceError ReferenceError 的构造函数
SyntaxError SyntaxError 的构造函数
TypeError TypeError 的构造函数
URIError URIError 的构造函数
4. window 对象
虽然ECMA-262 没有规定直接访问Global 对象的方式,但浏览器将window 对象实现为Global
对象的代理。因此,所有全局作用域中声明的变量和函数都变成了window 的属性。来看下面的例子:
这里定义了一个名为color 的全局变量和一个名为sayColor()的全局函数。在sayColor()内部,
通过window.color 访问了color 变量,说明全局变量变成了window 的属性。接着,又通过window
对象直接调用了window.sayColor()函数,从而输出字符串。
注意 window 对象在JavaScript 中远不止实现了ECMAScript 的Global 对象那么简单。
关于window 对象的更多介绍,请参考第12 章。
另一种获取Global 对象的方式是使用如下的代码:
这段代码创建一个立即调用的函数表达式,返回了this 的值。如前所述,当一个函数在没有明确
(通过成为某个对象的方法,或者通过call()/apply())指定this 值的情况下执行时,this 值等于
Global 对象。因此,调用一个简单返回this 的函数是在任何执行上下文中获取Global 对象的通用
方式。


5.4.2 Math


ECMAScript 提供了Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助
计算的属性和方法。
注意 Math 对象上提供的计算要比直接在JavaScript 实现的快得多,因为Math 对象上的
计算使用了JavaScript 引擎中更高效的实现和处理器指令。但使用Math 计算的问题是精
度会因浏览器、操作系统、指令集和硬件而异。
1. Math 对象属性
Math 对象有一些属性,主要用于保存数学中的一些特殊值。下表列出了这些属性。
属 性 说 明
Math.E 自然对数的基数e 的值
Math.LN10 10为底的自然对数
Math.LN2 2为底的自然对数
Math.LOG2E 以2 为底e 的对数
Math.LOG10E 以10 为底e 的对数
Math.PI π 的值
Math.SQRT1_2 1/2的平方根
Math.SQRT2 2的平方根
这些值的含义和用法超出了本书的范畴,但都是ECMAScript 规范定义的,并可以在你需要时使用。
2. min()和max()方法
Math 对象也提供了很多辅助执行简单或复杂数学计算的方法。
min()和max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数,如
下面的例子所示:
在3、54、32 和16 中,Math.max()返回54,Math.min()返回3。使用这两个方法可以避免使用
额外的循环和if 语句来确定一组数值的最大最小值。
要知道数组中的最大值和最小值,可以像下面这样使用扩展操作符:
3. 舍入方法
接下来是用于把小数值舍入为整数的4 个方法:Math.ceil()、Math.floor()、Math.round()
和Math.fround()。这几个方法处理舍入的方式如下所述。
 Math.ceil()方法始终向上舍入为最接近的整数。
Math.floor()方法始终向下舍入为最接近的整数。
 Math.round()方法执行四舍五入。
 Math.fround()方法返回数值最接近的单精度(32 位)浮点值表示。
以下示例展示了这些方法的用法:
对于25 和26(不包含)之间的所有值,Math.ceil()都会返回26,因为它始终向上舍入。
Math.round()只在数值大于等于25.5 时返回26,否则返回25。最后,Math.floor()对所有25 和
26(不包含)之间的值都返回25。
4. random()方法
Math.random()方法返回一个0~1 范围内的随机数,其中包含0 但不包含1。对于希望显示随机名
言或随机新闻的网页,这个方法是非常方便的。可以基于如下公式使用Math.random()从一组整数中
随机选择一个数:
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)
这里使用了Math.floor()方法,因为Math.random()始终返回小数,即便乘以一个数再加上一
个数也是小数。因此,如果想从1~10 范围内随机选择一个数,代码就是这样的:
let num = Math.floor(Math.random() * 10 + 1);
这样就有10 个可能的值(1~10),其中最小的值是1。如果想选择一个2~10 范围内的值,则代码就
要写成这样:
2~10 只有9 个数,所以可选总数(total_number_of_choices)是9,而最小可能的值
(first_possible_value)是2。很多时候,通过函数来算出可选总数和最小可能的值可能更方便,
比如:
这里的函数selectFrom()接收两个参数:应该返回的最小值和最大值。通过将这两个值相减再加1 得到可选总数,然后再套用上面的公式。于是,调用selectFrom(2,10)就可以从2~10(包含)
范围内选择一个值了。使用这个函数,从一个数组中随机选择一个元素就很容易,比如:
在这个例子中,传给selecFrom()的第二个参数是数组长度减1,即数组最大的索引值。
注意 Math.random()方法在这里出于演示目的是没有问题的。如果是为了加密而需要
生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用window.crypto.
getRandomValues()。


5. 其他方法


Math 对象还有很多涉及各种简单或高阶数运算的方法。讨论每种方法的具体细节或者它们的适用
场景超出了本书的范畴。不过,下表还是总结了Math 对象的其他方法。
方 法 说 明
Math.abs(x) 返回x 的绝对值
Math.exp(x) 返回Math.E 的x 次幂
Math.expm1(x) 等于Math.exp(x) - 1
Math.log(x) 返回x 的自然对数
Math.log1p(x) 等于1 + Math.log(x)
Math.pow(x, power) 返回x 的power 次幂
Math.hypot(...nums) 返回nums 中每个数平方和的平方根
Math.clz32(x) 返回32 位整数x 的前置零的数量
Math.sign(x) 返回表示x 符号的1、0、-0 或-1
Math.trunc(x) 返回x 的整数部分,删除所有小数
Math.sqrt(x) 返回x 的平方根
Math.cbrt(x) 返回x 的立方根
Math.acos(x) 返回x 的反余弦
Math.acosh(x) 返回x 的反双曲余弦
Math.asin(x) 返回x 的反正弦
Math.asinh(x) 返回x 的反双曲正弦
Math.atan(x) 返回x 的反正切
Math.atanh(x) 返回x 的反双曲正切
Math.atan2(y, x) 返回y/x 的反正切
Math.cos(x) 返回x 的余弦
Math.sin(x) 返回x 的正弦
Math.tan(x) 返回x 的正切
即便这些方法都是由ECMA-262 定义的,对正弦、余弦、正切等计算的实现仍然取决于浏览器,因
为计算这些值的方式有很多种。结果,这些方法的精度可能因实现而异。


5.5 小结


JavaScript 中的对象称为引用值,几种内置的引用类型可用于创建特定类型的对象。
 引用值与传统面向对象编程语言中的类相似,但实现不同。
 Date 类型提供关于日期和时间的信息,包括当前日期、时间及相关计算。
 RegExp 类型是ECMAScript 支持正则表达式的接口,提供了大多数基础的和部分高级的正则表
达式功能。
JavaScript 比较独特的一点是,函数实际上是Function 类型的实例,也就是说函数也是对象。因
为函数也是对象,所以函数也有方法,可以用于增强其能力。
由于原始值包装类型的存在,JavaScript 中的原始值可以被当成对象来使用。有3 种原始值包装类
型:Boolean、Number 和String。它们都具备如下特点。
 每种包装类型都映射到同名的原始类型。
 以读模式访问原始值时,后台会实例化一个原始值包装类型的对象,借助这个对象可以操作相
应的数据。
 涉及原始值的语句执行完毕后,包装对象就会被销毁。
当代码开始执行时,全局上下文中会存在两个内置对象:Global 和Math。其中,Global 对象在
大多数ECMAScript 实现中无法直接访问。不过,浏览器将其实现为window 对象。所有全局变量和函
数都是Global 对象的属性。Math 对象包含辅助完成复杂计算的属性和方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值