第 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,以此类推)、日(123)、分、秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为1日。其他参数的默认值都是0。下面是使用Date.UTC()
的两个例子:
// GMT时间2000年1月1日零点
let y2k = new Date(Date.UTC(2000, 0));
// GMT时间2005年5月5日下午5点55分55秒
let allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));
这个例子创建了两个日期 。第一个日期是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()
接收的参数是一样的。因此,如果第一个参数是数值,则构造函数假设它是日期中的年,第二个参数就是月,以此类推。前面的例子也可以这样来写:
// 本地时间2000年1月1日零点
let y2k = new Date(2000, 0);
// 本地时间2005年5月5日下午5点55分55秒
let allFives = new Date(2005, 4, 5, 17, 55, 55);
以上代码创建了与前面例子中相同的两个日期,但这次的两个日期是(由于系统设置决定的)本地时区的日期。
ECMAScript还提供了Date.now()
方法,返回表示方法执行时日期和时间的毫秒数。这个方法可以方便地用在代码分析中:
// 起始时间
let start = Date.now();
// 调用函数
doSomething();
// 结束时间
let stop = Date.now(),
result = stop - start;
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() - 2/1/2019 12:00:00 AM
toString() - Thu Feb 1 2019 00:00:00 GMT-0800 (Pacific Standard Time)
现代浏览器在这两个方法的输出上已经趋于一致。在比较老的浏览器上,每个方法返回的结果可能在每个浏览器上都是不同的。这些差异意味着toLocaleString()
和toString()
可能只对调试有用,不能用于显示。
Date
类型的valueOf()
方法根本就不返回字符串,这个方法被重写后返回的是日期的毫秒表示。因此,操作符(如小于号和大于号)可以直接使用它返回的值。比如下面的例子:
let date1 = new Date(2019, 0, 1); // 2019年1月1日
let date2 = new Date(2019, 1, 1); // 2019年2月1日
console.log(date1 < date2); // true
console.log(date1 > date2); // false
日期2019年1月1日在2019年2月1日之前,所以说前者小于后者没问题。因为2019年1月1日的毫秒表示小于2019年2月1日的毫秒表示,所以用小于号比较这两个日期时会返回true
。这也是确保日期先后的一个简单方式。
5.1.2 日期格式化方法
Date
类型有几个专门用于格式化日期的方法,它们都会返回字符串:
toDateString()
显示日期中的周几、月、日、年(格式特定于实现);toTimeString()
显示日期中的时、分、秒和时区(格式特定于实现);toLocaleDateString()
显示日期中的周几、月、日、年(格式特定于实现和地区);toLocaleTimeString()
显示日期中的时、分、秒(格式特定于实现);toUTCString()
显示完整的UTC日期(格式特定于实现)。
这些方法的输出与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
)。
使用不同模式和标记可以创建出各种正则表达式,比如:
// 匹配字符串中的所有"at"
let pattern1 = /at/g;
// 匹配第一个"bat"或"cat",忽略大小写
let pattern2 = /[bc]at/i;
// 匹配所有以"at"结尾的三字符组合,忽略大小写
let pattern3 = /.at/gi;
与其他语言中的正则表达式类似,所有元字符在模式中也必须转义,包括:
( [ { \ ^ $ | ) ] } ? * + .
元字符在正则表达式中都有一种或多种特殊功能,所以要匹配上面这些字符本身,就必须使用反斜杠来转义。下面是几个例子:
// 匹配第一个"bat"或"cat",忽略大小写
let pattern1 = /[bc]at/i;
// 匹配第一个"[bc]at",忽略大小写
let pattern2 = /\[bc\]at/i;
// 匹配所有以"at"结尾的三字符组合,忽略大小写
let pattern3 = /.at/gi;
// 匹配所有".at",忽略大小写
let pattern4 = /\.at/gi;
这里的pattern1
匹配"bat"
或"cat"
,不区分大小写。要直接匹配"[bc]at"
,左右中括号都必须像pattern2
中那样使用反斜杠转义。在pattern3
中,点号表示"at"
前面的任意字符都可以匹配。如果想匹配".at"
,那么要像pattern4
中那样对点号进行转义。
前面例子中的正则表达式都是使用字面量形式定义的。正则表达式也可以使用RegExp
构造函数来创建,它接收两个参数:模式字符串和(可选的)标记字符串。任何使用字面量定义的正则表达式也可以通过构造函数来创建,比如:
// 匹配第一个"bat"或"cat",忽略大小写
let pattern1 = /[bc]at/i;
// 跟pattern1一样,只不过是用构造函数创建的
let pattern2 = new RegExp("[bc]at", "i");
这里的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
也可以基于已有的正则表达式实例,并可选择性地修改它们的标记:
const re1 = /cat/g;
console.log(re1); // "/cat/g"
const re2 = new RegExp(re1);
console.log(re2); // "/cat/g"
const re3 = new RegExp(re1, "i");
console.log(re3); // "/cat/i"
5.2.1 RegExp
实例属性
每个RegExp
实例都有下列属性,提供有关模式的各方面信息。
global
:布尔值,表示是否设置了g
标记。ignoreCase
:布尔值,表示是否设置了i
标记。unicode
:布尔值,表示是否设置了u
标记。sticky
:布尔值,表示是否设置了y
标记。lastIndex
:整数,表示在源字符串中下一次搜索的开始位置,始终从0开始。multiline
:布尔值,表示是否设置了m
标记。dotAll
:布尔值,表示是否设置了s
标记。source
:正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的斜杠。flags
:正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回(没有前后斜杠)。
通过这些属性可以全面了解正则表达式的信息,不过实际开发中用得并不多,因为模式声明中包含这些信息。下面是一个例子:
let pattern1 = /\[bc\]at/i;
console.log(pattern1.global); // false
console.log(pattern1.ignoreCase); // true
console.log(pattern1.multiline); // false
console.log(pattern1.lastIndex); // 0
console.log(pattern1.source); // "\[bc\]at"
console.log(pattern1.flags); // "i"
let pattern2 = new RegExp("\\[bc\\]at", "i");
console.log(pattern2.global); // false
console.log(pattern2.ignoreCase); // true
console.log(pattern2.multiline); // false
console.log(pattern2.lastIndex); // 0
console.log(pattern2.source); // "\[bc\]at"
console.log(pattern2.flags); // "i"
注意,虽然第一个模式是通过字面量创建的,第二个模式是通过RegExp
构造函数创建的,但两个模式的source
和flags
属性是相同的。source
和flags
属性返回的是规范化之后可以在字面量中使用的形式。
5.2.2 RegExp
实例方法
RegExp
实例的主要方法是exec()
,主要用于配合捕获组使用。这个方法只接收一个参数,即要应用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回null
。返回的数组虽然是Array
的实例,但包含两个额外的属性:index
和input
。index
是字符串中匹配模式的起始位置,input
是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串,其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。来看下面的例子:
let text = "mom and dad and baby";
let pattern = /mom( and dad( and baby)?)?/gi;
let matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches.input); // "mom and dad and baby"
console.log(matches[0]); // "mom and dad and baby"
console.log(matches[1]); // " and dad and baby"
console.log(matches[2]); // " and baby"
在这个例子中,模式包含两个捕获组:最内部的匹配项" and baby"
,以及外部的匹配项" and dad"
或" and dad and baby"
。调用exec()
后找到了一个匹配项。因为整个字符串匹配模式,所以matchs
数组的index
属性就是0。数组的第一个元素是匹配的整个字符串,第二个元素是匹配第一个捕获组的字符串,第三个元素是匹配第二个捕获组的字符串。
如果模式设置了全局标记,则每次调用exec()
方法会返回一个匹配的信息。如果没有设置全局标记,则无论对同一个字符串调用多少次exec()
,也只会返回第一个匹配的信息。
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern.lastIndex); // 0
matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern.lastIndex); // 0
上面例子中的模式没有设置全局标记,因此调用exec()
只返回第一个匹配项("cat"
)。lastIndex
在非全局模式下始终不变。
如果在这个模式上设置了g
标记,则每次调用exec()
都会在字符串中向前搜索下一个匹配项,如下面的例子所示:
let text = "cat, bat, sat, fat";
let pattern = /.at/g;
let matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern.lastIndex); // 3
matches = pattern.exec(text);
console.log(matches.index); // 5
console.log(matches[0]); // bat
console.log(pattern.lastIndex); // 8
matches = pattern.exec(text);
console.log(matches.index); // 10
console.log(matches[0]); // sat
console.log(pattern.lastIndex); // 13
这次模式设置了全局标记,因此每次调用exec()
都会返回字符串中的下一个匹配项,直到搜索到字符串末尾。注意模式的lastIndex
属性每次都会变化。在全局匹配模式下,每次调用exec()
都会更新lastIndex
值,以反映上次匹配的最后一个字符的索引。
如果模式设置了粘附标记y
,则每次调用exec()
就只会在lastIndex
的位置上寻找匹配项。粘附标记覆盖全局标记。
let text = "cat, bat, sat, fat";
let pattern = /.at/y;
let matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern.lastIndex); // 3
// 以索引3对应的字符开头找不到匹配项,因此exec()返回null
// exec()没找到匹配项,于是将lastIndex设置为0
matches = pattern.exec(text);
console.log(matches); // null
console.log(pattern.lastIndex); // 0
// 向前设置lastIndex可以让粘附的模式通过exec()找到下一个匹配项:
pattern.lastIndex = 5;
matches = pattern.exec(text);
console.log(matches.index); // 5
console.log(matches[0]); // bat
console.log(pattern.lastIndex); // 8
正则表达式的另一个方法是test()
,接收一个字符串参数。如果输入的文本与模式匹配,则参数返回true
,否则返回false
。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。test()
经常用在if
语句中:
let text = "000-00-0000";
let pattern = /\d{3}-\d{2}-\d{4}/;
if (pattern.test(text)) {
console.log("The pattern was matched.");
}
在这个例子中,正则表达式用于测试特定的数值序列。如果输入的文本与模式匹配,则显示匹配成功的消息。这个用法常用于验证用户输入,此时我们只在乎输入是否有效,不关心为什么无效。
无论正则表达式是怎么创建的,继承的方法toLocaleString()
和toString()
都返回正则表达式的字面量表示。比如:
let pattern = new RegExp("\\[bc\\]at", "gi");
console.log(pattern.toString()); // /\[bc\]at/gi
console.log(pattern.toLocaleString()); // /\[bc\]at/gi
这里的模式是通过RegExp
构造函数创建的,但toLocaleString()
和toString()
返回的都是其字面量的形式。
注意 正则表达式的
valueOf()
方法返回正则表达式本身。
5.2.3 RegExp
构造函数属性
RegExp
构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了RegExp
构造函数的属性。
全名 | 简写 | 说明 | |
---|---|---|---|
input |
$_ |
最后搜索的字符串 | |
lastMatch |
$& |
最后匹配的文本 | |
lastParen |
$+ |
最后匹配的捕获组 | |
leftContext |
`$`` | input 字符串中出现在lastMatch 前面的文本 |
|
rightContext |
$' |
input 字符串中出现在lastMatch 后面的文本 |
通过这些属性可以提取出与exec()
和test()
执行的操作相关的信息。来看下面的例子:
let text = "this has been a short summer";
let pattern = /(.)hort/g;
if (pattern.test(text)) {
console.log(RegExp.input); // this has been a short summer
console.log(RegExp.leftContext); // this has been a
console.log(RegExp.rightContext); // summer
console.log(RegExp.lastMatch); // short
console.log(RegExp.lastParen); // s
}
以上代码创建了一个模式,用于搜索任何后跟"hort"
的字符,并把第一个字符放在了捕获组中。不同属性包含的内容如下。
input
属性中包含原始的字符串。leftConext
属性包含原始字符串中"short"
之前的内容,rightContext
属性包含"short"
之后的内容。lastMatch
属性包含匹配整个正则表达式的上一个字符串,即"short"
。lastParen
属性包含捕获组的上一次匹配,即"s"
。
这些属性名也可以替换成简写形式,只不过要使用中括号语法来访问,如下面的例子所示,因为大多数简写形式都不是合法的ECMAScript标识符:
let text = "this has been a short summer";
let pattern = /(.)hort/g;
/*
* 注意:Opera不支持简写属性名
* IE不支持多行匹配
*/
if (pattern.test(text)) {
console.log(RegExp.$_); // this has been a short summer
console.log(RegExp["$`"]); // this has been a
console.log(RegExp["$'"]); // summer
console.log(RegExp["$&"]); // short
console.log(RegExp["$+"]); // s
console.log(RegExp["$*"]); // false
}
RegExp
还有其他几个构造函数属性,可以存储最多9个捕获组的匹配项。这些属性通过RegExp.$1~RegExp.$9
来访问,分别包含第1~9个捕获组的匹配项。在调用exec()
或test()
时,这些属性就会被填充,然后就可以像下面这样使用它们:
let text = "this has been a short summer";
let pattern = /(..)or(.)/g;
if (pattern.test(text)) {
console.log(RegExp.$1); // sh
console.log(RegExp.$2); // t
}
在这个例子中,模式包含两个捕获组。调用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代码:
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上3步也会在后台发生,只不过使用的是Boolean
和Number
包装类型而已。
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过new
实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined
这里的第二行代码尝试给字符串s1
添加了一个color
属性。可是,第三行代码访问color
属性时,它却不见了。原因就是第二行代码运行时会临时创建一个String
对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的String
对象,但这个对象没有color
属性。
可以显式地使用Boolean
、Number
和String
构造函数创建原始值包装对象。不过应该在确实必要时再这么做,否则容易让开发者疑惑,分不清它们到底是原始值还是引用值。在原始值包装类型的实例上调用typeof
会返回"object"
,所有原始值包装对象都会转换为布尔值true
。
另外,Object
构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。比如:
let obj = new Object("some text");
console.log(obj instanceof String); // true
如果传给Object
的是字符串,则会创建一个String
的实例。如果是数值,则会创建Number
的实例。布尔值则会得到Boolean
的实例。
注意,使用new
调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:
let value = "25";
let number = Number(value); // 转型函数
console.log(typeof number); // "number"
let obj = new Number(value); // 构造函数
console.log(typeof obj); // "object"
在这个例子中,变量number
中保存的是一个值为25的原始数值,而变量obj
中保存的是一个Number
的实例。
虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。每个原始值包装类型都有相应的一套方法来方便数据操作。
5.3.1 Boolean
Boolean
是对应布尔值的引用类型。要创建一个Boolean
对象,就使用Boolean
构造函数并传入true
或false
,如下例所示:
let booleanObject = new Boolean(true);
Boolean
的实例会重写valueOf()
方法,返回一个原始值true
或false
。toString()
方法被调用时也会被覆盖,返回字符串"true"
或"false"
。不过,Boolean
对象在ECMAScript中用得很少。不仅如此,它们还容易引起误会,尤其是在布尔表达式中使用Boolean
对象时,比如:
let falseObject = new Boolean(false);
let result = falseObject && true;
console.log(result); // true
let falseValue = false;
result = falseValue && true;
console.log(result); // false
在这段代码中,我们创建一个值为false
的Boolean
对象。然后,在一个布尔表达式中通过&&
操作将这个对象与一个原始值true
组合起来。在布尔算术中,false && true
等于false
。可是,这个表达式是对falseObject
对象而不是对它表示的值(false
)求值。前面刚刚说过,所有对象在布尔表达式中都会自动转换为true
,因此falseObject
在这个表达式里实际上表示一个true
值。那么true && true
当然是true
。
除此之外,原始值和引用值(Boolean
对象)还有几个区别。首先,typeof
操作符对原始值返回"boolean"
,但对引用值返回"object"
。同样,Boolean
对象是Boolean
类型的实例,在使用instaceof
操作符时返回true
,但对原始值则返回false
,如下所示:
console.log(typeof falseObject); // object
console.log(typeof falseValue); // boolean
console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean); // false
理解原始布尔值和Boolean
对象之间的区别非常重要,强烈建议永远不要使用后者。
5.3.2 Number
Number
是对应数值的引用类型。要创建一个Number
对象,就使用Number
构造函数并传入一个数值,如下例所示:
let numberObject = new Number(10);
与Boolean
类型一样,Number
类型重写了valueOf()
、toLocaleString()
和toString()
方法。valueOf()
方法返回Number
对象表示的原始数值,另外两个方法返回数值字符串。toString()
方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
除了继承的方法,Number
类型还提供了几个用于将数值格式化为字符串的方法。
toFixed()
方法返回包含指定小数点位数的数值字符串,如:
let num = 10;
console.log(num.toFixed(2)); // "10.00"
这里的toFixed()
方法接收了参数2
,表示返回的数值字符串要包含两位小数。结果返回值为"10.00"
,小数位填充了0。如果数值本身的小数位超过了参数指定的位数,则四舍五入到最接近的小数位:
let num = 10.005;
console.log(num.toFixed(2)); // "10.01"
toFixed()
自动舍入的特点可以用于处理货币。不过要注意的是,多个浮点数值的数学计算不一定得到精确的结果。比如,0.1 + 0.2 = 0.30000000000000004
。
注意
toFixed()
方法可以表示有0~20个小数位的数值。某些浏览器可能支持更大的范围,但这是通常被支持的范围。
另一个用于格式化数值的方法是toExponential()
,返回以科学记数法(也称为指数记数法)表示的数值字符串。与toFixed()
一样,toExponential()
也接收一个参数,表示结果中小数的位数。来看下面的例子:
let num = 10;
console.log(num.toExponential(1)); // "1.0e+1"
这段代码的输出为"1.0e+1"
。一般来说,这么小的数不用表示为科学记数法形式。如果想得到数值最适当的形式,那么可以使用toPrecision()
。
toPrecision()
方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:
let num = 99;
console.log(num.toPrecision(1)); // "1e+2"
console.log(num.toPrecision(2)); // "99"
console.log(num.toPrecision(3)); // "99.0"
在这个例子中,首先要用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
操作符会返回不同的结果,如下所示:
let numberObject = new Number(10);
let numberValue = 10;
console.log(typeof numberObject); // "object"
console.log(typeof numberValue); // "number"
console.log(numberObject instanceof Number); // true
console.log(numberValue instanceof Number); // false
原始数值在调用typeof
时始终返回"number"
,而Number
对象则返回"object"
。类似地,Number
对象是Number
类型的实例,而原始数值不是。
isInteger()方法与安全整数
ES6新增了Number.isInteger()
方法,用于辨别一个数值是否保存为整数。有时候,小数位的0可能会让人误以为数值是一个浮点值:
console.log(Number.isInteger(1)); // true
console.log(Number.isInteger(1.00)); // true
console.log(Number.isInteger(1.01)); // false
IEEE 754数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个数值范围从Number.MIN_SAFE_INTEGER
()到Number.MAX_SAFE_INTEGER
()。对超出这个范围的数值,即使尝试保存为整数,IEEE 754编码格式也意味着二进制值可能会表示一个完全不同的数值。为了鉴别整数是否在这个范围内,可以使用Number.isSafeInteger()
方法:
console.log(Number.isSafeInteger(-1 * (2 ** 53))); // false
console.log(Number.isSafeInteger(-1 * (2 ** 53) + 1)); // true
console.log(Number.isSafeInteger(2 ** 53)); // false
console.log(Number.isSafeInteger((2 ** 53) - 1)); // true
5.3.3 String
String
是对应字符串的引用类型。要创建一个String
对象,使用String
构造函数并传入一个数值,如下例所示:
let stringObject = new String("hello world");
String
对象的方法可以在所有字符串原始值上调用。3个继承的方法valueOf()
、toLcaleString()
和toString()
都返回对象的原始字符串值。
每个String
对象都有一个length
属性,表示字符串中字符的数量。来看下面的例子:
let stringValue = "hello world";
console.log(stringValue.length); // "11"
这个例子输出了字符串"hello world"
中包含的字符数量:11
。注意,即使字符串中包含双字节字符(而不是单字节的ASCII字符),也仍然会按单字符来计数。
String
类型提供了很多方法来解析和操作字符串。
- JavaScript字符
JavaScript字符串由16位码元(code unit)组成。对多数字符来说,每16位码元对应一个字符。换句话说,字符串的length
属性表示字符串包含多少16位码元: ``` let message = “abcde”;
console.log(message.length); // 5
1. 此外,`charAt()`方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的16位码元,并返回该码元对应的字符:
let message = “abcde”;
console.log(message.charAt(2)); // “c”
1. 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?”。
1. 使用`charCodeAt()`方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。比如:
let message = “abcde”;
// Unicode “Latin small letter C”的编码是U+0063 console.log(message.charCodeAt(2)); // 99
// 十进制99等于十六进制63 console.log(99 === 0x63); // true
1. `fromCharCode()`方法用于根据给定的UTF-16码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:
// Unicode “Latin small letter A”的编码是U+0061 // Unicode “Latin small letter B”的编码是U+0062 // Unicode “Latin small letter C”的编码是U+0063 // Unicode “Latin small letter D”的编码是U+0064 // Unicode “Latin small letter E”的编码是U+0065
console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65)); // “abcde”
// 0x0061 === 97 // 0x0062 === 98 // 0x0063 === 99 // 0x0064 === 100 // 0x0065 === 101
console.log(String.fromCharCode(97, 98, 99, 100, 101)); // “abcde”
1. 对于U+0000~U+FFFF范围内的字符,`length`、`charAt()`、`charCodeAt()`和`fromCharCode()`返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16位表示的,而这几个方法也都基于16位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。<br />这个对应关系在扩展到Unicode增补字符平面时就不成立了。问题很简单,即16位只能唯一表示65 536个字符。这对于大多数语言字符集是足够了,在Unicode中称为**基本多语言平面**(BMP)。为了表示更多的字符,Unicode采用了一个策略,即每个字符使用另外16位去选择一个**增补平面**。这种每个字符使用两个16位码元的策略称为**代理对**。<br />在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。比如,下面的例子中使用了一个笑脸表情符号,也就是一个使用代理对编码的字符:
// “smiling face with smiling eyes” 表情符号的编码是U+1F60A // 0x1F60A === 128522 let message = “ab☺de”;
console.log(message.length); // 6 console.log(message.charAt(1)); // b console.log(message.charAt(2)); // <?> console.log(message.charAt(3)); // <?> console.log(message.charAt(4)); // d
console.log(message.charCodeAt(1)); // 98 console.log(message.charCodeAt(2)); // 55357 console.log(message.charCodeAt(3)); // 56842 console.log(message.charCodeAt(4)); // 100
console.log(String.fromCodePoint(0x1F60A)); // ☺
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab☺de
1. 这些方法仍然将16位码元当作一个字符,事实上索引2和索引3对应的码元应该被看成一个代理对,只对应一个字符。`fromCharCode()`方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个Unicode笑脸字符。<br />为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用`codePointAt()`来代替`charCodeAt()`。跟使用`charCodeAt()`时类似,`codePointAt()`接收16位码元的索引并返回该索引位置上的码点(code point)。**码点**是Unicode中一个字符的完整标识。比如,`"c"`的码点是0x0063,而"☺"的码点是0x1F60A。码点可能是16位,也可能是32位,而`codePointAt()`方法可以从指定码元位置识别完整的码点。
let message = “ab☺de”;
console.log(message.codePointAt(1)); // 98 console.log(message.codePointAt(2)); // 128522 console.log(message.codePointAt(3)); // 56842 console.log(message.codePointAt(4)); // 100
1. 注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点:
console.log([…”ab☺de”]); // [“a”, “b”, “☺”, “d”, “e”]
1. 与`charCodeAt()`有对应的`codePointAt()`一样,`fromCharCode()`也有一个对应的`fromCodePoint()`。这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab☺de console.log(String.fromCodePoint(97, 98, 128522, 100, 101)); // ab☺de
2. **normalize()方法**<br />某些Unicode字符可以有多种编码方式。有的字符既可以通过一个BMP字符表示,也可以通过一个代理对表示。比如:
// U+00C5:上面带圆圈的大写拉丁字母A console.log(String.fromCharCode(0x00C5)); // Å
// U+212B:长度单位“埃” console.log(String.fromCharCode(0x212B)); // Å
// U+004:大写拉丁字母A // U+030A:上面加个圆圈 console.log(String.fromCharCode(0x0041, 0x030A)); // Å
2. 比较操作符不在乎字符看起来是什么样的,因此这3个字符互不相等。
let a1 = String.fromCharCode(0x00C5), a2 = String.fromCharCode(0x212B), a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1, a2, a3); // Å, Å, Å
console.log(a1 === a2); // false console.log(a1 === a3); // false console.log(a2 === a3); // false
2. 为解决这个问题,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”。
2. 通过比较字符串与其调用`normalize()`的返回值,就可以知道该字符串是否已经规范化了:
let a1 = String.fromCharCode(0x00C5), a2 = String.fromCharCode(0x212B), a3 = String.fromCharCode(0x0041, 0x030A);
// U+00C5是对0+212B进行NFC/NFKC规范化之后的结果 console.log(a1 === a1.normalize(“NFD”)); // false console.log(a1 === a1.normalize(“NFC”)); // true console.log(a1 === a1.normalize(“NFKD”)); // false console.log(a1 === a1.normalize(“NFKC”)); // true
// U+212B是未规范化的 console.log(a2 === a2.normalize(“NFD”)); // false console.log(a2 === a2.normalize(“NFC”)); // false console.log(a2 === a2.normalize(“NFKD”)); // false console.log(a2 === a2.normalize(“NFKC”)); // false
// U+0041/U+030A是对0+212B进行NFD/NFKD规范化之后的结果 console.log(a3 === a3.normalize(“NFD”)); // true console.log(a3 === a3.normalize(“NFC”)); // false console.log(a3 === a3.normalize(“NFKD”)); // true console.log(a3 === a3.normalize(“NFKC”)); // false
2. 选择同一种规范化形式可以让比较操作符返回正确的结果:
let a1 = String.fromCharCode(0x00C5), a2 = String.fromCharCode(0x212B), a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1.normalize(“NFD”) === a2.normalize(“NFD”)); // true console.log(a2.normalize(“NFKC”) === a3.normalize(“NFKC”)); // true console.log(a1.normalize(“NFC”) === a3.normalize(“NFC”)); // true
3. **字符串操作方法**<br />本节介绍几个操作字符串值的方法。首先是`concat()`,用于将一个或多个字符串拼接成一个新字符串。来看下面的例子:
let stringValue = “hello “; let result = stringValue.concat(“world”);
console.log(result); // “hello world” console.log(stringValue); // “hello”
3. 在这个例子中,对`stringValue`调用`concat()`方法的结果是得到`"hello world"`,但`stringValue`的值保持不变。`concat()`方法可以接收任意多个参数,因此可以一次性拼接多个字符串,如下所示:
let stringValue = “hello “; let result = stringValue.concat(“world”, “!”);
console.log(result); // “hello world!” console.log(stringValue); // “hello”
3. 这个修改后的例子将字符串`"world"`和`"!"`追加到了`"hello "`后面。虽然`concat()`方法可以拼接字符串,但更常用的方式是使用加号操作符(`+`)。而且多数情况下,对于拼接多个字符串来说,使用加号更方便。<br />ECMAScript提供了3个从字符串中提取子字符串的方法:`slice()`、`substr()`和`substring()`。这3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。对`slice()`和`substring()`而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对`substr()`而言,第二个参数表示返回的子字符串数量。任何情况下,省略第二个参数都意味着提取到字符串末尾。与`concat()`方法一样,`slice()`、`substr()`和`substring()`也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。来看下面的例子:
let stringValue = “hello world”; console.log(stringValue.slice(3)); // “lo world” console.log(stringValue.substring(3)); // “lo world” console.log(stringValue.substr(3)); // “lo world” console.log(stringValue.slice(3, 7)); // “lo w” console.log(stringValue.substring(3,7)); // “lo w” console.log(stringValue.substr(3, 7)); // “lo worl”
3. 在这个例子中,`slice()`、`substr()`和`substring()`是以相同方式被调用的,而且多数情况下返回的值也相同。如果只传一个参数`3`,则所有方法都将返回`"lo world"`,因为`"hello"`中`"l"`位置为3。如果传入两个参数`3`和`7`,则`slice()`和`substring()`返回`"lo w"`(因为`"world"`中`"o"`在位置7,不包含),而`substr()`返回`"lo worl"`,因为第二个参数对它而言表示返回的字符数。<br />当某个参数是负值时,这3个方法的行为又有不同。比如,`slice()`方法将所有负值参数都当成字符串长度加上负参数值。<br />而`substr()`方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为0。`substring()`方法会将所有负参数值都转换为0。看下面的例子:
let stringValue = “hello world”; console.log(stringValue.slice(-3)); // “rld” console.log(stringValue.substring(-3)); // “hello world” console.log(stringValue.substr(-3)); // “rld” console.log(stringValue.slice(3, -4)); // “lo w” console.log(stringValue.substring(3, -4)); // “hel” console.log(stringValue.substr(3, -4)); // “” (empty string)
3. 这个例子明确演示了3个方法的差异。在给`slice()`和`substr()`传入负参数时,它们的返回结果相同。这是因为`-3`会被转换为`8`(长度加上负参数),实际上调用的是`slice(8)`和`substr(8)`。而`substring()`方法返回整个字符串,因为`-3`会转换为`0`。<br />在第二个参数是负值时,这3个方法各不相同。`slice()`方法将第二个参数转换为`7`,实际上相当于调用`slice(3, 7)`,因此返回`"lo w"`。而`substring()`方法会将第二个参数转换为`0`,相当于调用`substring(3, 0)`,等价于`substring(0, 3)`,这是因为这个方法会将较小的参数作为起点,将较大的参数作为终点。对`substr()`来说,第二个参数会被转换为`0`,意味着返回的字符串包含零个字符,因而会返回一个空字符串。
4. **字符串位置方法**<br />有两个方法用于在字符串中定位子字符串:`indexOf()`和`lastIndexOf()`。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回`-1`)。两者的区别在于,`indexOf()`方法从字符串开头开始查找子字符串,而`lastIndexOf()`方法从字符串末尾开始查找子字符串。来看下面的例子:
let stringValue = “hello world”; console.log(stringValue.indexOf(“o”)); // 4 console.log(stringValue.lastIndexOf(“o”)); // 7
4. 这里,字符串中第一个`"o"`的位置是4,即`"hello"`中的`"o"`。最后一个`"o"`的位置是7,即`"world"`中的`"o"`。如果字符串中只有一个`"o"`,则`indexOf()`和`lastIndexOf()`返回同一个位置。<br />这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,`indexOf()`会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;`lastIndexOf()`则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。下面看一个例子:
let stringValue = “hello world”; console.log(stringValue.indexOf(“o”, 6)); // 7 console.log(stringValue.lastIndexOf(“o”, 6)); // 4
4. 在传入第二个参数`6`以后,结果跟前面的例子恰好相反。这一次,`indexOf()`返回`7`,因为它从位置6(字符`"w"`)开始向后搜索字符串,在位置7找到了`"o"`。而`lastIndexOf()`返回`4`,因为它从位置6开始反向搜索至字符串开头,因此找到了`"hello"`中的`"o"`。像这样使用第二个参数并循环调用`indexOf()`或`lastIndexOf()`,就可以在字符串中找到所有的目标子字符串,如下所示:
let stringValue = “Lorem ipsum dolor sit amet, consectetur adipisicing elit”; let positions = new Array(); let pos = stringValue.indexOf(“e”);
while(pos > -1) { positions.push(pos); pos = stringValue.indexOf(“e”, pos + 1); }
console.log(positions); // [3,24,32,35,52]
4. 这个例子逐步增大开始搜索的位置,通过`indexOf()`遍历了整个字符串。首先取得第一个`"e"`的位置,然后进入循环,将上一次的位置加1再传给`indexOf()`,确保搜索到最后一个子字符串实例之后。每个位置都保存在`positions`数组中,可供以后使用。
5. **字符串包含方法**<br />ECMAScript 6增加了3个用于判断字符串中是否包含另一个字符串的方法:`startsWith()`、`endsWith()`和`includes()`。这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值。它们的区别在于,`startsWith()`检查开始于索引0的匹配项,`endsWith()`检查开始于索引`(string.length - substring.length)`的匹配项,而`includes()`检查整个字符串:
let message = “foobarbaz”;
console.log(message.startsWith(“foo”)); // true console.log(message.startsWith(“bar”)); // false
console.log(message.endsWith(“baz”)); // true console.log(message.endsWith(“bar”)); // false
console.log(message.includes(“bar”)); // true console.log(message.includes(“qux”)); // false
5. `startsWith()`和`includes()`方法接收可选的第二个参数,表示开始搜索的位置。如果传入第二个参数,则意味着这两个方法会从指定位置向着字符串末尾搜索,忽略该位置之前的所有字符。下面是一个例子:
let message = “foobarbaz”;
console.log(message.startsWith(“foo”)); // true console.log(message.startsWith(“foo”, 1)); // false
console.log(message.includes(“bar”)); // true console.log(message.includes(“bar”, 4)); // false
5. `endsWith()`方法接收可选的第二个参数,表示应该当作字符串末尾的位置。如果不提供这个参数,那么默认就是字符串长度。如果提供这个参数,那么就好像字符串只有那么多字符一样:
let message = “foobarbaz”;
console.log(message.endsWith(“bar”)); // false console.log(message.endsWith(“bar”, 6)); // true
6. **trim()方法**<br />ECMAScript在所有字符串上都提供了`trim()`方法。这个方法会创建字符串的一个副本,删除前、后所有空格符,再返回结果。比如:
let stringValue = “ hello world “; let trimmedStringValue = stringValue.trim(); console.log(stringValue); // “ hello world “ console.log(trimmedStringValue); // “hello world”
6. 由于`trim()`返回的是字符串的副本,因此原始字符串不受影响,即原本的前、后空格符都会保留。<br />另外,`trimeLeft()`和`trimRight()`方法分别用于从字符串开始和末尾清理空格符。
7. **repeat()方法**<br />ECMAScript在所有字符串上都提供了`repeat()`方法。这个方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。
let stringValue = “na “; console.log(stringValue.repeat(16) + “batman”); // na na na na na na na na na na na na na na na na batman
8. **padStart()和padEnd()方法**<br />`padStart()`和`padEnd()`方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格(U+0020)。
let stringValue = “foo”;
console.log(stringValue.padStart(6)); // “ foo” console.log(stringValue.padStart(9, “.”)); // “……foo”
console.log(stringValue.padEnd(6)); // “foo “ console.log(stringValue.padEnd(9, “.”)); // “foo……”
8. 可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。
let stringValue = “foo”;
console.log(stringValue.padStart(8, “bar”)); // “barbafoo” console.log(stringValue.padStart(2)); // “foo”
console.log(stringValue.padEnd(8, “bar”)); // “foobarba” console.log(stringValue.padEnd(2)); // “foo”
9. **字符串迭代与解构**<br />字符串的原型上暴露了一个`@@iterator`方法,表示可以迭代字符串的每个字符。可以像下面这样手动使用迭代器:
let message = “abc”; let stringIterator = messageSymbol.iterator;
console.log(stringIterator.next()); // {value: “a”, done: false} console.log(stringIterator.next()); // {value: “b”, done: false} console.log(stringIterator.next()); // {value: “c”, done: false} console.log(stringIterator.next()); // {value: undefined, done: true}
9. 在`for-of`循环中可以通过这个迭代器按序访问每个字符:
for (const c of “abcde”) { console.log(c); }
// a // b // c // d // e
9. 有了这个迭代器之后,字符串就可以通过解构操作符来解构了。比如,可以更方便地把字符串分割为字符数组:
let message = “abcde”;
console.log([…message]); // [“a”, “b”, “c”, “d”, “e”]
10. **字符串大小写转换**<br />下一组方法涉及大小写转换,包括4个方法:`toLowerCase()`、`toLocaleLowerCase()`、`toUpperCase()`和`toLocaleUpperCase()`。`toLowerCase()`和`toUpperCase()`方法是原来就有的方法,与`java.lang.String`中的方法同名。`toLocaleLowerCase()`和`toLocaleUpperCase()`方法旨在基于特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语),Unicode大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。下面是几个例子:
let stringValue = “hello world”; console.log(stringValue.toLocaleUpperCase()); // “HELLO WORLD” console.log(stringValue.toUpperCase()); // “HELLO WORLD” console.log(stringValue.toLocaleLowerCase()); // “hello world” console.log(stringValue.toLowerCase()); // “hello world”
10. 这里,`toLowerCase()`和`toLocaleLowerCase()`都返回`hello world`,而`toUpperCase()`和`toLocaleUpperCase()`都返回`HELLO WORLD`。通常,如果不知道代码涉及什么语言,则最好使用地区特定的转换方法。
11. **字符串模式匹配方法**<br />`String`类型专门为在字符串中实现模式匹配设计了几个方法。第一个就是`match()`方法,这个方法本质上跟`RegExp`对象的`exec()`方法相同。`match()`方法接收一个参数,可以是一个正则表达式字符串,也可以是一个`RegExp`对象。来看下面的例子:
let text = “cat, bat, sat, fat”; let pattern = /.at/;
// 等价于pattern.exec(text) let matches = text.match(pattern); console.log(matches.index); // 0 console.log(matches[0]); // “cat” console.log(pattern.lastIndex); // 0
11. `match()`方法返回的数组与`RegExp`对象的`exec()`方法返回的数组是一样的:第一个元素是与整个模式匹配的字符串,其余元素则是与表达式中的捕获组匹配的字符串(如果有的话)。<br />另一个查找模式的字符串方法是`search()`。这个方法唯一的参数与`match()`方法一样:正则表达式字符串或`RegExp`对象。这个方法返回模式第一个匹配的位置索引,如果没找到则返回-1。`search()`始终从字符串开头向后匹配模式。看下面的例子:
let text = “cat, bat, sat, fat”; let pos = text.search(/at/); console.log(pos); // 1
11. 这里,`search(/at/)`返回`1`,即`"at"`的第一个字符在字符串中的位置。<br />为简化子字符串替换操作,ECMAScript提供了`replace()`方法。这个方法接收两个参数,第一个参数可以是一个`RegExp`对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记,如下面的例子所示:
let text = “cat, bat, sat, fat”; let result = text.replace(“at”, “ond”); console.log(result); // “cond, bat, sat, fat”
result = text.replace(/at/g, “ond”); console.log(result); // “cond, bond, sond, fond”
11. 在这个例子中,字符串`"at"`先传给`replace()`函数,而替换文本是`"ond"`。结果是`"cat"`被修改为`"cond"`,而字符串的剩余部分保持不变。通过将第一个参数改为带全局标记的正则表达式,字符串中的所有`"at"`都被替换成了`"ond"`。<br />第二个参数是字符串的情况下,有几个特殊的字符序列,可以用来插入正则表达式操作的值。ECMA-262中规定了下表中的值。
| 字符序列 | 替换文本 |
| :--- | :--- |
| `$$` | `$` |
| `$&` | 匹配整个模式的子字符串。与`RegExp.lastMatch`相同 |
| `$'` | 匹配的子字符串之前的字符串。与`RegExp.rightContext`相同 |
| `$`` | 匹配的子字符串之后的字符串。与`RegExp.leftContext`相同 | |
| `$*n*` | 匹配第`*n*`个捕获组的字符串,其中`*n*`是0~9。比如,`$1`是匹配第一个捕获组的字符串,`$2`是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串 |
| `$*nn*` | 匹配第`*nn*`个捕获组字符串,其中`*nn*`是01~99。比如,`$01`是匹配第一个捕获组的字符串,`$02`是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串 |
11. 使用这些特殊的序列,可以在替换文本中使用之前匹配的内容,如下面的例子所示:
let text = “cat, bat, sat, fat”; result = text.replace(/(.at)/g, “word ($1)”); console.log(result); // word (cat), word (bat), word (sat), word (fat)
11. 这里,每个以`"at"`结尾的词都会被替换成`"word"`后跟一对小括号,其中包含捕获组匹配的内容`$1`。<br />`replace()`的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到3个参数:与整个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串。在有多个捕获组的情况下,每个匹配捕获组的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和原始字符串。这个函数应该返回一个字符串,表示应该把匹配项替换成什么。使用函数作为第二个参数可以更细致地控制替换过程,如下所示:
function htmlEscape(text) { return text.replace(/[<>”&]/g, function(match, pos, originalText) { switch(match) { case “<”: return “<”; case “>”: return “>”; case “&”: return “&”; case “\””: return “"”; } }); }
console.log(htmlEscape(“
Hello world!
“)); // “<p class="greeting">Hello world!“
11. 这里,函数`htmlEscape()`用于将一段HTML中的4个字符替换成对应的实体:小于号、大于号、和号,还有双引号(都必须经过转义)。实现这个任务最简单的办法就是用一个正则表达式查找这些字符,然后定义一个函数,根据匹配的每个字符分别返回特定的HTML实体。<br />最后一个与模式匹配相关的字符串方法是`split()`。这个方法会根据传入的分隔符将字符串拆分成数组。作为分隔符的参数可以是字符串,也可以是`RegExp`对象。(字符串分隔符不会被这个方法当成正则表达式。)还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。来看下面的例子:
let colorText = “red,blue,green,yellow”; let colors1 = colorText.split(“,”); // [“red”, “blue”, “green”, “yellow”] let colors2 = colorText.split(“,”, 2); // [“red”, “blue”] let colors3 = colorText.split(/[^,]+/); // [“”, “,”, “,”, “,”, “”]
11. 在这里,字符串`colorText`是一个逗号分隔的颜色名称符串。调用`split(",")`会得到包含这些颜色名的数组,基于逗号进行拆分。要把数组元素限制为2个,传入第二个参数2即可。最后,使用正则表达式可以得到一个包含逗号的数组。注意在最后一次调用`split()`时,返回的数组前后包含两个空字符串。这是因为正则表达式指定的分隔符出现在了字符串开头(`"red"`)和末尾(`"yellow"`)。
12. **localeCompare()方法**<br />最后一个方法是`localeCompare()`,这个方法比较两个字符串,返回如下3个值中的一个。
- 如果按照字母表顺序,字符串应该排在字符串参数前头,则返回负值。(通常是`-1`,具体还要看与实际值相关的实现。)
- 如果字符串与字符串参数相等,则返回`0`。
- 如果按照字母表顺序,字符串应该排在字符串参数后头,则返回正值。(通常是`1`,具体还要看与实际值相关的实现。)
下面是一个例子:
let stringValue = “yellow”; console.log(stringValue.localeCompare(“brick”)); // 1 console.log(stringValue.localeCompare(“yellow”)); // 0 console.log(stringValue.localeCompare(“zoo”)); // -1
在这里,字符串`"yellow"`与3个不同的值进行了比较:`"brick"`、`"yellow"`和`"zoo"`。`"brick"`按字母表顺序应该排在`"yellow"`前头,因此`localeCompare()`返回1。`"yellow"`等于`"yellow"`,因此`"localeCompare()"`返回`0`。最后,`"zoo"`在`"yellow"`后面,因此`localeCompare()`返回`-1`。强调一下,因为返回的具体值可能因具体实现而异,所以最好像下面的示例中一样使用`localeCompare()`:
function determineOrder(value) {
let result = stringValue.localeCompare(value);
if (result < 0) {
console.log(The string 'yellow' comes before the string '${value}'.
);
} else if (result > 0) {
console.log(The string 'yellow' comes after the string '${value}'.
);
} else {
console.log(The string 'yellow' is equal to the string '${value}'.
);
}
}
determineOrder(“brick”); determineOrder(“yellow”); determineOrder(“zoo”);
这样一来,就可以保证在所有实现中都能正确判断字符串的顺序了。<br />`localeCompare()`的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串。在美国,英语是ECMAScript实现的标准语言,`localeCompare()`区分大小写,大写字母排在小写字母前面。但其他地区未必是这种情况。
13. **HTML方法**<br />早期的浏览器开发商认为使用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>` |
<a name="b7c311f1"></a>
## 5.4 单例内置对象
ECMA-262对内置对象的定义是“任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括`Object`、`Array`和`String`。本节介绍ECMA-262定义的另外两个单例内置对象:`Global`和`Math`。
<a name="9f985ceb"></a>
### 5.4.1 `Global`
`Global`对象是ECMAScript中最特别的对象,因为代码不会显式地访问它。ECMA-262规定`Global`对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成`Global`对象的属性 。本书前面介绍的函数,包括`isNaN()`、`isFinite()`、`parseInt()`和`parseFloat()`,实际上都是`Global`对象的方法。除了这些,`Global`对象上还有另外一些方法。
1. **URL编码方法**<br />`encodeURI()`和`encodeURIComponent()`方法用于编码统一资源标识符(URI),以便传给浏览器。有效的URI不能包含某些字符,比如空格。使用URI编码方法来编码URI可以让浏览器能够理解它们,同时又以特殊的UTF-8编码替换掉所有无效字符。<br />`ecnodeURI()`方法用于对整个URI进行编码,比如`"www.wrox.com/illegal value.js"`。而`encodeURIComponent()`方法用于编码URI中单独的组件,比如前面URL中的`"illegal value.js"`。这两个方法的主要区别是,`encodeURI()`不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号,而`encodeURIComponent()`会编码它发现的所有非标准字符。来看下面的例子:
let uri = “http:// www.wrox.com/illegal value.js#start”;
// “http:// www.wrox.com/illegal%20value.js#start” console.log(encodeURI(uri));
// “http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start” console.log(encodeURIComponent(uri));
1. 这里使用`encodeURI()`编码后,除空格被替换为`%20`之外,没有任何变化。而`ecnodeURIComponent()`方法将所有非字母字符都替换成了相应的编码形式。这就是使用`encodeURI()`编码整个URI,但只使用`encodeURIComponent()`编码那些会追加到已有URI后面的字符串的原因。
> **注意** 一般来说,使用`encodeURIComponent()`应该比使用`encodeURI()`的频率更高,这是因为编码查询字符串参数比编码基准URI的次数更多。
1. 与`encodeURI()`和`encodeURIComponent()`相对的是`decodeURI()`和`decodeURIComponent()`。`decodeURI()`只对使用`encodeURI()`编码过的字符解码。例如,`%20`会被替换为空格,但`%23`不会被替换为井号(`#`),因为井号不是由`encodeURI()`替换的。类似地,`decodeURIComponent()`解码所有被`encodeURIComponent()`编码的字符,基本上就是解码所有特殊值。来看下面的例子:
let uri = “http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start”;
// http%3A%2F%2Fwww.wrox.com%2Fillegal value.js%23start console.log(decodeURI(uri));
// http:// www.wrox.com/illegal value.js#start console.log(decodeURIComponent(uri));
1. 这里,`uri`变量中包含一个使用`encodeURIComponent()`编码过的字符串。首先输出的是使用`decodeURI()`解码的结果,可以看到只用空格替换了`%20`。然后是使用`decodeURIComponent()`解码的结果,其中替换了所有特殊字符,并输出了没有包含任何转义的字符串。(这个字符串不是有效的URL。)
> **注意** URI方法`encodeURI()`、`encodeURIComponent()`、`decodeURI()`和`decodeURIComponent()`取代了`escape()`和`unescape()`方法,后者在ECMA-262第3版中就已经废弃了。URI方法始终是首选方法,因为它们对所有Unicode字符进行编码,而原来的方法只能正确编码ASCII字符。不要在生产环境中使用`escape()`和`unescape()`。
2. **eval()方法**<br />最后一个方法可能是整个ECMAScript语言中最强大的了,它就是`eval()`。这个方法就是一个完整的ECMAScript解释器,它接收一个参数,即一个要执行的ECMAScript(JavaScript)字符串。来看一个例子:
eval(“console.log(‘hi’)”);
2. 上面这行代码的功能与下一行等价:
console.log(“hi”);
2. 当解释器发现`eval()`调用时,会将参数解释为实际的ECMAScript语句,然后将其插入到该位置。通过`eval()`执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意味着定义在包含上下文中的变量可以在`eval()`调用内部被引用,比如下面这个例子:
let msg = “hello world”; eval(“console.log(msg)”); // “hello world”
2. 这里,变量`msg`是在`eval()`调用的外部上下文中定义的,而`console.log()`显示了文本`"hello world"`。这是因为第二行代码会被替换成一行真正的函数调用代码。类似地,可以在`eval()`内部定义一个函数或变量,然后在外部代码中引用,如下所示:
eval(“function sayHi() { console.log(‘hi’); }”); sayHi();
2. 这里,函数`sayHi()`是在`eval()`内部定义的。因为该调用会被替换为真正的函数定义,所以才可能在下一行代码中调用`sayHi()`。对于变量也是一样的:
eval(“let msg = ‘hello world’;”); console.log(msg); // Reference Error: msg is not defined
2. 通过`eval()`定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在`eval()`执行的时候才会被创建。<br />在严格模式下,在`eval()`内部创建的变量和函数无法被外部访问。换句话说,最后两个例子会报错。同样,在严格模式下,赋值给`eval`也会导致错误:
“use strict”; eval = “hi”; // 导致错误
> **注意** 解释代码字符串的能力是非常强大的,但也非常危险。在使用`eval()`的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对XSS利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。
3. **Global对象属性**<br />`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对象**<br />虽然ECMA-262没有规定直接访问`Global`对象的方式,但浏览器将`window`对象实现为`Global`对象的代理。因此,所有全局作用域中声明的变量和函数都变成了`window`的属性。来看下面的例子:
var color = “red”;
function sayColor() { console.log(window.color); }
window.sayColor(); // “red”
4. 这里定义了一个名为`color`的全局变量和一个名为`sayColor()`的全局函数。在`sayColor()`内部,通过`window.color`访问了`color`变量,说明全局变量变成了`window`的属性。接着,又通过`window`对象直接调用了`window.sayColor()`函数,从而输出字符串。
> **注意** `window`对象在JavaScript中远不止实现了`ECMAScript`的`Global`对象那么简单。关于`window`对象的更多介绍,请参考第12章。
4. 另一种获取`Global`对象的方式是使用如下的代码:
let global = function() { return this; }();
4. 这段代码创建一个立即调用的函数表达式,返回了`this`的值。如前所述,当一个函数在没有明确(通过成为某个对象的方法,或者通过`call()`/`apply()`)指定`this`值的情况下执行时,`this`值等于`Global`对象。因此,调用一个简单返回`this`的函数是在任何执行上下文中获取`Global`对象的通用方式。
<a name="6ef0fe02"></a>
### 5.4.2 `Math`
ECMAScript提供了`Math`对象作为保存数学公式、信息和计算的地方。`Math`对象提供了一些辅助计算的属性和方法。
> **注意** `Math`对象上提供的计算要比直接在JavaScript实现的快得多,因为`Math`对象上的计算使用了JavaScript引擎中更高效的实现和处理器指令。但使用`Math`计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。
1. **Math对象属性**<br />`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的平方根 |
1. 这些值的含义和用法超出了本书的范畴,但都是ECMAScript规范定义的,并可以在你需要时使用。
2. **min()和max()方法**<br />`Math`对象也提供了很多辅助执行简单或复杂数学计算的方法。<br />`min()`和`max()`方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数,如下面的例子所示:
let max = Math.max(3, 54, 32, 16); console.log(max); // 54
let min = Math.min(3, 54, 32, 16); console.log(min); // 3
2. 在3、54、32和16中,`Math.max()`返回54,`Math.min()`返回3。使用这两个方法可以避免使用额外的循环和`if`语句来确定一组数值的最大最小值。<br />要知道数组中的最大值和最小值,可以像下面这样使用扩展操作符:
let values = [1, 2, 3, 4, 5, 6, 7, 8]; let max = Math.max(…values);
3. **舍入方法**<br />接下来是用于把小数值舍入为整数的4个方法:`Math.ceil()`、`Math.floor()`、`Math.round()`和`Math.fround()`。这几个方法处理舍入的方式如下所述。
- `Math.ceil()`方法始终向上舍入为最接近的整数。
- `Math.floor()`方法始终向下舍入为最接近的整数。
- `Math.round()`方法执行四舍五入。
- `Math.fround()`方法返回数值最接近的单精度(32位)浮点值表示。
以下示例展示了这些方法的用法:
console.log(Math.ceil(25.9)); // 26 console.log(Math.ceil(25.5)); // 26 console.log(Math.ceil(25.1)); // 26
console.log(Math.round(25.9)); // 26 console.log(Math.round(25.5)); // 26 console.log(Math.round(25.1)); // 25
console.log(Math.fround(0.4)); // 0.4000000059604645 console.log(Math.fround(0.5)); // 0.5 console.log(Math.fround(25.9)); // 25.899999618530273
console.log(Math.floor(25.9)); // 25 console.log(Math.floor(25.5)); // 25 console.log(Math.floor(25.1)); // 25
对于25和26(不包含)之间的所有值,`Math.ceil()`都会返回26,因为它始终向上舍入。`Math.round()`只在数值大于等于`25.5`时返回26,否则返回25。最后,`Math.floor()`对所有25和26(不包含)之间的值都返回25。
4. **random()方法**<br />`Math.random()`方法返回一个0~1范围内的随机数,其中包含0但不包含1。对于希望显示随机名言或随机新闻的网页,这个方法是非常方便的。可以基于如下公式使用`Math.random()`从一组整数中随机选择一个数:
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)
4. 这里使用了`Math.floor()`方法,因为`Math.random()`始终返回小数,即便乘以一个数再加上一个数也是小数。因此,如果想从1~10范围内随机选择一个数,代码就是这样的:
let num = Math.floor(Math.random() * 10 + 1);
4. 这样就有10个可能的值(110范围内的值,则代码就要写成这样:
let num = Math.floor(Math.random() * 9 + 2);
4. 2~10只有9个数,所以可选总数(`total_number_of_choices`)是9,而最小可能的值(`first_possible_value`)是2。很多时候,通过函数来算出可选总数和最小可能的值可能更方便,比如:
function selectFrom(lowerValue, upperValue) { let choices = upperValue - lowerValue + 1; return Math.floor(Math.random() * choices + lowerValue); }
let num = selectFrom(2,10); console.log(num); // 2~10范围内的值,其中包含2和10
4. 这里的函数`selectFrom()`接收两个参数:应该返回的最小值和最大值。通过将这两个值相减再加`1`得到可选总数,然后再套用上面的公式。于是,调用`selectFrom(2,10)`就可以从2~10(包含)范围内选择一个值了。使用这个函数,从一个数组中随机选择一个元素就很容易,比如:
let colors = [“red”, “green”, “blue”, “yellow”, “black”, “purple”, “brown”]; let color = colors[selectFrom(0, colors.length-1)]; ```
在这个例子中,传给
selecFrom()
的第二个参数是数组长度减1,即数组最大的索引值。注意
Math.random()
方法在这里出于演示目的是没有问题的。如果是为了加密而需要生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用window.crypto.getRandomValues()
。其他方法
Math
对象还有很多涉及各种简单或高阶数运算的方法。讨论每种方法的具体细节或者它们的适用场景超出了本书的范畴。不过,下表还是总结了Math
对象的其他方法。 | 方法 | 说明 | | :—- | :—- | |Math.abs(*x*)
| 返回*x*
的绝对值 | |Math.e*x*p(*x*)
| 返回Math.E
的*x*
次幂 | |Math.e*x*pm1(*x*)
| 等于Math.e*x*p(*x*) - 1
| |Math.log(*x*)
| 返回*x*
的自然对数 | |Math.log1p(*x*)
| 等于1 + Math.log(*x*)
| |Math.pow(*x*, *power*)
| 返回*x*
的*power*
次幂 | |Math.pow(*...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
对象包含辅助完成复杂计算的属性和方法。