有些数据类型,如数字和字符串(第3章)、对象(第6章)和数组(第7章)是JavaScript的基础,我们可以将它们视为语言本身的一部分。本章介绍了其他重要但不太基础的API,它们可以被认为是JavaScript的“标准库”:这些是JavaScript内置的有用类和函数,可用于web浏览器和Node中的所有JavaScript程序。1
这一章的章节是相互独立的,你可以按任何顺序阅读。它们包括:
本章中的一些部分,特别是,关于类型化数组和正则表达式的部分相当长,因为在有效地使用这些类型之前,您需要了解一些重要的背景信息。然而,其他许多部分都很短:它们只是介绍了一个新的API并展示了一些使用它的示例。
JavaScript的Object类型是一种通用的数据结构,可以用来将字符串(对象的属性名)映射到任意值。当映射到的值是固定的,比如true,那么对象实际上就是一组字符串。
在JavaScript编程中,对象实际上经常被用作映射和集合,但这又受到属性必须是字符串的限制,并且由于对象通常继承名为“toString”的属性而变得复杂,这些属性通常不是映射或集合的一部分。
因此,ES6引入了真正的Set和Map类,我们将在下面的小节中介绍这些类。
集合是值的集合,就像数组一样。但是,与数组不同的是,集合不被排序或编制索引,并且它们不允许重复:一个值要么是集合的成员,要么不是一个成员;不可能询问一个值在一个集合中出现了多少次。
使用Set()构造函数创建Set对象:
let s = new Set(); // 一个新的空集合
let t = new Set([1, s]); // 一个有2个成员的新集合
Set()构造函数的参数不必是数组:允许任何可迭代对象(包括其他Set对象):
let t = new Set(s); // 复制s元素的新集合。
let unique = new Set("Mississippi"); // 4个元素:“M”、“i”、“s”和“p”
集合的size属性类似于数组的length属性:它告诉您集合包含多少个值:
unique.size // => 4
集合创建时不需要必须初始化。可以使用add()、delete()和clear()随时添加和删除元素。请记住,集合不能包含重复项,因此在集合中已包含该值时向其添加值不会产生任何效果:
let s = new Set(); // 空集合
s.size // => 0
s.add(1); // 添加一个数字
s.size // => 1; 现在集合有一个成员
s.add(1); // 再添加同样的数字
s.size // => 1; 大小不变
s.add(true); // 添加另一个值;注意可以添加混合类型
s.size // => 2
s.add([1, 2, 3]); // 添加数组值
s.size // => 3; 数组已添加,而不是其元素
s.delete(1) // => true: 已成功删除元素1
s.size // => 2: 大小减小为2
s.delete("test") // => false: “test”不是成员,删除失败
s.delete(true) // => true: 删除成功
s.delete([1, 2, 3]) // => false: 与集合中的数组不是同一个
s.size // => 1: 集合中仍然有一个数组
s.clear(); // 清空集合
s.size // => 0
关于此代码,有几点需要注意:
===
运算符所执行的那样。集合可以同时包含数字1和字符串“1”,因为它认为它们是不同的值。当值是对象(或数组或函数)时,它们也会进行比较,就像使用===
。这就是为什么我们不能从这段代码中的集合中删除数组元素。我们在集合中添加了一个数组,然后试图通过向delete()方法传递一个不同的数组(尽管元素相同)来删除该数组。为了使其工作,我们必须传递一个完全相同数组的引用。Python程序员要注意:这是JavaScript和Python集之间的一个显著区别。Python集比较成员的相等性,而不是引用的相等性,但是折衷的是Python集只允许添加不可变的成员(如元组),而不允许将列表和词典添加到集合中。
实际上,我们对集合做的最重要的事情不是在集合中添加或删除元素,而是检查指定的值是否是集合的成员。我们使用has()方法执行此操作:
let oneDigitPrimes = new Set([2, 3, 5, 7]);
oneDigitPrimes.has(2) // => true: 2是一个一位数的素数
oneDigitPrimes.has(3) // => true: 3也是
oneDigitPrimes.has(4) // => false: 4 不是素数
oneDigitPrimes.has("5") // => false: "5" 甚至不是一个数字
关于集合最重要的一点是,它们是为成员资格测试而优化的,无论集合有多少成员,has()方法都会非常快。数组的includes()方法也执行成员资格测试,但所需的时间与数组的大小成正比,使用数组作为集合可能比使用真正的集合对象慢得多。
Set类是可迭代的,这意味着您可以使用for/of循环来枚举集合的所有元素:
let sum = 0;
for (let p of oneDigitPrimes) {
// 循环一位数素数的数组
sum += p; // 求和
}
sum // => 17: 2 + 3 + 5 + 7
由于集合是可迭代的,你可以使用…展开运算符把它们转化成数组或者参数列表:
[...oneDigitPrimes] // => [2,3,5,7]: 转换为数组的集合
Math.max(...oneDigitPrimes) // => 7: 集合元素作为函数参数传递
集合通常被描述为“无序集合”。然而,对于JavaScript集合类来说,这并不完全正确。JavaScript集合没有索引:不能像数组那样要求集合的第一个或第三个元素。但是JavaScript Set类总是记住元素插入的顺序,并且在迭代集合时总是使用这个顺序:插入的第一个元素将是第一个迭代的元素(假设您没有首先删除它),最近插入的元素将是最后一个迭代的元素。2
除了可迭代之外,Set类还实现了一个与同名的与数组方法类似的forEach()方法:
let product = 1;
oneDigitPrimes.forEach(n => {
product *= n; });
product // => 210: 2 * 3 * 5 * 7
数组的forEach()将数组索引作为第二个参数传递给指定的函数。集合没有索引,因此Set类的这个方法只将元素值同时作为第一个和第二个参数传递。
映射Map对象表示一组称为键的值,其中每个键都有另一个与之关联(或“映射到”)的值。从某种意义上说,映射就像数组,但它不使用一组连续的整数作为键,而是允许我们使用任意值作为“索引”。与数组一样,映射速度很快:无论映射有多大,查找与键相关联的值都会很快(尽管不如索引数组那么快)。
使用Map()构造函数创建新映射:
let m = new Map(); // 创建一个新的空映射
let n = new Map([ // 一个新映射,使用字符串作为键,数字作为值来初始化
["one", 1],
["two", 2]
]);
Map()构造函数的可选参数应该是一个可生成两元素[key,value]数组的可迭代对象。在实践中,这意味着如果您想在创建映射时初始化它,您通常会将所需的键和相关联的值写成数组。但也可以使用Map()构造函数复制其他映射或从现有对象复制属性名称和值:
let copy = new Map(n); // 与映射n具有相同键和值的新映射
let o = {
x: 1, y: 2}; // 具有两个属性的对象
let p = new Map(Object.entries(o)); // 等同于new Map([["x", 1], ["y", 2]])
创建映射对象后,可以使用get()查询与给定键关联的值,也可以使用set()添加新的键/值对。不过,请记住,映射是一组键,每个键都有一个关联的值。这与一组键/值对不太一样。如果使用映射中已存在的键调用set(),则将更改与该键关联的值,而不是添加新的键/值映射。除了get()和set()之外,Map类还定义了与set方法类似的方法:使用has()检查映射是否包含指定的键;使用delete()从映射中删除键(及其关联的值);使用clear()从映射中删除所有键/值对;使用size属性确定映射包含多少个键。
let m = new Map(); // 从一个空映射开始
m.size // => 0: 空映射没有键
m.set("one", 1); // 键"one" 映射到值 1
m.set("two", 2); // 键"two" 映射到值 2.
m.size // => 2: 映射有2个键
m.get("two") // => 2: 返回与键“two”关联的值
m.get("three") // => undefined: 这个键不在映射中
m.set("one", true); // 更改现有键的映射值
m.size // => 2: 大小不变
m.has("one") // => true: 映射含有键"one"
m.has(true) // => false: 映射不含有键true
m.delete("one") // => true: 键存在,删除成功
m.size // => 1
m.delete("three") // => false: 无法删除不存在的键
m.clear(); // 清空映射
与Set的add()方法一样,Map的Set()方法也可以链接,这样就可以在不使用数组的数组的情况下初始化映射:
let m = new Map().set("one", 1).set("two", 2).set("three", 3);
m.size // => 3
m.get("two") // => 2
与Set一样,任何JavaScript值都可以用作映射中的键或值。这包括null、undefined和NaN,以及对象和数组等引用类型。与Set类一样,Map按标识(引用)而不是相等性来比较键,因此如果使用对象或数组作为键,则会认为它与其他所有对象和数组都不同,即使这些对象和数组具有完全相同的属性或元素:
let m = new Map(); // 一个空映射
m.set({
}, 1); // 把一个空对象关联值1
m.set({
}, 2); // 把另一个空对象关联值2
m.size // => 2: 映射中有2个键
m.get({
}) // => undefined: 但是这个空对象不是键
m.set(m, undefined); // 映射自身到值undefined
m.has(m) // => true: m是一个键
m.get(m) // => undefined: 如果m不是键,我们也会得到相同的值
Map对象是可迭代的,每个迭代值都是一个两元素数组,其中第一个元素是键,第二个元素是与该键关联的值。如果在Map对象中使用展开操作符,您将得到一个数组的数组,就像我们传递给Map()构造函数的数组一样。当使用for/of循环迭代映射时,惯用的做法是使用解构赋值将键和值分配给单独的变量:
let m = new Map([["x", 1], ["y", 2]]);
[...m] // => [["x", 1], ["y", 2]]
for (let [key, value] of m) {
// 在第一次迭代中,key为“x”,value为1
// 在第二次迭代中,key为“y”,value为2
}
像Set类一样,Map类按插入顺序迭代。迭代的第一个键/值对将是最早添加到映射中的,迭代的最后一个将是最近添加的一个。
如果只想迭代映射的键或关联值,请使用keys()和values()方法:这些方法返回可迭代对象,这些对象按插入顺序迭代键和值。(entries()方法返回迭代键/值对的可迭代对象,但这与直接迭代映射完全相同。)
[...m.keys()] // => ["x", "y"]: 只有键
[...m.values()] // => [1, 2]: 只有值
[...m.entries()] // => [["x", 1], ["y", 2]]: 类似 [...m]
映射对象也可以使用数组类首先实现的forEach()方法进行迭代。
m.forEach((value, key) => {
// 注意是 value, key, 不是 key, value
// 在第一次迭代中, value 为 1,key 为 "x"
// 在第二次迭代中, value 为 2,key 为 "y"
});
在上面的代码中,value参数出现在key参数之前似乎很奇怪,因为在for/of迭代中,key在前面。如本节开头所述,可以将映射视为一个通用数组,其中整数数组索引被任意键替换。数组的forEach()方法首先传递数组元素,然后传递数组索引,因此,通过类比,映射的forEach()方法首先传递映射值,然后传递映射健。
WeakMap类是Map类的一个变体(但不是实际的子类),它不会阻止其键值被垃圾回收。垃圾回收是JavaScript解释器回收不再“可访问”且程序无法使用的对象的内存的过程。一个常规的映射包含对其键值的“强”引用,即使对它们的所有其他引用都不存在,它们仍然可以通过映射访问。相反,WeakMap保留对其键值的“弱”引用,因此它们无法通过WeakMap访问,并且它们在映射中的存在不会阻止它们的内存被回收。
WeakMap()构造函数与Map()构造函数类似,但WeakMap和Map之间存在一些显著差异:
WeakMap的预期用途是允许您将值与对象关联,而不会导致内存泄漏。例如,假设您正在编写一个函数,该函数接受对象参数,并且需要对该对象执行一些耗时的计算。为了提高效率,您希望缓存计算的值,以便以后重用。如果使用映射对象实现缓存,则将防止任何对象被回收,但是通过使用WeakMap,可以避免此问题。(通常可以使用私有符号属性直接缓存对象上的计算值,从而获得类似的结果。见§6.10.3.)
WeakSet实现了一组不阻止这些对象被垃圾回收的对象。WeakSet()构造函数的工作方式与Set()构造函数类似,但WeakSet对象与Set对象的不同之处与WeakMap对象与Map对象的不同之处相同:
WeakSet并不常用:它的应用与WeakMap的应用类似。例如,如果要将对象标记(或“标识”)为具有某些特殊属性或类型,可以将其添加到WeakSet中。然后,在其他地方,当您要检查该属性或类型时,可以测试该WeakSet中的成员资格。使用普通集合执行此操作将防止所有标记的对象被垃圾回收,但在使用WeakSet时,这不是一个问题。
常规JavaScript数组可以有任何类型的元素,并且可以动态地增长或收缩。JavaScript实现执行许多优化,因此JavaScript数组的普通操作非常快。然而,它们仍然与C和Java等低级语言的数组类型有很大的不同。类型化数组是ES6中的新特性3,它更接近于这些语言的低级数组。类型化数组在技术上不是数组(Array.isArray()返回false),但它们实现了§7.8中描述的所有数组方法以及它们自己的一些方法。但是,它们在一些非常重要的方面与常规数组不同:
JavaScript不定义TypedArray类。相反,有11种类型的数组,每种类型都有不同的元素类型和构造函数:
构造函数 | 数字类型 |
---|---|
Int8Array() | 有符号字节 |
Uint8Array() | 无符号字节 |
Uint8ClampedArray() | 不带取模的无符号字节 |
Int16Array() | 有符号16位短整数 |
Uint16Array() | 无符号16位短整数 |
Int32Array() | 有符号32位整数 |
Uint32Array() | 无符号32位整数 |
BigInt64Array() | 带符号64位BigInt值(ES2020) |
BigUint64Array() | 无符号64位BigInt值(ES2020) |
Float32Array() | 32位浮点值 |
Float64Array() | 64位浮点值:常规JavaScript数字 |
名称以Int开头的类型包含1、2或4字节(8、16或32位)的有符号整数。名称以Uint开头的类型包含相同长度的无符号整数。“BigInt”和“BigUint”类型保存64位整数,用JavaScript表示为BigInt值(见§3.2.5)。以Float开头的类型保留浮点数。Float64Array的元素与普通JavaScript数字的类型相同。Float32Array的元素精度较低,范围较小,但只需要一半的内存。(这种类型在C和Java中称为float)
Uint8ClampedArray是Uint8Array上的一个特殊情况变体。这两种类型都包含无符号字节,可以表示0到255之间的数字。在Uint8Array中,如果将大于255或小于零的值存储到数组元素中,它将“取模舍入”并获得其他值。这就是计算机内存在低级模式下的工作方式,所以这是非常快的。Uint8ClampedArray执行一些额外的类型检查,这样,如果存储的值大于255或小于0,它将“固定”到255或0,并且不会"取模舍入"。(这个’Clamped’行为是HTML
每个类型化数组构造函数都有一个BYTES_PER_ELEMENT属性,值为1、2、4或8,具体取决于类型。
创建类型化数组的最简单方法是使用一个数字参数调用适当的构造函数,该参数指定您希望在数组中使用的元素数量:
let bytes = new Uint8Array(1024); // 1024 字节
let matrix = new Float64Array(9); // 一个 3x3 矩阵
let point = new Int16Array(3); // 三维空间中的点
let rgba = new Uint8ClampedArray(4); // 4字节RGBA像素值
let sudoku = new Int8Array(81); // 9x9数独板
以这种方式创建类型化数组时,保证数组元素都初始化为0、0n或0.0。但是,如果知道类型化数组中需要的值,则也可以在创建数组时指定这些值。每个类型化数组构造函数都有静态from()和of()工厂方法,其工作方式类似于Array.from()和Array.of():
let white = Uint8ClampedArray.of(255, 255, 255, 0); // RGBA不透明白色
回想一下Array.from()工厂方法需要一个类似数组或可迭代对象作为其第一个参数。对于类型化数组变量也是如此,只是可迭代或类似数组的对象也必须有数字元素。例如,字符串是可迭代的,但是将它们传递给类型化数组的from()工厂方法是没有意义的。
如果只使用from()的单参数版本,则可以删除.from并将可迭代或类似数组的对象直接传递给构造函数,后者的行为完全相同。请注意,构造函数和from()工厂方法都允许您复制现有类型化数组,同时可以更改类型:
let ints = Uint32Array.from(white); // 相同的4个数字,但作为整数
从现有数组、可迭代对象或类似数组的对象创建新的类型化数组时,这些值可能会被截断以适应数组的类型约束。发生这种情况时不会出现警告或错误:
// 浮点被截断为整数,较长的整数被截断为8位
Uint8Array.of(1.23, 2.99, 45000) // => new Uint8Array([1, 2, 200])
最后,还有一种方法可以创建包含ArrayBuffer类型的类型化数组。ArrayBuffer是对内存块的不透明引用。可以使用构造函数创建一个;只需传入要分配的内存字节数:
let buffer = new ArrayBuffer(1024*1024);
buffer.byteLength // => 1024*1024; 一兆字节内存
ArrayBuffer类不允许读取或写入已分配的任何字节。但是您可以创建类型化数组,这些数组使用缓冲区的内存,并且允许您读写内存。为此,调用类型化数组构造函数,第一个参数是ArrayBuffer,第二个参数是数组缓冲区内的字节偏移量,第三个参数是数组长度(以元素为单位,而不是字节)。第二个和第三个参数是可选的。如果两者都省略,那么数组将使用数组缓冲区中的所有内存。如果只省略length参数,则数组将使用数组开始位置和结束位置之间的所有可用内存。关于这种类型化数组构造函数,还要记住一件事:数组必须与内存对齐,因此如果指定字节偏移量,则值应该是类型大小的倍数。例如,Int32Array()构造函数需要4的倍数,Float64Array()需要8的倍数。
给定前面创建的ArrayBuffer,可以创建如下类型的数组:
let asbytes = new Uint8Array(buffer); //以字节形式查看
let asints = new Int32Array(buffer); // 以32位有符号整数形式查看
let lastK = new Uint8Array(buffer, 1023*1024); // 最后1K字节(字节)
let ints2 = new Int32Array(buffer, 1024, 256); // 把第2个1K字节看做256个整数
这四个类型的数组为ArrayBuffer表示的内存提供了四个不同的视图。理解所有类型化数组都有一个底层ArrayBuffer是很重要的,即使您没有显式地指定一个。如果您在不传递缓冲区对象的情况下调用类型数组构造函数,则会自动建立适当大小的缓冲区。如后所述,任何类型数组的buffer属性都引用其底层ArrayBuffer对象。直接使用ArrayBuffer对象的原因是,有时您可能需要单个缓冲区的多个类型化数组视图。
创建类型化数组后,可以使用普通方括号表示法读取和写入其元素,就像处理任何其他类似数组的对象一样:
// 返回小于n的最大素数,使用Eratosthenes筛法
function sieve(n) {
let a = new Uint8Array(n + 1); // 如果x是合数,a[x]为1
let max = Math.floor(Math.sqrt(n)); // 不要使用比这更高的系数
let p = 2; // 2 是第一个素数
while (p <= max) {
// 对于小于max的素数
for (let i = 2 * p; i <= n; i += p) // 将p的倍数标记为合数
a[i] = 1;
while (a[++p]) /* empty */; // 下一个未标记的索引是素数
}
while (a[n]) n--; // 向后循环找到最后一个素数
return n; // 返回它
}
这里的函数计算小于指定数字的最大质数。代码与普通JavaScript数组的代码完全相同,但是使用Uint8Array()而不是Array()可以使代码运行速度提高四倍多,并且在测试中使用的内存也减少了八倍。
类型化数组不是真正的数组,但它们重新实现了大多数数组方法,因此您可以像使用常规数组一样使用它们:
let ints = new Int16Array(10); // 10 个短整型
ints.fill(3).map(x=>x*x).join("") // => "9999999999"
请记住,类型化数组具有固定长度,因此length属性是只读的,并且不会为类型化数组实现更改数组长度的方法(如push()、pop()、unshift()、shift()和splice())。实现了在不更改长度的情况下更改数组内容的方法(例如sort()、reverse()和fill())。像map()和slice()这样返回新数组的方法返回的类型化数组与调用它们的类型相同。
除了标准数组方法外,类型化数组还实现了一些自己的方法。set()方法将普通数组或类型数组的元素复制到此类型数组中,同时设置类型数组的多个元素:
let bytes = new Uint8Array(1024); // 1K字节的缓冲区
let pattern = new Uint8Array([0,1,2,3]); // 4字节数组
bytes.set(pattern); // 将它们复制到另一个字节数组的开头
bytes.set(pattern, 4); // 以不同的偏移量再次复制它们
bytes.set([0,1,2,3], 8); // 或者直接从常规数组复制值
bytes.slice(0, 12) // => new Uint8Array([0,1,2,3,0,1,2,3,0,1,2,3])
set()方法的第一个参数是数组或类型数组,可选的第二个参数是元素偏移量,如果未指定,默认值为0。如果要将值从一个类型数组复制到另一个类型数组,则操作可能非常快。
类型数组还有一个subarray方法,该方法返回在其上调用的数组的一部分:
let ints = new Int16Array([0,1,2,3,4,5,6,7,8,9]); // 10 短整型
let last3 = ints.subarray(ints.length-3, ints.length); // 最后3个元素
last3[0] // => 7: 相当于ints[7]
subarray()采用与slice()方法相同的参数,工作方式似乎相同。但有一个重要的区别。slice()返回不与原始数组共享内存的新的独立类型数组中的指定元素。subarray()不复制任何内存;它只返回相同底层数据的新视图:
ints[9] = -1; // 更改原始数组中的值...
last3[2] // => -1: 它也会在子数组中发生变化
subarray()方法返回现有数组的新视图这一事实使我们回到ArrayBuffers的主题。每个类型数组都有三个与底层缓冲区相关的属性:
last3.buffer // 类型数组的ArrayBuffer对象
last3.buffer === ints.buffer // => true: 两者都是同一个缓冲区的视图
last3.byteOffset // => 14: 此视图从缓冲区的字节14开始
last3.byteLength // => 6: 此视图的长度为6字节(3个16位整数)
last3.buffer.byteLength // => 20: 但是底层缓冲区有20个字节
buffer属性是数组的ArrayBuffer。byteOffset是数组数据在底层缓冲区中的起始位置。byteLength是数组数据的字节长度。对于任何类型数组a,此不变量应始终为true:
a.length * a.BYTES_PER_ELEMENT === a.byteLength // => true
ArrayBuffer只是不透明的字节块。可以使用类型数组访问这些字节,但ArrayBuffer本身不是类型数组。但是要小心:您可以对ArrayBuffer使用数值数组索引,就像对任何JavaScript对象一样。这样做不能让您访问缓冲区中的字节,但可能会导致令人困惑的bug:
let bytes = new Uint8Array(8);
bytes[0] = 1; // 将第一个字节设置为1
bytes.buffer[0] // => undefined: 缓冲区没有索引0
bytes.buffer[1] = 255; // 尝试错误地在缓冲区中设置字节
bytes.buffer[1] // => 255: 这只是设置一个常规的JS属性
bytes[1] // => 0: 上一行的代码并没有修改该字节
我们在前面看到,可以使用ArrayBuffer()构造函数创建ArrayBuffer,然后创建使用该缓冲区的类型数组。另一种方法是创建初始类型数组,然后使用该数组的缓冲区创建其他视图:
let bytes = new Uint8Array(1024); // 1024 字节
let ints = new Uint32Array(bytes.buffer); // 或者 256 个整数
let floats = new Float64Array(bytes.buffer); // 或者 128 个浮点数
类型数组允许您以8、16、32或64位的块来查看相同的字节序列。这暴露了“字节序”:字节排列成更长字的顺序。为了提高效率,类型数组使用底层硬件的本地字节序。在小端系统中,一个数的字节在数组缓冲区中从最低有效值到最高有效值排列。在大端平台上,字节从最高有效位到最低有效位排列。您可以使用以下代码来确定底层平台的字节序:
// 如果整数0x00000001在内存中被安排为01 00 00 00,
// 那么我们就在一个小端平台上。在大端平台上,我们将得到字节00 00 00 01。
let littleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;
今天,最常见的CPU架构是小端。然而,许多网络协议和一些二进制文件格式都需要大端字节排序。如果要对来自网络或文件的数据使用类型数组,则不能仅假设平台字节序与数据的字节顺序匹配。通常,在处理外部数据时,可以使用Int8Array和Uint8Array将数据作为单个字节的数组查看,但不应使用其他具有多字节字大小的类型数组。相反,您可以使用DataView类,该类定义从ArrayBuffer读取和写入值的方法,该方法具有显式指定的字节顺序:
// 假设我们有一个二进制数据的类型化数组要处理。
// 首先,我们创建一个DataView对象,这样我们就可以灵活地从这些字节读写值
let view = new DataView(bytes.buffer,
bytes.byteOffset,
bytes.byteLength);
let int = view.getInt32(0); // 从字节0读取大端字节序的带符号整型
int = view.getInt32(4, false); // 下一个整型也是大端字节序
int = view.getUint32(8, true); // 下一个是小端字节序并且是无符号的
view.setUint32(8, int, false); // 用大端序把数据写回去
DataView为10个类型数组类(不包括Uint8ClampedArray)定义10个get方法。它们的名称类似于getInt16()、getUint32()、getBigInt64()和getFloat64()。第一个参数是ArrayBuffer中值开始的字节偏移量。除了getInt8()和getUint8()之外,所有这些get方法都接受一个可选的布尔值作为它们的第二个参数。如果第二个参数被省略或为false,则使用大端字节顺序。如果第二个参数为真,则使用小端字节序。
DataView还定义了10个相应的set方法,这些方法将值写入底层ArrayBuffer。第一个参数是值开始的偏移量。第二个参数是要写入的值。除setInt8()和setUint8()外,每个方法都接受可选的第三个参数。如果参数被省略或为false,则以大端格式写入值,其中最重要的字节在前面。如果参数为true,则以小端格式写入值,并将最低有效字节放在第一位。
类型数组和DataView类为您提供了处理二进制数据所需的所有工具,并使您能够编写JavaScript程序来执行诸如解压缩ZIP文件或从JPEG文件中提取元数据等操作。
正则表达式是描述文本模式的对象。JavaScript RegExp类表示正则表达式,String和RegExp都定义了使用正则表达式对文本执行强大的模式匹配、搜索和替换功能的方法。然而,为了有效地使用RegExp API,还必须学习如何使用正则表达式语法来描述文本模式,正则表达式语法本质上是一种小型编程语言。幸运的是,JavaScript正则表达式语法与许多其他编程语言使用的语法非常相似,因此您可能已经熟悉它了。(如果不是这样,那么在学习JavaScript正则表达式方面所做的努力可能对您在其他编程环境中也很有用。)
下面的小节首先描述正则表达式语法,然后在解释如何编写正则表达式之后,解释如何将它们用于String和RegExp类的方法。
在JavaScript中,正则表达式由RegExp对象表示。当然,可以使用RegExp()构造函数创建RegExp对象,但它们通常是使用特殊的字面量语法创建的。就像字符串字面量被指定为引号内的字符一样,正则表达式字面量也被指定为一对斜杠(/)字符包围的字符串。因此,您的JavaScript代码可能包含以下行:
let pattern = /s$/;
这行代码创建一个新的RegExp对象并将其分配给变量pattern。这个特定的RegExp对象匹配任何以字母“s”结尾的字符串。这个正则表达式可以用等效地RegExp()构造函数定义,如下所示:
let pattern = new RegExp("s$");
正则表达式模式规范由一系列字符组成。大多数字符,包括所有字母数字字符,都只是按字面意思描述要匹配的字符。因此,正则表达式/java/匹配任何包含子字符串“java”的字符串。正则表达式中的其他字符不按字面匹配,但具有特殊意义。例如,正则表达式/s / 包 含 两 个 字 符 。 第 一 个 “ s ” 与 字 面 意 思 相 符 。 第 二 个 “ /包含两个字符。第一个“s”与字面意思相符。第二个“ /包含两个字符。第一个“s”与字面意思相符。第二个“”是与字符串结尾匹配的特殊元字符。因此,此正则表达式匹配包含字母“s”作为其最后一个字符的任何字符串。
我们将看到,正则表达式也可以有一个或多个标志字符,这些字符会影响它们的工作方式。标志在RegExp文本中的第二个斜杠字符之后指定,或者作为RegExp()构造函数的第二个字符串参数指定。例如,如果我们想匹配以“s”或“S”结尾的字符串,我们可以将i标志与正则表达式一起使用,以指示我们需要不区分大小写的匹配:
let pattern = /s$/i;
下面几节介绍JavaScript正则表达式中使用的各种字符和元字符。
所有字母字符和数字在正则表达式中完全匹配自身。JavaScript正则表达式语法还通过以反斜杠(\)开头的转义序列支持某些非字母字符。例如,序列\n与字串中的换行符匹配。表11-1列出了这些字符。
表11-1. 正则表达式字面量字符
字符 | 匹配 |
---|---|
字母数字字符 | 自身 |
\0 | NUL 字符 (\u0000) |
\t | 制表符 (\u0009) |
\n | 换行符 (\u000A) |
\v | 垂直制表符 (\u000B) |
\f | 换页符 (\u000C) |
\r | 回车符(\u000D) |
\xnn | 由十六进制数nn指定的拉丁字符;例如,\x0A与\n相同。 |
\uxxxx | 由十六进制数xxxx指定的Unicode字符;例如,\u0009与\t相同。 |
\u{n} | 由码位n指定的Unicode字符,其中n是0到10FFFF之间的1到6个十六进制数字。请注意,只有使用u标志的正则表达式才支持此语法。 |
\cX | 控制字符^X;例如,\cJ相当于换行符\n。 |
许多标点符号在正则表达式中具有特殊的含义。他们是:
^ $ . * + ? = ! : | \ / ( ) [ ] { }
这些字符的含义将在下面的章节中讨论。只有在某些特定的语境中,其中一些些字符才有特定的意义。但是,一般来说,如果要在正则表达式中按字面意思包括这些标点字符中的任何一个,则必须在它们前面加一个\。其他标点符号,如引号和@,没有特殊含义,只是在正则表达式中按字面匹配。
如果您记不清哪些标点字符需要用反斜杠转义,可以安全地将反斜杠放在任何标点字符之前。另一方面,请注意,许多字母和数字在前面加反斜杠时有特殊的含义,因此任何要匹配的字母或数字都不应该用反斜杠转义。当然,要在正则表达式中包括反斜杠字符,必须用反斜杠对其进行转义。例如,以下正则表达式匹配任何包含反斜杠的字符串:/\/。(如果使用RegExp()构造函数,请记住正则表达式中的任何反斜杠都需要加倍,因为字符串也使用反斜杠作为转义字符。)
单个字面量字符可以通过放在方括号内组合成字符类。字符类与其中包含的任何一个字符匹配。因此,正则表达式/[abc]/匹配字母a、b或c中的任何一个。还可以定义否定字符类;这些类匹配除括号内的字符外的任何字符。通过将插入脱字符(^)作为左括号内的第一个字符来指定否定字符类。正则表达式/[^abc]/匹配a、b或c以外的任何一个字符。字符类可以使用连字符来表示字符范围。要匹配拉丁字母表中的任何一个小写字符,请使用/[a-z]/,要匹配拉丁字母表中的任何字母或数字,请使用/[a-zA-Z0-9]/。(如果您想在字符类中包含一个实际的连字符,只需将其作为右括号前的最后一个字符。)
由于通常使用某些字符类,JavaScript正则表达式语法包含特殊字符和转义序列来表示这些公共类。例如,\s与空格字符、制表符和任何其他Unicode空白字符匹配;\S与非“Unicode空白”的任何字符匹配。表11-2列出了这些字符,并总结了字符类语法。(注意,这些字符类转义序列中的一些仅匹配ASCII字符,并且未扩展到使用Unicode字符。但是,您可以显式定义自己的Unicode字符类;例如,/[\u0400-\u04FF]/匹配任何一个西里尔字符。)
表11-2. 正则表达式字符类
字符 | 匹配 |
---|---|
[…] | 方括号内的任意字符 |
[^…] | 不在方括号内的任意字符 |
. | 除换行符或其他Unicode行终止符之外的任何字符。或者,如果RegExp使用s标志,则句点匹配任何字符,包括行结束符。 |
\w | 任何ASCII字字符。相当于[a-zA-Z0-9_]。 |
\W | 不是ASCII字字符的任何字符。相当于[^a-zA-Z0-9_]。 |
\s | 任何Unicode空白字符。 |
\S | 非Unicode空白的任何字符。 |
\d | 任何ASCII数字。相当于[0-9]。 |
\D | ASCII数字以外的任何字符。相当于[^0-9]。 |
[\b] | 字面量的退格(特殊情况)。 |
注意,特殊字符类转义符可以用在方括号内。\s匹配任何空白字符,而\d匹配任何数字,因此/[\s\d]/匹配任何一个空白字符或数字。注意有一个特殊情况。正如您稍后将看到的,转义符\b具有特殊的含义。在字符类中使用时,它表示退格字符。因此,要在正则表达式中按字面量表示退格字符,请使用带有一个元素的字符类类:/[\b]/。
Unicode字符类
在ES2018中,如果正则表达式使用u标志,则支持字符类\p{…}及其否定\P{…}。(到2020年初,Node、Chrome、Edge和Safari都已经支持,Firefox还不支持。)这些字符类基于Unicode标准定义的属性,它们所代表的字符集可能会随着Unicode的发展而改变。\d字符类只匹配ASCII数字。如果要匹配世界上任何书写系统中的一个十进制数字,可以使用/\p{Decimal_Number}/u。如果要匹配任何语言中不是十进制数字的任何一个字符,则可以将p大写并写入\P{Decimal_Number}。如果要匹配任何数字(如字符),包括分数和罗马数字,可以使用\p{Number}。注意,“Decimal_Number”和“Number”不是JavaScript或正则表达式语法特有的:它是由Unicode标准定义的一类字符的名称。
\w 字符类仅适用于ASCII文本,但使用\p,我们按如下写法实现国际化版本:
/[\p{Alphabetic}\p{Decimal_Number}\p{Mark}]/u
(尽管要与世界上复杂的语言完全兼容,我们还需要添加“Connector_Punctuation”和“Join_Control”这两个类别。)
作为最后一个示例,\p语法还允许我们定义与特定字母表或脚本中的字符匹配的正则表达式:
let greekLetter = /\p{Script=Greek}/u; let cyrillicLetter = /\p{Script=Cyrillic}/u;
使用到目前为止所学的正则表达式语法,可以将两位数的数字描述为/\d\d/,将四位数的数字描述为/\d\d\d\d/。但是你没有任何方法来描述,例如,一个数字可以是任意数字,或者是一个由三个字母组成的字符串,后跟一个可选的数字。这些更复杂的模式使用正则表达式语法来指定正则表达式的元素可以重复多少次。
指定重复的字符始终遵循其应用的模式。因为某些类型的重复非常普遍,所以有一些特殊的字符来表示这些情况。例如,+匹配前一个模式的一个或多个副本。
表11-3总结了重复语法。
表11-3. 正则表达式重复字符
字符 | 含义 |
---|---|
{n,m} | 与前一项匹配至少n次,但不超过m次。 |
{n,} | 将前一项匹配n次或更多次。 |
{n} | 匹配前一项n次。 |
? | 匹配前一项的0次或1次。也就是说,前一项是可选的。相当于{0,1}。 |
+ | 匹配前一项1次或多次。相当于{1,}。 |
* | 匹配0次或多次。相当于{0,}。 |
以下几行显示了一些示例:
let r = /\d{2,4}/; // 匹配2~4个数字
r = /\w{3}\d?/; // 匹配3个字符和一个可选数字
r = /\s+java\s+/; // 匹配前后带有一个或多个空格的字符串'java'
r = /[^(]*/; // 匹配一个或多个非左括号的字符
请注意,在所有这些示例中,重复说明符应用于它们前面的单个字符或字符类。如果要匹配更复杂表达式的重复,则需要用括号定义一个组,这将在下面的部分中进行解释。
使用*和?重复字符时要小心。由于这些字符可能与前面任何字符匹配0次,因此允许它们不匹配任何内容。例如,正则表达式/a*/实际上与字符串“bbbb”匹配,因为该字符串中字母a的出现次数为零!
表11-3中列出的重复字符尽可能多地匹配,同时仍然允许正则表达式的任何后续部分匹配。我们说这种重复是“贪婪的”,也可以规定重复应该以非贪婪的方式进行。只需在重复字符后面加一个问号:??, +?, *?,甚至{1,5}?。例如,正则表达式/a+/匹配一个或多个字母a。当应用于字符串“aaa”时,它将匹配所有三个字母。但是/a+?/匹配字母a的一个或多个出现,根据需要匹配尽可能少的字符。当应用于同一个字符串时,此模式只匹配第一个字母a。
使用非贪婪的重复可能并不总是产生你期望的结果。考虑模式/a+b/,它匹配一个或多个a,后跟字母b。当应用于字符串“aaab”时,它匹配整个字符串。现在让我们使用非贪婪版本:/a+?b/。这应该与字母b前面的字母a尽可能少匹配。当应用于同一个字符串“aaab”时,您可能希望它只匹配一个a和最后一个字母b。但是,实际上,这个模式匹配整个字符串,就像贪婪版本的模式一样。这是因为正则表达式模式匹配是通过查找字符串中第一个可能匹配的位置来完成的。因为可以从字符串的第一个字符开始匹配,所以从不考虑从后续字符开始的较短匹配。
正则表达式语法包含用于指定替代项、分组子表达式和引用先前子表达式的特殊字符。|字符分隔备选方案。例如,/ab|cd|ef/匹配字符串“ab”或字符串“cd”或字符串“ef”。/\d{3}|[a-z]{4}/匹配三位数或四个小写字母。
请注意,在找到匹配项之前,将从左到右考虑备选方案。如果左边备选匹配了,那么就会忽略右边的备选方案,即使它是一个比较好的匹配。因此,当模式/a|ab/应用于字符串“ab”时,它只匹配第一个字母。
圆括号在正则表达式中有多种用途。一个目的是将单独的项分组到一个子表达式中,以便可以通过|,*,+,?应用于整个子表达式。例如,/java(script)?/匹配“java”后跟可选的“script”。/(ab|cd)+|ef/匹配字符串“ef”或字符串“ab”或“cd”的一个或多个重复。
圆括号在正则表达式的另一个目的是在正则表达式中定义子模式。当正则表达式与目标字符串成功匹配时,可以提取目标字符串中与任何特定的带圆括号的子模式匹配的部分。(您将在本节后面看到如何获得这些匹配的子字符串。)例如,假设您正在查找一个或多个小写字母,后跟一个或多个数字。您可以使用/[a-z]+\d+/模式。但是假设你只关心每一场比赛结束时的数字。如果将模式的这一部分放在括号(/[a-z]+(\d+)/)中,则可以从找到的任何匹配项中提取数字,如后面所述。
圆括号子表达式的相关用法是允许您稍后在同一正则表达式中引用子表达式。这是通过在\字符后面加上一个或多个数字来完成的。数字表示带圆括号的子表达式在正则表达式中的位置。例如,\ 1表示第一个子表达式,\ 3表示第三个子表达式。请注意,因为子表达式可以嵌套在其他表达式中,所以它的位置是参与计数的左括号的位置。例如,在以下正则表达式中,嵌套子表达式([Ss]script)可以用\2来代替:
/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/
对正则表达式中前一个子表达式的引用,并不是指该子表达式模式的引用,而是指与该模式匹配的文本的引用。因此,可以使用引用来强制约束字符串的各个部分包含完全相同的字符。例如,以下正则表达式匹配单引号或双引号内的零个或多个字符。但是,它不要求左引号和右引号匹配(即两个单引号或两个双引号):
/[’"][^’"]*[’"]/
为了要求引号匹配,请使用引用:
/([’"])[^’"]*\1/
\1与第一个带圆括号的子表达式匹配。在本例中,它强制要求右引号与左引号匹配。此正则表达式不允许在双引号字符串中使用单引号,反之亦然。(在字符类中使用引用是不合法的,因此不能写入:/([’”])[^\1]*\1/)
当我们稍后讨论RegExp API时,您将看到这种对带圆括号的子表达式的引用是正则表达式搜索和替换操作的强大功能。
同样,在正则表达式中不用创建带数字编码的引用,也可以对子表达式进行分组。它不是以"(“和”)“进行分组,而是以”(?:“和”)"来进行分组,比如,考虑下面这个模式:
/([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/
在本例中,子表达式(?:[Ss]cript)?)只用于分组,那么?重复字符可以应用于这个组。但是这些修改过的圆括号不会生成引用,因此在此正则表达式中,\2引用匹配的文本(fun\w*)。
表11-4总结了正则表达式的选择、分组和引用运算符。
表11-4. 正则表达式选择、分组和引用字符
字符 | 含义 |
---|---|
选择:匹配左边的子表达式或者右边的子表达式。 | |
(…) | 分组:将几个项分组一个单元,这个单元可以通过 *, +, ?, | 进行修饰,而且可以供后面引用使用。 |
(?:…) | 仅分组:将项目分组到一个单元中,但不记住与该组匹配的字符。 |
\n | 和第n个分组第一次匹配的字符相匹配,组是圆括号中的子表达式(也有可能是嵌套的),组索引是从左到右的左括号数,"(?:"形式的分组不编码。 |
命名捕获分组
ES2018标准化了一个新功能,它可以使正则表达式更加语义清晰,更易于理解。这个新特性被称为“命名捕获组”,它允许我们将名称与正则表达式中的每个左括号相关联,这样我们就可以按名称而不是数字来引用匹配的文本。同样重要的是:使用名称可以让阅读代码的人更容易理解正则表达式这一部分的用途。到2020年初,这个特性已经在Node、Chrome、Edge和Safari中实现,但Firefox还没有实现。要命名组,请使用(?<…>代替(,并将名称放在尖括号之间。例如,以下是一个正则表达式,可用于检查美国邮寄地址最后一行的格式:
/(?
\w+) (? [A-Z]{2}) (? \d{5})(? -\d{4})?/ 请注意组名提供了多少上下文以使正则表达式更易于理解。在§11.3.2中,当我们讨论String的replace()和match()方法以及RegExp的exec()方法时,您将看到RegExp API如何允许您引用按名称而不是按位置匹配每个组的文本。
如果要在正则表达式中引用命名的捕获组,也可以按名称执行。我们必须在前面的正则表达式中使用一个双引号来匹配前面的引用。我们可以使用命名的捕获组和命名的反向引用重写此RegExp,如下所示:
/(?
['"])[^'"]*\k/\k
是对捕获左引号的命名组的命名反向引用。
如前所述,正则表达式的许多元素与字符串中的单个字符匹配。例如,\s与空白字符匹配。其他正则表达式元素匹配字符之间的位置,而不是实际字符。例如,\b匹配ASCII单词边界,即\w(ASCII单词)和\W(非ASCII单词)之间的边界,或ASCII单词与字符串开头或结尾之间的边界4。像\b这样的元素不匹配某个可见的字符,它们指定的是可以发生匹配的合法位置。有时,这些元素被称为正则表达式的锚,因为它们将模式定位到搜索字符串中的特定位置。最常用的锚定元素是^,它用来匹配字符串的开头,锚元素$用来匹配字符串的末尾。
例如,要在一行中单独匹配单词“JavaScript”,可以使用正则表达式/^JavaScript$/。如果您想将“Java”单独作为一个单词来搜索(而不是像“JavaScript”中那样作为前缀),那么可以尝试使用/\sJava\s/模式,这需要在单词前后加一个空格。但是这个解决方案有两个问题。首先,它与字符串开头或结尾的“Java”不匹配,但前提是它在两边都有空格。其次,当这个模式找到匹配项时,它返回的匹配字符串有前导空格和尾随空格,这不是所需要的。因此,与其将实际空格字符与\s匹配,不如将单词边界与\b匹配(或定位)。结果表达式为/\bJava\b/。元素\B将匹配项定位到非单词边界的位置。因此,/\B[Ss]script/模式匹配“JavaScript”和“postscript”,但不匹配“script”或“Scripting”。
任意正则表达式都可以作为定位条件。如果在符号"(?=“和”)"之间加入一个表达式,它就是一个先行断言,用以说明圆括号内的表达式必须正确匹配,但并不是真正意义上的匹配。比如,要匹配一种常用的程序设计语言的名字,但只在其后有冒号时才匹配,可以使用/[Jj]ava([Ss]cript)?(?=:)/。这个正则表达式可以匹配“JavaScript” in “JavaScript: The Definitive Guide”,但是不能匹配“Java in a Nutshell”中的“Java”,因为它后面没有 冒号。
带有"(?!"的断言是负向先行断言,用来指定接下来的字符都不必匹配。例如,/Java(?! Script)([A-Z]\w*)/可以匹配“Java”后跟一个大写字母和任意数量的ASCII单词,只要“Java”后面不跟“Script”。它匹配“JavaBeans”,但不匹配“Javanese”,它匹配“JavaScrip”,但不匹配“JavaScript”或“JavaScripter”。表11-5总结了正则表达式的锚。
表11-5. 正则表达式锚字符
字符 | 含义 |
---|---|
^ | 匹配字符串的开头,或者用m标志的多行检索中匹配一行的开头。 |
$ | 匹配字符串的结尾,或者用m标志的多行检索中匹配一行的结尾。 |
\b | 匹配单词边界。也就是说,匹配\w字符和\w字符或\w字符与字符串开头或结尾之间的位置。(但是请注意[\b]匹配的是退格符。) |
\B | 匹配非单词边界的位置。 |
(?=p) | 零宽正向先行断言,要求接下来的字符都与p匹配,但不能包括匹配p的那些字符 |
(?!p) | 零宽负向先行断言,要求接下来的字符不与p匹配 |
后行断言
ES2018扩展了正则表达式语法以允许“后行”断言。这些类似于先行断言,但引用当前匹配位置之前的文本。到2020年初,这些都是在Node、Chrome和Edge中实现的,而Firefox或Safari并没有实现。
使用(?<=…)指定一个正向后行断言,(?/(?<= [A-Z]{2} )\d{5}/
您可以将前面没有Unicode货币符号的一串数字与如下所示的负向后行断言相匹配:
/(?
每个正则表达式都可以有一个或多个与其关联的标志,以更改其匹配行为。JavaScript定义了六个可能的标志,每个标志都由一个字母表示。标志在正则表达式文本的第二个/字符后指定,或作为作为第二个参数传递给RegExp()构造函数的字符串。支持的标志及其含义是:
g
i
m
s
u
y
这些标志可以任何组合和顺序指定。例如,如果您希望正则表达式能够识别Unicode以进行不区分大小写的匹配,并且打算使用它来查找字符串中的多个匹配项,则可以指定标志uig、gui或这三个字母的任何其他排列。
到目前为止,我们一直在描述用于定义正则表达式的语法,但没有解释如何在JavaScript代码中实际使用这些正则表达式。现在我们将讨论使用RegExp对象的API。本节首先解释使用正则表达式执行模式匹配以及搜索和替换操作的字符串方法。接下来的章节通过讨论RegExp对象及其方法和属性,继续讨论JavaScript正则表达式的模式匹配。
字符串支持四种使用正则表达式的方法。最简单的是search()。此方法接受正则表达式参数并返回第一个匹配子字符串开头的字符位置,如果不匹配,则返回-1:
"JavaScript".search(/script/ui) // => 4
"Python".search(/script/ui) // => -1
如果search()的参数不是正则表达式,则首先通过将其传递给RegExp构造函数将其转换为正则表达式。search()不支持全局搜索;它忽略正则表达式参数的g标志。
replace()方法执行搜索和替换操作。它以正则表达式作为第一个参数,以替换字符串作为第二个参数。它在调用它的字符串中搜索与指定模式匹配的项。如果正则表达式设置了g标志,replace()方法将用替换字符串中的所有匹配项;否则,它只替换它找到的第一个匹配项。如果replace()的第一个参数是字符串而不是正则表达式,则该方法将按字面量方式搜索该字符串,而不是像search()那样使用RegExp()构造函数将其转换为正则表达式。例如,您可以使用replace(),按如下所示的方法在文本字符串中搜索替换为统一的大写单词“JavaScript”:
// 不管它是如何大写的,都要用正确的大写字母替换它
text.replace(/javascript/gi, "JavaScript");
然而,replace()的功能比这更强大。回想一下,正则表达式的带圆括号的子表达式是从左到右编号的,并且正则表达式会记住每个子表达式匹配的文本。如果替换字符串参数中出现一个$后跟一个数字,则replace()将用与指定子表达式匹配的文本替换这两个字符。这是一个非常有用的功能。例如,可以使用它将字符串中的引号替换为其他字符:
// 引用是一个引号后跟任意数量的非引号字符(我们捕获这些字符),再接着是另一个引号。
let quote = /"([^"]*)"/g;
// 用书名号替换双引号,保留引号文本(存储在$1中)不变。
'He said "stop"'.replace(quote, '«$1»') // => 'He said «stop»'
如果RegExp使用命名捕获组,则可以按名称而不是按编号引用匹配的文本:
let quote = /"(?[^"]*)"/g ;
'He said "stop"'.replace(quote, '«$»' ) // => 'He said «stop»'
您也可以传递一个函数来计算替换值,而不是将替换字符串作为第二个参数传递给replace()。替换函数使用多个参数调用。首先是整个匹配文本。接下来,如果RegExp有捕获组,则这些组捕获的子字符串将作为参数传递。下一个参数是在字符串中找到匹配项的位置。之后,将传递调用replace()的整个字符串。最后,如果RegExp包含任何命名的捕获组,则替换函数的最后一个参数是一个对象,其属性名与捕获组名匹配,其值为匹配的文本。例如,以下代码使用替换函数将字符串中的十进制整数转换为十六进制:
let s = "15 times 15 is 225";
s.replace(/\d+/gu, n => parseInt(n).toString(16)) // => "f times f is e1"
match()方法是最通用的字符串正则表达式方法。它将正则表达式作为其唯一的参数(或通过将其传递给RegExp()构造函数将其参数转换为正则表达式),并返回包含匹配结果的数组,如果未找到匹配项,则返回null。如果正则表达式设置了g标志,则该方法将返回字符串中出现的所有匹配项的数组。例如:
"7 plus 8 equals 15".match(/\d+/g) // => ["7", "8", "15"]
如果正则表达式没有设置g标志,match()不会执行全局搜索;它只搜索第一个匹配项。在这种非全局的情况下,match()仍然返回一个数组,但是数组元素完全不同。如果没有g标志,则返回数组的第一个元素是匹配字符串,其余元素都是匹配正则表达式中带圆括号的捕获组的子字符串。因此,如果match()返回数组a,[0]包含完全匹配,则[1]包含匹配第一个带圆括号表达式的子字符串,依此类推。要用replace()方法类比,a[1]与$1相同,a[2]与$2相同,依此类推。
例如,考虑使用以下代码解析URL5:
// 一个非常简单的URL解析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 = /(?\w+):\/\/(?[\w.]+)\/(?\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"
根据RegExp是否设置了g标志,可以看到match()的行为完全不同。当设置y标志时,在行为上也有一些重要但不那么显著的差异。回想一下,y标志通过限制字符串中匹配的起始位置使正则表达式变得“粘滞”。如果RegExp同时设置了g和y标志,那么match()将返回一个匹配字符串数组,就像在g没有y的情况下一样。但是第一个匹配必须从字符串的开头开始,并且每个后续匹配必须从紧跟在前一个匹配项之后的字符开始。
如果设置了y标志但是没有g,那么match()将尝试查找单个匹配项,并且在默认情况下,此匹配项被约束到字符串的开头。但是,可以通过在要匹配的索引处设置RegExp对象的lastIndex属性来更改此默认匹配开始位置。如果找到匹配项,则此lastIndex将自动更新为匹配后的第一个字符,因此,如果再次调用match(),在本例中,它将查找后续匹配项。(对于指定开始下一个匹配的位置的属性,lastIndex可能看起来是一个奇怪的名称。在讨论RegExp的exec()方法时,我们将再次看到它,它的名称在该上下文中可能更有意义。)
let vowel = /[aeiou]/y; // 粘滞元音匹配
"test".match(vowel) // => null: "test" 没有以元音开头
vowel.lastIndex = 1; // 指定不同的匹配位置
"test".match(vowel)[0] // => "e": 我们在位置1找到了一个元音
vowel.lastIndex // => 2: lastIndex已自动更新
"test".match(vowel) // => null: 位置2没有元音
vowel.lastIndex // => 0: lastIndex在匹配失败后重置
值得注意的是,将非全局正则表达式传递给字符串的match()方法与将字符串传递给正则表达式的exec()方法相同:返回的数组及其属性在两种情况下都是相同的。
matchAll()方法在ES2020中定义,到2020年初,它由现代web浏览器和Node实现。matchAll()需要一个设置了g标志的RegExp。但是,它不是像match()那样返回匹配子字符串的数组,而是返回一个迭代器,该迭代器生成match()在与非全局RegExp一起使用时返回的match对象的类型。这使得matchAll()成为遍历字符串中所有匹配项的最简单、最通用的方法。
您可以使用matchAll()在文本字符串中循环遍历单词:
// 单词边界之间的一个或多个Unicode字母字符
const words = /\b\p{Alphabetic}+\b/gu; // \p Firefox尚不支持
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}.`);
}
String对象的最后一个正则表达式方法是split()。此方法使用参数作为分隔符,将调用它的字符串拆分为子字符串数组。它可以与以下字符串参数一起使用:
"123,456,789".split(",") // => ["123", "456", "789"]
split()方法还可以使用正则表达式作为参数,这允许您指定更通用的分隔符。这里我们指定一个分隔符,它的两边都包含任意数量的空白:
"1, 2, 3,\n4, 5".split(/\s*,\s*/) // => ["1", "2", "3", "4", "5"]
令人惊讶的是,如果使用RegExp分隔符调用split(),并且正则表达式包含捕获组,则与捕获组匹配的文本将包含在返回的数组中。例如:
const htmlTag = /<([^>]+)>/; // < 后跟一个或多个非 >,然后是 >
"Testing
1,2,3".split(htmlTag) // => ["Testing", "br/", "1,2,3"]
本节介绍RegExp()构造函数、RegExp实例的属性以及RegExp类定义的两个重要模式匹配方法。
RegExp()构造函数接受一个或两个字符串参数并创建一个新的RegExp对象。此构造函数的第一个参数是一个字符串,它包含正则表达式的正文,即在正则表达式文本中斜杠中出现的文本。请注意,字符串文本和正则表达式都将\字符用于转义序列,因此当您将正则表达式作为字符串文本传递给RegExp()时,必须将每个\字符替换为\\。RegExp()的第二个参数是可选的。如果提供,则表示正则表达式标志。它应该是g,i,m,s,u,y,或者这些字母的任意组合。
例如:
// 在一个字符串中找到所有五位数。注意这个例子中的双\\。
let zipcode = new RegExp("\\d{5}", "g");
当动态创建正则表达式时,RegExp()构造函数非常有用,因此无法用正则表达式字面量语法表示。例如,要搜索用户输入的字符串,必须在运行时使用RegExp()创建正则表达式。
RegExp()除了第一个参数传递字符串外,也可以传递RegExp对象。这允许您复制正则表达式并更改其标志:
let exactMatch = /JavaScript/;
let caseInsensitive = new RegExp(exactMatch, "i");
RegExp对象具有以下属性:
source
flags
global
ignoreCase
multiline
dotAll
unicode
sticky
lastIndex
RegExp类的test()方法是使用正则表达式的最简单方法。它接受一个字符串参数,如果字符串与模式匹配,则返回true;如果不匹配,则返回false。
test()只需调用下一节中描述的(更复杂的)exec()方法,并在exec()返回非空值时返回true。因此,如果将test()与使用g或y标志的RegExp一起使用,则其行为取决于RegExp对象的lastIndex属性的值,该值可能会意外更改。请参阅稍后的“lastIndex属性和RegExp重用”
小节来了解更多详细信息。
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(),以便在字符串中循环所有正则表达式匹配项。(尽管我们已经描述过,在ES2020和更高版本中,String的matchAll()方法是遍历所有匹配项的更简单的方法。)例如,以下代码中的循环将运行两次:
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}`);
}
lastIndex属性和RegExp重用
正如您已经看到的,JavaScript的正则表达式API非常复杂。在这个API中,使用带有g和y标志的lastIndex属性是一个特别尴尬的部分。使用这些标志时,在调用match()、exec()或test()方法时需要特别小心,因为这些方法的行为取决于lastIndex,而lastIndex的值取决于以前对RegExp对象所做的操作。这使得编写有缺陷的代码变得很容易。
例如,假设我们想要在一个HTML文本字符串中找到所有标记的索引。我们可以这样写代码:
let match, positions = []; while((match = /
/g
.exec(html)) !== null) { // 可能的无限循环 positions.push(match.index); }这段代码做不到我们想要的。如果html字符串至少包含一个
标记,那么它将永远循环。问题是我们在while循环条件中使用RegExp字面量。对于循环的每次迭代,我们都会创建一个新的RegExp对象,lastIndex设置为0,因此exec()总是从字符串的开头开始,如果有匹配项,它将不断匹配。当然,解决方案是定义一次RegExp,并将其保存到一个变量中,以便在循环的每次迭代中使用相同的RegExp对象。
另一方面,有时重用RegExp对象是错误的。例如,假设我们要遍历字典中的所有单词,以查找包含双字母对的单词:
let dictionary = ["apple", "book", "coffee"]; let doubleLetterWords = []; let doubleLetter = /(\w)\1/g; for (let word of dictionary) { if (doubleLetter.test(word)) { doubleLetterWords.push(word); } } doubleLetterWords // => ["apple", "coffee"]: "book" 丢失了!
因为我们在RegExp上设置了g标志,所以在成功匹配之后,lastIndex属性将被更改,test()方法(基于exec())开始在lastIndex指定的位置搜索匹配项。在匹配“apple”中的“pp”之后,lastIndex是3,因此我们开始在第3位搜索单词“book”,但是没有看到它包含的“oo”。
我们可以通过删除g标志来解决这个问题(在这个特定的示例中,这实际上不是必需的),或者将RegExp字面量移动到循环的主体中,以便在每次迭代时重新创建它,或者在每次调用test()之前显式地将lastIndex重置为零。
这里的思想是,lastIndex使RegExp API容易出错。所以在使用g或y标志和循环时要格外小心。在ES2020和更高版本中,使用字符串matchAll()方法而不是exec()来避免这个问题,因为match All()不会修改lastIndex。
Date类是JavaScript处理日期和时间的API。使用Date()构造函数创建一个Date对象。它不带参数,返回表示当前日期和时间的Date对象:
let now = new Date(); // 当前时间
如果传递一个数值参数,Date()构造函数将该参数解释为自1970年以来的毫秒数:
let epoch = new Date(0); // 1970年1月1日午夜格林尼治标准时间
如果指定两个或多个整数参数,它们将被解释为本地时区中的年、月、日、时、分、秒和毫秒,如下所示:
let century = new Date(2100, // 2100 年
0, // 1月
1, // 1日
2, 3, 4, 5); // 02:03:04.005, 本地时间
DateAPI的一个怪癖是一年的第一个月是数字0,但是一个月的第一天是数字1。如果省略时间字段,Date()构造函数会将它们全部默认为0,将时间设置为午夜。
注意,当用多个数字调用时,Date()构造函数使用本地计算机设置的任何时区来解释它们。如果要以UTC(世界协调时间,又称GMT)指定日期和时间,则可以使用Date.UTC()。 此静态方法采用与Date()构造函数相同的参数,以UTC格式解释它们,并返回一个毫秒时间戳,您可以将其传递给Date()构造函数:
// 英格兰2100年1月1日的午夜
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"); // ISO格式的日期
一旦有了日期对象,各种get和set方法允许您查询和修改日期的年、月、日、时、分、秒和毫秒字段。这些方法都有两种形式:一种使用本地时间获取或设置,另一种使用UTC时间获取或设置。例如,要获取或设置日期对象的年份,可以使用getFullYear()、getUTCFullYear()、setFullYear()或setUTCFullYear():
let d = new Date(); // 从当前日期开始
d.setFullYear(d.getFullYear() + 1); // 增加年份
要获取或设置日期的其他字段,请将方法名称中的“FullYear”替换为“Month”、“Date”、“Hours”、“Minutes”、“Seconds”或“Milliseconds”。有些日期set方法允许您一次设置多个字段。setFullYear()和setUTC FullYear()还允许您设置月份和月份的日期。setHours()和setUTCHours()允许您在小时字段之外指定分钟、秒和毫秒字段。
注意,查询日期的方法是getDate()和getUTCDate()。更自然的函数getDay()和getUTCDay()返回星期几(0表示星期日,6表示星期六)。星期几是只读的,因此没有相应的setDay()方法。
JavaScript在内部将日期表示为整数,指定自UTC时间1970年1月1日午夜(或之前)起的毫秒数。支持8640000000000000这样大的整数,因此JavaScript不会在超过270000年的时间内耗尽毫秒。
对于任何日期对象,getTime()方法返回这个内部值,setTime()方法设置它。因此,您可以使用以下代码为日期增加30秒,例如:
d.setTime(d.getTime() + 30000);
这些毫秒值有时称为时间戳,直接使用它们比使用日期对象更有用。静态的Date.now()方法以时间戳的形式返回当前时间,当您要度量代码运行所需的时间时,该方法非常有用:
let startTime = Date.now();
reticulateSplines(); // 做一些耗时的操作
let endTime = Date.now();
console.log(`Spline reticulation took ${
endTime - startTime}ms.`);
高精度时间戳
Date.now()返回的时间戳以毫秒为单位。对于计算机来说,毫秒实际上是一个相对较长的时间,有时您可能希望用更高的精度来测量经过的时间。这个performance.now()函数允许这样做:它还返回基于毫秒的时间戳,但返回值不是整数,因此它包含毫秒的分数。performance.now()不像Date.now()返回的值是一个绝对的时间戳。相反,它只是指示自加载网页或节点进程启动以来已过的时间。
performance对象是一个更大的Performance API的一部分,它不是由ECMAScript标准定义的,而是由web浏览器和Node实现的。要在Node中使用performance对象,必须将其导入:允许网络上的高精度计时可能会让无良网站对访问者进行指纹级别程度的识别,因此浏览器(尤其是Firefox)默认可能会降低performance.now()的精准度。作为一个web开发人员,您应该能够以某种方式重新启用高精度计时(例如通过在Firefox中设置 privacy.reduceTimerPrecision为false)。const { performance } = require("perf_hooks");
日期对象可以使用JavaScript的标准<、<=、>和>=比较运算符进行比较。你可以用一个日期对象减去另一个日期对象来确定两个日期之间的毫秒数。(这是因为Date类定义了一个返回时间戳的valueOf()方法。)
如果要从日期中添加或减去指定的秒数、分钟数或小时数,通常最简单的方法是修改时间戳,如前一个示例所示,我们给一个日期增加30秒。如果你想增加天数,这项技术会变得更加麻烦,而且它对不同的月或年根本不起作用,因为它们的天数不同。要进行包含天、月和年的日期算术,可以使用setDate()、setMonth()和setYear()。例如,这里的代码在当前日期的基础上增加了三个月零两个星期:
let d = new Date();
d.setMonth(d.getMonth() + 3, d.getDate() + 14);
日期设置方法即使溢出也能正常工作。当我们将当前月份加上三个月后,我们可以得到一个大于11(代表12月份)的值。setMonth()通过根据需要增加年份来处理这个问题。类似地,当我们将一个月的日期设置为一个大于该月天数的值时,该月将相应地递增。
如果您使用Date类来实际跟踪日期和时间(而不仅仅是测量时间间隔),那么您可能需要向代码的用户显示日期和时间。Date类定义了许多将Date对象转换为字符串的不同方法。以下是一些示例:
let d = new Date(2020, 0, 1, 17, 10, 30); // 2020年元旦下午5:10:30
d.toString() // => "Wed Jan 01 2020 17:10:30 GMT+0800 (中国标准时间)"
d.toUTCString() // => "Wed, 01 Jan 2020 09:10:30 GMT"
d.toLocaleDateString() // => "2020/1/1",中国时区
d.toLocaleTimeString() // => "下午5:10:30",中国时区
d.toISOString() // => "2020-01-01T09:10:30.000Z"
以下是Date类的字符串格式设置方法的完整列表:
toString()
toUTCString()
toISOString()
toLocaleString()
toDateString()
toLocaleDateString()
toTimeString()
toLocaleTimeString()
当格式化要显示给最终用户的日期和时间时,这些日期到字符串的方法都不理想。请参见§11.7.2,以了解更通用和基于区域设置的日期和时间格式化技术。
最后,除了这些将日期对象转换为字符串的方法外,还有一个静态的Date.parse()方法,该方法将字符串作为参数,尝试将其解析为日期和时间,并返回表示该日期的时间戳。Date.parse()能够解析与Date()构造函数相同的字符串,并且保证能够解析toISOString()、toutString()和toString()的输出。
JavaScript throw和catch语句可以抛出和捕捉任何JavaScript值,包括原始值。没有必须用于发出错误信号的异常类型。不过,JavaScript确实定义了一个Error类,传统的做法是在用throw发出错误信号时使用Error实例或子类。使用Error对象的一个很好的原因是,当您创建错误时,它会捕获JavaScript堆栈的状态,如果异常未捕获,则堆栈跟踪将显示错误消息,这将帮助您调试问题。(请注意,堆栈跟踪显示错误对象的创建位置,而不是throw语句将其抛出的位置。如果始终在使用throw new Error()抛出对象之前创建该对象,则不会造成任何混乱。)
Error对象有两个属性:message和name,以及一个toString()方法。message属性的值是传递给Error()构造函数的值,如有必要,将其转换为字符串。对于使用Error()创建的Error对象,name属性始终为“Error”。toString()方法只返回name属性的值,后跟冒号和空格,以及message属性的值。
尽管它不是ECMAScript标准的一部分,Node和所有现代浏览器也在错误对象上定义stack属性。此属性的值是一个多行字符串,其中包含创建错误对象时JavaScript调用堆栈的堆栈跟踪。当捕捉到意外错误时,这可能是有用的日志信息。
除了Error类之外,JavaScript还定义了许多子类,用它们来表示ECMAScript定义的特定类型的错误。这些子类是EvalError、RangeError、ReferenceError、SyntaxError、TypeError和URIError。如果这些错误类看起来合适,可以在自己的代码中使用它们。与基本错误类一样,每个子类都有一个接受单个消息参数的构造函数。每个子类的实例都有一个name属性,其值与构造函数名称相同。
您可以随意定义自己的错误子类,这样能最好地封装您自己程序的错误条件。请注意,您不限于name和message属性。如果创建子类,则可以定义新属性以提供错误详细信息。例如,如果您正在编写一个解析器,您可能会发现定义一个ParseError类会更有用,该类具有指定解析失败的确切位置的行和列属性。或者,如果您正在处理HTTP请求,您可能需要定义一个HTTPError类,该类的status属性包含失败请求的HTTP状态代码(例如404或500)。
例如:
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"
当一个程序需要保存数据或需要通过网络连接将数据传输到另一个程序时,它必须将其内存中的数据结构转换成可以保存或传输的字节或字符串,然后再进行解析以恢复原来的内存中数据结构。将数据结构转换为字节或字符流的过程称为序列化(或封送处理,甚至叫pickling)。
在JavaScript中序列化数据的最简单方法是使用称为JSON的序列化格式。这个缩略词代表“JavaScript对象表示法”,顾名思义,该格式使用JavaScript对象和数组字面量语法将由对象和数组组成的数据结构转换为字符串。JSON支持原始数字和字符串,以及true、false和null值,以及从这些原始值构建的数组和对象。JSON不支持其他JavaScript类型,如Map、Set、RegExp、Date或类型数组。尽管如此,它已经被证明是一种非常通用的数据格式,并且即使在非基于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]}
如果我们省略了序列化数据保存到文件或通过网络发送的部分,我们可以使用这对函数作为创建对象深层副本的低效方法:
// 制作任何可序列化对象或数组的深层副本
function deepcopy(o) {
return JSON.parse(JSON.stringify(o));
}
JSON是JavaScript的一个子集
当数据序列化为JSON格式时,结果是表达式的有效JavaScript源代码,该表达式的计算结果是原始数据结构的副本。如果将var data=作为JSON字符串的前缀,并将结果传递给eval(),则将获得分配给变量数据的原始数据结构的副本。但是,您永远不应该这样做,因为这是一个巨大的安全漏洞,如果攻击者可以将任意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()对我们将字符串转换回数据结构的能力没有影响。
如果JSON.stringify()被要求序列化JSON格式本身不支持的值,它将查看该值是否具有toJSON()方法,如果是,则调用该方法,然后将返回值字符串化以代替原始值。Date对象实现toJSON():它返回与toISOString()方法相同的字符串。这意味着,如果序列化包含日期的对象,则日期将自动转换为字符串。当您解析序列化字符串时,重新创建的数据结构将与您开始使用的结构不完全相同,因为原始对象是一个日期,而重建的只是一个字符串。
如果需要重新创建日期对象(或以任何其他方式修改已解析的对象),可以将“恢复”函数作为第二个参数传递给JSON.parse()。如果指定了,则对从输入字符串解析的每个原始值(但不包括包含这些原语值的对象或数组)调用一次该“恢复”函数。使用两个参数调用函数。第一个是属性名,可以是对象属性名,也可以是转换为字符串的数组索引。第二个参数是该对象属性或数组元素的原始值。此外,该函数是作为包含原始值的对象或数组的方法调用的,所以您可以使用this关键字引用包含该对象的对象。
恢复函数的返回值将成为命名属性的新值。如果返回第二个参数,则属性将保持不变。如果它返回undefined,JSON.parse()返回给用户之前,该命名属性将从之前的对象或数组中删除。
例如,下面是一个调用JSON.parse(),它使用恢复函数筛选某些属性并重新创建日期对象:
let data = JSON.parse(text, function (key, value) {
// 删除属性名以下划线开头的任何值
if (key[0] === "_") return undefined;
// 如果值是ISO 8601日期格式的字符串,请将其转换为日期。
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);
}
// 否则,返回值不变
return value;
});
除了使用前面描述的toJSON()之外,JSON.stringify()还允许通过将数组或函数作为可选的第二个参数来自定义其输出。
如果将字符串数组(或数字–它们会被转化为字符串)作为第二个参数传递,则这些参数将用作对象属性(或数组元素)的名称。任何名称不在数组中的属性都将从字符串化中省略。此外,返回的字符串将包括属性,其顺序与它们在数组中出现的顺序相同(这在编写测试时非常有用)。
如果你传递一个函数,它实际上是一个替代函数,它是你可以传递给JSON.parse()的可选恢复函数的相反函数. 如果指定了,将为每个要字符串化的值调用替代函数。替代函数的第一个参数是该对象内值的对象属性名或数组索引,第二个参数是值本身。替代函数作为包含要字符串化的值的对象或数组的方法调用。替换器函数的返回值被字符串化以代替原始值。如果替换器返回undefined或根本不返回任何值,则该值(及其数组元素或对象属性)将从字符串化中省略。
// 指定要序列化的字段以及序列化它们的顺序
let text = JSON.stringify(address, ["city","state","country"]);
// 指定省略RegExp值属性的替换函数
let json = JSON.stringify(o, (k, v) => v instanceof RegExp ? undefined : v);
这里的两个JSON.stringify()调用以一种良性的方式使用了第二个参数,生成可以反序列化的序列化输出,而不需要特殊的恢复函数。但是,通常,如果为类型定义了toJSON()方法,或者使用了一个替代函数,该函数实际上将不可序列化的值替换为可序列化的值,则通常使用JSON.parse()时需要一个恢复函数以恢复原始数据结构。如果您这样做,您应该明白,您正在定义一个定制的数据格式,并且牺牲了与JSON兼容的工具和语言的大型生态系统的可移植性和兼容性。
JavaScript国际化API由三个类组成,包括Intl.NumberFormat, Intl.DateTimeFormat,和Intl.Collator,它们允许我们以适合于区域设置的方式格式化数字(包括货币金额和百分比)、日期和时间,并以适合区域设置的方式比较字符串。这些类不是ECMAScript标准的一部分,但被定义为ECMA402标准的一部分,并且受到web浏览器的良好支持。Node中也支持Intl API,但在编写本文时,预编译的Node二进制文件并不包含使它们能够使用美式英语以外的语言环境所需的本地化数据。因此,为了在Node中使用这些类,您可能需要下载一个单独的数据包或使用Node的自定义构建。
国际化最重要的部分之一是显示已翻译成用户语言的文本。有多种方法可以实现这一点,但它们都不在本文介绍的Intl API的范围内。
世界各地的用户都希望数字可以用不同的方式格式化。小数点可以是句点或逗号。千位分隔符可以是逗号或句点,而且并不是在所有地方都每三位数使用一次。有些货币分为百分之一,有些分为千分之一,有些没有细分。最后,尽管所谓的“阿拉伯数字”0到9在许多语言中都有使用,但这并不普遍,一些国家的用户会希望看到用他们自己的手写体数字书写的数字。
这个Intl.NumberFormat类定义了一个format()方法,该方法考虑了所有这些格式化可能性。构造函数接受两个参数。第一个参数指定应为其设置数字格式的区域设置,第二个参数是一个对象,用于指定有关如何格式化数字的详细信息。如果第一个参数被省略或未定义,那么将使用系统语言环境(我们假设它是用户的首选语言环境)。如果第一个参数是字符串,则它指定所需的区域设置,例如“en-US”(美国使用的英语)、“fr”(法语)或“zh-Hans-CN”(中文,使用简体中文书写系统,在中国)。第一个参数也可以是区域设置字符串的数组,在本例中,Intl.NumberFormat将选择其中支持最好的一个。
Intl.NumberFormat()构造函数第二个参数(如果指定)应是定义以下一个或多个属性的对象:
style
currency
currencyDisplay
useGrouping
minimumIntegerDigits
minimumFractionDigits, maximumFractionDigits
minimumSignificantDigits, maximumSignificantDigits
一旦您根据所需的区域设置和选项创建了Intl.NumberFormat,可以通过向其format()方法传递一个数字来使用它,该方法将返回一个格式正确的字符串。例如:
let euros = Intl.NumberFormat("es", {
style: "currency", currency: "EUR" });
console.log(euros.format(10)) // => "10,00 €": 10欧元,西班牙语格式
let pounds = Intl.NumberFormat("cn", {
style: "currency", currency: "CNY" });
console.log(pounds.format(1000)) // => "¥1,000.00": 1000元,中文格式
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;
console.log(arabic(1234567890)) // => "١٢٣٤٥٦٧٨٩٠"
其他语言,如印地语,使用一个有自己的数字集的脚本,但在默认情况下倾向于使用ASCII数字0-9。如果要覆盖用于数字的默认脚本,请在区域设置中添加-u-nu-,并在其后面加上缩写脚本名称。您可以使用印度风格分组和天成文书数字来格式化数字,例如:
let hindi = Intl.NumberFormat("hi-IN-u-nu-deva").format;
hindi(1234567890) // => "१,२३,४५,६७,८९०"
区域中的-u-指定接下来的是Unicode扩展。nu是编号系统的扩展名,deva是Devanagari的缩写。国际API标准定义了许多其他编号系统的名称,主要是南亚和东南亚的印度语。
这个Intl.DateTimeFormat类很像Intl.NumberFormat类。这个Intl.DateTimeFormat()构造函数与Intl.NumberFormat()采用相同的两个参数:一个区域设置或区域设置数组参数以及一个格式化选项对象。使用Intl.DateTimeFormat实例,通过调用其format()方法将日期对象转换为字符串。
如§11.4中所述,Date类定义了简单的toLocaleDateString()和toLocaleTimeString()方法,这些方法为用户的语言环境生成与语言环境相适应的输出。但这些方法不能让您控制显示日期和时间的字段。也许您想省略年份,但在日期格式中添加一个星期几。您想用数字表示月份还是按名称拼写?这个Intl.DateTimeFormat类根据作为第二个参数传递给构造函数的选项对象中的属性提供对输出内容的细粒度控制。不过,请注意Intl.DateTimeFormat不能总是准确地显示您要求的内容。如果您指定了设置小时和秒格式的选项,但忽略了分钟,您会发现格式化程序仍然显示分钟。其思想是使用选项对象指定要向用户显示的日期和时间字段,以及希望如何格式化这些字段(例如,按名称或数字),然后格式化程序将查找与您所请求的内容最匹配的适合区域设置的格式。
可用选项如下。只指定要在格式化输出中显示的日期和时间字段的属性。
year
month
day
weekday
era
hour, minute, second
timeZone
timeZoneName
hour12
hourCycle
以下是一些示例:
let d = new Date("2020-01-02T13:14:15Z"); // 2020年1月2日,13:14:15 UTC
// 没有选项,我们得到一个基本的数字日期格式
console.log(Intl.DateTimeFormat("zh-CN").format(d)) // => "2020/1/2"
console.log(Intl.DateTimeFormat("en-US").format(d)) // => "1/2/2020"
// 写明星期几和月份
let opts = {
weekday: "long", month: "long", year: "numeric", day: "numeric" };
console.log(Intl.DateTimeFormat("zh-CN", opts).format(d)) // => "2020年1月2日星期四"
console.log(Intl.DateTimeFormat("en-US", opts).format(d)) // => "Thursday, January 2, 2020"
// The time in New York, for a French-speaking Canadian
opts = {
hour: "numeric", minute: "2-digit", timeZone: "America/New_York" };
console.log(Intl.DateTimeFormat("fr-CA", opts).format(d)) // => "8 h 14"
国际日期时间格式可以使用基于基督教纪元的默认儒略历以外的日历显示日期。尽管某些区域设置默认情况下可能使用非基督教日历,但您始终可以通过在区域设置中添加-u-ca-并在其后加上日历名称来显式指定要使用的日历。可能的日历名称包括“buddhist”, “chinese”, “coptic”, “ethiopic”, “gregory”, “hebrew”, “indian”, “islamic”, “iso8601”, “japanese”, and “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) // => "2019(ji-hai)"
Intl.DateTimeFormat("en-u-ca-japanese", opts).format(d) // => "2 Reiwa"
将字符串按字母顺序(或非字母拼音的更一般的“排序顺序”)排序的问题比讲英语的人通常意识到的更具挑战性。英语使用一个相对较小的字母表,没有重音字母,我们有一个字符编码(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
sensitivity
ignorePunctuation
numeric
caseFirst
根据所需的区域设置和选项,一旦您创建了Intl.Collator对象,可以使用其compare()方法比较两个字符串。此方法返回一个数字。如果返回值小于零,则第一个字符串在第二个字符串之前。如果它大于零,则第一个字符串在第二个字符串之后。如果compare()返回零,那么这两个字符串就这个排序器而言是相等的。
这个compare()方法接受两个字符串并返回一个小于、等于或大于零的数字,这正是数组sort()方法对其可选参数的期望值。Intl.Collator自动将compare()方法绑定到它的实例,这样就可以直接将它传递给sort(),而不必编写包装器函数并通过排序器对象调用它。以下是一些示例:
// 用于在用户区域设置中进行排序的基本比较器。
// 不要在不传递以下内容的情况下对可读的字符串进行排序:
const collator = new Intl.Collator().compare;
["a", "z", "A", "Z"].sort(collator) // => ["a", "A", "z", "Z"]
// 文件名通常包含数字,所以我们应该对它们进行特殊排序
const filenameOrder = new Intl.Collator(undefined, {
numeric: true }).compare;
["page10", "page9"].sort(filenameOrder) // => ["page9", "page10"]
// 查找与目标字符串松散匹配的所有字符串
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”。
// 1994年以前,CH和LL在西班牙被视为单独的字母
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"]
你在本书中看到了console.log()函数:在web浏览器中,它在浏览器的“开发者工具”窗格的“控制台”选项卡中打印一个字符串,这在调试时非常有用。在Node中,console.log()是一个通用输出函数,它将其参数打印到进程的标准输出流中,在该流中,它通常作为程序输出出现在终端窗口中。
除了console.log()之外,控制台API还定义了许多有用的函数。这些API不是任何ECMAScript标准的一部分,但是它受到浏览器和Node的支持,并且已经在https://console.spec.whatwg.org被标准化了。
控制台API定义了以下函数:
console.log()
console.debug(), console.info(), console.warn(), console.error()
console.assert()
console.clear()
console.table()
译者注
这个我还真没用过,慢慢研究一下看看。
传入一个对象数组看看let a = []; for (let i = 0, j = i; i < 5; i++, j += 2) { a.push({ i,j}); } console.table(a);
输出是:
┌─────────┬───┬───┐ │ (index) │ i │ j │ ├─────────┼───┼───┤ │ 0 │ 0 │ 0 │ │ 1 │ 1 │ 2 │ │ 2 │ 2 │ 4 │ │ 3 │ 3 │ 6 │ │ 4 │ 4 │ 8 │ └─────────┴───┴───┘
其它的就不试了,应该比较容易看明白了。
console.trace()
console.count()
console.countReset()
console.group()
console.groupCollapsed()
console.groupEnd()
console.time()
console.timeLog()
console.timeEnd()
像console.log()这样打印参数的控制台函数有一个鲜为人知的特性:如果第一个参数是一个包含%s、%i、%d、%f、%o、%o或%c的字符串,则第一个参数将被视为格式字符串,后续的字符串参数将会替换格式串中对应的两字符%序列。
序列的含义如下:
%s
%i 和 %d
%f
%o and %O
%c
译者注
看着挺稀奇,我们看一下用法吧。console.log('123 %c 456 %c 789','font-size:20px;color:#ff0000;','font-size:16px;color:#00f');
在浏览器的控制台输出:
123 456 789
请注意,通常不需要在控制台函数中使用格式字符串:通过简单地将一个或多个值(包括对象)传递给函数,控制台实现会以有用的方式进行输出,这通常很容易获得适当的输出。例如,请注意,如果将Error对象传递给console.log(),它与堆栈跟踪一起自动打印。
由于JavaScript在web浏览器和web服务器中经常使用,所以JavaScript代码通常需要操作URL。URL类解析URL并允许对现有URL进行修改(例如添加搜索参数或更改路径)。它还可以正确地处理URL的各个组成部分的转义和反转义。
URL类不是任何ECMAScript标准的一部分,但它可以在Node和除IE之外的所有济览器中工作。它的标准化信息请参考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:[email protected]/");
url.href // => "ftp://admin:[email protected]/"
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 // => "": 还没有查询参数
url.searchParams.append("q", "term"); // 添加一个查询参数
url.search // => "?q=term"
url.searchParams.set("q", "x"); // 更改此参数的值
url.search // => "?q=x"
url.searchParams.get("q") // => "x": 查询参数值
url.searchParams.has("q") // => true: 有一个q参数
url.searchParams.has("p") // => false: 没有p参数
url.searchParams.append("opts", "1"); // 添加另一个查询参数
url.search // => "?q=x&opts=1"
url.searchParams.append("opts", "&"); // 为同一名称添加另一个值
url.search // => "?q=x&opts=1&opts=%26": 注意转义
url.searchParams.get("opts") // => "1": 第1个值
url.searchParams.getAll("opts") // => ["1", "&"]: 所有值
url.searchParams.sort(); // 以字母序排序参数
url.search // => "?opts=1&opts=%26&q=x"
url.searchParams.set("opts", "y"); // 修改opts参数
url.search // => "?opts=y&q=x"
// searchParams 可迭代
[...url.searchParams] // => [["opts", "y"], ["q", "x"]]
url.searchParams.delete("opts"); // 删除opts参数
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"
在前面描述的URL API定义之前,已经有多次尝试在核心JavaScript语言中支持URL转义和反转义。第一次尝试是全局定义的escape()和unescape()函数,它们现在已被弃用,但仍被广泛实现。它们不应该被使用。
当escape()和unescape()被弃用时,ECMAScript引入了两对可选的全局函数:
encodeURI() 和 decodeURI()
encodeURIComponent() 和 decodeURIComponent()
所有这些遗留函数的根本问题是,当URL的不同部分使用不同的编码时,它们试图对URL的所有部分应用单一的编码方案。如果您想要一个正确格式化和编码的URL,解决方案就是简单地将URL类用于您所做的所有URL操作。
从JavaScript最早的时代起,web浏览器定义了两个函数,setTimeout()和setInterval(),允许程序在经过指定的时间后请求浏览器调用某个函数,或者在指定的时间间隔内重复调用该函数。这些函数从未作为核心语言的一部分进行过标准化,但它们可以在所有浏览器和Node中工作,并且实际上是JavaScript标准库的一部分。
setTimeout()的第一个参数是一个函数,第二个参数是一个数字,指定在调用函数之前应该经过多少毫秒。在指定的时间之后(如果系统很忙,可能会更长一点),函数将不带参数地被调用。例如,这里有三个setTimeout()调用,它们在1秒钟、2秒钟和3秒钟后打印控制台消息:
setTimeout(() => {
console.log("Ready..."); }, 1000);
setTimeout(() => {
console.log("set..."); }, 2000);
setTimeout(() => {
console.log("go!"); }, 3000);
请注意,setTimeout()不会等到时间过了才返回。本例中的三行代码几乎都是立即运行的,除非1000毫秒之后,否则什么也不会发生。
如果省略setTimeout()的第二个参数,则默认为0。但是,这并不意味着立即调用您指定的函数。相反,该函数注册为“尽快”调用。如果浏览器忙于处理用户输入或其他事件,则调用该函数可能需要10毫秒或更长时间。
setTimeout()注册要调用一次的函数。有时,该函数本身将调用setTimeout()以在将来安排另一次调用。但是,如果要反复调用函数,则通常使用setInterval()会更简单。setInterval()采用与setTimeout()相同的两个参数,但每次经过指定的毫秒数(大约)后,都会重复调用该函数。
setTimeout()和setInterval()都返回一个值。如果将此值保存在变量中,则以后可以使用它来取消函数的执行,方法是将其传递给clearTimeout()或clearInterval()。在web浏览器中,返回的值通常是一个数字,是Node中,返回的是一个对象。实际类型并不重要,您应该将其视为不透明值。对于这个值,您唯一能做的就是将它传递给clearTimeout(),以取消使用setTimeout()注册的函数的执行(假设尚未调用它),或者停止重复执行使用setInterval()注册的函数。
下面是一个示例,演示如何使用setTimeout()、setInterval()和clearInterval()通过控制台API显示一个简单的数字时钟:
// 一秒调用一次: 清除控制台并打印当前时间
let clock = setInterval(() => {
console.clear();
console.log(new Date().toLocaleTimeString());
}, 1000);
// 10秒后:停止重复上述代码。
setTimeout(() => {
clearInterval(clock); }, 10000);
在第13章讨论异步编程时,我们将再次看到setTimeout()和setInterval()。
学习编程语言不仅仅是掌握语法。同样重要的是研究标准库,以便您熟悉该语言附带的所有工具。本章介绍了JavaScript的标准库,其中包括:
并不是这里描述的所有东西都是由JavaScript语言规范定义的:这里记录的一些类和函数首先在web浏览器中实现,然后被Node采用,使它们成为JavaScript标准库的实际成员。 ↩︎
这种可预测的迭代顺序是关于JavaScript集合的另一个方面,Python程序员可能会感到惊讶。 ↩︎
当web浏览器增加了对WebGL图形的支持时,类型化数组首先被引入到客户端JavaScript中。ES6的新功能是把它们已经被提升为核心语言功能。 ↩︎
除了在字符类(方括号)中,\b匹配退格符。 ↩︎
用正则表达式解析url不是一个好主意。参见§11.9了解更强大的URL解析器。 ↩︎