JavaScript 权威指南第七版(GPT 重译)(四)(2)https://developer.aliyun.com/article/1485342
10.3.6 使用 import()进行动态导入
我们已经看到 ES6 的 import 和 export 指令是完全静态的,并且使 JavaScript 解释器和其他 JavaScript 工具能够在加载模块时通过简单的文本分析确定模块之间的关系,而无需实际执行模块中的任何代码。使用静态导入的模块,你可以确保导入到模块中的值在你的模块中的任何代码开始运行之前就已经准备好供使用。
在 Web 上,代码必须通过网络传输,而不是从文件系统中读取。一旦传输,该代码通常在相对较慢的移动设备上执行。这不是静态模块导入(需要在任何代码运行之前加载整个程序)有很多意义的环境。
Web 应用程序通常只加载足够的代码来渲染用户看到的第一页。然后,一旦用户有一些初步内容可以交互,它们就可以开始加载通常需要更多的代码来完成网页应用程序的其余部分。Web 浏览器通过使用 DOM API 将新的 <script> 标签注入到当前 HTML 文档中,使动态加载代码变得容易,而 Web 应用程序多年来一直在这样做。
尽管动态加载已经很久了,但它并不是语言本身的一部分。这在 ES2020 中发生了变化(截至 2020 年初,支持 ES6 模块的所有浏览器都支持动态导入)。你将一个模块规范传递给 import(),它将返回一个代表加载和运行指定模块的异步过程的 Promise 对象。当动态导入完成时,Promise 将“完成”(请参阅 第十三章 了解有关异步编程和 Promise 的完整细节),并产生一个对象,就像你使用静态导入语句的 import * as 形式一样。
因此,我们可以像这样静态导入“./stats.js”模块:
import * as stats from "./stats.js";
我们可以像这样动态导入并使用它:
import("./stats.js").then(stats => { let average = stats.mean(data); })
或者,在一个 async 函数中(再次,你可能需要在理解这段代码之前阅读 第十三章),我们可以用 await 简化代码:
async analyzeData(data) { let stats = await import("./stats.js"); return { average: stats.mean(data), stddev: stats.stddev(data) }; }
import() 的参数应该是一个模块规范,就像你会在静态 import 指令中使用的那样。但是使用 import(),你不受限于使用常量字符串文字:任何表达式只要以正确形式评估为字符串即可。
动态 import() 看起来像一个函数调用,但实际上不是。相反,import() 是一个操作符,括号是操作符语法的必需部分。这种不寻常的语法之所以存在是因为 import() 需要能够将模块规范解析为相对于当前运行模块的 URL,这需要一些实现魔法,这是不合法的放在 JavaScript 函数中的。在实践中,函数与操作符的区别很少有影响,但如果尝试编写像 console.log(import); 或 let require = import; 这样的代码,你会注意到这一点。
最后,请注意动态 import() 不仅适用于 Web 浏览器。代码打包工具如 webpack 也可以很好地利用它。使用代码捆绑器的最简单方法是告诉它程序的主入口点,让它找到所有静态 import 指令并将所有内容组装成一个大文件。然而,通过策略性地使用动态 import() 调用,你可以将这个单一的庞大捆绑拆分成一组可以按需加载的较小捆绑。
10.3.7 import.meta.url
ES6 模块系统的最后一个特性需要讨论。在 ES6 模块中(但不在常规的 <script> 或使用 require() 加载的 Node 模块中),特殊语法 import.meta 指的是一个包含有关当前执行模块的元数据的对象。该对象的 url 属性是加载模块的 URL。(在 Node 中,这将是一个 file:// URL。)
import.meta.url 的主要用例是能够引用存储在与模块相同目录中(或相对于模块)的图像、数据文件或其他资源。URL() 构造函数使得相对 URL 相对于绝对 URL(如 import.meta.url)容易解析。例如,假设你编写了一个模块,其中包含需要本地化的字符串,并且本地化文件存储在与模块本身相同目录中的 l10n/ 目录中。你的模块可以使用类似这样的函数创建的 URL 加载其字符串:
function localStringsURL(locale) { return new URL(`l10n/${locale}.json`, import.meta.url); }
10.4 总结
模块化的目标是允许程序员隐藏其代码的实现细节,以便来自各种来源的代码块可以组装成大型程序,而不必担心一个代码块会覆盖另一个的函数或变量。本章已经解释了三种不同的 JavaScript 模块系统:
- 在 JavaScript 的早期,模块化只能通过巧妙地使用立即调用的函数表达式来实现。
- Node 在 JavaScript 语言之上添加了自己的模块系统。Node 模块通过
require()导入,并通过设置 Exports 对象的属性或设置module.exports属性来定义它们的导出。 - 在 ES6 中,JavaScript 终于拥有了自己的模块系统,使用
import和export关键字,而 ES2020 正在添加对使用import()进行动态导入的支持。
¹ 例如:经常进行增量更新并且用户频繁返回访问的 Web 应用程序可能会发现,使用小模块而不是大捆绑包可以更好地利用用户浏览器缓存,从而导致更好的平均加载时间。
第十一章:JavaScript 标准库
一些数据类型,如数字和字符串(第三章)、对象(第六章)和数组(第七章)对于 JavaScript 来说是如此基础,以至于我们可以将它们视为语言本身的一部分。本章涵盖了其他重要但不太基础的 API,可以被视为 JavaScript 的“标准库”:这些是内置于 JavaScript 中的有用类和函数,可供所有 Web 浏览器和 Node 中的 JavaScript 程序使用。¹
本章的各节相互独立,您可以按任意顺序阅读它们。它们涵盖了:
- Set 和 Map 类,用于表示值的集合和从一个值集合到另一个值集合的映射。
- 类型化数组(TypedArrays)等类似数组的对象,表示二进制数据的数组,以及用于从非数组二进制数据中提取值的相关类。
- 正则表达式和 RegExp 类,定义文本模式,对文本处理很有用。本节还详细介绍了正则表达式语法。
- Date 类用于表示和操作日期和时间。
- Error 类及其各种子类的实例,当 JavaScript 程序发生错误时抛出。
- JSON 对象,其方法支持对由对象、数组、字符串、数字和布尔值组成的 JavaScript 数据结构进行序列化和反序列化。
- Intl 对象及其定义的类,可帮助您本地化 JavaScript 程序。
- Console 对象,其方法以特别有用的方式输出字符串,用于调试程序和记录程序的行为。
- URL 类简化了解析和操作 URL 的任务。本节还涵盖了用于对 URL 及其组件进行编码和解码的全局函数。
setTimeout()及相关函数用于指定在经过指定时间间隔后执行的代码。
本章中的一些部分,尤其是关于类型化数组和正则表达式的部分,由于您需要理解的重要背景信息较多,因此相当长。然而,其他许多部分很短:它们只是介绍一个新的 API 并展示其使用示例。
11.1 集合和映射
JavaScript 的 Object 类型是一种多功能的数据结构,可以用来将字符串(对象的属性名称)映射到任意值。当被映射的值是像true这样固定的值时,那么对象实际上就是一组字符串。
在 JavaScript 编程中,对象实际上经常被用作映射和集合,但由于限制为字符串并且对象通常继承具有诸如“toString”之类名称的属性,这使得使用起来有些复杂,通常这些属性并不打算成为映射或集合的一部分。
出于这个原因,ES6 引入了真正的 Set 和 Map 类,我们将在接下来的子章节中介绍。
11.1.1 Set 类
集合是一组值,类似于数组。但与数组不同,集合没有顺序或索引,并且不允许重复:一个值要么是集合的成员,要么不是成员;无法询问一个值在集合中出现多少次。
使用Set()构造函数创建一个 Set 对象:
let s = new Set(); // A new, empty set let t = new Set([1, s]); // A new set with two members
Set()构造函数的参数不一定是数组:任何可迭代对象(包括其他 Set 对象)都是允许的:
let t = new Set(s); // A new set that copies the elements of s. let unique = new Set("Mississippi"); // 4 elements: "M", "i", "s", and "p"
集合的size属性类似于数组的length属性:它告诉你集合包含多少个值:
unique.size // => 4
创建集合时无需初始化。您可以随时使用add()、delete()和clear()添加和删除元素。请记住,集合不能包含重复项,因此向集合添加已包含的值不会产生任何效果:
let s = new Set(); // Start empty s.size // => 0 s.add(1); // Add a number s.size // => 1; now the set has one member s.add(1); // Add the same number again s.size // => 1; the size does not change s.add(true); // Add another value; note that it is fine to mix types s.size // => 2 s.add([1,2,3]); // Add an array value s.size // => 3; the array was added, not its elements s.delete(1) // => true: successfully deleted element 1 s.size // => 2: the size is back down to 2 s.delete("test") // => false: "test" was not a member, deletion failed s.delete(true) // => true: delete succeeded s.delete([1,2,3]) // => false: the array in the set is different s.size // => 1: there is still that one array in the set s.clear(); // Remove everything from the set 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 is a one-digit prime number oneDigitPrimes.has(3) // => true: so is 3 oneDigitPrimes.has(4) // => false: 4 is not a prime oneDigitPrimes.has("5") // => false: "5" is not even a number
关于集合最重要的一点是它们被优化用于成员测试,无论集合有多少成员,has()方法都会非常快。数组的includes()方法也执行成员测试,但所需时间与数组的大小成正比,使用数组作为集合可能比使用真正的 Set 对象慢得多。
Set 类是可迭代的,这意味着您可以使用for/of循环枚举集合的所有元素:
let sum = 0; for(let p of oneDigitPrimes) { // Loop through the one-digit primes sum += p; // and add them up } sum // => 17: 2 + 3 + 5 + 7
因为 Set 对象是可迭代的,您可以使用...扩展运算符将它们转换为数组和参数列表:
[...oneDigitPrimes] // => [2,3,5,7]: the set converted to an Array Math.max(...oneDigitPrimes) // => 7: set elements passed as function arguments
集合经常被描述为“无序集合”。然而,对于 JavaScript Set 类来说,这并不完全正确。JavaScript 集合是无索引的:您无法像数组那样请求集合的第一个或第三个元素。但是 JavaScript Set 类始终记住元素插入的顺序,并且在迭代集合时始终使用此顺序:插入的第一个元素将是首个迭代的元素(假设您尚未首先删除它),最近插入的元素将是最后一个迭代的元素。²
除了可迭代外,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(); // Create a new, empty map let n = new Map([ // A new map initialized with string keys mapped to numbers ["one", 1], ["two", 2] ]);
Map() 构造函数的可选参数应该是一个可迭代对象,产生两个元素 [key, value] 数组。在实践中,这意味着如果你想在创建 map 时初始化它,你通常会将所需的键和关联值写成数组的数组。但你也可以使用 Map() 构造函数复制其他 map,或从现有对象复制属性名和值:
let copy = new Map(n); // A new map with the same keys and values as map n let o = { x: 1, y: 2}; // An object with two properties let p = new Map(Object.entries(o)); // Same as new map([["x", 1], ["y", 2]])
一旦你创建了一个 Map 对象,你可以使用 get() 查询与给定键关联的值,并可以使用 set() 添加新的键/值对。但请记住,map 是一组键,每个键都有一个关联的值。这与一组键/值对并不完全相同。如果你使用一个已经存在于 map 中的键调用 set(),你将改变与该键关联的值,而不是添加一个新的键/值映射。除了 get() 和 set(),Map 类还定义了类似 Set 方法的方法:使用 has() 检查 map 是否包含指定的键;使用 delete() 从 map 中删除一个键(及其关联的值);使用 clear() 从 map 中删除所有键/值对;使用 size 属性查找 map 包含多少个键。
let m = new Map(); // Start with an empty map m.size // => 0: empty maps have no keys m.set("one", 1); // Map the key "one" to the value 1 m.set("two", 2); // And the key "two" to the value 2. m.size // => 2: the map now has two keys m.get("two") // => 2: return the value associated with key "two" m.get("three") // => undefined: this key is not in the set m.set("one", true); // Change the value associated with an existing key m.size // => 2: the size doesn't change m.has("one") // => true: the map has a key "one" m.has(true) // => false: the map does not have a key true m.delete("one") // => true: the key existed and deletion succeeded m.size // => 1 m.delete("three") // => false: failed to delete a nonexistent key m.clear(); // Remove all keys and values from the map
像 Set 的 add() 方法一样,Map 的 set() 方法可以链接,这允许初始化 map 而不使用数组的数组:
let m = new Map().set("one", 1).set("two", 2).set("three", 3); m.size // => 3 m.get("two") // => 2
与 Set 一样,任何 JavaScript 值都可以用作 Map 中的键或值。这包括 null、undefined 和 NaN,以及对象和数组等引用类型。与 Set 类一样,Map 通过标识比较键,而不是通过相等性比较,因此如果你使用对象或数组作为键,它将被认为与每个其他对象和数组都不同,即使它们具有完全相同的属性或元素:
let m = new Map(); // Start with an empty map. m.set({}, 1); // Map one empty object to the number 1. m.set({}, 2); // Map a different empty object to the number 2. m.size // => 2: there are two keys in this map m.get({}) // => undefined: but this empty object is not a key m.set(m, undefined); // Map the map itself to the value undefined. m.has(m) // => true: m is a key in itself m.get(m) // => undefined: same value we'd get if m wasn't a key
Map 对象是可迭代的,每个迭代的值都是一个包含两个元素的数组,第一个元素是键,第二个元素是与该键关联的值。如果你使用展开运算符与 Map 对象一起使用,你将得到一个类似于我们传递给 Map() 构造函数的数组的数组。在使用 for/of 循环迭代 map 时,惯用的做法是使用解构赋值将键和值分配给单独的变量:
let m = new Map([["x", 1], ["y", 2]]); [...m] // => [["x", 1], ["y", 2]] for(let [key, value] of m) { // On the first iteration, key will be "x" and value will be 1 // On the second iteration, key will be "y" and value will be 2 }
像 Set 类一样,Map 类按插入顺序进行迭代。迭代的第一个键/值对将是最近添加到 map 中的键/值对,而迭代的最后一个键/值对将是最近添加的键/值对。
如果你想仅迭代 map 的键或仅迭代关联的值,请使用 keys() 和 values() 方法:这些方法返回可迭代对象,按插入顺序迭代键和值。(entries() 方法返回一个可迭代对象,按键/值对迭代,但这与直接迭代 map 完全相同。)
[...m.keys()] // => ["x", "y"]: just the keys [...m.values()] // => [1, 2]: just the values [...m.entries()] // => [["x", 1], ["y", 2]]: same as [...m]
Map 对象也可以使用首次由 Array 类实现的 forEach() 方法进行迭代。
m.forEach((value, key) => { // note value, key NOT key, value // On the first invocation, value will be 1 and key will be "x" // On the second invocation, value will be 2 and key will be "y" });
可能会觉得上面的代码中值参数在键参数之前有些奇怪,因为在 for/of 迭代中,键首先出现。正如本节开头所述,你可以将 map 视为一个广义的数组,其中整数数组索引被任意键值替换。数组的 forEach() 方法首先传递数组元素,然后传递数组索引,因此,类比地,map 的 forEach() 方法首先传递 map 值,然后传递 map 键。
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 的预期用途是允许您将值与对象关联而不会导致内存泄漏。例如,假设您正在编写一个函数,该函数接受一个对象参数并需要对该对象执行一些耗时的计算。为了效率,您希望缓存计算后的值以供以后重用。如果使用 Map 对象来实现缓存,将阻止任何对象被回收,但使用 WeakMap,您可以避免这个问题。(您通常可以使用私有 Symbol 属性直接在对象上缓存计算后的值来实现类似的结果。参见§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 中的新功能,³它们更接近这些语言的低级数组。类型化数组在技术上不是数组(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 位整数,以 BigInt 值的形式表示在 JavaScript 中(参见 §3.2.5)。以 Float 开头的类型保存浮点数。Float64Array 的元素与普通的 JavaScript 数字相同类型。Float32Array 的元素精度较低,范围较小,但只需一半的内存。 (在 C 和 Java 中,此类型称为 float。)
Uint8ClampedArray 是 Uint8Array 的特殊变体。这两种类型都保存无符号字节,可以表示 0 到 255 之间的数字。对于 Uint8Array,如果将大于 255 或小于零的值存储到数组元素中,它会“环绕”,并且会得到其他值。这是计算机内存在低级别上的工作原理,因此速度非常快。Uint8ClampedArray 进行了一些额外的类型检查,以便如果存储大于 255 或小于 0 的值,则会“夹紧”到 255 或 0,而不会环绕。 (这种夹紧行为是 HTML <canvas> 元素的低级 API 用于操作像素颜色所必需的。)
每个类型化数组构造函数都有一个 BYTES_PER_ELEMENT 属性,其值为 1、2、4 或 8,取决于类型。
11.2.2 创建类型化数组
创建类型化数组的最简单方法是调用适当的构造函数,并提供一个数字参数,指定数组中要包含的元素数量:
let bytes = new Uint8Array(1024); // 1024 bytes let matrix = new Float64Array(9); // A 3x3 matrix let point = new Int16Array(3); // A point in 3D space let rgba = new Uint8ClampedArray(4); // A 4-byte RGBA pixel value let sudoku = new Int8Array(81); // A 9x9 sudoku board
通过这种方式创建类型化数组时,数组元素都保证初始化为 0、0n 或 0.0。但是,如果您知道要在类型化数组中使用的值,也可以在创建数组时指定这些值。每个类型化数组构造函数都有静态的 from() 和 of() 工厂方法,类似于 Array.from() 和 Array.of():
let white = Uint8ClampedArray.of(255, 255, 255, 0); // RGBA opaque white
请记住,Array.from() 工厂方法的第一个参数应为类似数组或可迭代对象。对于类型化数组变体也是如此,只是可迭代或类似数组的对象还必须具有数值元素。例如,字符串是可迭代的,但将它们传递给类型化数组的 from() 工厂方法是没有意义的。
如果只使用 from() 的单参数版本,可以省略 .from 并直接将可迭代或类似数组对象传递给构造函数,其行为完全相同。请注意,构造函数和 from() 工厂方法都允许您复制现有的类型化数组,同时可能更改类型:
let ints = Uint32Array.from(white); // The same 4 numbers, but as ints
当从现有数组、可迭代对象或类似数组对象创建新的类型化数组时,值可能会被截断以符合数组的类型约束。当发生这种情况时,不会有警告或错误:
// Floats truncated to ints, longer ints truncated to 8 bits Uint8Array.of(1.23, 2.99, 45000) // => new Uint8Array([1, 2, 200])
最后,还有一种使用 ArrayBuffer 类型创建 typed arrays 的方法。ArrayBuffer 是一个对一块内存的不透明引用。你可以用构造函数创建一个,只需传入你想要分配的内存字节数:
let buffer = new ArrayBuffer(1024*1024); buffer.byteLength // => 1024*1024; one megabyte of memory
ArrayBuffer 类不允许你读取或写入你分配的任何字节。但你可以创建使用 buffer 内存的 typed arrays,并且允许你读取和写入该内存。为此,调用 typed array 构造函数,第一个参数是一个 ArrayBuffer,第二个参数是数组缓冲区内的字节偏移量,第三个参数是数组长度(以元素而不是字节计算)。第二和第三个参数是可选的。如果两者都省略,则数组将使用数组缓冲区中的所有内存。如果只省略长度参数,则你的数组将使用从起始位置到数组结束的所有可用内存。关于这种形式的 typed array 构造函数还有一件事要记住:数组必须是内存对齐的,所以如果你指定了一个字节偏移量,该值应该是你的类型大小的倍数。例如,Int32Array() 构造函数需要四的倍数,而 Float64Array() 需要八的倍数。
给定之前创建的 ArrayBuffer,你可以创建这样的 typed arrays:
let asbytes = new Uint8Array(buffer); // Viewed as bytes let asints = new Int32Array(buffer); // Viewed as 32-bit signed ints let lastK = new Uint8Array(buffer, 1023*1024); // Last kilobyte as bytes let ints2 = new Int32Array(buffer, 1024, 256); // 2nd kilobyte as 256 integers
这四种 typed arrays 提供了对由 ArrayBuffer 表示的内存的四种不同视图。重要的是要理解,所有 typed arrays 都有一个底层的 ArrayBuffer,即使你没有明确指定一个。如果你调用一个 typed array 构造函数而没有传递一个 buffer 对象,一个适当大小的 buffer 将会被自动创建。正如后面所描述的,任何 typed array 的 buffer 属性都指向它的底层 ArrayBuffer 对象。直接使用 ArrayBuffer 对象的原因是有时你可能想要有一个单一 buffer 的多个 typed array 视图。
11.2.3 使用 Typed Arrays
一旦你创建了一个 typed array,你可以用常规的方括号表示法读取和写入它的元素,就像你对待任何其他类似数组的对象一样:
// Return the largest prime smaller than n, using the sieve of Eratosthenes function sieve(n) { let a = new Uint8Array(n+1); // a[x] will be 1 if x is composite let max = Math.floor(Math.sqrt(n)); // Don't do factors higher than this let p = 2; // 2 is the first prime while(p <= max) { // For primes less than max for(let i = 2*p; i <= n; i += p) // Mark multiples of p as composite a[i] = 1; while(a[++p]) /* empty */; // The next unmarked index is prime } while(a[n]) n--; // Loop backward to find the last prime return n; // And return it }
这里的函数计算比你指定的数字小的最大质数。代码与使用常规 JavaScript 数组完全相同,但在我的测试中使用 Uint8Array() 而不是 Array() 使代码运行速度超过四倍,并且使用的内存少了八倍。
Typed arrays 不是真正的数组,但它们重新实现了大多数数组方法,所以你可以几乎像使用常规数组一样使用它们:
let ints = new Int16Array(10); // 10 short integers ints.fill(3).map(x=>x*x).join("") // => "9999999999"
记住,typed arrays 有固定的长度,所以 length 属性是只读的,而改变数组长度的方法(如 push()、pop()、unshift()、shift() 和 splice())对 typed arrays 没有实现。改变数组内容而不改变长度的方法(如 sort()、reverse() 和 fill())是实现的。返回新数组的 map() 和 slice() 等方法返回与调用它们的 typed array 相同类型的 typed array。
11.2.4 Typed Array 方法和属性
除了标准数组方法外,typed arrays 也实现了一些自己的方法。set() 方法通过将常规或 typed array 的元素复制到 typed array 中一次设置多个元素:
let bytes = new Uint8Array(1024); // A 1K buffer let pattern = new Uint8Array([0,1,2,3]); // An array of 4 bytes bytes.set(pattern); // Copy them to the start of another byte array bytes.set(pattern, 4); // Copy them again at a different offset bytes.set([0,1,2,3], 8); // Or just copy values direct from a regular array bytes.slice(0, 12) // => new Uint8Array([0,1,2,3,0,1,2,3,0,1,2,3])
set() 方法以数组或 typed array 作为第一个参数,以元素偏移量作为可选的第二个参数,如果未指定则默认为 0。如果你从一个 typed array 复制值到另一个,这个操作可能会非常快。
Typed arrays 还有一个 subarray 方法,返回调用它的数组的一部分:
let ints = new Int16Array([0,1,2,3,4,5,6,7,8,9]); // 10 short integers let last3 = ints.subarray(ints.length-3, ints.length); // Last 3 of them last3[0] // => 7: this is the same as ints[7]
subarray()接受与slice()方法相同的参数,并且似乎工作方式相同。但有一个重要的区别。slice()返回一个新的、独立的类型化数组,其中包含指定的元素,不与原始数组共享内存。subarray()不复制任何内存;它只返回相同底层值的新视图:
ints[9] = -1; // Change a value in the original array and... last3[2] // => -1: it also changes in the subarray
subarray()方法返回现有数组的新视图,这让我们回到了 ArrayBuffers 的话题。每个类型化数组都有三个与底层缓冲区相关的属性:
last3.buffer // The ArrayBuffer object for a typed array last3.buffer === ints.buffer // => true: both are views of the same buffer last3.byteOffset // => 14: this view starts at byte 14 of the buffer last3.byteLength // => 6: this view is 6 bytes (3 16-bit ints) long last3.buffer.byteLength // => 20: but the underlying buffer has 20 bytes
buffer属性是数组的 ArrayBuffer。byteOffset是数组数据在底层缓冲区中的起始位置。byteLength是数组数据的字节长度。对于任何类型化数组a,这个不变式应该始终成立:
a.length * a.BYTES_PER_ELEMENT === a.byteLength // => true
ArrayBuffer 只是不透明的字节块。您可以使用类型化数组访问这些字节,但 ArrayBuffer 本身不是类型化数组。但要小心:您可以像在任何 JavaScript 对象上一样使用数字数组索引访问 ArrayBuffers。这样做并不会让您访问缓冲区中的字节,但可能会导致混乱的错误:
let bytes = new Uint8Array(8); bytes[0] = 1; // Set the first byte to 1 bytes.buffer[0] // => undefined: buffer doesn't have index 0 bytes.buffer[1] = 255; // Try incorrectly to set a byte in the buffer bytes.buffer[1] // => 255: this just sets a regular JS property bytes[1] // => 0: the line above did not set the byte
我们之前看到,您可以使用ArrayBuffer()构造函数创建一个 ArrayBuffer,然后创建使用该缓冲区的类型化数组。另一种方法是创建一个初始类型化数组,然后使用该数组的缓冲区创建其他视图:
let bytes = new Uint8Array(1024); // 1024 bytes let ints = new Uint32Array(bytes.buffer); // or 256 integers let floats = new Float64Array(bytes.buffer); // or 128 doubles
11.2.5 DataView 和字节顺序
类型化数组允许您以 8、16、32 或 64 位的块查看相同的字节序列。这暴露了“字节序”:字节被排列成更长字的顺序。为了效率,类型化数组使用底层硬件的本机字节顺序。在小端系统上,数字的字节从最不重要到最重要的顺序排列在 ArrayBuffer 中。在大端平台上,字节从最重要到最不重要的顺序排列。您可以使用以下代码确定底层平台的字节顺序:
// If the integer 0x00000001 is arranged in memory as 01 00 00 00, then // we're on a little-endian platform. On a big-endian platform, we'd get // bytes 00 00 00 01 instead. let littleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;
如今,最常见的 CPU 架构是小端。然而,许多网络协议和一些二进制文件格式要求大端字节顺序。如果您正在使用来自网络或文件的数据的类型化数组,您不能仅仅假设平台的字节顺序与数据的字节顺序相匹配。一般来说,在处理外部数据时,您可以使用 Int8Array 和 Uint8Array 将数据视为单个字节的数组,但不应使用其他具有多字节字长的类型化数组。相反,您可以使用 DataView 类,该类定义了用于从具有明确定义的字节顺序的 ArrayBuffer 中读取和写入值的方法:
// Assume we have a typed array of bytes of binary data to process. First, // we create a DataView object so we can flexibly read and write // values from those bytes let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let int = view.getInt32(0); // Read big-endian signed int from byte 0 int = view.getInt32(4, false); // Next int is also big-endian int = view.getUint32(8, true); // Next int is little-endian and unsigned view.setUint32(8, int, false); // Write it back in big-endian format
DataView 为每个 10 个类型化数组类定义了 10 个get方法(不包括 Uint8ClampedArray)。它们的名称类似于getInt16()、getUint32()、getBigInt64()和getFloat64()。第一个参数是 ArrayBuffer 中数值开始的字节偏移量。除了getInt8()和getUint8()之外,所有这些获取方法都接受一个可选的布尔值作为第二个参数。如果省略第二个参数或为false,则使用大端字节顺序。如果第二个参数为true,则使用小端顺序。
DataView 还定义了 10 个相应的 Set 方法,用于将值写入底层 ArrayBuffer。第一个参数是值开始的偏移量。第二个参数是要写入的值。除了setInt8()和setUint8()之外,每个方法都接受一个可选的第三个参数。如果省略参数或为false,则以大端格式写入值,最重要的字节在前。如果参数为true,则以小端格式写入值,最不重要的字节在前。
类型化数组和 DataView 类为您提供了处理二进制数据所需的所有工具,并使您能够编写执行诸如解压缩 ZIP 文件或从 JPEG 文件中提取元数据等操作的 JavaScript 程序。
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”作为最后一个字符的字符串。
正如我们将看到的,正则表达式也可以有一个或多个标志字符,影响它们的工作方式。标志是在 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 之间的一到六个十六进制数字。请注意,此语法仅在使用 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 数字之外的字符。等同于 [⁰-9]。 |
[\b] |
一个字面退格(特殊情况)。 |
请注意,特殊字符类转义可以在方括号内使用。\s 匹配任何空白字符,\d 匹配任何数字,因此 /[\s\d]/ 匹配任何一个空白字符或数字。请注意有一个特殊情况。正如稍后将看到的,\b 转义具有特殊含义。但是,在字符类中使用时,它表示退格字符。因此,要在正则表达式中字面表示退格字符,请使用具有一个元素的字符类:/[\b]/。
重复
到目前为止,您学到的正则表达式语法可以将两位数描述为 /\d\d/,将四位数描述为 /\d\d\d\d/。但是,您没有任何方法来描述,例如,可以具有任意数量的数字或三个字母后跟一个可选数字的字符串。这些更复杂的模式使用指定正则表达式元素可以重复多少次的正则表达式语法。
指定重复的字符始终跟随其应用的模式。由于某些类型的重复非常常见,因此有特殊字符来表示这些情况。例如,+ 匹配前一个模式的一个或多个出现。
表 11-3 总结了重复语法。
表 11-3. 正则表达式重复字符
| 字符 | 含义 |
{n,m} |
匹配前一个项目至少 n 次但不超过 m 次。 |
{n,} |
匹配前一个项目 n 次或更多次。 |
{n} |
匹配前一个项目的 n 次出现。 |
? |
匹配前一个项目的零次或一次出现。也就是说,前一个项目是可选的。等同于 {0,1}。 |
+ |
匹配前一个项目的一个或多个出现。等同于 {1,}。 |
* |
匹配前一个项目的零次或多次。等同于 {0,}。 |
以下行显示了一些示例:
let r = /\d{2,4}/; // Match between two and four digits r = /\w{3}\d?/; // Match exactly three word characters and an optional digit r = /\s+java\s+/; // Match "java" with one or more spaces before and after r = /[^(]*/; // Match zero or more characters that are not open parens
请注意,在所有这些示例中,重复说明符应用于它们之前的单个字符或字符类。如果要匹配更复杂表达式的重复,您需要使用括号定义一个组,这将在以下部分中解释。
使用 * 和 ? 重复字符时要小心。由于这些字符可能匹配前面的内容的零次,它们允许匹配空内容。例如,正则表达式 /a*/ 实际上匹配字符串“bbbb”,因为该字符串不包含字母 a 的任何出现!
非贪婪重复
表 11-3 中列出的重复字符尽可能多次匹配,同时仍允许正则表达式的任何后续部分匹配。我们说这种重复是“贪婪的”。还可以指定以非贪婪方式进行重复。只需在重复字符后面跟一个问号:??、+?、*?,甚至 {1,5}?。例如,正则表达式 /a+/ 匹配一个或多个字母 a 的出现。当应用于字符串“aaa”时,它匹配所有三个字母。但是 /a+?/ 匹配一个或多个字母 a 的出现,尽可能少地匹配字符。当应用于相同字符串时,此模式仅匹配第一个字母 a。
使用非贪婪重复可能不总是产生您期望的结果。考虑模式/a+b/,它匹配一个或多个 a,后跟字母 b。当应用于字符串“aaab”时,它匹配整个字符串。现在让我们使用非贪婪版本:/a+?b/。这应该匹配由尽可能少的 a 前导的字母 b。当应用于相同字符串“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]cript)被称为\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 时匹配的相同字符。组是括号内的子表达式(可能是嵌套的)。组号是通过从左到右计算左括号来分配的。使用(?:形成的组不编号。 |
指定匹配位置
正如前面所述,正则表达式的许多元素匹配字符串中的单个字符。例如,\s匹配单个空白字符。其他正则表达式元素匹配字符之间的位置而不是实际字符。例如,\b匹配 ASCII 单词边界——\w(ASCII 单词字符)和\W(非单词字符)之间的边界,或者 ASCII 单词字符和字符串的开头或结尾之间的边界。[⁴] 元素如\b不指定要在匹配的字符串中使用的任何字符;但它们指定的是合法的匹配位置。有时这些元素被称为正则表达式锚点,因为它们将模式锚定到搜索字符串中的特定位置。最常用的锚定元素是^,将模式绑定到字符串的开头,以及$,将模式锚定到字符串的结尾。
例如,要匹配单独一行的单词“JavaScript”,可以使用正则表达式/^JavaScript$/。如果要搜索“Java”作为单独的单词(而不是作为“JavaScript”中的前缀),可以尝试模式/\sJava\s/,这需要单词前后有空格。但是这种解决方案有两个问题。首先,它不匹配字符串的开头或结尾的“Java”,而只有在两侧有空格时才匹配。其次,当此模式找到匹配时,返回的匹配字符串具有前导和尾随空格,这不是所需的。因此,与其用\s匹配实际空格字符,不如用\b匹配(或锚定)单词边界。得到的表达式是/\bJava\b/。元素\B将匹配锚定到不是单词边界的位置。因此,模式/\B[Ss]cript/匹配“JavaScript”和“postscript”,但不匹配“script”或“Scripting”。
您还可以使用任意正则表达式作为锚定条件。如果在(?=和)字符之间包含一个表达式,那么这是一个前瞻断言,并且它指定封闭字符必须匹配,而不实际匹配它们。例如,要匹配一个常见编程语言的名称,但只有在后面跟着一个冒号时,您可以使用/[Jj]ava([Ss]cript)?(?=\:)/。这个模式匹配“JavaScript”中的单词“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。 |
标志
每个正则表达式都可以有一个或多个与之关联的标志,以改变其匹配行为。JavaScript 定义了六个可能的标志,每个标志由一个字母表示。标志在正则表达式字面量的第二个/字符之后指定,或者作为传递给RegExp()构造函数的第二个参数的字符串。支持的标志及其含义如下:
g
g标志表示正则表达式是“全局”的,也就是说,我们打算在字符串中找到所有匹配项,而不仅仅是找到第一个匹配项。这个标志不会改变匹配模式的方式,但正如我们稍后将看到的,它确实以重要的方式改变了 String match()方法和 RegExp exec()方法的行为。
i
i标志指定匹配模式时应该忽略大小写。
m
m标志指定匹配应该在“多行”模式下进行。它表示正则表达式将与多行字符串一起使用,并且^和$锚点应该匹配字符串的开头和结尾,以及字符串中各行的开头和结尾。
s
与m标志类似,s标志在处理包含换行符的文本时也很有用。通常,正则表达式中的“.”匹配除行终止符之外的任何字符。但是,当使用s标志时,“.”将匹配任何字符,包括行终止符。s标志在 ES2018 中添加到 JavaScript 中,并且截至 2020 年初,在 Node、Chrome、Edge 和 Safari 中支持,但在 Firefox 中不支持。
u
u标志代表 Unicode,它使正则表达式匹配完整的 Unicode 代码点,而不是匹配 16 位值。这个标志是在 ES6 中引入的,你应该养成在所有正则表达式上使用它的习惯,除非你有某种理由不这样做。如果你不使用这个标志,那么你的正则表达式将无法很好地处理包含表情符号和其他需要超过 16 位的字符(包括许多中文字符)的文本。没有u标志,".“字符匹配任何 1 个 UTF-16 16 位值。然而,有了这个标志,”."匹配一个 Unicode 代码点,包括那些超过 16 位的代码点。在正则表达式上设置u标志还允许你使用新的\u{...}转义序列来表示 Unicode 字符,并且还启用了\p{...}表示 Unicode 字符类。
y
y标志表示正则表达式是“粘性”的,应该在字符串的开头或上一个匹配项后的第一个字符处匹配。当与旨在找到单个匹配项的正则表达式一起使用时,它有效地将该正则表达式视为以^开头以将其锚定到字符串开头。这个标志在重复使用用于在字符串中找到所有匹配项的正则表达式时更有用。在这种情况下,它导致 String match()方法和 RegExp exec()方法的特殊行为,以强制每个后续匹配项都锚定到上一个匹配项结束的字符串位置。
这些标志可以以任何组合和任何顺序指定。例如,如果你希望你的正则表达式能够识别 Unicode 以进行不区分大小写的匹配,并且打算在字符串中查找多个匹配项,你可以指定标志uig,gui或这三个字母的任何其他排列。
11.3.2 用于模式匹配的字符串方法
到目前为止,我们一直在描述用于定义正则表达式的语法,但没有解释这些正则表达式如何在 JavaScript 代码中实际使用。我们现在转而介绍使用 RegExp 对象的 API。本节首先解释了使用正则表达式执行模式匹配和搜索替换操作的字符串方法。接下来的部分将继续讨论使用 JavaScript 正则表达式进行模式匹配,讨论 RegExp 对象及其方法和属性。
search()
字符串支持四种使用正则表达式的方法。最简单的是search()。这个方法接受一个正则表达式参数,并返回第一个匹配子字符串的起始字符位置,如果没有匹配则返回-1:
"JavaScript".search(/script/ui) // => 4 "Python".search(/script/ui) // => -1
如果search()的参数不是正则表达式,则首先通过将其传递给RegExp构造函数将其转换为正则表达式。search()不支持全局搜索;它会忽略其正则表达式参数的g标志。
replace()
replace()方法执行搜索替换操作。它将正则表达式作为第一个参数,替换字符串作为第二个参数。它在调用它的字符串中搜索与指定模式匹配的内容。如果正则表达式设置了g标志,replace()方法将在字符串中替换所有匹配项为替换字符串;否则,它只会替换找到的第一个匹配项。如果replace()的第一个参数是一个字符串而不是正则表达式,该方法会直接搜索该字符串而不是像search()那样将其转换为正则表达式。例如,你可以使用replace()如下提供文本字符串中“JavaScript”一词的统一大写格式:
// No matter how it is capitalized, replace it with the correct capitalization text.replace(/javascript/gi, "JavaScript");
然而,replace()比这更强大。回想一下,正则表达式的括号子表达式从左到右编号,并且正则表达式记住了每个子表达式匹配的文本。如果替换字符串中出现了$后跟一个数字,replace()将用指定子表达式匹配的文本替换这两个字符。这是一个非常有用的功能。例如,你可以使用它将字符串中的引号替换为其他字符:
// A quote is a quotation mark, followed by any number of // nonquotation mark characters (which we capture), followed // by another quotation mark. let quote = /"([^"]*)"/g; // Replace the straight quotation marks with guillemets // leaving the quoted text (stored in $1) unchanged. 'He said "stop"'.replace(quote, '«$1»') // => 'He said «stop»'
如果你的正则表达式使用了命名捕获组,那么你可以通过名称而不是数字引用匹配的文本:
let quote = /"(?<quotedText>[^"]*)"/g; 'He said "stop"'.replace(quote, '«$<quotedText>»') // => 'He said «stop»'
不需要将替换字符串作为第二个参数传递给replace(),你也可以传递一个函数作为替换值的计算方法。替换函数会被调用并传入多个参数。首先是整个匹配的文本。接下来,如果正则表达式有捕获组,那么被这些组捕获的子字符串将作为参数传递。下一个参数是匹配被找到的字符串中的位置。之后,调用replace()的整个字符串也会被传递。最后,如果正则表达式包含任何命名捕获组,替换函数的最后一个参数是一个对象,其属性名与捕获组名匹配,值为匹配的文本。例如,这里是使用替换函数将字符串中的十进制整数转换为十六进制的代码:
let s = "15 times 15 is 225"; s.replace(/\d+/gu, n => parseInt(n).toString(16)) // => "f times f is e1"
match()
match()方法是 String 正则表达式方法中最通用的。它将正则表达式作为唯一参数(或通过将其传递给RegExp()构造函数将其参数转换为正则表达式)并返回一个包含匹配结果的数组,如果没有找到匹配则返回null。如果正则表达式设置了g标志,该方法将返回出现在字符串中的所有匹配项的数组。例如:
"7 plus 8 equals 15".match(/\d+/g) // => ["7", "8", "15"]
如果正则表达式没有设置g标志,match()不会进行全局搜索;它只是搜索第一个匹配项。在这种非全局情况下,match()仍然返回一个数组,但数组元素完全不同。没有g标志时,返回数组的第一个元素是匹配的字符串,任何剩余的元素是正则表达式中括号捕获组匹配的子字符串。因此,如果match()返回一个数组a,a[0]包含完整匹配,a[1]包含匹配第一个括号表达式的子字符串,依此类推。与replace()方法类比,a[1]与$1相同,a[2]与$2相同,依此类推。
JavaScript 权威指南第七版(GPT 重译)(四)(4)https://developer.aliyun.com/article/1485345