Skip to content

js读书笔记-基本引用类型

引言

引用值(或者对象)是某个特定引用类型的实例。在 ECMAScript 中,引用类型是把数据和功能组 织到一起的结构,经常被人错误地称作“类”。虽然从技术上讲 JavaScript 是一门面向对象语言,但 ECMAScript 缺少传统的面向对象编程语言所具备的某些基本结构,包括类和接口。引用类型有时候也 被称为对象定义,因为它们描述了自己的对象应有的属性和方法。

对象被认为是某个特定引用类型的实例。新对象通过使用 new 操作符后跟一个构造函数(constructor) 8 来创建。构造函数就是用来创建新对象的函数,比如下面这行代码:

js
let now = new Date();

这行代码创建了引用类型 Date 的一个新实例,并将它保存在变量 now 中。Date()在这里就是构造函数,它负责创建一个只有默认属性和方法的简单对象。ECMAScript 提供了很多像 Date 这样的原 生引用类型,帮助开发者实现常见的任务。

Date

ECMAScript 的 Date 类型参考了 Java 早期版本中的 java.util.Date。为此,Date 类型将日期 保存为自协调世界时(UTC,Universal Time Coordinated)时间 1970 年 1 月 1 日午夜(零时)至今所 经过的毫秒数。使用这种存储格式,Date 类型可以精确表示 1970 年 1 月 1 日之前及之后 285 616 年的 日期。

在不给 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 的实现)。

如果传给Date.parse()的字符串并不表示日期,则该方法会返回NaN。如果直接把表示日期的字 符串传给 Date 构造函数,那么 Date 会在后台调用 Date.parse()。

js
let someDate = new Date(Date.parse("May 23, 2019"));
// 等价
let someDate = new Date("May 23, 2019");

Date.UTC()方法也返回日期的毫秒表示,但使用的是跟 Date.parse()不同的信息来生成这个值。 传给 Date.UTC()的参数是年、零起点月数(1 月是 0,2 月是 1,以此类推)、日(1~31)、时(0~23)、 分、秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为 1 日。其他 参数的默认值都是 0。

与 Date.parse()一样,Date.UTC()也会被 Date 构造函数隐式调用,但有一个区别:这种情况 下创建的是本地日期,不是 GMT 日期。不过 Date 构造函数跟 Date.UTC()接收的参数是一样的。因 此,如果第一个参数是数值,则构造函数假设它是日期中的年,第二个参数就是月,以此类推。

js
// 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日零点 
let y2k = new Date(2000, 0);
// 本地时间2005年5月5日下午5点55分55秒
let allFives = new Date(2005, 4, 5, 17, 55, 55);

继承的方法

与其他类型一样,Date 类型重写了 toLocaleString()、toString()和 valueOf()方法。但与 其他类型不同,重写后这些方法的返回值不一样。Date 类型的 toLocaleString()方法返回与浏览器 运行的本地环境一致的日期和时间。这通常意味着格式中包含针对时间的 AM(上午)或 PM(下午), 但不包含时区信息(具体格式可能因浏览器而不同)。toString()方法通常返回带时区信息的日期和时 间,而时间也是以 24 小时制(0~23)表示的。下面给出了 toLocaleString()和 toString()返回的 2024 年 1 月 11 日零点的示例:

js
toLocaleString() - 1/11/2024, 2:46:20 PM
toString() - Thu Jan 11 2024 14:46:20 GMT+0800 (Central Standard Time)

Date 类型的 valueOf()方法根本就不返回字符串,这个方法被重写后返回的是日期的毫秒表示。

因此,操作符(如小于号和大于号)可以直接使用它返回的值。比如下面的例子:

js
let date2 = new Date(2019, 1, 1); // 2019年2月1日
let date1 = new Date(2019, 0, 1); // 2019年1月1日 
console.log(date1 < date2); // true
console.log(date1 > date2); // false

日期 2019 年 1 月 1 日在 2019 年 2 月 1 日之前,所以说前者小于后者没问题。因为 2019 年 1 月 1 日 的毫秒表示小于 2019 年 2 月 1 日的毫秒表示,所以用小于号比较这两个日期时会返回 true。这也是确 保日期先后的一个简单方式。

日期格式化方法

Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串:

  • toDateString()显示日期中的周几、月、日、年(格式特定于实现);
  • toTimeString()显示日期中的时、分、秒和时区(格式特定于实现);
  • toLocaleDateString()显示日期中的周几、月、日、年(格式特定于实现和地区);
  • toLocaleTimeString()显示日期中的时、分、秒(格式特定于实现和地区);
  • toUTCString()显示完整的 UTC 日期(格式特定于实现)。

这些方法的输出与 toLocaleString()和 toString()一样,会因浏览器而异。因此不能用于在用户界面上一致地显示日期。

注意 还有一个方法叫 toGMTString(),这个方法跟 toUTCString()是一样的,目的 是为了向后兼容。不过,规范建议新代码使用 toUTCString()。

日期/时间组件方法

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,则加日)

RegExp

ECMAScript 通过 RegExp 类型支持正则表达式。正则表达式使用类似 Perl 的简洁语法来创建:

js
let expression = /pattern/flags;

这个正则表达式的 pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、 分组、向前查找和反向引用。每个正则表达式可以带零个或多个 flags(标记),用于控制正则表达式 的行为。下面给出了表示匹配模式的标记。

  • g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。
  • i:不区分大小写,表示在查找匹配时忽略 pattern 和字符串的大小写。
  • m:多行模式,表示查找到一行文本末尾时会继续查找。
  • y:粘附模式,表示只查找从 lastIndex 开始及之后的字符串。
  • u:Unicode 模式,启用 Unicode 匹配。
  • s:dotAll 模式,表示元字符.匹配任何字符(包括\n 或\r)。

使用不同模式和标记可以创建出各种正则表达式。

与其他语言中的正则表达式类似,所有元字符在模式中也必须转义,包括

sh
([{\^$|)]}?*+.

元字符在正则表达式中都有一种或多种特殊功能,所以要匹配上面这些字符本身,就必须使用反斜杠来转义。

RegExp实例属性

每个 RegExp 实例都有下列属性,提供有关模式的各方面信息。

  • global:布尔值,表示是否设置了 g 标记。
  • ignoreCase:布尔值,表示是否设置了 i 标记。
  • unicode:布尔值,表示是否设置了 u 标记。
  • sticky:布尔值,表示是否设置了 y 标记。
  • lastIndex:整数,表示在源字符串中下一次搜索的开始位置,始终从 0 开始。
  • multiline:布尔值,表示是否设置了 m 标记。
  • dotAll:布尔值,表示是否设置了 s 标记。
  • source:正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的斜杠。
  • flags:正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回(没有前后斜杠)。

RegExp实例方法

RegExp 实例的主要方法是 exec(),主要用于配合捕获组使用。这个方法只接收一个参数,即要应用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回 null。返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。index 是字符串中匹配模式的起始位置,input 是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串, 其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。来看下面的例子:

js
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(),也只会返回第一个匹配的信息。

js
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()都会在字符串中向前搜索下一个匹配项,如 下面的例子所示:

js
let text = "cat, bat, sat, fat";
let pattern = /.at/g;
let matches = pattern.exec(text);
console.log(matches.index); // 9
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 的位置上寻找匹配项。粘附标记覆盖全局标记。

js
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 语句中。

注意 正则表达式的 valueOf()方法返回正则表达式本身。

RegExp构造函数属性

RegExp 构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了RegExp 构造函数的属性。

全名简写说明
input$_最后搜索的字符串(非标准特性)
lastMatch$&最后匹配的文本
lastParen$+最后匹配的捕获组(非标准特性)
leftContext$`input 字符串中出现在 lastMatch 前面的文本
rightContext$'input 字符串中出现在 lastMatch 后面的文本

通过这些属性可以提取出与 exec()和 test()执行的操作相关的信息。来看下面的例子:

js
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 标识符:

js
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
}

RegExp 还有其他几个构造函数属性,可以存储最多 9 个捕获组的匹配项。这些属性通过 RegExp. $1~RegExp.$9 来访问,分别包含第 1~9 个捕获组的匹配项。在调用 exec()或 test()时,这些属性就会被填充,然后就可以像下面这样使用它们:

js
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 属性取得的两个捕获组匹配的内容。

模式局限

虽然 ECMAScript 对正则表达式的支持有了长足的进步,但仍然缺少 Perl 语言中的一些高级特性。 下列特性目前还没有得到 ECMAScript 的支持:

  • \A 和\Z 锚(分别匹配字符串的开始和末尾)
  • 联合及交叉类
  • 原子组
  • x(忽略空格)匹配模式
  • 条件式匹配
  • 正则表达式注释

虽然还有这些局限,但 ECMAScript 的正则表达式已经非常强大,可以用于大多数模式匹配任务。

原始值包装类型

为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型:Boolean、Number 和 String。 这些类型具有本章介绍的其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的 各种方法。来看下面的例子:

js
let s1 = "some text";
let s2 = s1.substring(2);

在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring() 方法,并把结果保存在 s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上 这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:

  • 创建一个 String 类型的实例;
  • 调用实例上的特定方法;
  • 销毁实例。

可以把这 3 步想象成执行了如下 3 行 ECMAScript 代码:

js
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;

这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上 3 步也会在后台发生,只不过 使用的是 Boolean 和 Number 包装类型而已。

引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到 的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期 间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:

js
let s1 = "some text";
s1.color = "red";
console.log(s1.color);  // undefined

这里的第二行代码尝试给字符串 s1 添加了一个 color 属性。可是,第三行代码访问 color 属性时, 它却不见了。原因就是第二行代码运行时会临时创建一个 String 对象,而当第三行代码执行时,这个对 象已经被销毁了。实际上,第三行代码在这里创建了自己的 String 对象,但这个对象没有 color 属性。

Object 构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。比如:

js
let obj = new Object("some text");
console.log(obj instanceof String);  // true

如果传给 Object 的是字符串,则会创建一个 String 的实例。如果是数值,则会创建 Number 的 实例。布尔值则会得到 Boolean 的实例。

注意,使用 new 调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:

js
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 的实例。 虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。每个原始 值包装类型都有相应的一套方法来方便数据操作。

Boolean

Boolean 的实例会重写 valueOf()方法,返回一个原始值 true 或 false。toString()方法被调 用时也会被覆盖,返回字符串"true"或"false"。不过,Boolean 对象在 ECMAScript 中用得很少。 不仅如此,它们还容易引起误会,尤其是在布尔表达式中使用 Boolean 对象时,比如:

js
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。

理解原始布尔值和 Boolean 对象之间的区别非常重要,强烈建议永远不要使用后者。

Number

Number 是对应数值的引用类型。要创建一个 Number 对象,就使用 Number 构造函数并传入一个数值。

与 Boolean 类型一样,Number 类型重写了 valueOf()、toLocaleString()和 toString()方 法。valueOf()方法返回 Number 对象表示的原始数值,另外两个方法返回数值字符串。toString() 方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:

js
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()方法返回包含指定小数点位数的数值字符串,如:

js
let num = 10;
console.log(num.toFixed(2)); // "10.00"

这里的 toFixed()方法接收了参数 2,表示返回的数值字符串要包含两位小数。结果返回值为 "10.00",小数位填充了 0。如果数值本身的小数位超过了参数指定的位数,则四舍五入到最接近的小数位

另一个用于格式化数值的方法是 toExponential(),返回以科学记数法(也称为指数记数法)表 示的数值字符串。与 toFixed()一样,toExponential()也接收一个参数,表示结果中小数的位数。 来看下面的例子:

js
let num = 10;
console.log(num.toExponential(1));  // "1.0e+1"

这段代码的输出为"1.0e+1"。一般来说,这么小的数不用表示为科学记数法形式。如果想得到数 值最适当的形式,那么可以使用 toPrecision()。

toPrecision()方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法 形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:

js
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 个方法都会向上或向下舍入。

isInteger()方法与安全整数

ES6 新增了 Number.isInteger()方法,用于辨别一个数值是否保存为整数。有时候,小数位的 0 可能会让人误以为数值是一个浮点值:

js
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(2^53 + 1)到 Number.MAX_SAFE_INTEGER(2^53 - 1)。对超出这 3 个范围的数值,即使尝试保存为整数,IEEE 754 编码格式也意味着二进制值可能会表示一个完全不同的 数值。为了鉴别整数是否在这个范围内,可以使用 Number.isSafeInteger()方法

js
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

String

String 是对应字符串的引用类型。要创建一个 String 对象,使用 String 构造函数并传入一个数值。

avaScript 字符

JavaScript 字符串由 16 位码元(code unit)组成。对多数字符来说,每 16 位码元对应一个字符。换 句话说,字符串的 length 属性表示字符串包含多少 16 位码元:

js
let message = "abcde";
console.log(message.length); // 5

此外,charAt()方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的 16 位码元,并返回该码元对应的字符:

js
let message = "abcde";
console.log(message.charAt(2)); // "c"

使用 charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索 引以整数指定。比如:

js
let message = "abcde";
// Unicode "Latin small letter C"的编码是 U+0063 
console.log(message.charCodeAt(2)); // 99
// 十进制99等于十六进制63
console.log(99 === 0x63); // true

fromCharCode()方法用于根据给定的 UTF-16 码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:

js
// 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"

对于 U+0000~U+FFFF 范围内的字符,length、charAt()、charCodeAt()和 fromCharCode() 返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用 16 位表示的,而这几个方法 也都基于 16 位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。

这个对应关系在扩展到 Unicode 增补字符平面时就不成立了。问题很简单,即 16 位只能唯一表示 65 536 个字符。这对于大多数语言字符集是足够了,在 Unicode 中称为基本多语言平面(BMP)。为了 表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外 16 位去选择一个增补平面。这种每个字符使用两个 16 位码元的策略称为代理对。

在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。比如,下面的例子中使用了一个笑脸表情符号,也就是一个使用代理对编码的字符:

js
// "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

些方法仍然将 16 位码元当作一个字符,事实上索引 2 和索引 3 对应的码元应该被看成一个代理 对,只对应一个字符。fromCharCode()方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个 Unicode 笑脸字符。

为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用 codePointAt()来代替 charCodeAt()。跟使用 charCodeAt()时类似,codePointAt()接收 16 位码元的索引并返回该索引 位置上的码点(code point)。码点是 Unicode 中一个字符的完整标识。比如,"c"的码点是 0x0063,而 "😊"的码点是 0x1F60A。码点可能是 16 位,也可能是 32 位,而 codePointAt()方法可以从指定码元 位置识别完整的码点。

js
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 8

注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符 的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代 理对的码点:

js
console.log([..."ab😊de"]); // ["a", "b", "😊", "d", "e"]

与 charCodeAt()有对应的 codePointAt()一样,fromCharCode()也有一个对应的 fromCodePoint()。 10 这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:

与 charCodeAt()有对应的 codePointAt()一样,fromCharCode()也有一个对应的 fromCodePoint()。 10 这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:

js
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab😊de 
console.log(String.fromCodePoint(97, 98, 128522, 100, 101)); // ab😊de

normalize()方法

某些 Unicode 字符可以有多种编码方式。有的字符既可以通过一个 BMP 字符表示,也可以通过一个代理对表示。

Unicode 提供了 4 种规范化形式,可以将字符规范化为一致的格式,无论 底层字符的代码是什么。这 4 种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、 NFKD(Normalization Form KD)和 NFKC(Normalization Form KC)。可以使用 normalize()方法对字 符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:"NFD"、"NFC"、"NFKD"或"NFKC"。

选择同一种规范化形式可以让比较操作符返回正确的结果:

js
let a1 = String.fromCharCode(0x00C5),// Å
    a2 = String.fromCharCode(0x212B),// Å 
    a3 = String.fromCharCode(0x0041, 0x030A); // Å

console.log(a1 === a2);  // false
console.log(a1 === a3);  // false
console.log(a2 === a3);  // false

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

字符串操作方法

concat()

用于将一个或多个字符串拼接成一个新字符串。

js
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result);      // "hello world"
console.log(stringValue); // "hello"

在这个例子中,对 stringValue 调用 concat()方法的结果是得到"hello world",但 stringValue 的值保持不变。concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串。

虽然 concat()方法可以拼接 字符串,但更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加号更方便。

slice()、substr() 和 substring()

ECMAScript 提供了 3 个从字符串中提取子字符串的方法:slice()、substr()和 substring()。这 3 个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。对 slice()和 substring()而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对 substr()而言,第二个参数表示返回的子字符串数量。 任何情况下,省略第二个参数都意味着提取到字符串末尾。与 concat()方法一样,slice()、substr()和 substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。

js
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() 方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为 0。 substring()方法会将所有负参数值都转换为 0。看下面的例子:

js
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.substr(3, -3)); // ''(empty string)
console.log(stringValue.slice(3, -4)); // lo w
console.log(stringValue.substring(3, -4));  // "hel"
console.log(stringValue.substr(3, -4));     // "" (empty string)

substring()方法会将第二个参数转换为 0,相当于调用 substring(3, 0),等价于 substring(0, 3),这是因为这个方法会将较小的参数作为起点,将较 大的参数作为终点。

字符串位置方法

有两个方法用于在字符串中定位子字符串:indexOf() 和 lastIndexOf()。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()方法 从字符串开头开始查找子字符串,而 lastIndexOf()方法从字符串末尾开始查找子字符串。来看下面的例子:

js
let stringValue = "hello world";
console.log(stringValue.indexOf("o"));     // 4
console.log(stringValue.lastIndexOf("o")); // 7

这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,indexOf()会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;lastIndexOf()则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。下面看一个例子:

js
let stringValue = "hello world";
console.log(stringValue.indexOf("o", 6));     // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4

字符串包含方法

ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()、 endsWith()和 includes()。这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含 的布尔值。它们的区别在于,startsWith()检查开始于索引 0 的匹配项,endsWith()检查开始于索引(string.length - substring.length)的匹配项,而 includes()检查整个字符串:

js
 let message = "foobarbaz";
console.log(message.endsWith("baz"));// true
console.log(message.endsWith("bar"));// false

console.log(message.startsWith("foo")); // true 
console.log(message.startsWith("bar")); // false

console.log(message.includes("bar"));// true
console.log(message.includes("qux"));// false

startsWith()和 includes()方法接收可选的第二个参数,表示开始搜索的位置。如果传入第二个参数,则意味着这两个方法会从指定位置向着字符串末尾搜索,忽略该位置之前的所有字符。

endsWith()方法接收可选的第二个参数,表示应该当作字符串末尾的位置。如果不提供这个参数,那么默认就是字符串长度。如果提供这个参数,那么就好像字符串只有那么多字符一样。

trim()方法

ECMAScript 在所有字符串上都提供了 trim()方法。这个方法会创建字符串的一个副本,删除前、 后所有空格符,再返回结果。比如:

js
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim(); 
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"

由于 trim()返回的是字符串的副本,因此原始字符串不受影响,即原本的前、后空格符都会保留。另外,trimLeft()和 trimRight()方法分别用于从字符串开始和末尾清理空格符。

repeat()方法

ECMAScript 在所有字符串上都提供了 repeat()方法。这个方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。

padStart()和 padEnd()方法

padStart()和 padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格 (U+0020)。

js
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......"

可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。

js
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"

字符串迭代与解构

字符串的原型上暴露了一个 @@iterator 方法,表示可以迭代字符串的每个字符。可以像下面这样 手动使用迭代器:

js
let message = "abc";
let stringIterator = message[Symbol.iterator]();
console.log(stringIterator.next()); // {value: "a", done: false} 
console.log(stringIterator.next()); // {value: "b", done: false} c
onsole.log(stringIterator.next()); // {value: "c", done: false} 
console.log(stringIterator.next()); // {value: undefined, done: true}

在 for-of 循环中可以通过这个迭代器按序访问每个字符:

js
for (const c of "abcde") {
    console.log(c);
}
// a
// b
// c
// d
// e

有了这个迭代器之后,字符串就可以通过解构操作符来解构了。比如,可以更方便地把字符串分割 为字符数组:

js
let message = "abcde";
console.log([...message]); // ["a", "b", "c", "d", "e"]

字符串大小写转换

包括 4 个方法:toLowerCase()、toLocaleLowerCase()、toUpperCase()和 toLocaleUpperCase()。

toLowerCase()和 toUpperCase()方法是原来就有的方法,与 java.lang.String 中的方法同名。toLocaleLowerCase()和 toLocaleUpperCase()方法旨在基于特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语), Unicode 大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。

下面是几个例子:

js
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"

这里,toLowerCase()和 toLocaleLowerCase()都返回 hello world,而 toUpperCase()和 toLocaleUpperCase()都返回 HELLO WORLD。通常,如果不知道代码涉及什么语言,则最好使用地区特定的转换方法。

字符串模式匹配方法

match

String 类型专门为在字符串中实现模式匹配设计了几个方法。第一个就是 match()方法,这个方法本质上跟 RegExp 对象的 exec() 方法相同。match()方法接收一个参数,可以是一个正则表达式字 符串,也可以是一个 RegExp 对象。来看下面的例子:

js
et 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

match()方法返回的数组与 RegExp 对象的 exec() 方法返回的数组是一样的:第一个元素是与整个模式匹配的字符串,其余元素则是与表达式中的捕获组匹配的字符串(如果有的话)。

另一个查找模式的字符串方法是search()。这个方法唯一的参数与match()方法一样: 正则表达式字符串或 RegExp 对象。这个方法返回模式第一个匹配的位置索引,如果没找到则返回-1。search() 始终从字符串开头向后匹配模式。看下面的例子:

js
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos);  // 1

这里,search(/at/)返回 1,即"at"的第一个字符在字符串中的位置。

replace

为简化子字符串替换操作,ECMAScript 提供了 replace()方法。这个方法接收两个参数,第一个 参数可以是一个 RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是 一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记,如下面的例子所示:

js
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"

在这个例子中,字符串"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 是匹配第 二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串

使用这些特殊的序列,可以在替换文本中使用之前匹配的内容,如下面的例子所示:

js
let text = "cat, bat, sat, fat";
result = text.replace(/(.at)/g, "word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)

result = text.replace(/(.at)/g, "word ($&)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)

这里,每个以"at"结尾的词都会被替换成"word"后跟一对小括号,其中包含捕获组匹配的内容$1。

replace()的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到 3 个参数:与整个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串。

在有多个捕获组的情况下,每个匹配捕获组的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和 原始字符串。这个函数应该返回一个字符串,表示应该把匹配项替换成什么。使用函数作为第二个参数可以更细致地控制替换过程,如下所示:

js
function htmlEscape(text) {
  return text.replace(/[<>"&]/g, function (match, pos, originalText) {
    switch (match) {
      case "<":
        return "&lt;";
      case ">":
        return "&gt;";
      case "&":
        return "&amp;";
      case '"':
        return "&quot;";
    }
  });
}

console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>")); // "&lt;p class=&quot;greeting&quot;&gt;Hello world!</p>"

这里,函数 htmlEscape()用于将一段 HTML 中的 4 个字符替换成对应的实体:小于号、大于号、 和号,还有双引号(都必须经过转义)。实现这个任务最简单的办法就是用一个正则表达式查找这些字符,然后定义一个函数,根据匹配的每个字符分别返回特定的 HTML 实体。

split

最后一个与模式匹配相关的字符串方法是 split()。这个方法会根据传入的分隔符将字符串拆分成数组。作为分隔符的参数可以是字符串,也可以是 RegExp 对象。(字符串分隔符不会被这个方法当成正则表达式。)还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。来看下面的 例子:

js
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(/[^,]+/);// ["", ",", ",", ",", ""]

localeCompare()方法

最后一个方法是 localeCompare(),这个方法比较两个字符串,返回如下 3 个值中的一个:

  • 如果按照字母表顺序,字符串应该排在字符串参数前头,则返回负值。(通常是-1,具体还要看与实际值相关的实现。)
  • 如果字符串与字符串参数相等,则返回 0。
  • 如果按照字母表顺序,字符串应该排在字符串参数后头,则返回正值。(通常是 1,具体还要看与实际值相关的实现。) 下面是一个例子:
js
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():

js
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");

这样一来,就可以保证在所有实现中都能正确判断字符串的顺序了。 localeCompare()的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串。在美国,英语是 ECMAScript 实现的标准语言,localeCompare()区分大小写,大写字母排在小写字母前面。但其他地区未必是这种情况。

单例内置对象

ECMA-262 对内置对象的定义是“任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括 Object、Array 和 String。本节介绍 ECMA-262 定义的另外两个单例内置对象:Global 和 Math。

Global

Global 对象是 ECMAScript 中最特别的对象,因为代码不会显式地访问它。ECMA-262 规定 Global 对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成 Global 对象的属性。

本书前面介绍的函数, 包括 isNaN()、isFinite()、parseInt()和 parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法。

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()会编码它发现的所有非标准字符。来看下面的例子:

js
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));

这里使用 encodeURI()编码后,除空格被替换为 %20 之外,没有任何变化。而 encodeURIComponent()方法将所有非字母字符都替换成了相应的编码形式。这就是使用 encodeURI()编码整个 URI,但只使用 encodeURIComponent() 编码那些会追加到已有 URI 后面的字符串的原因。

注意 一般来说,使用 encodeURIComponent()应该比使用 encodeURI()的频率更高, 这是因为编码查询字符串参数比编码基准 URI 的次数更多。

与 encodeURI()和 encodeURIComponent()相对的是 decodeURI()和 decodeURIComponent()。 decodeURI()只对使用 encodeURI()编码过的字符解码。例如,%20 会被替换为空格,但 %23 不会被 替换为井号(#),因为井号不是由 encodeURI()替换的。类似地,decodeURIComponent()解码所有被 encodeURIComponent()编码的字符,基本上就是解码所有特殊值。来看下面的例子:

js
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));

这里,uri 变量中包含一个使用 encodeURIComponent() 编码过的字符串。首先输出的是使用 decodeURI() 解码的结果,可以看到只用空格替换了%20。然后是使用 decodeURIComponent() 解码的 结果,其中替换了所有特殊字符,并输出了没有包含任何转义的字符串。(这个字符串不是有效的 URL。)

eval()方法

最后一个方法可能是整个 ECMAScript 语言中最强大的了,它就是 eval()。这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(JavaScript)字符串。来看一个例子:

js
eval("console.log('hi')");

上面这行代码的功能与下一行等价:

js
console.log("hi");

当解释器发现 eval()调用时,会将参数解释为实际的 ECMAScript 语句,然后将其插入到该位置。 通过 eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意 味着定义在包含上下文中的变量可以在 eval()调用内部被引用, 类似地,可以在 eval()内部定义 一个函数或变量,然后在外部代码中引用,如下所示:

js
eval("function sayHi() { console.log('hi'); }");
sayHi();

这里,函数 sayHi()是在 eval()内部定义的。因为该调用会被替换为真正的函数定义,所以才可能在下一行代码中调用 sayHi()。对于变量也是一样的

注意 解释代码字符串的能力是非常强大的,但也非常危险。在使用 eval()的时候必须 极为慎重,特别是在解释用户输入的内容时。因为这个方法会对 XSS 利用暴露出很大的 攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。

window 对象

虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global 对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性。

Math

ECMAScript 提供了 Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助 计算的属性和方法。

注意 Math对象上提供的计算要比直接在JavaScript实现的快得多,因为Math对象上的 计算使用了 JavaScript 引擎中更高效的实现和处理器指令。但使用 Math 计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。

Math 对象属性

Math 对象有一些属性,主要用于保存数学中的一些特殊值。下表列出了这些属性。

属性说明
Math.E自然对数的基数 e 的值
Math.LN1010 为底的自然对数
Math.LN22 为底的自然对数
Math.LOG2E以 2 为底 e 的对数
Math.LOG10E以 10 为底 e 的对数
Math.PIπ 的值
Math.SQRT1_21/2 的平方根
Math.SQRT22 的平方根

min()和 max()方法

Math 对象也提供了很多辅助执行简单或复杂数学计算的方法。

min()和 max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数。

舍入方法

接下来是用于把小数值舍入为整数的 4 个方法:Math.ceil()、Math.floor()、Math.round() 和 Math.fround()。

这几个方法处理舍入的方式如下所述。

  • Math.ceil()方法始终向上舍入为最接近的整数
  • Math.floor()方法始终向下舍入为最接近的整数。
  • Math.round()方法执行四舍五入。
  • Math.fround()方法返回数值最接近的单精度(32 位)浮点值表示。

andom()方法

Math.random()方法返回一个 0~1 范围内的随机数,其中包含 0 但不包含 1。可以基于如下公式使用 Math.random()从一组整数中 随机选择一个数:

js
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)

这里使用了 Math.floor()方法,因为 Math.random()始终返回小数,即便乘以一个数再加上一个数也是小数。因此,如果想从 2~10 范围内随机选择一个数,代码就是这样的:

js
 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

注意 Math.random()方法在这里出于演示目的是没有问题的。如果是为了加密而需要 生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用 window.crypto.getRandomValues()。

其他方法

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的正切

小结

JavaScript 中的对象称为引用值,几种内置的引用类型可用于创建特定类型的对象。

  • 引用值与传统面向对象编程语言中的类相似,但实现不同。
  • Date 类型提供关于日期和时间的信息,包括当前日期、时间及相关计算。
  • RegExp 类型是 ECMAScript 支持正则表达式的接口,提供了大多数基础的和部分高级的正则表达式功能。

JavaScript 比较独特的一点是,函数实际上是 Function 类型的实例,也就是说函数也是对象。因为函数也是对象,所以函数也有方法,可以用于增强其能力。

由于原始值包装类型的存在,JavaScript 中的原始值可以被当成对象来使用。有 3 种原始值包装类型:Boolean、Number 和 String。它们都具备如下特点。

  • 每种包装类型都映射到同名的原始类型。
  • 以读模式访问原始值时,后台会实例化一个原始值包装类型的对象,借助这个对象可以操作相应的数据。
  • 涉及原始值的语句执行完毕后,包装对象就会被销毁。

当代码开始执行时,全局上下文中会存在两个内置对象:Global 和 Math。其中,Global 对象在大多数 ECMAScript 实现中无法直接访问。不过,浏览器将其实现为 window 对象。所有全局变量和函 数都是 Global 对象的属性。Math 对象包含辅助完成复杂计算的属性和方法。

备案号:闽ICP备2024028309号-1