第11章 JavaScript标准库

  • 11.1 Set和Map
    • 11.1.1 Set类
    • 11.1.2 Map类
    • 11.1.3 WeakMap和WeakSet
  • 11.2 类型数组和二进制数据
    • 11.2.1 类型数组的类型
    • 11.2.2 创建类型数组
    • 11.2.3 使用类型数组
    • 11.2.4 类型数组的方法和属性
    • 11.2.5 数据视图和字节序
  • 11.3 正则表达式模式匹配
    • 11.3.1 定义正则表达式
      • 字面量字符
      • 字符类
      • 重复
      • 非贪婪重复
      • 选择,分组和引用
      • 指定匹配位置
      • 标志(或叫修饰符)
    • 11.3.2 模式匹配的字符串方法
      • search()
      • replace()
      • match()
      • matchAll()
      • split()
    • 11.3.3 RegExp类
      • RegExp属性
      • test()
      • exec()
  • 11.4 日期和时间
    • 11.4.1 时间戳
    • 11.4.2 日期算术
    • 11.4.3 格式化和解析日期字符串
  • 11.5 Error类
  • 11.6 JSON序列化和解析
    • 11.6.1 JSON自定义
  • 11.7 国际化API
    • 11.7.1 格式化数字
    • 11.7.2 日期和时间格式化
    • 11.7.3 比较字符串
  • 11.8 控制台(Console)API
    • 11.8.1 控制台的格式化输出
  • 11.9 URL API
    • 11.9.1 遗留的URL函数
  • 11.10 计时器
  • 11.11 总结

有些数据类型,如数字和字符串(第3章)、对象(第6章)和数组(第7章)是JavaScript的基础,我们可以将它们视为语言本身的一部分。本章介绍了其他重要但不太基础的API,它们可以被认为是JavaScript的“标准库”:这些是JavaScript内置的有用类和函数,可用于web浏览器和Node中的所有JavaScript程序。1

这一章的章节是相互独立的,你可以按任何顺序阅读。它们包括:

  • 集合类Set和映射类Map,用于表示一组值以及从一组值到另一组值的映射。
  • 称为TypedArrays的类数组对象,表示二进制数据的数组,以及从非二进制数据数组中提取值的相关类。
  • 正则表达式和RegExp类,它们定义文本模式,对文本处理很有用。本节还将详细介绍正则表达式语法。
  • 用于表示和操作日期和时间的Date类。
  • Error类及其各种子类,当JavaScript程序中发生错误时,将抛出它们的实例。
  • JSON对象,其方法支持由对象、数组、字符串、数字和布尔值组成的JavaScript数据结构的序列化和反序列化。
  • Intl对象及其定义的类可以帮助您本地化JavaScript程序。
  • Console对象,其中的方法主要输出字符串,对调试程序和记录这些程序的行为特别有用。
  • URL类,它简化了解析和操作URL的任务。本节还介绍了对URL及其组件进行编码和解码的全局函数。
  • setTimeout()和相关函数,用于指定经过一定时间间隔后要执行的代码。

本章中的一些部分,特别是,关于类型化数组和正则表达式的部分相当长,因为在有效地使用这些类型之前,您需要了解一些重要的背景信息。然而,其他许多部分都很短:它们只是介绍了一个新的API并展示了一些使用它的示例。

11.1 Set和Map

JavaScript的Object类型是一种通用的数据结构,可以用来将字符串(对象的属性名)映射到任意值。当映射到的值是固定的,比如true,那么对象实际上就是一组字符串。

在JavaScript编程中,对象实际上经常被用作映射和集合,但这又受到属性必须是字符串的限制,并且由于对象通常继承名为“toString”的属性而变得复杂,这些属性通常不是映射或集合的一部分。

因此,ES6引入了真正的Set和Map类,我们将在下面的小节中介绍这些类。

11.1.1 Set类

集合是值的集合,就像数组一样。但是,与数组不同的是,集合不被排序或编制索引,并且它们不允许重复:一个值要么是集合的成员,要么不是一个成员;不可能询问一个值在一个集合中出现了多少次。

使用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

关于此代码,有几点需要注意:

  • add()方法接受一个参数;如果传递一个数组,它将数组本身添加到集合中,而不是单个数组元素。add()总是返回调用它的集合,因此如果要向一个集合添加多个值,可以使用链式方法调用,如s.add(‘a’).add(‘b’).add(‘c’);。
  • delete()方法一次也只删除一个集合元素。但是,与add()不同,delete()返回一个布尔值。如果指定的值实际上是集合的成员,那么delete()将删除它并返回true。否则,它什么也不做,返回false。
  • 最后,了解集合成员资格是基于严格的相等性检查的,这一点非常重要,就像===运算符所执行的那样。集合可以同时包含数字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类的这个方法只将元素值同时作为第一个和第二个参数传递。

11.1.2 Map类

映射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()方法首先传递映射值,然后传递映射健。

11.1.3 WeakMap和WeakSet

WeakMap类是Map类的一个变体(但不是实际的子类),它不会阻止其键值被垃圾回收。垃圾回收是JavaScript解释器回收不再“可访问”且程序无法使用的对象的内存的过程。一个常规的映射包含对其键值的“强”引用,即使对它们的所有其他引用都不存在,它们仍然可以通过映射访问。相反,WeakMap保留对其键值的“弱”引用,因此它们无法通过WeakMap访问,并且它们在映射中的存在不会阻止它们的内存被回收。

WeakMap()构造函数与Map()构造函数类似,但WeakMap和Map之间存在一些显著差异:

  • WeakMap键必须是对象或数组;原始值不受垃圾回收的影响,不能用作键。
  • WeakMap只实现get()、set()、has()和delete()方法。特别是,WeakMap是不可迭代的,并且不定义keys()、values()或forEach()。如果WeakMap是可迭代的,那么它的键就可以访问,那它就不是弱引用。
  • 类似地,WeakMap不实现size属性,因为WeakMap的大小在对象被垃圾回收时随时可能更改。

WeakMap的预期用途是允许您将值与对象关联,而不会导致内存泄漏。例如,假设您正在编写一个函数,该函数接受对象参数,并且需要对该对象执行一些耗时的计算。为了提高效率,您希望缓存计算的值,以便以后重用。如果使用映射对象实现缓存,则将防止任何对象被回收,但是通过使用WeakMap,可以避免此问题。(通常可以使用私有符号属性直接缓存对象上的计算值,从而获得类似的结果。见§6.10.3.)

WeakSet实现了一组不阻止这些对象被垃圾回收的对象。WeakSet()构造函数的工作方式与Set()构造函数类似,但WeakSet对象与Set对象的不同之处与WeakMap对象与Map对象的不同之处相同:

  • WeakSet不允许原始值作为成员。
  • WeakSet只实现add()、has()和delete()方法,不可迭代。
  • WeakSet没有size属性。

WeakSet并不常用:它的应用与WeakMap的应用类似。例如,如果要将对象标记(或“标识”)为具有某些特殊属性或类型,可以将其添加到WeakSet中。然后,在其他地方,当您要检查该属性或类型时,可以测试该WeakSet中的成员资格。使用普通集合执行此操作将防止所有标记的对象被垃圾回收,但在使用WeakSet时,这不是一个问题。

11.2 类型数组和二进制数据

常规JavaScript数组可以有任何类型的元素,并且可以动态地增长或收缩。JavaScript实现执行许多优化,因此JavaScript数组的普通操作非常快。然而,它们仍然与C和Java等低级语言的数组类型有很大的不同。类型化数组是ES6中的新特性3,它更接近于这些语言的低级数组。类型化数组在技术上不是数组(Array.isArray()返回false),但它们实现了§7.8中描述的所有数组方法以及它们自己的一些方法。但是,它们在一些非常重要的方面与常规数组不同:

  • 类型化数组的元素都是数字。但是,与常规JavaScript数字不同,类型化数组允许您指定要存储在数组中的数字的类型(有符号和无符号整数与IEEE-754浮点数)和大小(8位到64位)。
  • 创建类型化数组时必须指定其长度,并且该长度永远不会更改。
  • 类型化数组的元素在创建数组时总是初始化为0。

11.2.1 类型数组的类型

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<canvas>元素的低级API所必需的,用于操作像素颜色)

每个类型化数组构造函数都有一个BYTES_PER_ELEMENT属性,值为1、2、4或8,具体取决于类型。

11.2.2 创建类型数组

创建类型化数组的最简单方法是使用一个数字参数调用适当的构造函数,该参数指定您希望在数组中使用的元素数量:

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对象的原因是,有时您可能需要单个缓冲区的多个类型化数组视图。

11.2.3 使用类型数组

创建类型化数组后,可以使用普通方括号表示法读取和写入其元素,就像处理任何其他类似数组的对象一样:

// 返回小于n的最大素数,使用Eratosthenes筛法
function sieve(n) {let a = new Uint8Array(n + 1); // 如果x是合数,a[x]为1let 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()这样返回新数组的方法返回的类型化数组与调用它们的类型相同。

11.2.4 类型数组的方法和属性

除了标准数组方法外,类型化数组还实现了一些自己的方法。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 个浮点数

11.2.5 数据视图和字节序

类型数组允许您以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文件中提取元数据等操作。

11.3 正则表达式模式匹配

正则表达式是描述文本模式的对象。JavaScript RegExp类表示正则表达式,String和RegExp都定义了使用正则表达式对文本执行强大的模式匹配、搜索和替换功能的方法。然而,为了有效地使用RegExp API,还必须学习如何使用正则表达式语法来描述文本模式,正则表达式语法本质上是一种小型编程语言。幸运的是,JavaScript正则表达式语法与许多其他编程语言使用的语法非常相似,因此您可能已经熟悉它了。(如果不是这样,那么在学习JavaScript正则表达式方面所做的努力可能对您在其他编程环境中也很有用。)

下面的小节首先描述正则表达式语法,然后在解释如何编写正则表达式之后,解释如何将它们用于String和RegExp类的方法。

11.3.1 定义正则表达式

在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还没有实现。

要命名组,请使用(?<…>代替(,并将名称放在尖括号之间。例如,以下是一个正则表达式,可用于检查美国邮寄地址最后一行的格式:

/(?<city>\w+) (?<state>[A-Z]{2}) (?<zipcode>\d{5})(?<zip9>-\d{4})?/

请注意组名提供了多少上下文以使正则表达式更易于理解。在§11.3.2中,当我们讨论String的replace()和match()方法以及RegExp的exec()方法时,您将看到RegExp API如何允许您引用按名称而不是按位置匹配每个组的文本。

如果要在正则表达式中引用命名的捕获组,也可以按名称执行。我们必须在前面的正则表达式中使用一个双引号来匹配前面的引用。我们可以使用命名的捕获组和命名的反向引用重写此RegExp,如下所示:

/(?<quote>['"])[^'"]*\k<quote>/

\k<quote>是对捕获左引号的命名组的命名反向引用。

指定匹配位置

如前所述,正则表达式的许多元素与字符串中的单个字符匹配。例如,\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并没有实现。
使用(?<=…)指定一个正向后行断言,(?<!..)指定一个后向先行断言。例如,如果您使用的是美国邮寄地址,则可以匹配5位数字的邮政编码,但仅当邮政编码跟在两个字母的州缩写之后,例如:

/(?<= [A-Z]{2} )\d{5}/

您可以将前面没有Unicode货币符号的一串数字与如下所示的负向后行断言相匹配:

/(?<![\p{Currency_Symbol}\d.])\d+(\.\d+)?/u

标志(或叫修饰符)

每个正则表达式都可以有一个或多个与其关联的标志,以更改其匹配行为。JavaScript定义了六个可能的标志,每个标志都由一个字母表示。标志在正则表达式文本的第二个/字符后指定,或作为作为第二个参数传递给RegExp()构造函数的字符串。支持的标志及其含义是:

g

  • g标志表示正则表达式是“全局的”——也就是说,我们打算用它来查找字符串中的所有匹配项,而不仅仅是查找第一个匹配项。这个标志不会改变模式匹配的方式,但是,我们将在后面看到,它确实以重要的方式改变String的match()方法和RegExp的exec()方法的行为。

i

  • i标志指定模式匹配应该不区分大小写。

m

  • m标志指定匹配应该在“多行”模式下完成。它说RegExp将与多行字符串一起使用,并且^和$锚元素应该匹配字符串的开头和结尾,以及字符串中各个行的开始和结束。

s

  • 与m标志一样,s标志在处理包含换行符的文本时也很有用。通常,正则表达式中的“.”与除行结束符外的任何字符匹配。但是,当使用s标志时,“.”将匹配任何字符,包括行结束符。s标志是在ES2018中添加到JavaScript中的,到2020年初,Node、Chrome、Edge和Safari都支持s标志,但Firefox不支持。

u

  • u标志代表Unicode,它使正则表达式匹配完整的Unicode码位,而不是匹配16位值。这个标志是在ES6中引入的,您应该养成在所有正则表达式上使用它的习惯,除非您有理由不这样做。如果不使用此标志,则RegExp将无法很好地处理包含表情符号和其他需要16位以上字符(包括许多汉字)的文本。如果没有u标志,“.”字符匹配任何1个UTF-16 16位值。但是,使用标志,“.”匹配一个Unicode码位,包括那些超过16位的码位。在RegExp上设置u标志还允许您对Unicode字符使用新的\u{…}转义序列,并为Unicode字符类启用\p{…}符号。

y

  • y标志表示正则表达式是“粘滞”的,应该在字符串开头或前一个匹配后的第一个字符处匹配。当与设计用于查找单个匹配项的正则表达式一起使用时,它有效地将该正则表达式视为以^开头将其锚定到字符串的开头。此标志对于反复用于查找字符串中所有匹配项的正则表达式更有用。在这种情况下,它会导致String的match()方法和RegExp的exec()方法的特殊行为,强制将每个后续匹配定位到最后一个匹配结束的字符串位置。

这些标志可以任何组合和顺序指定。例如,如果您希望正则表达式能够识别Unicode以进行不区分大小写的匹配,并且打算使用它来查找字符串中的多个匹配项,则可以指定标志uig、gui或这三个字母的任何其他排列。

11.3.2 模式匹配的字符串方法

到目前为止,我们一直在描述用于定义正则表达式的语法,但没有解释如何在JavaScript代码中实际使用这些正则表达式。现在我们将讨论使用RegExp对象的API。本节首先解释使用正则表达式执行模式匹配以及搜索和替换操作的字符串方法。接下来的章节通过讨论RegExp对象及其方法和属性,继续讨论JavaScript正则表达式的模式匹配。

search()

字符串支持四种使用正则表达式的方法。最简单的是search()。此方法接受正则表达式参数并返回第一个匹配子字符串开头的字符位置,如果不匹配,则返回-1:

"JavaScript".search(/script/ui) // => 4
"Python".search(/script/ui) // => -1

如果search()的参数不是正则表达式,则首先通过将其传递给RegExp构造函数将其转换为正则表达式。search()不支持全局搜索;它忽略正则表达式参数的g标志。

replace()

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 = /"(?<quotedText>[^"]*)"/g;
'He said "stop"'.replace(quote, '«$<quotedText>»') // => '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()

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 = /(?<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"

根据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()

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}.`);
}

split()

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<br/>1,2,3".split(htmlTag) // => ["Testing", "br/", "1,2,3"]

11.3.3 RegExp类

本节介绍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属性

RegExp对象具有以下属性:

source

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

flags

  • 此只读属性是一个字符串,指定表示RegExp标志的一组字母。

global

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

ignoreCase

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

multiline

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

dotAll

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

unicode

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

sticky

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

lastIndex

  • 此属性是一个可读/写的整数。对于带有g或y标志的模式,它指定下一次搜索开始的字符位置。它由exec()和test()方法使用,在接下来的两个小节中介绍。

test()

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

test()只需调用下一节中描述的(更复杂的)exec()方法,并在exec()返回非空值时返回true。因此,如果将test()与使用g或y标志的RegExp一起使用,则其行为取决于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(),以便在字符串中循环所有正则表达式匹配项。(尽管我们已经描述过,在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文本字符串中找到所有<p>标记的索引。我们可以这样写代码:

let match, positions = [];
while((match = /<p>/g.exec(html)) !== null) { // 可能的无限循环positions.push(match.index);
}

这段代码做不到我们想要的。如果html字符串至少包含一个<p>标记,那么它将永远循环。问题是我们在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。

11.4 日期和时间

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()方法。

11.4.1 时间戳

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对象,必须将其导入:

const { performance } = require("perf_hooks");

允许网络上的高精度计时可能会让无良网站对访问者进行指纹级别程度的识别,因此浏览器(尤其是Firefox)默认可能会降低performance.now()的精准度。作为一个web开发人员,您应该能够以某种方式重新启用高精度计时(例如通过在Firefox中设置 privacy.reduceTimerPrecision为false)。

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); // 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()

  • 此方法使用UTC时区,但不以支持区域设置的方式格式化日期。

toISOString()

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

toLocaleString()

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

toDateString()

  • 此方法只格式化日期的日期部分而忽略时间。它使用本地时区,不进行与区域设置相适应的格式设置。

toLocaleDateString()

  • 此方法只格式化日期。它使用本地时区和本地适当的日期格式。

toTimeString()

  • 此方法只格式化时间而忽略日期。它使用本地时区,但不以支持区域设置的方式格式化时间。

toLocaleTimeString()

  • 此方法以支持区域设置的方式格式化时间,并使用本地时区。

当格式化要显示给最终用户的日期和时间时,这些日期到字符串的方法都不理想。请参见§11.7.2,以了解更通用和基于区域设置的日期和时间格式化技术。

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

11.5 Error类

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"

11.6 JSON序列化和解析

当一个程序需要保存数据或需要通过网络连接将数据传输到另一个程序时,它必须将其内存中的数据结构转换成可以保存或传输的字节或字符串,然后再进行解析以恢复原来的内存中数据结构。将数据结构转换为字节或字符流的过程称为序列化(或封送处理,甚至叫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()对我们将字符串转换回数据结构的能力没有影响。

11.6.1 JSON自定义

如果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兼容的工具和语言的大型生态系统的可移植性和兼容性。

11.7 国际化API

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

国际化最重要的部分之一是显示已翻译成用户语言的文本。有多种方法可以实现这一点,但它们都不在本文介绍的Intl API的范围内。

11.7.1 格式化数字

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

这个Intl.NumberFormat类定义了一个format()方法,该方法考虑了所有这些格式化可能性。构造函数接受两个参数。第一个参数指定应为其设置数字格式的区域设置,第二个参数是一个对象,用于指定有关如何格式化数字的详细信息。如果第一个参数被省略或未定义,那么将使用系统语言环境(我们假设它是用户的首选语言环境)。如果第一个参数是字符串,则它指定所需的区域设置,例如“en-US”(美国使用的英语)、“fr”(法语)或“zh-Hans-CN”(中文,使用简体中文书写系统,在中国)。第一个参数也可以是区域设置字符串的数组,在本例中,Intl.NumberFormat将选择其中支持最好的一个。

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

style

  • 指定所需的数字格式的类型。默认为“十进制”。指定“percent”将数字格式化为百分比,或指定“currency”将数字指定为金额。

currency

  • 如果style为“currency”,则需要此属性指定所需货币的三个字母的ISO货币代码(例如“USD”表示美元或“GBP”表示英制磅)。

currencyDisplay

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

useGrouping

  • 如果不希望数字包含千位分隔符(或其区域设置相应的等效值),请将此属性设置为false。

minimumIntegerDigits

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

minimumFractionDigits, maximumFractionDigits

  • 这两个属性控制数字小数部分的格式。如果一个数字的小数位数少于最小值,它将在右边用零填充。如果大于最大值,则小数部分将被舍入。两个属性的合法值都在0到20之间。默认最小值为0,默认最大值为3,除非格式化货币金额时,小数部分的长度根据指定的货币而变化。

minimumSignificantDigits, maximumSignificantDigits

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

一旦您根据所需的区域设置和选项创建了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标准定义了许多其他编号系统的名称,主要是南亚和东南亚的印度语。

11.7.2 日期和时间格式化

这个Intl.DateTimeFormat类很像Intl.NumberFormat类。这个Intl.DateTimeFormat()构造函数与Intl.NumberFormat()采用相同的两个参数:一个区域设置或区域设置数组参数以及一个格式化选项对象。使用Intl.DateTimeFormat实例,通过调用其format()方法将日期对象转换为字符串。

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

可用选项如下。只指定要在格式化输出中显示的日期和时间字段的属性。

year

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

month

  • 使用“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”。

hour, minute, second

  • 这些属性指定时间的显示方式。使用“numeric”表示一位或两位字段,或使用“2-digit”强制在左侧用0填充单个数字。

timeZone

  • 此属性指定应为其设置日期格式的所需时区。如果省略,则使用本地时区。实现始终可以识别“UTC”,也可以识别互联网数字分配机构(IANA)时区名称,例如“America/Los_Angeles”。

timeZoneName

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

hour12

  • 此布尔属性指定是否使用12小时时间。默认值依赖于区域设置,但您可以使用此属性重写它。

hourCycle

  • 此属性允许您指定午夜是写入0小时、12小时还是24小时。默认值依赖于区域设置,但您可以使用此属性重写默认值。请注意,hour12优先于此属性。使用值“h11”指定午夜为0,午夜前一小时为晚上11点。使用“h12”指定午夜为12。使用“h23”指定午夜为0,午夜前一小时为23。并使用“h24”指定午夜为24。

以下是一些示例:

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"

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

  • 此属性指定如何使用排序器(Collator)对象。默认值为“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(),而不必编写包装器函数并通过排序器对象调用它。以下是一些示例:

// 用于在用户区域设置中进行排序的基本比较器。
// 不要在不传递以下内容的情况下对可读的字符串进行排序:
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"]

11.8 控制台(Console)API

你在本书中看到了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.log()相同。在Node中,console.error()将其输出发送到stderr流而不是stdout流,但其他函数是console.log()的别名。在浏览器中,由这些函数生成的输出消息的前缀可能是一个图标,指示其级别或严重性,开发人员控制台还允许开发人员按级别筛选控制台消息。

console.assert()

  • 如果第一个参数是真的(即如果断言通过),那么这个函数什么也不做。但是,如果第一个参数是false或另一个假值,那么剩余的参数将被打印,就像它们被传递给console.error(),加上前缀“Assertion failed”。注意,与典型的assert()函数不同,当断言失败时,console.assert()不引发异常。

console.clear()

  • 此函数在可能的情况下清除控制台。当在浏览器中或在Node中时,向终端显示输出时,这个函数就会起作用。但若Node的输出已重定向到文件或管道,则调用此函数无效。

console.table()

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

译者注
这个我还真没用过,慢慢研究一下看看。
传入一个对象数组看看

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.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.time()之前,调用console.timeLog()是非法的。

11.8.1 控制台的格式化输出

像console.log()这样打印参数的控制台函数有一个鲜为人知的特性:如果第一个参数是一个包含%s、%i、%d、%f、%o、%o或%c的字符串,则第一个参数将被视为格式字符串,后续的字符串参数将会替换格式串中对应的两字符%序列。

序列的含义如下:

%s

  • 参数转换为字符串。

%i 和 %d

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

%f

  • 参数转换为数字

%o and %O

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

%c

  • 在web浏览器中,参数被解释为CSS样式的字符串,并用于设置后面的任何文本的样式(直到下一个%c序列或字符串的末尾)。在Node中,只忽略%c序列及其相应的参数。

译者注
看着挺稀奇,我们看一下用法吧。

console.log('123 %c 456 %c 789','font-size:20px;color:#ff0000;','font-size:16px;color:#00f');

在浏览器的控制台输出:

123 456 789

请注意,通常不需要在控制台函数中使用格式字符串:通过简单地将一个或多个值(包括对象)传递给函数,控制台实现会以有用的方式进行输出,这通常很容易获得适当的输出。例如,请注意,如果将Error对象传递给console.log(),它与堆栈跟踪一起自动打印。

11.9 URL API

由于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: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 // => "": 还没有查询参数
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"

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。

encodeURIComponent() 和 decodeURIComponent()

  • 这两个函数的工作原理与encodeURI()和decodeURI()相同,只是它们用于转义URI的各个组成部分,因此它们可以转义/,?,和用于分离组成部分的#。这些是遗留URL函数中最有用的,但是请注意,encodeURIComponent()将转义路径中的/字符,即使你可能并不希望进行转义。它将把查询参数中的空格转换为%20,即使在URL的那个部分应该用+来转义空格。

所有这些遗留函数的根本问题是,当URL的不同部分使用不同的编码时,它们试图对URL的所有部分应用单一的编码方案。如果您想要一个正确格式化和编码的URL,解决方案就是简单地将URL类用于您所做的所有URL操作。

11.10 计时器

从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()。

11.11 总结

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

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

  1. 并不是这里描述的所有东西都是由JavaScript语言规范定义的:这里记录的一些类和函数首先在web浏览器中实现,然后被Node采用,使它们成为JavaScript标准库的实际成员。 ↩︎

  2. 这种可预测的迭代顺序是关于JavaScript集合的另一个方面,Python程序员可能会感到惊讶。 ↩︎

  3. 当web浏览器增加了对WebGL图形的支持时,类型化数组首先被引入到客户端JavaScript中。ES6的新功能是把它们已经被提升为核心语言功能。 ↩︎

  4. 除了在字符类(方括号)中,\b匹配退格符。 ↩︎

  5. 用正则表达式解析url不是一个好主意。参见§11.9了解更强大的URL解析器。 ↩︎

《JavaScript权威指南第7版》第11章 JavaScript标准库相关推荐

  1. 翻译:《JavaScript 权威指南(第5版)》第一章(一)

    声明:翻译只有一个目的:学习用途.若有版权问题请及时联系本人. 本贴文根据篇幅将第一章的翻译分为两个部分,这是第一部分的内容. Chapter 1. Introduction to JavaScrip ...

  2. 《JavaScript权威指南第六版》学习笔记-JavaScript概述

    第一章.JavaScript概述 JavaScript是一门高端的.动态的.弱类型的编程语言,非常适合面向对象和函数式的编程风格.JavaScript的语法源自Java,它的一等函数(first-cl ...

  3. 【JavaScript权威指南(第七版)】之阅读学习总结

    写在前面 最近借着空闲时间断断续续两个月看完了<JavaScript权威指南(第七版)>,<JavaScript权威指南>一直以来被称为"犀牛书",前面的第 ...

  4. JavaScript权威指南(第6版)

    JavaScript权威指南(第6版) 编辑推荐 经典权威的JavaScript工具书 本书是程序员学习核心JavaScript语言和由Web浏览器定义的JavaScript API的指南和综合参考手 ...

  5. 《JavaScript权威指南第四版》 电子版 电子书下载

    JavaScript权威指南第四版 图书评价:★★★★☆ 图书语言:简体图书 图书大小:19.11MB 图书格式:PDF 图书作者:David Flanagan 更新日期:2006-05-23 下载次 ...

  6. JavaScript权威指南(第6版)(中文版).pdf

    pdf 电子版书籍, 百度云盘:[JavaScript权威指南(第6版)(中文版)] 提取密码:b0tf

  7. JavaScript权威指南(第6版)中文版pdf

    JavaScript权威指南(第6版)中文版pdf JavaScript权威指南(第6版)中文版pdf 百度网盘下载链接:点击下载

  8. JavaScript权威指南(第5版)pdf

    下载地址:网盘下载 内容简介 编辑 第5版针对Ajax和Web 2.0技术进行了全新的改版.和上一版相比,更新的内容较多,总体上接近整个篇幅的1/2,而这也正是本书姗姗来迟的原因之一.具体来说,第5版 ...

  9. JavaScript权威指南 第6版 中文版 pdf

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 下载地址 ...

最新文章

  1. 想 new 个对象过七夕,她却抛了异常
  2. synchronous_commit 参数的再次说明
  3. _​_​i​n​t​6​4​ ​与​l​o​n​g​ ​l​o​n​g​ ​i​n​t
  4. java程序的运行结果依赖操作系统吗_java模拟试卷及答案及解析4
  5. 一文弄懂神经网络中的反向传播法
  6. 窗体控件随窗体大小改变(仍有不足)
  7. .NET 云原生架构师训练营(模块二 基础巩固 安全)--学习笔记
  8. whatlies包 | 简单玩转词向量可视化
  9. 【OS学习笔记】十六 保护模式四:进入保护模式与在保护模式下访问内存的汇编代码
  10. 写代码获取全国疫情地图
  11. 怎么在表格中转换html格式,图解Excel与Html格式之间的互相转换
  12. careercup-数学与概率 7.7
  13. WPF中Binding的验证
  14. java全世界各国城市地址解析
  15. 惠普局域网共享打印机设置_惠普打印机局域网共享
  16. 渗透测试流程(一)(千峰学习笔记)
  17. c#获取外网IP地址
  18. ietester测试本地html,网站浏览器兼容测试软件–IETester
  19. 海龟交易法的“道”和“术”
  20. html5微信分享图片不显示,微信分享ios 不显示图片和简介问题总结

热门文章

  1. 浅析Kafka实时数据处理系统
  2. 清华大学计算机系刘景财,2017年清华大学计算机系硕士录取名单
  3. 阿里云 IOT 物联网平台简单使用【随笔】
  4. 安装小豚当家监控摄像头
  5. 手机OTG 我的世界_手机触摸失灵不更换屏幕,如何把手机里面的资料导出来,值得收藏...
  6. 简单维修MacBook Air——更换SSD硬盘
  7. 替代SSD?Crossbar进军中国存储市场
  8. 美国大学生数学建模竞赛赛题题型分类
  9. QQ群排名优化到霸屏的策略怎么做?
  10. python web面试题部分汇总