JavaScript 权威指南第七版(GPT 重译)(四)(4)

简介: JavaScript 权威指南第七版(GPT 重译)(四)

JavaScript 权威指南第七版(GPT 重译)(四)(3)https://developer.aliyun.com/article/1485344

例如,考虑使用以下代码解析 URL⁵:

// A very simple URL parsing RegExp
let url = /(\w+):\/\/([\w.]+)\/(\S*)/;
let text = "Visit my blog at http://www.example.com/~david";
let match = text.match(url);
let fullurl, protocol, host, path;
if (match !== null) {
    fullurl = match[0];   // fullurl == "http://www.example.com/~david"
    protocol = match[1];  // protocol == "http"
    host = match[2];      // host == "www.example.com"
    path = match[3];      // path == "~david"
}

在这种非全局情况下,match()返回的数组除了编号数组元素外还有一些对象属性。input属性指的是调用match()的字符串。index属性是匹配开始的字符串位置。如果正则表达式包含命名捕获组,那么返回的数组还有一个groups属性,其值是一个对象。这个对象的属性与命名组的名称匹配,值为匹配的文本。例如,我们可以像这样重新编写之前的 URL 解析示例:

let url = /(?<protocol>\w+):\/\/(?<host>[\w.]+)\/(?<path>\S*)/;
let text = "Visit my blog at http://www.example.com/~david";
let match = text.match(url);
match[0]               // => "http://www.example.com/~david"
match.input            // => text
match.index            // => 17
match.groups.protocol  // => "http"
match.groups.host      // => "www.example.com"
match.groups.path      // => "~david"

我们已经看到,match() 的行为在正则表达式是否设置了 g 标志时会有很大不同。当设置了 y 标志时,行为也会有重要但不那么显著的差异。请记住,y 标志通过限制匹配开始的位置使正则表达式“粘滞”。如果一个正则表达式同时设置了 gy 标志,那么 match() 返回一个匹配字符串的数组,就像在设置了 g 而没有设置 y 时一样。但第一个匹配必须从字符串的开头开始,每个后续匹配必须从前一个匹配的字符紧随其后开始。

如果设置了 y 标志但没有设置 g,那么 match() 会尝试找到单个匹配,并且默认情况下,此匹配受限于字符串的开头。然而,您可以通过设置 RegExp 对象的 lastIndex 属性来更改此默认匹配开始位置,指定要匹配的索引位置。如果找到匹配,那么 lastIndex 将自动更新为匹配后的第一个字符,因此如果再次调用 match(),它将寻找下一个匹配。(lastIndex 可能看起来是一个奇怪的属性名称,它指定开始 下一个 匹配的位置。当我们讨论 RegExp exec() 方法时,我们将再次看到它,这个名称在那种情况下可能更有意义。)

let vowel = /[aeiou]/y;  // Sticky vowel match
"test".match(vowel)      // => null: "test" does not begin with a vowel
vowel.lastIndex = 1;     // Specify a different match position
"test".match(vowel)[0]   // => "e": we found a vowel at position 1
vowel.lastIndex          // => 2: lastIndex was automatically updated
"test".match(vowel)      // => null: no vowel at position 2
vowel.lastIndex          // => 0: lastIndex gets reset after failed match

值得注意的是,将非全局正则表达式传递给字符串的 match() 方法与将字符串传递给正则表达式的 exec() 方法是相同的:返回的数组及其属性在这两种情况下都是相同的。

matchAll()

matchAll() 方法在 ES2020 中定义,并且在 2020 年初已被现代 Web 浏览器和 Node 实现。matchAll() 期望一个设置了 g 标志的正则表达式。然而,与 match() 返回匹配子字符串的数组不同,它返回一个迭代器,该迭代器产生与使用非全局 RegExp 时 match() 返回的匹配对象相同的对象。这使得 matchAll() 成为遍历字符串中所有匹配的最简单和最通用的方法。

您可以使用 matchAll() 遍历文本字符串中的单词,如下所示:

// One or more Unicode alphabetic characters between word boundaries
const words = /\b\p{Alphabetic}+\b/gu; // \p is not supported in Firefox yet
const text = "This is a naïve test of the matchAll() method.";
for(let word of text.matchAll(words)) {
    console.log(`Found '${word[0]}' at index ${word.index}.`);
}

您可以设置 RegExp 对象的 lastIndex 属性,告诉 matchAll() 在字符串中的哪个索引开始匹配。然而,与其他模式匹配方法不同,matchAll() 永远不会修改您调用它的 RegExp 的 lastIndex 属性,这使得它在您的代码中更不容易出错。

split()

String 对象的正则表达式方法中的最后一个是 split()。这个方法将调用它的字符串分割成一个子字符串数组,使用参数作为分隔符。它可以像这样使用一个字符串参数:

"123,456,789".split(",")           // => ["123", "456", "789"]

split() 方法也可以接受正则表达式作为参数,这样可以指定更通用的分隔符。在这里,我们使用一个包含任意数量空白的分隔符来调用它:

"1, 2, 3,\n4, 5".split(/\s*,\s*/)  // => ["1", "2", "3", "4", "5"]
• 1

令人惊讶的是,如果你使用包含捕获组的正则表达式分隔符调用 split(),那么匹配捕获组的文本将包含在返回的数组中。例如:

const htmlTag = /<([^>]+)>/;  // < followed by one or more non->, followed by >
"Testing<br/>1,2,3".split(htmlTag)  // => ["Testing", "br/", "1,2,3"]

11.3.3 RegExp 类

本节介绍了 RegExp() 构造函数、RegExp 实例的属性以及 RegExp 类定义的两个重要模式匹配方法。

RegExp() 构造函数接受一个或两个字符串参数,并创建一个新的 RegExp 对象。这个构造函数的第一个参数是一个包含正则表达式主体的字符串——在正则表达式字面量中出现在斜杠内的文本。请注意,字符串字面量和正则表达式都使用 \ 字符作为转义序列,因此当您将正则表达式作为字符串字面量传递给 RegExp() 时,必须将每个 \ 字符替换为 \\RegExp() 的第二个参数是可选的。如果提供,它表示正则表达式的标志。它应该是 gimsuy,或这些字母的任意组合。

例如:

// Find all five-digit numbers in a string. Note the double \\ in this case.
let zipcode = new RegExp("\\d{5}", "g");

RegExp() 构造函数在动态创建正则表达式时非常有用,因此无法使用正则表达式字面量语法表示。例如,要搜索用户输入的字符串,必须在运行时使用 RegExp() 创建正则表达式。

除了将字符串作为 RegExp() 的第一个参数传递之外,您还可以传递一个 RegExp 对象。这允许您复制正则表达式并更改其标志:

let exactMatch = /JavaScript/;
let caseInsensitive = new RegExp(exactMatch, "i");

RegExp 属性

RegExp 对象具有以下属性:

source

这是正则表达式的源文本的只读属性:在 RegExp 字面量中出现在斜杠之间的字符。

flags

这是一个只读属性,指定表示 RegExp 标志的字母集合的字符串。

global

一个只读的布尔属性,如果设置了 g 标志,则为 true。

ignoreCase

一个只读的布尔属性,如果设置了 i 标志,则为 true。

multiline

一个只读的布尔属性,如果设置了 m 标志,则为 true。

dotAll

一个只读的布尔属性,如果设置了 s 标志,则为 true。

unicode

一个只读的布尔属性,如果设置了 u 标志,则为 true。

sticky

一个只读的布尔属性,如果设置了 y 标志,则为 true。

lastIndex

这个属性是一个读/写整数。对于具有 gy 标志的模式,它指定下一次搜索开始的字符位置。它由 exec()test() 方法使用,这两个方法在下面的两个小节中描述。

test()

RegExp 类的 test() 方法是使用正则表达式的最简单的方法。它接受一个字符串参数,并在字符串与模式匹配时返回 true,否则返回 false

test() 的工作原理是简单地调用(更复杂的)下一节中描述的 exec() 方法,并在 exec() 返回非空值时返回 true。因此,如果您使用带有 gy 标志的 RegExp 来使用 test(),那么它的行为取决于 RegExp 对象的 lastIndex 属性的值,这个值可能会意外更改。有关更多详细信息,请参阅“lastIndex 属性和 RegExp 重用”。

exec()

RegExp exec() 方法是使用正则表达式的最通用和强大的方式。它接受一个字符串参数,并在该字符串中查找匹配项。如果找不到匹配项,则返回 null。但是,如果找到匹配项,则返回一个数组,就像对于非全局搜索的 match() 方法返回的数组一样。数组的第 0 个元素包含与正则表达式匹配的字符串,任何后续的数组元素包含与任何捕获组匹配的子字符串。返回的数组还具有命名属性:index 属性包含匹配发生的字符位置,input 属性指定被搜索的字符串,如果定义了 groups 属性,则指的是一个保存与任何命名捕获组匹配的子字符串的对象。

与 String 的 match() 方法不同,exec() 无论正则表达式是否有全局 g 标志,都返回相同类型的数组。回想一下,当传递一个全局正则表达式时,match() 返回一个匹配数组。相比之下,exec() 总是返回一个单一匹配,并提供关于该匹配的完整信息。当在具有全局 g 标志或粘性 y 标志的正则表达式上调用 exec() 时,它会查看 RegExp 对象的 lastIndex 属性,以确定从哪里开始查找匹配。如果设置了 y 标志,它还会限制匹配从该位置开始。对于新创建的 RegExp 对象,lastIndex 为 0,并且搜索从字符串的开头开始。但每次 exec() 成功找到一个匹配时,它会更新 lastIndex 属性为匹配文本后面的字符的索引。如果 exec() 未找到匹配,它会将 lastIndex 重置为 0。这种特殊行为允许你重复调用 exec() 以循环遍历字符串中的所有正则表达式匹配。例如,以下代码中的循环将运行两次:

let pattern = /Java/g;
let text = "JavaScript > Java";
let match;
while((match = pattern.exec(text)) !== null) {
    console.log(`Matched ${match[0]} at ${match.index}`);
    console.log(`Next search begins at ${pattern.lastIndex}`);
}

11.4 日期和时间

Date 类是 JavaScript 用于处理日期和时间的 API。使用 Date() 构造函数创建一个 Date 对象。如果没有参数,它会返回一个代表当前日期和时间的 Date 对象:

let now = new Date();     // The current time

如果你传递一个数字参数,Date() 构造函数会将该参数解释为自 1970 年起的毫秒数:

let epoch = new Date(0);  // Midnight, January 1st, 1970, GMT

如果你指定两个或更多整数参数,它们会被解释为年、月、日、小时、分钟、秒和毫秒,使用你的本地时区,如下所示:

let century = new Date(2100,         // Year 2100
                       0,            // January
                       1,            // 1st
                       2, 3, 4, 5);  // 02:03:04.005, local time

Date API 的一个怪癖是,一年中的第一个月是数字 0,但一个月中的第一天是数字 1。如果省略时间字段,Date() 构造函数会将它们全部默认为 0,将时间设置为午夜。

请注意,当使用多个数字调用 Date() 构造函数时,它会使用本地计算机设置的任何时区进行解释。如果你想在 UTC(协调世界时,又称 GMT)中指定日期和时间,那么你可以使用 Date.UTC()。这个静态方法接受与 Date() 构造函数相同的参数,在 UTC 中解释它们,并返回一个毫秒时间戳,你可以传递给 Date() 构造函数:

// Midnight in England, January 1, 2100
let century = new Date(Date.UTC(2100, 0, 1));

如果你打印一个日期(例如使用 console.log(century)),默认情况下会以你的本地时区打印。如果你想在 UTC 中显示一个日期,你应该明确地将其转换为字符串,使用 toUTCString()toISOString()

最后,如果你将一个字符串传递给 Date() 构造函数,它将尝试将该字符串解析为日期和时间规范。构造函数可以解析由 toString()toUTCString()toISOString() 方法生成的格式指定的日期:

let century = new Date("2100-01-01T00:00:00Z");  // An ISO format date

一旦你有了一个 Date 对象,各种获取和设置方法允许你查询和修改 Date 的年、月、日、小时、分钟、秒和毫秒字段。每个方法都有两种形式:一种使用本地时间进行获取或设置,另一种使用 UTC 时间进行获取或设置。例如,要获取或设置 Date 对象的年份,你可以使用 getFullYear()getUTCFullYear()setFullYear()setUTCFullYear()

let d = new Date();                  // Start with the current date
d.setFullYear(d.getFullYear() + 1);  // Increment the year

要获取或设置 Date 的其他字段,将方法名称中的“FullYear”替换为“Month”、“Date”、“Hours”、“Minutes”、“Seconds”或“Milliseconds”。一些日期设置方法允许你一次设置多个字段。setFullYear()setUTCFullYear() 还可选择设置月份和日期。而 setHours()setUTCHours() 还允许你指定分钟、秒和毫秒字段,除了小时字段。

请注意,查询日期的方法是getDate()getUTCDate()。更自然的函数getDay()getUTCDay()返回星期几(星期日为 0,星期六为 6)。星期几是只读的,因此没有相应的setDay()方法。

11.4.1 时间戳

JavaScript 将日期内部表示为整数,指定自 1970 年 1 月 1 日午夜(或之前)以来的毫秒数。支持的整数最大为 8,640,000,000,000,000,因此 JavaScript 在 270,000 年后不会用尽毫秒。

对于任何日期对象,getTime()方法返回内部值,而setTime()方法设置它。因此,您可以像这样为日期添加 30 秒:

d.setTime(d.getTime() + 30000);

这些毫秒值有时被称为时间戳,直接使用它们而不是 Date 对象有时很有用。静态的Date.now()方法返回当前时间作为时间戳,当您想要测量代码运行时间时很有帮助:

let startTime = Date.now();
reticulateSplines(); // Do some time-consuming operation
let endTime = Date.now();
console.log(`Spline reticulation took ${endTime - startTime}ms.`);

11.4.2 日期算术

可以使用 JavaScript 的标准<<=>>=比较运算符比较日期对象。您可以从一个日期对象中减去另一个日期对象以确定两个日期之间的毫秒数。(这是因为 Date 类定义了一个返回时间戳的valueOf()方法。)

如果要从日期中添加或减去指定数量的秒、分钟或小时,通常最简单的方法是修改时间戳,就像前面示例中添加 30 秒到日期一样。如果要添加天数,这种技术变得更加繁琐,对于月份和年份则根本不起作用,因为它们的天数不同。要进行涉及天数、月份和年份的日期算术,可以使用setDate()setMonth()setYear()。例如,以下是将三个月和两周添加到当前日期的代码:

let d = new Date();
d.setMonth(d.getMonth() + 3, d.getDate() + 14);

即使溢出,日期设置方法也能正常工作。当我们向当前月份添加三个月时,可能得到大于 11 的值(代表 12 月)。setMonth()通过根据需要递增年份来处理这一点。同样,当我们将月份的日期设置为大于该月份天数的值时,月份会适当递增。

11.4.3 格式化和解析日期字符串

如果您使用 Date 类实际跟踪日期和时间(而不仅仅是测量时间间隔),那么您可能需要向代码的用户显示日期和时间。Date 类定义了许多不同的方法来将 Date 对象转换为字符串。以下是一些示例:

let d = new Date(2020, 0, 1, 17, 10, 30); // 5:10:30pm on New Year's Day 2020
d.toString()  // => "Wed Jan 01 2020 17:10:30 GMT-0800 (Pacific Standard Time)"
d.toUTCString()         // => "Thu, 02 Jan 2020 01:10:30 GMT"
d.toLocaleDateString()  // => "1/1/2020": 'en-US' locale
d.toLocaleTimeString()  // => "5:10:30 PM": 'en-US' locale
d.toISOString()         // => "2020-01-02T01:10:30.000Z"

这是 Date 类的字符串格式化方法的完整列表:

toString()

此方法使用本地时区,但不以区域感知方式格式化日期和时间。

toUTCString()

此方法使用 UTC 时区,但不以区域感知方式格式化日期。

toISOString()

此方法以 ISO-8601 标准的标准年-月-日小时:分钟:秒.ms 格式打印日期和时间。字母“T”将输出的日期部分与时间部分分开。时间以 UTC 表示,并且最后一个字母“Z”表示这一点。

toLocaleString()

此方法使用本地时区和适合用户区域的格式。

toDateString()

此方法仅格式化日期部分并省略时间。它使用本地时区,不进行区域适当的格式化。

toLocaleDateString()

此方法仅格式化日期。它使用本地时区和适合区域的日期格式。

toTimeString()

此方法仅格式化时间并省略日期。它使用本地时区,但不以区域感知方式格式化时间。

toLocaleTimeString()

这种方法以区域感知方式格式化时间,并使用本地时区。

当将日期和时间格式化为向最终用户显示时,这些日期转换为字符串的方法都不是理想的。查看 §11.7.2 以获取更通用且区域感知的日期和时间格式化技术。

最后,除了这些将 Date 对象转换为字符串的方法之外,还有一个静态的 Date.parse() 方法,它以字符串作为参数,尝试将其解析为日期和时间,并返回表示该日期的时间戳。Date.parse() 能够解析 Date() 构造函数可以解析的相同字符串,并且保证能够解析 toISOString()toUTCString()toString() 的输出。

11.5 错误类

JavaScript 的 throwcatch 语句可以抛出和捕获任何 JavaScript 值,包括原始值。没有必须用于信号错误的异常类型。但是,JavaScript 确实定义了一个 Error 类,并且在使用 throw 信号错误时传统上使用 Error 的实例或子类。使用 Error 对象的一个很好的理由是,当您创建一个 Error 时,它会捕获 JavaScript 堆栈的状态,如果异常未被捕获,堆栈跟踪将显示在错误消息中,这将帮助您调试问题。(请注意,堆栈跟踪显示 Error 对象的创建位置,而不是 throw 语句抛出它的位置。如果您总是在使用 throw new Error() 抛出之前创建对象,这将不会引起任何混淆。)

Error 对象有两个属性:messagename,以及一个 toString() 方法。message 属性的值是您传递给 Error() 构造函数的值,必要时转换为字符串。对于使用 Error() 创建的错误对象,name 属性始终为“Error”。toString() 方法简单地返回 name 属性的值,后跟一个冒号和空格,以及 message 属性的值。

尽管它不是 ECMAScript 标准的一部分,但 Node 和所有现代浏览器也在 Error 对象上定义了一个 stack 属性。该属性的值是一个多行字符串,其中包含 JavaScript 调用堆栈在创建 Error 对象时的堆栈跟踪。当捕获到意外错误时,这可能是有用的信息进行记录。

除了 Error 类之外,JavaScript 还定义了一些子类,用于信号 ECMAScript 定义的特定类型的错误。这些子类包括 EvalError、RangeError、ReferenceError、SyntaxError、TypeError 和 URIError。如果看起来合适,您可以在自己的代码中使用这些错误类。与基本 Error 类一样,这些子类的每个都有一个接受单个消息参数的构造函数。并且每个这些子类的实例都有一个 name 属性,其值与构造函数名称相同。

您可以随意定义最能封装您自己程序的错误条件的 Error 子类。请注意,您不仅限于 namemessage 属性。如果创建一个子类,您可以定义新属性以提供错误详细信息。例如,如果您正在编写解析器,可能会发现定义一个具有指定解析失败确切位置的 linecolumn 属性的 ParseError 类很有用。或者,如果您正在处理 HTTP 请求,可能希望定义一个具有保存失败请求的 HTTP 状态码(例如 404 或 500)的 status 属性的 HTTPError 类。

例如:

class HTTPError extends Error {
    constructor(status, statusText, url) {
        super(`${status} ${statusText}: ${url}`);
        this.status = status;
        this.statusText = statusText;
        this.url = url;
    }
    get name() { return "HTTPError"; }
}
let error = new HTTPError(404, "Not Found", "http://example.com/");
error.status        // => 404
error.message       // => "404 Not Found: http://example.com/"
error.name          // => "HTTPError"

11.6 JSON 序列化和解析

当程序需要保存数据或需要将数据通过网络连接传输到另一个程序时,它必须将其内存中的数据结构转换为一串字节或字符,这些字节或字符可以被保存或传输,然后稍后被解析以恢复原始的内存中的数据结构。将数据结构转换为字节流或字符流的过程称为序列化(或编组甚至腌制)。

在 JavaScript 中序列化数据的最简单方法使用了一种称为 JSON 的序列化格式。这个首字母缩写代表“JavaScript 对象表示法”,正如名称所示,该格式使用 JavaScript 对象和数组文字语法将由对象和数组组成的数据结构转换为字符串。JSON 支持原始数字和字符串,以及值truefalsenull,以及由这些原始值构建的数组和对象。JSON 不支持 Map、Set、RegExp、Date 或类型化数组等其他 JavaScript 类型。尽管如此,它已被证明是一种非常多才多艺的数据格式,即使在非基于 JavaScript 的程序中也被广泛使用。

JavaScript 支持使用两个函数JSON.stringify()JSON.parse()进行 JSON 序列化和反序列化,这两个函数在§6.8 中简要介绍过。给定一个不包含任何非可序列化值(如 RegExp 对象或类型化数组)的对象或数组(任意深度嵌套),您可以通过将其传递给JSON.stringify()来简单地序列化对象。正如名称所示,此函数的返回值是一个字符串。并且给定JSON.stringify()返回的字符串,您可以通过将字符串传递给JSON.parse()来重新创建原始数据结构:

let o = {s: "", n: 0, a: [true, false, null]};
let s = JSON.stringify(o);  // s == '{"s":"","n":0,"a":[true,false,null]}'
let copy = JSON.parse(s);   // copy == {s: "", n: 0, a: [true, false, null]}

如果我们忽略序列化数据保存到文件或通过网络发送的部分,我们可以将这对函数用作创建对象的深层副本的一种效率较低的方式:

// Make a deep copy of any serializable object or array
function deepcopy(o) {
    return JSON.parse(JSON.stringify(o));
}

JSON 是 JavaScript 的一个子集

当数据序列化为 JSON 格式时,结果是一个有效的 JavaScript 源代码,用于评估为原始数据结构的副本。如果您在 JSON 字符串前面加上var data =并将结果传递给eval(),您将获得将原始数据结构的副本分配给变量data的结果。但是,您绝对不应该这样做,因为这是一个巨大的安全漏洞——如果攻击者可以将任意 JavaScript 代码注入 JSON 文件中,他们可以使您的程序运行他们的代码。只需使用JSON.parse()来解码 JSON 格式化数据,这样更快速和安全。

JSON 有时被用作人类可读的配置文件格式。如果您发现自己手动编辑 JSON 文件,请注意 JSON 格式是 JavaScript 的一个非常严格的子集。不允许注释,属性名称必须用双引号括起来,即使 JavaScript 不需要这样做。

通常,您只向JSON.stringify()JSON.parse()传递单个参数。这两个函数都接受一个可选的第二个参数,允许我们扩展 JSON 格式,接下来将对此进行描述。JSON.stringify()还接受一个可选的第三个参数,我们将首先讨论这个参数。如果您希望您的 JSON 格式化字符串可读性强(例如用作配置文件),那么应将null作为第二个参数传递,并将数字或字符串作为第三个参数传递。第三个参数告诉JSON.stringify()应该将数据格式化为多个缩进行。如果第三个参数是一个数字,则它将使用该数字作为每个缩进级别的空格数。如果第三个参数是一个空格字符串(例如'\t'),它将使用该字符串作为每个缩进级别。

let o = {s: "test", n: 0};
JSON.stringify(o, null, 2)  // => '{\n  "s": "test",\n  "n": 0\n}'

JSON.parse()会忽略空格,因此向JSON.stringify()传递第三个参数对我们将字符串转换回数据结构的能力没有影响。

11.6.1 JSON 自定义

如果JSON.stringify()被要求序列化一个 JSON 格式不支持的值,它会查看该值是否有一个toJSON()方法,如果有,它会调用该方法,然后将返回值序列化以替换原始值。Date 对象实现了toJSON():它返回与toISOString()方法相同的字符串。这意味着如果序列化包含 Date 的对象,日期将自动转换为字符串。当您解析序列化的字符串时,重新创建的数据结构将不会与您开始的完全相同,因为它将在原始对象有 Date 的地方有一个字符串。

如果需要重新创建 Date 对象(或以任何其他方式修改解析的对象),可以将“恢复器”函数作为第二个参数传递给JSON.parse()。如果指定了,这个“恢复器”函数将被用于从输入字符串解析的每个原始值(但不包含这些原始值的对象或数组)。该函数被调用时带有两个参数。第一个是属性名称—一个对象属性名称或转换为字符串的数组索引。第二个参数是该对象属性或数组元素的原始值。此外,该函数作为包含原始值的对象或数组的方法被调用,因此您可以使用this关键字引用该包含对象。

恢复函数的返回值将成为命名属性的新值。如果它返回其第二个参数,则属性将保持不变。如果返回undefined,则在JSON.parse()返回给用户之前,命名属性将从对象或数组中删除。

作为示例,这里是一个调用JSON.parse()的示例,使用恢复器函数来过滤一些属性并重新创建 Date 对象:

let data = JSON.parse(text, function(key, value) {
    // Remove any values whose property name begins with an underscore
    if (key[0] === "_") return undefined;
    // If the value is a string in ISO 8601 date format convert it to a Date.
    if (typeof value === "string" &&
        /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/.test(value)) {
        return new Date(value);
    }
    // Otherwise, return the value unchanged
    return value;
});

除了前面描述的toJSON()的使用,JSON.stringify()还允许通过将数组或函数作为可选的第二个参数来自定义其输出。

如果作为第二个参数传递的是字符串数组(或数字—它们会被转换为字符串),那么这些将被用作对象属性(或数组元素)的名称。任何名称不在数组中的属性都将被省略。此外,返回的字符串将按照它们在数组中出现的顺序包括属性(在编写测试时非常有用)。

如果传递一个函数,它是一个替换函数—实际上是您可以传递给JSON.parse()的可选恢复函数的反函数。如果指定了替换函数,那么替换函数将被用于要序列化的每个值。替换函数的第一个参数是该对象中值的对象属性名称或数组索引,第二个参数是值本身。替换函数作为包含要序列化值的对象或数组的方法被调用。替换函数的返回值将被序列化以替换原始值。如果替换函数返回undefined或根本没有返回任何内容,则该值(及其数组元素或对象属性)将被省略在序列化中。

// Specify what fields to serialize, and what order to serialize them in
let text = JSON.stringify(address, ["city","state","country"]);
// Specify a replacer function that omits RegExp-value properties
let json = JSON.stringify(o, (k, v) => v instanceof RegExp ? undefined : v);

这里的两个JSON.stringify()调用以一种良性的方式使用第二个参数,产生的序列化输出可以在不需要特殊恢复函数的情况下反序列化。然而,一般来说,如果为类型定义了toJSON()方法,或者使用一个实际上用可序列化值替换不可序列化值的替换函数,那么通常需要使用自定义恢复函数与JSON.parse()一起来获取原始数据结构。如果这样做,你应该明白你正在定义一种自定义数据格式,并牺牲了与大量 JSON 兼容工具和语言的可移植性和兼容性。

11.7 国际化 API

JavaScript 国际化 API 由三个类 Intl.NumberFormat、Intl.DateTimeFormat 和 Intl.Collator 组成,允许我们以区域设置适当的方式格式化数字(包括货币金额和百分比)、日期和时间,并以区域设置适当的方式比较字符串。这些类不是 ECMAScript 标准的一部分,但作为ECMA402 标准的一部分定义,并得到 Web 浏览器的良好支持。Intl API 也受 Node 支持,但在撰写本文时,预构建的 Node 二进制文件不包含所需的本地化数据,以使它们能够与除美国英语以外的区域设置一起使用。因此,为了在 Node 中使用这些类,您可能需要下载一个单独的数据包或使用自定义构建的 Node。

国际化中最重要的部分之一是显示已翻译为用户语言的文本。有各种方法可以实现这一点,但这些方法都不在此处描述的 Intl API 的范围内。

11.7.1 格式化数字

世界各地的用户期望以不同的方式格式化数字。小数点可以是句点或逗号。千位分隔符可以是逗号或句点,并且并非在所有地方每三位数字都使用。一些货币被分成百分之一,一些被分成千分之一,一些没有细分。最后,尽管所谓的“阿拉伯数字”0 到 9 在许多语言中使用,但这并非普遍,一些国家的用户期望看到使用其自己脚本中的数字编写的数字。

Intl.NumberFormat 类定义了一个format()方法,考虑到所有这些格式化可能性。构造函数接受两个参数。第一个参数指定应为其格式化数字的区域设置,第二个是一个对象,指定有关如何格式化数字的更多详细信息。如果省略或undefined第一个参数,则将使用系统区域设置(我们假设为用户首选区域设置)。如果第一个参数是字符串,则指定所需的区域设置,例如"en-US"(美国使用的英语)、"fr"(法语)或"zh-Hans-CN"(中国使用简体汉字书写系统)。第一个参数也可以是区域设置字符串数组,在这种情况下,Intl.NumberFormat 将选择最具体且受支持的区域设置。

如果指定了Intl.NumberFormat()构造函数的第二个参数,则应该是一个定义一个或多个以下属性的对象:

style

指定所需的数字格式化类型。默认值为"decimal"。指定"percent"将数字格式化为百分比,或指定"currency"将数字格式化为货币金额。

currency

如果样式为"currency",则需要此属性来指定所需货币的三个字母 ISO 货币代码(例如"USD"表示美元或"GBP"表示英镑)。

currencyDisplay

如果样式为"currency",则此属性指定货币的显示方式。默认值"symbol"使用货币符号(如果货币有符号)。值"code"使用三个字母 ISO 代码,值"name"以长形式拼写货币名称。

useGrouping

将此属性设置为false,如果您不希望数字具有千位分隔符(或其相应的区域设置等价物)。

minimumIntegerDigits

用于显示数字整数部分的最小位数。如果数字的位数少于此值,则将在左侧用零填充。默认值为 1,但可以使用高达 21 的值。

minimumFractionDigitsmaximumFractionDigits

这两个属性控制数字的小数部分的格式。如果一个数字的小数位数少于最小值,它将在右侧用零填充。如果小数位数超过最大值,那么小数部分将被四舍五入。这两个属性的合法值介于 0 和 20 之间。默认最小值为 0,最大值为 3,除了在格式化货币金额时,小数部分的长度会根据指定的货币而变化。

minimumSignificantDigitsmaximumSignificantDigits

这些属性控制在格式化数字时使用的有效数字位数,使其适用于格式化科学数据等情况。如果指定了这些属性,它们将覆盖先前列出的整数和小数位数属性。合法值介于 1 和 21 之间。

一旦您使用所需的区域设置和选项创建了一个 Intl.NumberFormat 对象,您可以通过将数字传递给其format()方法来使用它,该方法将返回一个适当格式化的字符串。例如:

let euros = Intl.NumberFormat("es", {style: "currency", currency: "EUR"});
euros.format(10)    // => "10,00 €": ten euros, Spanish formatting
let pounds = Intl.NumberFormat("en", {style: "currency", currency: "GBP"});
pounds.format(1000) // => "£1,000.00": One thousand pounds, English formatting

Intl.NumberFormat(以及其他 Intl 类)的一个有用功能是它的format()方法绑定到它所属的 NumberFormat 对象。因此,您可以将format()方法分配给一个变量,并像独立函数一样使用它,而不是定义一个引用格式化对象的变量,然后在该变量上调用format()方法,就像这个例子中一样:

let data = [0.05, .75, 1];
let formatData = Intl.NumberFormat(undefined, {
    style: "percent",
    minimumFractionDigits: 1,
    maximumFractionDigits: 1
}).format;
data.map(formatData)   // => ["5.0%", "75.0%", "100.0%"]: in en-US locale

一些语言,比如阿拉伯语,使用自己的脚本来表示十进制数字:

let arabic = Intl.NumberFormat("ar", {useGrouping: false}).format;
arabic(1234567890)   // => "١٢٣٤٥٦٧٨٩٠"

其他语言,比如印地语,使用自己的数字字符集,但默认情况下倾向于使用 ASCII 数字 0-9。如果要覆盖用于数字的默认字符集,请在区域设置中添加-u-nu-,然后跟上简写的字符集名称。例如,您可以这样格式化数字,使用印度风格的分组和天城数字:

let hindi = Intl.NumberFormat("hi-IN-u-nu-deva").format;
hindi(1234567890)    // => "१,२३,४५,६७,८९०"

在区域设置中的-u-指定接下来是一个 Unicode 扩展。nu是编号系统的扩展名称,deva是 Devanagari 的缩写。Intl API 标准为许多其他编号系统定义了名称,主要用于南亚和东南亚的印度语言。

11.7.2 格式化日期和时间

Intl.DateTimeFormat 类与 Intl.NumberFormat 类非常相似。Intl.DateTimeFormat()构造函数接受与Intl.NumberFormat()相同的两个参数:区域设置或区域设置数组以及格式选项对象。使用 Intl.DateTimeFormat 实例的方法是调用其format()方法,将 Date 对象转换为字符串。

如§11.4 中所述,Date 类定义了简单的toLocaleDateString()toLocaleTimeString()方法,为用户的区域设置生成适当的输出。但是这些方法不会让您控制显示的日期和时间字段。也许您想省略年份,但在日期格式中添加一个工作日。您希望月份是以数字形式表示还是以名称拼写出来?Intl.DateTimeFormat 类根据传递给构造函数的第二个参数中的选项对象中的属性提供对输出的细粒度控制。但是,请注意,Intl.DateTimeFormat 不能总是精确显示您要求的内容。如果指定了格式化小时和秒的选项但省略了分钟,您会发现格式化程序仍然会显示分钟。这个想法是您使用选项对象指定要向用户呈现的日期和时间字段以及您希望如何格式化这些字段(例如按名称或按数字),然后格式化程序将查找最接近您要求的内容的适合区域设置的格式。

可用的选项如下。只为您希望出现在格式化输出中的日期和时间字段指定属性。

使用"numeric"表示完整的四位数年份,或使用"2-digit"表示两位数缩写。

使用"numeric"表示可能的短数字,如“1”,或"2-digit"表示始终有两位数字的数字表示,如“01”。使用"long"表示全名,如“January”,"short"表示缩写,如“Jan”,"narrow"表示高度缩写,如“J”,不保证唯一。

day

使用"numeric"表示一位或两位数字,或"2-digit"表示月份的两位数字。

weekday

使用"long"表示全名,如“Monday”,"short"表示缩写,如“Mon”,"narrow"表示高度缩写,如“M”,不保证唯一。

era

此属性指定日期是否应以时代(如 CE 或 BCE)格式化。如果您正在格式化很久以前的日期或使用日本日历,则可能很有用。合法值为"long""short""narrow"

hourminutesecond

这些属性指定您希望如何显示时间。使用"numeric"表示一位或两位数字字段,或"2-digit"强制将单个数字左侧填充为 0。

timeZone

此属性指定应为其格式化日期的所需时区。如果省略,将使用本地时区。实现始终识别“UTC”,并且还可以识别互联网分配的数字管理局(IANA)时区名称,例如“America/Los_Angeles”。

timeZoneName

此属性指定应如何在格式化的日期或时间中显示时区。使用"long"表示完全拼写的时区名称,"short"表示缩写或数字时区。

hour12

这个布尔属性指定是否使用 12 小时制。默认是与地区相关的,但你可以用这个属性来覆盖它。

hourCycle

此属性允许您指定午夜是写作 0 小时、12 小时还是 24 小时。默认是与地区相关的,但您可以用此属性覆盖默认值。请注意,hour12优先于此属性。使用值"h11"指定午夜为 0,午夜前一小时为 11pm。使用"h12"指定午夜为 12。使用"h23"指定午夜为 0,午夜前一小时为 23。使用"h24"指定午夜为 24。

以下是一些示例:

let d = new Date("2020-01-02T13:14:15Z");  // January 2nd, 2020, 13:14:15 UTC
// With no options, we get a basic numeric date format
Intl.DateTimeFormat("en-US").format(d) // => "1/2/2020"
Intl.DateTimeFormat("fr-FR").format(d) // => "02/01/2020"
// Spelled out weekday and month
let opts = { weekday: "long", month: "long", year: "numeric", day: "numeric" };
Intl.DateTimeFormat("en-US", opts).format(d) // => "Thursday, January 2, 2020"
Intl.DateTimeFormat("es-ES", opts).format(d) // => "jueves, 2 de enero de 2020"
// The time in New York, for a French-speaking Canadian
opts = { hour: "numeric", minute: "2-digit", timeZone: "America/New_York" };
Intl.DateTimeFormat("fr-CA", opts).format(d) // => "8 h 14"

Intl.DateTimeFormat 可以使用除基于基督教时代的默认儒略历之外的其他日历显示日期。尽管一些地区可能默认使用非基督教日历,但您始终可以通过在地区后添加-u-ca-并在其后跟日历名称来明确指定要使用的日历。可能的日历名称包括“buddhist”、“chinese”、“coptic”、“ethiopic”、“gregory”、“hebrew”、“indian”、“islamic”、“iso8601”、“japanese”和“persian”。继续前面的示例,我们可以确定各种非基督教历法中的年份:

let opts = { year: "numeric", era: "short" };
Intl.DateTimeFormat("en", opts).format(d)                // => "2020 AD"
Intl.DateTimeFormat("en-u-ca-iso8601", opts).format(d)   // => "2020 AD"
Intl.DateTimeFormat("en-u-ca-hebrew", opts).format(d)    // => "5780 AM"
Intl.DateTimeFormat("en-u-ca-buddhist", opts).format(d)  // => "2563 BE"
Intl.DateTimeFormat("en-u-ca-islamic", opts).format(d)   // => "1441 AH"
Intl.DateTimeFormat("en-u-ca-persian", opts).format(d)   // => "1398 AP"
Intl.DateTimeFormat("en-u-ca-indian", opts).format(d)    // => "1941 Saka"
Intl.DateTimeFormat("en-u-ca-chinese", opts).format(d)   // => "36 78"
Intl.DateTimeFormat("en-u-ca-japanese", opts).format(d)  // => "2 Reiwa"

11.7.3 比较字符串

将字符串按字母顺序排序(或对于非字母脚本的更一般“排序顺序”)的问题比英语使用者通常意识到的更具挑战性。英语使用相对较小的字母表,没有重音字母,并且我们有字符编码(ASCII,已合并到 Unicode 中)的好处,其数值完全匹配我们的标准字符串排序顺序。在其他语言中情况并不那么简单。例如,西班牙语将ñ视为一个独立的字母,位于 n 之后和 o 之前。立陶宛语将 Y 排在 J 之前,威尔士语将 CH 和 DD 等二合字母视为单个字母,CH 排在 C 之后,DD 排在 D 之后。

如果要按用户自然顺序显示字符串,仅使用数组字符串的sort()方法是不够的。但是,如果创建 Intl.Collator 对象,可以将该对象的compare()方法传递给sort()方法,以执行符合区域设置的字符串排序。Intl.Collator 对象可以配置为使compare()方法执行不区分大小写的比较,甚至只考虑基本字母并忽略重音和其他变音符号的比较。

Intl.NumberFormat()Intl.DateTimeFormat()一样,Intl.Collator()构造函数接受两个参数。第一个指定区域设置或区域设置数组,第二个是一个可选对象,其属性精确指定要执行的字符串比较类型。支持的属性如下:

usage

此属性指定如何使用排序器对象。默认值为"sort",但也可以指定"search"。想法是,在对字符串进行排序时,通常希望排序器尽可能区分多个字符串以产生可靠的排序。但是,在比较两个字符串时,某些区域设置可能希望进行较不严格的比较,例如忽略重音。

sensitivity

此属性指定比较字符串时,排序器是否对大小写和重音敏感。值为"base"会忽略大小写和重音,只考虑每个字符的基本字母。(但请注意,某些语言认为某些带重音的字符是不同的基本字母。)"accent"考虑重音但忽略大小写。"case"考虑大小写但忽略重音。"variant"执行严格的比较,考虑大小写和重音。当usage"sort"时,此属性的默认值为"variant"。如果usage"search",则默认灵敏度取决于区域设置。

ignorePunctuation

将此属性设置为true以在比较字符串时忽略空格和标点符号。将此属性设置为true后,例如,字符串“any one”和“anyone”将被视为相等。

numeric

如果要比较的字符串是整数或包含整数,并且希望它们按数字顺序而不是按字母顺序排序,请将此属性设置为true。设置此选项后,例如,字符串“Version 9”将在“Version 10”之前排序。

caseFirst

此属性指定哪种大小写应该优先。如果指定为"upper",则“A”将在“a”之前排序。如果指定为"lower",则“a”将在“A”之前排序。无论哪种情况,请注意相同字母的大写和小写变体将按顺序排列在一起,这与 Unicode 词典排序(数组sort()方法的默认行为)不同,在该排序中,所有 ASCII 大写字母都排在所有 ASCII 小写字母之前。此属性的默认值取决于区域设置,并且实现可能会忽略此属性并不允许您覆盖大小写排序顺序。

一旦为所需区域设置和选项创建了 Intl.Collator 对象,就可以使用其compare()方法比较两个字符串。此方法返回一个数字。如果返回值小于零,则第一个字符串在第二个字符串之前。如果大于零,则第一个字符串在第二个字符串之后。如果compare()返回零,则这两个字符串在此排序器的意义上相等。

此接受两个字符串并返回小于、等于或大于零的数字的compare()方法正是数组sort()方法期望的可选参数。此外,Intl.Collator 会自动将compare()方法绑定到其实例,因此可以直接将其传递给sort(),而无需编写包装函数并通过排序器对象调用它。以下是一些示例:

// A basic comparator for sorting in the user's locale.
// Never sort human-readable strings without passing something like this:
const collator = new Intl.Collator().compare;
["a", "z", "A", "Z"].sort(collator)      // => ["a", "A", "z", "Z"]
// Filenames often include numbers, so we should sort those specially
const filenameOrder = new Intl.Collator(undefined, { numeric: true }).compare;
["page10", "page9"].sort(filenameOrder)  // => ["page9", "page10"]
// Find all strings that loosely match a target string
const fuzzyMatcher = new Intl.Collator(undefined, {
    sensitivity: "base",
    ignorePunctuation: true
}).compare;
let strings = ["food", "fool", "Føø Bar"];
strings.findIndex(s => fuzzyMatcher(s, "foobar") === 0)  // => 2

一些地区有多种可能的排序顺序。例如,在德国,电话簿使用的排序顺序比字典稍微更加语音化。在西班牙,在 1994 年之前,“ch” 和 “ll” 被视为单独的字母,因此该国现在有现代排序顺序和传统排序顺序。在中国,排序顺序可以基于字符编码、每个字符的基本部首和笔画,或者基于字符的拼音罗马化。这些排序变体不能通过 Intl.Collator 选项参数进行选择,但可以通过在区域设置字符串中添加 -u-co- 并添加所需变体的名称来选择。例如,在德国使用 "de-DE-u-co-phonebk" 进行电话簿排序,在台湾使用 "zh-TW-u-co-pinyin" 进行拼音排序。

// Before 1994, CH and LL were treated as separate letters in Spain
const modernSpanish = Intl.Collator("es-ES").compare;
const traditionalSpanish = Intl.Collator("es-ES-u-co-trad").compare;
let palabras = ["luz", "llama", "como", "chico"];
palabras.sort(modernSpanish)      // => ["chico", "como", "llama", "luz"]
palabras.sort(traditionalSpanish) // => ["como", "chico", "luz", "llama"]

11.8 控制台 API

你在本书中看到了 console.log() 函数的使用:在网页浏览器中,它会在浏览器的开发者工具窗格的“控制台”选项卡中打印一个字符串,这在调试时非常有帮助。在 Node 中,console.log() 是一个通用输出函数,将其参数打印到进程的 stdout 流中,在终端窗口中通常会显示给用户作为程序输出。

控制台 API 除了 console.log() 外还定义了许多有用的函数。该 API 不是任何 ECMAScript 标准的一部分,但受到浏览器和 Node 的支持,并已经正式编写和标准化在 https://console.spec.whatwg.org

控制台 API 定义了以下函数:

console.log()

这是控制台函数中最为人熟知的。它将其参数转换为字符串并将它们输出到控制台。它在参数之间包含空格,并在输出所有参数后开始新的一行。

console.debug(), console.info(), console.warn(), console.error()

这些函数几乎与 console.log() 完全相同。在 Node 中,console.error() 将其输出发送到 stderr 流而不是 stdout 流,但其他函数是 console.log() 的别名。在浏览器中,每个函数生成的输出消息可能会以指示其级别或严重性的图标为前缀,并且开发者控制台还可以允许开发者按级别过滤控制台消息。

console.assert()

如果第一个参数为真值(即如果断言通过),则此函数不执行任何操作。但如果第一个参数为 false 或其他假值,则剩余的参数将被打印,就像它们已经被传递给带有“Assertion failed”前缀的 console.error() 一样。请注意,与典型的 assert() 函数不同,当断言失败时,console.assert() 不会抛出异常。

console.clear()

此函数在可能的情况下清除控制台。这在浏览器和在 Node 将其输出显示到终端时有效。但是,如果 Node 的输出已被重定向到文件或管道,则调用此函数没有效果。

console.table()

这个函数是一个非常强大但鲜为人知的功能,用于生成表格输出,特别适用于需要总结数据的 Node 程序。console.table()尝试以表格形式显示其参数(尽管如果无法做到这一点,它会使用常规的console.log()格式)。当参数是一个相对较短的对象数组,并且数组中的所有对象具有相同(相对较小)的属性集时,这种方法效果最佳。在这种情况下,数组中的每个对象被格式化为表格的一行,每个属性是表格的一列。您还可以将属性名称数组作为可选的第二个参数传递,以指定所需的列集。如果传递的是对象而不是对象数组,则输出将是一个具有属性名称列和属性值列的表格。或者,如果这些属性值本身是对象,则它们的属性名称将成为表格中的列。

console.trace()

这个函数像console.log()一样记录其参数,并且在输出后跟随一个堆栈跟踪。在 Node 中,输出会发送到 stderr 而不是 stdout。

console.count()

这个函数接受一个字符串参数,并记录该字符串,然后记录调用该字符串的次数。在调试事件处理程序时,这可能很有用,例如,如果需要跟踪事件处理程序被触发的次数。

console.countReset()

这个函数接受一个字符串参数,并重置该字符串的计数器。

console.group()

这个函数将其参数打印到控制台,就像它们已被传递给console.log()一样,然后设置控制台的内部状态,以便所有后续的控制台消息(直到下一个console.groupEnd()调用)将相对于刚刚打印的消息进行缩进。这允许将一组相关消息视觉上分组并缩进。在 Web 浏览器中,开发者控制台通常允许将分组消息折叠和展开为一组。console.group()的参数通常用于为组提供解释性名称。

console.groupCollapsed()

这个函数与console.group()类似,但在 Web 浏览器中,默认情况下,该组将“折叠”,并且它包含的消息将被隐藏,除非用户点击以展开该组。在 Node 中,此函数是console.group()的同义词。

console.groupEnd()

这个函数不接受任何参数。它不产生自己的输出,但结束了由最近调用的console.group()console.groupCollapsed()引起的缩进和分组。

console.time()

这个函数接受一个字符串参数,记录调用该字符串的时间,并不产生输出。

console.timeLog()

这个函数将一个字符串作为其第一个参数。如果该字符串之前已传递给console.time(),则打印该字符串,然后是自console.time()调用以来经过的时间。如果console.timeLog()有任何额外的参数,它们将被打印,就像它们已被传递给console.log()一样。

console.timeEnd()

这个函数接受一个字符串参数。如果之前已将该参数传递给console.time(),则打印该参数和经过的时间。在调用console.timeEnd()之后,再次调用console.timeLog()而不先调用console.time()是不合法的。

11.8.1 使用控制台进行格式化输出

类似console.log()打印其参数的控制台函数有一个鲜为人知的功能:如果第一个参数是包含%s%i%d%f%o%O%c的字符串,则此第一个参数将被视为格式字符串,⁶,并且后续参数的值将替换两个字符%序列的位置。

序列的含义如下:

%s

参数被转换为字符串。

%i%d

参数被转换为数字,然后截断为整数。

%f

参数被转换为数字

%o%O

参数被视为对象,并显示属性名称和值。(在 Web 浏览器中,此显示通常是交互式的,用户可以展开和折叠属性以探索嵌套的数据结构。)%o%O 都显示对象的详细信息。大写变体使用一个依赖于实现的输出格式,被认为对软件开发人员最有用。

%c

在 Web 浏览器中,参数被解释为一串 CSS 样式,并用于为接下来的任何文本设置样式(直到下一个 %c 序列或字符串结束)。在 Node 中,%c 序列及其对应的参数会被简单地忽略。

请注意,通常不需要在控制台函数中使用格式字符串:通常只需将一个或多个值(包括对象)传递给函数,让实现以有用的方式显示它们即可。例如,请注意,如果将 Error 对象传递给 console.log(),它将自动打印出其堆栈跟踪。

11.9 URL API

由于 JavaScript 在 Web 浏览器和 Web 服务器中被广泛使用,JavaScript 代码通常需要操作 URL。URL 类解析 URL 并允许修改(例如添加搜索参数或更改路径)现有的 URL。它还正确处理了 URL 的各个组件的转义和解码这一复杂主题。

URL 类不是任何 ECMAScript 标准的一部分,但它在 Node 和除了 Internet Explorer 之外的所有互联网浏览器中都可以使用。它在 https://url.spec.whatwg.org 上标准化。

使用 URL() 构造函数创建一个 URL 对象,将绝对 URL 字符串作为参数传递。或者将相对 URL 作为第一个参数传递,将其相对的绝对 URL 作为第二个参数传递。一旦创建了 URL 对象,它的各种属性允许您查询 URL 的各个部分的未转义版本:

let url = new URL("https://example.com:8000/path/name?q=term#fragment");
url.href        // => "https://example.com:8000/path/name?q=term#fragment"
url.origin      // => "https://example.com:8000"
url.protocol    // => "https:"
url.host        // => "example.com:8000"
url.hostname    // => "example.com"
url.port        // => "8000"
url.pathname    // => "/path/name"
url.search      // => "?q=term"
url.hash        // => "#fragment"

尽管不常用,URL 可以包含用户名或用户名和密码,URL 类也可以解析这些 URL 组件:

let url = new URL("ftp://admin:1337!@ftp.example.com/");
url.href       // => "ftp://admin:1337!@ftp.example.com/"
url.origin     // => "ftp://ftp.example.com"
url.username   // => "admin"
url.password   // => "1337!"

这里的 origin 属性是 URL 协议和主机(包括指定的端口)的简单组合。因此,它是一个只读属性。但前面示例中演示的每个其他属性都是读/写的:您可以设置这些属性中的任何一个来设置 URL 的相应部分:

let url = new URL("https://example.com");  // Start with our server
url.pathname = "api/search";               // Add a path to an API endpoint
url.search = "q=test";                     // Add a query parameter
url.toString()  // => "https://example.com/api/search?q=test"

URL 类的一个重要特性是在需要时正确添加标点符号并转义 URL 中的特殊字符:

let url = new URL("https://example.com");
url.pathname = "path with spaces";
url.search = "q=foo#bar";
url.pathname  // => "/path%20with%20spaces"
url.search    // => "?q=foo%23bar"
url.href      // => "https://example.com/path%20with%20spaces?q=foo%23bar"

这些示例中的 href 属性是一个特殊的属性:读取 href 等同于调用 toString():它将 URL 的所有部分重新组合成 URL 的规范字符串形式。将 href 设置为新字符串会重新运行 URL 解析器,就好像再次调用 URL() 构造函数一样。

在前面的示例中,我们一直使用 search 属性来引用 URL 的整个查询部分,该部分由问号到 URL 结尾的第一个井号字符组成。有时,将其视为单个 URL 属性就足够了。然而,HTTP 请求通常使用 application/x-www-form-urlencoded 格式将多个表单字段或多个 API 参数的值编码到 URL 的查询部分中。在此格式中,URL 的查询部分是一个问号,后面跟着一个或多个名称/值对,它们之间用和号分隔。同一个名称可以出现多次,导致具有多个值的命名搜索参数。

如果你想将这些名称/值对编码到 URL 的查询部分中,那么searchParams属性比search属性更有用。search属性是一个可读/写的字符串,允许你获取和设置 URL 的整个查询部分。searchParams属性是一个只读引用,指向一个 URLSearchParams 对象,该对象具有用于获取、设置、添加、删除和排序编码到 URL 查询部分的参数的 API:

let url = new URL("https://example.com/search");
url.search                            // => "": no query yet
url.searchParams.append("q", "term"); // Add a search parameter
url.search                            // => "?q=term"
url.searchParams.set("q", "x");       // Change the value of this parameter
url.search                            // => "?q=x"
url.searchParams.get("q")             // => "x": query the parameter value
url.searchParams.has("q")             // => true: there is a q parameter
url.searchParams.has("p")             // => false: there is no p parameter
url.searchParams.append("opts", "1"); // Add another search parameter
url.search                            // => "?q=x&opts=1"
url.searchParams.append("opts", "&"); // Add another value for same name
url.search                            // => "?q=x&opts=1&opts=%26": note escape
url.searchParams.get("opts")          // => "1": the first value
url.searchParams.getAll("opts")       // => ["1", "&"]: all values
url.searchParams.sort();              // Put params in alphabetical order
url.search                            // => "?opts=1&opts=%26&q=x"
url.searchParams.set("opts", "y");    // Change the opts param
url.search                            // => "?opts=y&q=x"
// searchParams is iterable
[...url.searchParams]                 // => [["opts", "y"], ["q", "x"]]
url.searchParams.delete("opts");      // Delete the opts param
url.search                            // => "?q=x"
url.href                              // => "https://example.com/search?q=x"

searchParams属性的值是一个 URLSearchParams 对象。如果你想将 URL 参数编码到查询字符串中,可以创建一个 URLSearchParams 对象,追加参数,然后将其转换为字符串并设置在 URL 的search属性上:

let url = new URL("http://example.com");
let params = new URLSearchParams();
params.append("q", "term");
params.append("opts", "exact");
params.toString()               // => "q=term&opts=exact"
url.search = params;
url.href                        // => "http://example.com/?q=term&opts=exact"

11.9.1 传统 URL 函数

在之前描述的 URL API 定义之前,已经有多次尝试在核心 JavaScript 语言中支持 URL 转义和解码。第一次尝试是全局定义的escape()unescape()函数,现在已经被弃用,但仍然被广泛实现。不应该使用它们。

escape()unescape()被弃用时,ECMAScript 引入了两对替代的全局函数:

encodeURI()decodeURI()

encodeURI()以字符串作为参数,返回一个新字符串,其中非 ASCII 字符和某些 ASCII 字符(如空格)被转义。decodeURI()则相反。需要转义的字符首先被转换为它们的 UTF-8 编码,然后该编码的每个字节被替换为一个%xx转义序列,其中xx是两个十六进制数字。因为encodeURI()旨在对整个 URL 进行编码,它不会转义 URL 分隔符字符,如/?#。但这意味着encodeURI()无法正确处理 URL 中包含这些字符的各个组件的 URL。

encodeURIComponent()decodeURIComponent()

这一对函数的工作方式与encodeURI()decodeURI()完全相同,只是它们旨在转义 URI 的各个组件,因此它们还会转义用于分隔这些组件的字符,如/?#。这些是传统 URL 函数中最有用的,但请注意,encodeURIComponent()会转义路径名中的/字符,这可能不是你想要的。它还会将查询参数中的空格转换为%20,尽管在 URL 的这部分中应该用+转义空格。

所有这些传统函数的根本问题在于,它们试图对 URL 的所有部分应用单一的编码方案,而事实上 URL 的不同部分使用不同的编码。如果你想要一个格式正确且编码正确的 URL,解决方案就是简单地使用 URL 类来进行所有的 URL 操作。

11.10 定时器

自 JavaScript 诞生以来,Web 浏览器就定义了两个函数——setTimeout()setInterval()——允许程序要求浏览器在指定的时间过去后调用一个函数,或者在指定的时间间隔内重复调用函数。这些函数从未作为核心语言的一部分标准化,但它们在所有浏览器和 Node 中都有效,并且是 JavaScript 标准库的事实部分。

setTimeout()的第一个参数是一个函数,第二个参数是一个数字,指定在调用函数之前应该经过多少毫秒。在指定的时间过去后(如果系统繁忙可能会稍长一些),函数将被调用,不带任何参数。这里,例如,是三个setTimeout()调用,分别在一秒、两秒和三秒后打印控制台消息:

setTimeout(() => { console.log("Ready..."); }, 1000);
setTimeout(() => { console.log("set..."); }, 2000);
setTimeout(() => { console.log("go!"); }, 3000);

请注意,setTimeout()在返回之前不会等待时间过去。这个示例中的三行代码几乎立即运行,但在经过 1,000 毫秒后才会发生任何事情。

如果省略setTimeout()的第二个参数,则默认为 0。然而,这并不意味着您指定的函数会立即被调用。相反,该函数被注册为“尽快”调用。如果浏览器忙于处理用户输入或其他事件,可能需要 10 毫秒或更长时间才能调用该函数。

setTimeout()注册一个函数,该函数将在一次调用后被调用。有时,该函数本身会调用setTimeout()以安排在将来的某个时间再次调用。然而,如果要重复调用一个函数,通常更简单的方法是使用setInterval()setInterval()接受与setTimeout()相同的两个参数,但每当指定的毫秒数(大约)过去时,它会重复调用函数。

setTimeout()setInterval()都会返回一个值。如果将此值保存在变量中,您随后可以使用它通过传递给clearTimeout()clearInterval()来取消函数的执行。返回的值在 Web 浏览器中通常是一个数字,在 Node 中是一个对象。实际类型并不重要,您应该将其视为不透明值。您可以使用此值的唯一操作是将其传递给clearTimeout()以取消使用setTimeout()注册的函数的执行(假设尚未调用)或停止使用setInterval()注册的函数的重复执行。

这是一个示例,演示了如何使用setTimeout()setInterval()clearInterval()来显示一个简单的数字时钟与 Console API:

// Once a second: clear the console and print the current time
let clock = setInterval(() => {
    console.clear();
    console.log(new Date().toLocaleTimeString());
}, 1000);
// After 10 seconds: stop the repeating code above.
setTimeout(() => { clearInterval(clock); }, 10000);

当我们讨论异步编程时,我们将再次看到setTimeout()setInterval(),详见第十三章。

11.11 总结

学习一门编程语言不仅仅是掌握语法。同样重要的是研究标准库,以便熟悉语言附带的所有工具。本章记录了 JavaScript 的标准库,其中包括:

  • 重要的数据结构,如 Set、Map 和类型化数组。
  • 用于处理日期和 URL 的 Date 和 URL 类。
  • JavaScript 的正则表达式语法及其用于文本模式匹配的 RegExp 类。
  • JavaScript 的国际化库,用于格式化日期、时间和数字以及对字符串进行排序。
  • 用于序列化和反序列化简单数据结构的JSON对象和用于记录消息的console对象。

¹ 这里记录的并非 JavaScript 语言规范定义的所有内容:这里记录的一些类和函数首先是在 Web 浏览器中实现的,然后被 Node 采用,使它们成为 JavaScript 标准库的事实成员。

² 这种可预测的迭代顺序是 JavaScript 集合中的另一件事,可能会让 Python 程序员感到惊讶。

³ 当 Web 浏览器添加对 WebGL 图形的支持时,类型化数组首次引入到客户端 JavaScript 中。ES6 中的新功能是它们已被提升为核心语言特性。

⁴ 除了在字符类(方括号)内部,\b匹配退格字符。

⁵ 使用正则表达式解析 URL 并不是一个好主意。请参见§11.9 以获取更健壮的 URL 解析器。

⁶ C 程序员将从printf()函数中认出许多这些字符序列。

相关文章
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
24 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
33 0
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
42 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
60 0
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
93 2
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
13天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
55 4
|
13天前
|
XML 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(2)
JavaScript 权威指南第七版(GPT 重译)(六)
60 4
JavaScript 权威指南第七版(GPT 重译)(六)(2)
|
13天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(六)(1)
JavaScript 权威指南第七版(GPT 重译)(六)
28 3
JavaScript 权威指南第七版(GPT 重译)(六)(1)
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(4)
JavaScript 权威指南第七版(GPT 重译)(五)
39 9
|
13天前
|
前端开发 JavaScript 程序员
JavaScript 权威指南第七版(GPT 重译)(五)(3)
JavaScript 权威指南第七版(GPT 重译)(五)
36 8