探索-JavaScript-ES2025-版--十一-

探索 JavaScript(ES2025 版)(十一)

原文:exploringjs.com/js/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

44 异步函数 ES2017

原文:exploringjs.com/js/book/ch_async-functions.html

  1. 44.1 异步函数:基础知识

    1. 44.1.1 await运算符使 Promise 同步

    2. 44.1.2 从异步函数返回值解析函数的结果

    3. 44.1.3 异步可调用实体

  2. 44.2 await可以使用哪些值?

    1. 44.2.1 等待被解决的 Promise

    2. 44.2.2 等待被拒绝的 Promise

    3. 44.2.3 等待非 Promise 值

  3. 44.3 await在哪里可以使用?

    1. 44.3.1 在模块的最高级别使用await^(ES2022)

    2. 44.3.2 等待是浅层的

    3. 44.3.3 示例:使用异步函数作为回调的.map()

  4. 44.4 异步函数中的return

    1. 44.4.1 异步函数的结果始终是一个 Promise

    2. 44.4.2 返回一个 Promise 解析结果 Promise

  5. 44.5 异步函数同步开始,异步解决

  6. 44.6 使用异步函数的技巧

    1. 44.6.1 如果我们“发射并忘记”,则不需要await

    2. 44.6.2 有时可以await并忽略结果

    3. 44.6.3 return await的优缺点

  7. 44.7 并发和await(高级)

    1. 44.7.1 await:按顺序运行基于 Promise 的函数

    2. 44.7.2 await:并发运行基于 Promise 的函数

异步函数提供了更好的语法来编写使用 Promise 的代码。因此,Promise 是理解异步函数的必备知识。它们在上一章中有解释。

44.1 异步函数:基础知识

考虑以下异步函数:

async function fetchJsonAsync(url) {
  try {
    const request = await fetch(url); // async
    const text = await request.text(); // async
    return JSON.parse(text); // sync
  }
  catch (error) {
    assert.fail(error);
  }
}

两个关键字很重要:

  • function之前的async关键字表示这是一个异步函数。

  • await运算符应用于 Promise,要么提取履行值,要么抛出拒绝值。关于它的更多信息将在后面介绍。

之前的代码看起来相当同步,等价于以下直接使用 Promise 的代码:

function fetchJsonViaPromises(url) {
  return fetch(url) // async
  .then(request => request.text()) // async
  .then(text => JSON.parse(text)) // sync
  .catch((error) => {
    assert.fail(error);
  });
}

fetchJsonAsync()fetchJsonViaPromises()的调用方式完全相同——例如,像这样:

fetchJsonAsync('http://example.com/person.json')
.then((obj) => {
  assert.deepEqual(obj, {
    first: 'Jane',
    last: 'Doe',
  });
});

图标“详情”异步函数与直接使用 Promise 的函数一样基于 Promise

从外部来看,几乎无法区分 async 函数和返回 Promise 的函数之间的差异。

44.1.1 await运算符使 Promise 同步

在 async 函数的主体内部,我们像同步代码一样编写基于 Promise 的代码。我们只需要在值是 Promise 时应用await运算符。该运算符暂停 async 函数,并在 Promise 解决后恢复:

  • 如果 Promise 被解决,await返回解决值。

    async function f() {
     assert.equal(
     await Promise.resolve('fulfilled'),
     'fulfilled'
     );
    }
    
    
  • 如果 Promise 被拒绝,await会抛出拒绝值。

    async function f() {
     try {
     await Promise.reject('rejected');
     } catch (err) {
     assert.equal(err, 'rejected');
     }
    }
    
    

44.1.2 从 async 函数返回值解决函数的结果

async 函数的结果始终是一个 Promise:

  • 任何返回的值(显式或隐式)都用于解决那个 Promise:

    async function f1() { return 'fulfilled' }
    f1().then(
     result => assert.equal(result, 'fulfilled')
    );
    
    
  • 任何抛出的异常都用于拒绝 Promise:

    async function f() { throw 'rejected' }
    f().catch(
     error => assert.equal(error, 'rejected')
    );
    
    

44.1.3 异步可调用实体

JavaScript 有以下同步可调用实体的异步版本。它们的作用始终是真实函数或方法。

// Async function declaration
async function func1() {}

// Async function expression
const func2 = async function () {};

// Async arrow function
const func3 = async () => {};

// Async method definition in an object literal
const obj = { async m() {} };

// Async method definition in a class definition
class MyClass { async m() {} } 

Icon “details”异步函数与 async 函数的比较

“异步函数”和“async 函数”这两个术语之间的区别细微但很重要:

  • 异步函数是指任何异步传递其结果的函数——例如,基于回调的函数或基于 Promise 的函数。

  • async 函数通过特殊语法定义,涉及关键字asyncawait。由于这两个关键字,它也被称为 async/await。Async 函数基于 Promise,因此也是异步函数(这有点令人困惑)。

也就是说:这两个术语也经常互换使用。

Icon “exercise”练习:通过 async 函数使用 Fetch API

exercises/async-functions/fetch_json2_test.mjs

44.2 await可以使用哪些值?

await运算符只能在 async 函数和 async 生成器(在“异步生成器”(§45.2)中解释)内部使用。其操作数通常是 Promise,并导致以下步骤执行:

  • 当前 async 函数处于暂停状态。

  • 当 Promise 解决时,async 函数会恢复:

    • 如果 Promise 被解决,await返回解决值。

    • 如果 Promise 被拒绝,await会抛出拒绝值。

关于暂停和恢复的确切含义的更多信息,请参阅“异步函数同步开始,异步解决”(§44.5)。

继续阅读以了解await如何处理各种值。

44.2.1 等待解决的 Promise

如果其操作数是一个已解决的 Promise,await返回其解决值:

assert.equal(
  await Promise.resolve('fulfilled'), 'fulfilled'
);

await的值异步传递:

async function awaitPromise() {
 queueMicrotask( // (A)
 () => console.log('OTHER TASK')
 );
 console.log('before');
 await Promise.resolve('fulfilled');
 console.log('after');
}
await awaitPromise();

输出:

before
OTHER TASK
after

在行 A 中,我们不能使用setTimeout()。我们必须使用queueMicrotask(),因为与 Promise 相关的任务被称为所谓的微观任务,它们与正常任务不同,并且总是先于它们处理(通过一个微观任务队列)。更多信息,请参阅 MDN 文章“深入:微观任务和 JavaScript 运行时环境”

44.2.2 等待拒绝的 Promises

如果其操作数是一个拒绝的 Promise,那么await会抛出拒绝值:

try {
  await Promise.reject(
    new Error('Problem!')
  );
  assert.fail(); // we never get here
} catch (err) {
  assert.deepEqual(err, new Error('Problem!'));
}

44.2.3 等待非 Promise 值

非 Promise 值也可以被await,并且简单地传递:

assert.equal(
  await 'non-Promise value', 'non-Promise value'
);

即使在这种情况下,await的结果也是异步传递的:

async function awaitNonPromiseValue() {
 queueMicrotask(() => console.log('OTHER TASK'));
 console.log('before');
 await 'non-Promise value';
 console.log('after');
}
await awaitNonPromiseValue();

输出:

before
OTHER TASK
after

44.3 await在哪里可以使用?

44.3.1 在模块的最高级别使用await^(ES2022)

我们可以在模块的最高级别使用await——例如:

let mylib;
try {
  mylib = await import('https://primary.example.com/mylib');
} catch {
  mylib = await import('https://secondary.example.com/mylib');
}

关于这个特性的更多信息,请参阅“模块中的顶级await^(ES2022)(高级)”(§29.15)。

44.3.2 await是浅层的

如果我们在异步函数内部,并想通过await暂停它,我们必须直接在该函数内部这样做;我们无法在嵌套函数(如回调)内部使用它。也就是说,暂停是浅层的

让我们来看看这意味着什么。在下面的代码中,我们尝试在嵌套函数内部使用await

async function f() {
 const nestedFunc = () => {
 const result = await Promise.resolve('abc'); // SyntaxError!
 return 'RESULT: ' + result;
 };
 return [ nestedFunc() ];
} 

然而,这甚至不是一个有效的语法,因为await不允许在同步函数(如nestedFunc())内部使用。如果我们把nestedFunc()变成一个异步函数会发生什么?

async function f() {
 const nestedFunc = async () => {
 const result = await Promise.resolve('abc'); // (A)
 return 'RESULT: ' + result;
 };
 return [ nestedFunc() ]; // (B)
}
const arr = await f(); // (C)
assert.equal(
 arr[0] instanceof Promise, true
); 

这次,行 A 中的await暂停了nestedFunc(),而不是f()nestedFunc()返回一个 Promise,它在行 B 中被包裹在一个数组中。注意行 C 中的顶级await

要使此代码工作,我们必须等待nestedFunc()的结果:

async function f() {
 const nestedFunc = async () => {
 const result = await Promise.resolve('abc');
 return 'RESULT: ' + result;
 };
 return [ await nestedFunc() ];
}
assert.deepEqual(
 await f(), ['RESULT: abc']
); 

总结一下:await只影响立即周围的函数(该函数必须是异步函数)。

44.3.3 示例:使用异步函数作为回调的.map()

如果我们将异步函数用作.map()的回调会发生什么?那么结果是一个 Promise 数组:

const arrayOfPromises = arr.map(
  async (x) => { /*···*/ }
);

我们可以使用Promise.all()将 Promise 数组转换为 Promise 数组,并等待该 Promise:

const array = await Promise.all(
  arr.map(
    async (x) => { /*···*/ }
  )
);

我们在以下代码中使用这种技术,该代码通过fetch()下载文件。每个文件的内容是其文件名。

const urls = [
  'http://example.com/file1.txt',
  'http://example.com/file2.txt',
];
const uppercaseTexts = await Promise.all( // (A)
  urls.map(async (url) => {
    const response = await fetch(url);
    const text = await response.text();
    return text.toUpperCase();
  })
);
assert.deepEqual(
  uppercaseTexts,
  ['FILE1.TXT', 'FILE2.TXT']
);

图标“练习”练习:异步映射和过滤

exercises/async-functions/map_async_test.mjs

44.4 异步函数中的return

44.4.1 异步函数的结果始终是 Promise

如果我们调用一个异步函数,结果总是一个 Promise - 即使异步函数抛出异常。在异步函数内部,我们可以通过返回非 Promise 值(行 A)来履行结果 Promise:

async function asyncFunc() {
 return 123; // (A)
}

asyncFunc()
.then((result) => {
 assert.equal(result, 123);
});

通常,如果我们没有明确返回任何内容,则会自动返回 undefined

async function asyncFunc() {}

asyncFunc()
.then((result) => {
 assert.equal(result, undefined);
});

我们通过 throw(行 A)拒绝结果 Promise:

async function asyncFunc() {
 throw new Error('Problem!'); // (A)
}

asyncFunc()
.catch((err) => {
 assert.deepEqual(err, new Error('Problem!'));
});

44.4.2 返回一个 Promise 解决结果 Promise

如果我们返回一个 Promise q,那么它将解决异步函数的结果 Promise pp 采用 q 的状态(q 实际上替换了 p)。解决永远不会嵌套 Promise。

返回一个已解决的 Promise 会履行结果 Promise:

async function asyncFunc1() {
 return Promise.resolve('fulfilled');
}
const p1 = asyncFunc1();
p1.then(
 result => assert.equal(result, 'fulfilled')
);

返回一个拒绝的 Promise 与抛出异常具有相同的效果:

async function asyncFunc2() {
 return Promise.reject('rejected');
}
const p2 = asyncFunc2();
p2.catch(
 error => assert.equal(error, 'rejected')
);

return 的行为与以下情况中如何处理 Promise q 类似:

  • return qpromise.then((result) => { ··· }) 内部

  • return qpromise.catch((err) => { ··· }) 内部

  • resolve(q)new Promise((resolve, reject) => { ··· }) 内部

44.5 异步函数同步启动,异步解决

异步函数的执行如下:

  • 当异步函数开始时,会创建结果 Promise resultPromise

  • 然后执行主体。执行可以离开主体的两种方式:

    • resultPromise 解决时,会发生永久退出

      • return 解决 resultPromise

      • throw 拒绝 resultPromise

    • 当存在一个操作数为 Promise pawait 时,会发生临时退出

      • 异步函数暂停,执行离开它(类似于 同步生成器 中的 yield 的工作方式)。

      • 一旦 p 解决,它就会异步地(在一个新任务中)恢复。

  • 在第一次(永久或临时)退出后返回 Promise resultPromise

注意,resultPromise 解决的通知是异步发生的,这与 Promise 总是如此的情况一样。

以下代码演示了异步函数是同步启动的(行 A),然后当前任务完成(行 C),然后结果 Promise 解决 - 异步(行 B)。

async function asyncFunc() {
 console.log('asyncFunc() starts'); // (A)
 return 'abc';
}
asyncFunc().
then((x) => { // (B)
 console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)

输出:

asyncFunc() starts
Task ends
Resolved: abc

44.6 使用异步函数的技巧

44.6.1 如果我们“发射并忘记”,则不需要 await

在使用基于 Promise 的函数时,不需要 await;我们只需要它来暂停并等待返回的 Promise 解决。如果我们只想启动一个异步操作,那么我们不需要它:

async function asyncFunc() {
 const writer = openFile('someFile.txt');
 writer.write('hello'); // don’t wait
 writer.write('world'); // don’t wait
 await writer.close(); // wait for file to close
}

在此代码中,我们没有等待 .write(),因为我们不关心它何时完成。然而,我们确实想要等待 .close() 完成。

注意:每次调用 .write() 都是同步开始的。这防止了竞争条件。

44.6.2 它可以在 await 和忽略结果时有意义

有时使用 await 是有意义的,即使我们忽略了它的结果 – 例如:

await longRunningAsyncOperation();
console.log('Done!');

在这里,我们使用 await 来连接一个长时间运行的异步操作。这确保了日志确实是在该操作完成后发生的。

44.6.3 return await 的优缺点

如果我们在返回之前等待一个 Promise,我们会在立即重新包装之前先解包它:

async function f() {
 return await Promise.resolve('result');
}

由于 return 解决了 f() 的结果 Promise,以下代码更简单且等效:

async function f() {
 return Promise.resolve('result');
}

然而,有三个原因让我们坚持使用 return await

  • 代码片段更容易移动。

  • 我们不依赖于 Promise 的一个稍微晦涩的功能:解析解包 Promise。

  • 它在 try-catch 语句(见下文)中表现更好。

让我们探索最后一个原因。如果我们在线 A 等待被拒绝的 Promise 返回之前,它会导致异常:

async function f() {
 try {
 return await Promise.reject('error'); // (A)
 } catch (err) {
 return 'Caught an error: ' + err;
 }
}
f().then((result) => {
 assert.equal(result, 'Caught an error: error');
});

相反地,如果我们不使用 await 返回,则不会抛出异常,并且 f() 的结果 Promise 会采用被拒绝的 Promise 的状态:

async function f() {
 try {
 return Promise.reject('error');
 } catch (err) {
 return 'Caught an error: ' + err;
 }
}
f().catch((reason) => {
 assert.equal(reason, 'error');
});

44.7 并发和 await(高级)

在接下来的两个小节中,我们将使用辅助函数 returnAfterPause()

async function returnAfterPause(id) {
  console.log('START ' + id);
  await delay(10); // pause
  console.log('END ' + id);
  return id;
}

/**
 * Resolves after `ms` milliseconds
 */
function delay(ms) {
  return new Promise((resolve, _reject) => {
    setTimeout(resolve, ms);
  });
}

44.7.1 await:按顺序运行基于 Promise 的函数

如果我们在多个基于 Promise 的函数调用前加上 await,那么这些函数将按顺序执行:

async function sequentialAwait() {
 const result1 = await returnAfterPause('first');
 assert.equal(result1, 'first');

 const result2 = await returnAfterPause('second');
 assert.equal(result2, 'second');
}

输出结果:

START first
END first
START second
END second

即,returnAfterPause('second') 仅在 returnAfterPause('first') 完全完成后才开始。

44.7.2 await:并发运行基于 Promise 的函数

如果我们想要并发运行多个基于 Promise 的函数,我们可以使用实用方法 Promise.all()

async function concurrentPromiseAll() {
 const result = await Promise.all([
 returnAfterPause('first'),
 returnAfterPause('second'),
 ]);
 assert.deepEqual(result, ['first', 'second']);
}

输出结果:

START first
START second
END first
END second

在这里,两个异步函数同时启动。一旦两者都确定,await 会给我们一个满足值的数组,或者 – 如果至少有一个 Promise 被拒绝 – 一个异常。

回想一下 之前,重要的是我们何时启动基于 Promise 的计算;而不是我们如何处理其结果。因此,以下代码与之前的代码一样“并发”:

async function concurrentAwait() {
 const resultPromise1 = returnAfterPause('first');
 const resultPromise2 = returnAfterPause('second');

 assert.equal(await resultPromise1, 'first');
 assert.equal(await resultPromise2, 'second');
}

输出结果:

START first
START second
END first
END second

45 异步迭代 ES2018

原文:exploringjs.com/js/book/ch_async-iteration.html

  1. 45.1 基本异步迭代

    1. 45.1.1 协议:异步迭代

    2. 45.1.2 直接使用异步迭代

    3. 45.1.3 通过for-await-of使用异步迭代

  2. 45.2 异步生成器

    1. 45.2.1 示例:通过异步生成器创建异步可迭代对象

    2. 45.2.2 示例:将同步可迭代对象转换为异步可迭代对象

    3. 45.2.3 示例:将异步可迭代对象转换为数组

    4. 45.2.4 示例:转换异步可迭代对象

    5. 45.2.5 示例:映射异步可迭代对象

  3. 45.3 Node.js 流的异步迭代

    1. 45.3.1 Node.js 流:通过回调(推送)进行异步操作

    2. 45.3.2 Node.js 流:通过异步迭代(拉取)进行异步操作

    3. 45.3.3 示例:从块到行

阅读图标所需知识

对于本章,你应该熟悉:

  • Promise

  • 异步函数

45.1 基本异步迭代

45.1.1 协议:异步迭代

要理解异步迭代是如何工作的,让我们首先回顾一下同步迭代。它包括以下接口:

interface Iterable<T> {
  [Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
  next() : IteratorResult<T>;
}
interface IteratorResult<T> {
  value: T;
  done: boolean;
}

  • Iterable是一个可以通过迭代访问其内容的数据结构。它是一个迭代器的工厂。

  • Iterator是一个迭代结果的工厂,我们通过调用.next()方法来检索它。

  • 每个IterationResult包含迭代的.value和一个布尔值.done,在最后一个元素之后为true,在之前为false

对于异步迭代的协议,我们只想改变一件事:.next()产生的值应该异步传递。有两种可行的选择:

  • .value可能包含一个Promise<T>

  • .next()可能返回Promise<IteratorResult<T>>

换句话说,问题在于是否将值或整个迭代器结果包裹在 Promise 中。

必须是后者,因为当.next()返回一个结果时,它开始一个异步计算。该计算是否产生值或指示迭代结束,只能在计算完成后确定。因此,.done.value都需要被包裹在一个 Promise 中。

异步迭代的接口如下所示。

interface AsyncIterable<T> {
  [Symbol.asyncIterator]() : AsyncIterator<T>;
}
interface AsyncIterator<T> {
  next() : Promise<IteratorResult<T>>; // (A)
}
interface IteratorResult<T> {
  value: T;
  done: boolean;
}

与同步接口的唯一区别是 .next() 的返回类型(行 A)。

45.1.2 直接使用异步迭代

以下代码直接使用异步迭代协议:

const asyncIterable = syncToAsyncIterable(['a', 'b']); // (A)
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

// Call .next() until .done is true:
asyncIterator.next() // (B)
.then(iteratorResult => {
  assert.deepEqual(
    iteratorResult,
    { value: 'a', done: false });
  return asyncIterator.next(); // (C)
})
.then(iteratorResult => {
  assert.deepEqual(
    iteratorResult,
    { value: 'b', done: false });
  return asyncIterator.next(); // (D)
})
.then(iteratorResult => {
  assert.deepEqual(
    iteratorResult,
     { value: undefined, done: true });
})
;

在行 A 中,我们创建了一个异步可迭代对象,它遍历值 'a''b'。我们将在后面看到 syncToAsyncIterable() 的实现。

我们在行 B、行 C 和行 D 中调用 .next()。每次,我们使用 .then() 来解包 Promise 并使用 assert.deepEqual() 来检查解包的值。

如果我们使用异步函数,我们可以简化此代码。现在我们通过 await 解包 Promise,代码看起来几乎就像我们正在进行同步迭代:

async function f() {
 const asyncIterable = syncToAsyncIterable(['a', 'b']);
 const asyncIterator = asyncIterable[Symbol.asyncIterator]();

 // Call .next() until .done is true:
 assert.deepEqual(
 await asyncIterator.next(),
 { value: 'a', done: false });
 assert.deepEqual(
 await asyncIterator.next(),
 { value: 'b', done: false });
 assert.deepEqual(
 await asyncIterator.next(),
 { value: undefined, done: true });
}

45.1.3 通过 for-await-of 使用异步迭代

异步迭代协议不打算直接使用。支持它的语言结构之一是 for-await-of 循环,它是 for-of 循环的异步版本。它可以在异步函数和 异步生成器(在本章后面介绍)中使用。以下是一个 for-await-of 的使用示例:

for await (const x of syncToAsyncIterable(['a', 'b'])) {
  console.log(x);
}

输出:

a
b

for-await-of 相对灵活。除了异步可迭代对象外,它还支持同步可迭代对象:

for await (const x of ['a', 'b']) {
  console.log(x);
}

输出:

a
b

它还支持在 Promise 中包装的值上的同步可迭代对象:

const arr = [Promise.resolve('a'), Promise.resolve('b')];
for await (const x of arr) {
  console.log(x);
}

输出:

a
b

练习图标“exercise” 练习:异步迭代

  • 将异步可迭代对象转换为数组:exercises/async-iteration/async_iterable_to_array_test.mjs

    • 警告:我们将在本章中很快看到这个练习的解决方案。
  • 实现异步可迭代队列:exercises/async-iteration/async-iter-queue_test.mjs

45.2 异步生成器

异步生成器与同步生成器非常相似;特别是 yieldyield* 操作符。因此,这里不再解释。有关更多信息,请参阅同步生成器章节。

异步生成器同时是两件事:

  • 异步函数(输入):我们可以使用 awaitfor-await-of 来检索数据。

  • 返回异步可迭代对象的生成器(输出):我们可以使用 yieldyield* 来产生数据。

因此,异步生成器的输入和输出是:

  • 输入可以是:

    • 同步(单个值,同步可迭代对象)

    • 异步(Promise,异步可迭代对象)

  • 输出是一个异步迭代器(也是可迭代的)。

它看起来如下所示:

// Input: Promise and async iterable
async function* asyncGen(somePromise, someAsyncIterable) {
  const x = await somePromise;
  for await (const y of someAsyncIterable) {
    // ···
  }

  // Output: iterable async iterator
  yield someValue;
  yield* otherAsyncGen();
}

45.2.1 示例:通过异步生成器创建异步可迭代对象

让我们来看一个例子。以下代码创建了一个包含三个数字的异步可迭代对象:

async function* yield123() {
 for (let i=1; i<=3; i++) {
 yield i;
 }
}

yield123() 的结果是否符合异步迭代协议?

async function check() {
 const asyncIterable = yield123();
 const asyncIterator = asyncIterable[Symbol.asyncIterator]();
 assert.deepEqual(
 await asyncIterator.next(),
 { value: 1, done: false });
 assert.deepEqual(
 await asyncIterator.next(),
 { value: 2, done: false });
 assert.deepEqual(
 await asyncIterator.next(),
 { value: 3, done: false });
 assert.deepEqual(
 await asyncIterator.next(),
 { value: undefined, done: true });
}
check();

45.2.2 示例:将同步可迭代对象转换为异步可迭代对象

下面的异步生成器将同步迭代器转换为异步迭代器。它实现了我们之前使用的syncToAsyncIterable()函数。

async function* syncToAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem;
  }
}

注意:在这种情况下,输入是同步的(不需要await)。

45.2.3 示例:将异步迭代器转换为数组

下面的函数是之前一个练习的解决方案。它将异步迭代器转换为数组(想象一下展开,但用于异步迭代器而不是同步迭代器)。

async function asyncIterableToArray(asyncIterable) {
  const result = [];
  for await (const value of asyncIterable) {
    result.push(value);
  }
  return result;
}

注意,在这种情况下我们不能使用异步生成器:我们通过for-await-of获取输入,并返回一个被 Promise 包装的数组。后者的要求排除了异步生成器。

这是一个对asyncIterableToArray()的测试:

async function* createAsyncIterable() {
 yield 'a';
 yield 'b';
}
const asyncIterable = createAsyncIterable();
assert.deepEqual(
 await asyncIterableToArray(asyncIterable), // (A)
 ['a', 'b']
);

注意行 A 中的await,这是展开asyncIterableToArray()返回的 Promise 所必需的。为了使await能够工作,这段代码必须在一个异步函数内部运行。

45.2.4 示例:转换异步迭代器

让我们实现一个异步生成器,它通过转换现有的异步迭代器来生成一个新的异步迭代器。

async function* timesTwo(asyncNumbers) {
  for await (const x of asyncNumbers) {
    yield x * 2;
  }
}

为了测试这个函数,我们使用上一节中的asyncIterableToArray()

async function* createAsyncIterable() {
 for (let i=1; i<=3; i++) {
 yield i;
 }
}
assert.deepEqual(
 await asyncIterableToArray(timesTwo(createAsyncIterable())),
 [2, 4, 6]
);

图标“练习”练习:异步生成器

警告:我们很快将在本章中看到这个练习的解决方案。

  • exercises/async-iteration/number_lines_test.mjs

45.2.5 示例:异步迭代器上的映射

作为提醒,这是如何在同步迭代器上映射:

function* mapSync(iterable, func) {
  let index = 0;
  for (const x of iterable) {
    yield func(x, index);
    index++;
  }
}
const syncIterable = mapSync(['a', 'b', 'c'], s => s.repeat(3));
assert.deepEqual(
  Array.from(syncIterable),
  ['aaa', 'bbb', 'ccc']);

异步版本如下所示:

async function* mapAsync(asyncIterable, func) { // (A)
  let index = 0;
  for await (const x of asyncIterable) { // (B)
    yield func(x, index);
    index++;
  }
}

注意同步实现和异步实现是多么相似。唯一的两个区别是行 A 中的async和行 B 中的await。这相当于从同步函数到异步函数的转变——我们只需要添加关键字async和偶尔的await

为了测试mapAsync(),我们使用辅助函数asyncIterableToArray() (本章前面展示):

async function* createAsyncIterable() {
 yield 'a';
 yield 'b';
}
const mapped = mapAsync(
 createAsyncIterable(), s => s.repeat(3));
assert.deepEqual(
 await asyncIterableToArray(mapped), // (A)
 ['aaa', 'bbb']);

再次强调,我们使用await来展开 Promise(行 A),并且这段代码必须在一个异步函数内部运行。

图标“练习”练习:filterAsyncIter()

exercises/async-iteration/filter_async_iter_test.mjs

45.3 Node.js 流上的异步迭代

由于数据流的异步特性,异步迭代很好地作为它们的抽象。特别是,异步生成器是转换数据流(输入和输出都是流)的一个优雅工具。

45.3.1 Node.js 流:通过回调的异步(推送)

传统上,从 Node.js 流异步读取是通过回调完成的:

function main(inputFilePath) {
  const readStream = fs.createReadStream(inputFilePath,
    { encoding: 'utf-8', highWaterMark: 1024 });
  readStream.on('data', (chunk) => {
    console.log('>>> '+chunk);
  });
  readStream.on('end', () => {
    console.log('### DONE ###');
  });
}

也就是说,流控制着并推动数据到读取器。

45.3.2 Node.js 流:通过异步迭代(拉取)

从 Node.js 10 开始,我们也可以使用异步迭代从流中读取:

async function main(inputFilePath) {
  const readStream = fs.createReadStream(inputFilePath,
    { encoding: 'utf-8', highWaterMark: 1024 });

  for await (const chunk of readStream) {
    console.log('>>> '+chunk);
  }
  console.log('### DONE ###');
}

这次,读取器处于控制地位,并从流中拉取数据。

45.3.3 示例:从数据块到行

Node.js 流包含 数据块(任意长度的数据片段)。以下异步生成器将异步可迭代的数据块转换为异步可迭代的行:

/**
 * @param chunkIterable An asynchronous or synchronous iterable
 * over “chunks” (arbitrary strings)
 * @returns An asynchronous iterable over “lines”
 * (strings with at most one newline that always appears at the end)
 */
async function* chunksToLines(chunkIterable) {
  let previous = '';
  for await (const chunk of chunkIterable) {
    let startSearch = previous.length;
    previous += chunk;
    while (true) {
      // Works for EOL === '\n' and EOL === '\r\n'
      const eolIndex = previous.indexOf('\n', startSearch);
      if (eolIndex < 0) break;
      // Line includes the EOL
      const line = previous.slice(0, eolIndex+1);
      yield line;
      previous = previous.slice(eolIndex+1);
      startSearch = 0;
    }
  }
  if (previous.length > 0) {
    yield previous;
  }
}

让我们将 chunksToLines() 应用到一个异步可迭代的数据块上(由 chunkIterable() 生成):

async function* chunkIterable() {
 yield 'First\nSec';
 yield 'ond\nThird\nF';
 yield 'ourth';
}
const linesIterable = chunksToLines(chunkIterable());
assert.deepEqual(
 await asyncIterableToArray(linesIterable),
 [
 'First\n',
 'Second\n',
 'Third\n',
 'Fourth',
 ]);

现在我们有了异步可迭代的行,我们可以使用之前练习中的解决方案 numberLines() 来编号这些行:

async function* numberLines(linesAsync) {
  let lineNumber = 1;
  for await (const line of linesAsync) {
    yield lineNumber + ': ' + line;
    lineNumber++;
  }
}
const numberedLines = numberLines(chunksToLines(chunkIterable()));
assert.deepEqual(
  await asyncIterableToArray(numberedLines),
  [
    '1: First\n',
    '2: Second\n',
    '3: Third\n',
    '4: Fourth',
  ]);

IX 更多标准库

原文:exploringjs.com/js/book/pt_more-standard-library.html

46 正则表达式(RegExp)

原文:exploringjs.com/js/book/ch_regexps.html

  1. 46.1 创建正则表达式

    1. 46.1.1 字面量与构造函数

    2. 46.1.2 小贴士:使用 String.raw 字面量与 new RegExp()

    3. 46.1.3 克隆和不可破坏地修改正则表达式

  2. 46.2 语法字符和转义

    1. 46.2.1 语法字符

    2. 46.2.2 非法顶层转义

    3. 46.2.3 字符类内部转义 ([···])

  3. 46.3 正则表达式:字符的概念

  4. 46.4 语法:匹配单个字符

  5. 46.5 语法:字符类转义

    1. 46.5.1 基本字符类转义(代码单元集合):\d \D \s \S \w \W

    2. 46.5.2 Unicode 属性转义:\p{}\P{}

    3. 46.5.3 Unicode 字符属性转义 ^(ES2018)

    4. 46.5.4 Unicode 字符串属性转义 ^(ES2024)

  6. 46.6 语法:字符类

    1. 46.6.1 字符类中的码点序列 ^(ES2024)

    2. 46.6.2 字符类集合操作 ^(ES2024)

  7. 46.7 语法:捕获组

  8. 46.8 语法:量词

  9. 46.9 语法:断言

    1. 46.9.1 前瞻断言

    2. 46.9.2 后瞻断言 ^(ES2018)

  10. 46.10 语法:析取(|

  11. 46.11 正则表达式标志

    1. 46.11.1 如何排序正则表达式标志?

    2. 46.11.2 没有 Unicode 标志 /u/v:字符是 UTF-16 码单元

    3. 46.11.3 标志 /u:字符是码点 ^(ES6)

    4. 46.11.4 标志 /v:对多码点图形簇的有限支持 ^(ES2024)

  12. 46.12 模式修饰符(内联标志)^(ES2025)

    1. 46.12.1 模式修饰符的语法

    2. 46.12.2 哪些标志被支持?

    3. 46.12.3 使用案例:改变正则表达式部分标志

    4. 46.12.4 使用案例:内联标志

    5. 46.12.5 使用案例:改变标志的正则表达式片段

  13. 46.13 正则表达式对象属性

    1. 46.13.1 属性作为属性

    2. 46.13.2 其他属性

  14. 46.14 匹配对象

    1. 46.14.1 匹配对象中的索引^(ES2022)
  15. 46.15 处理正则表达式的方法

    1. 46.15.1 默认情况下,正则表达式在字符串的任何位置进行匹配

    2. 46.15.2 string.match(regExp) 不带 /g:获取第一个匹配的匹配对象

    3. 46.15.3 string.match(regExp) 带有 /g:获取所有组 0 捕获^(ES3)

    4. 46.15.4 string.matchAll(regExp):获取所有匹配对象的迭代器^(ES2020)

    5. 46.15.5 regExp.exec(str):捕获组^(ES3)

    6. 46.15.6 string.match()string.matchAll()regExp.exec()

    7. 46.15.7 使用 string.replace()string.replaceAll() 进行替换

    8. 46.15.8 regExp.test(str):是否有匹配?^(ES3)

    9. 46.15.9 string.search(regExp):匹配在什么索引处?^(ES3)

    10. 46.15.10 string.split(separator, limit?):分割字符串^(ES3)

  16. 46.16 标志 /g/y,以及属性 .lastIndex(高级)

    1. 46.16.1 标志 /g/y

    2. 46.16.2 /g/y 如何影响方法?

    3. 46.16.3 /g/y 的四个陷阱以及如何处理它们

    4. 46.16.4 .lastIndex 的用法:从指定索引开始匹配

    5. 46.16.5 .lastIndex 的缺点和优点

  17. 46.17 RegExp.escape():转义文本以便在正则表达式中使用^(ES2025)

    1. 46.17.1 RegExp.escape() 的用法:替换所有文本出现

    2. 46.17.2 RegExp.escape() 的用法:正则表达式的一部分必须匹配给定的文本

  18. 46.18 匹配所有内容或无内容

  19. 46.19 使用正则表达式的技巧

    1. 46.19.1 技巧:使用标志 /v

    2. 46.19.2 技巧:按字母顺序排列标志

    3. 46.19.3 技巧:使用命名捕获组

    4. 46.19.4 技巧:通过 # 使用不重要的空白和行注释

    5. 46.19.5 技巧:为你的正则表达式编写测试

    6. 46.19.6 提示:在您的文档中提及示例

    7. 46.19.7 奖励提示:使用插值来重用模式

    8. 46.19.8 奖励提示:无需库的无意义空白

    9. 46.19.9 结论:这就是正则表达式应该被编写的方式

  20. 46.20 快速参考:正则表达式功能

    1. 46.20.1 摘要:.global (/g) 和 .sticky (/y)

    2. 46.20.2 String.prototype.*: 匹配和搜索

    3. 46.20.3 String.prototype.*: 分割和替换

    4. 46.20.4 RegExp.prototype.*

图标“阅读” 功能可用性

除非另有说明,否则每个正则表达式功能自 ES3 以来都可用。

46.1 创建正则表达式

46.1.1 字面量与构造函数

创建正则表达式的两种主要方式是:

  • 字面量:在加载时静态编译。

    /abc/iv
    
    
  • 构造函数:在运行时动态编译。

    new RegExp('abc', 'iv')
    
    

两个正则表达式都有相同的两个部分:

  • 主体 abc——实际的正则表达式。

  • 标志 iv。标志配置了模式是如何被解释的。例如,i 启用不区分大小写的匹配。可用的标志列表将在本章后面给出 正则表达式标志。

图标“提示” 推荐:标志 /v ^(ES2024)

标志 /v 启用重要功能,并推荐用于所有正则表达式。

46.1.2 提示:使用 String.raw 字面量与 new RegExp()

如果我们将普通字符串字面量作为 new RegExp() 的参数,则每个正则表达式反斜杠都必须转义。如果我们使用 String.raw 作为模板标签,则不需要这样做。以下三个正则表达式都是等效的:

> /^\*$/.test('*')
true
> new RegExp('^\\*$', 'v').test('*')
true
> new RegExp(String.raw`^\*$`, 'v').test('*')
true

如果我们经常使用 String.raw,我们可以将其缩写:

// “Import” from namespace `String`
const {raw} = String;

const regExp = new RegExp(raw`^\*$`, 'v');

46.1.3 克隆和不可破坏地修改正则表达式

RegExp() 构造函数有两种变体:

  • new RegExp(pattern : string, flags = '') ^(ES3)

    根据通过 pattern 指定创建一个新的正则表达式。如果 flags 缺失,则使用空字符串 ''

  • new RegExp(regExp : RegExp, flags = regExp.flags) ^(ES6)

    regExp 被克隆。如果提供了 flags,则它确定克隆的标志。

第二种变体在克隆正则表达式时很有用,可选地同时修改它们。标志是不可变的,这是更改它们的唯一方法——例如:

function copyAndAddFlags(regExp, flagsToAdd='') {
  // The constructor doesn’t allow duplicate flags;
  // make sure there aren’t any:
  const newFlags = Array.from(
    new Set(regExp.flags + flagsToAdd)
  ).join('');
  return new RegExp(regExp, newFlags);
}
assert.equal(/abc/i.flags, 'i');
assert.equal(copyAndAddFlags(/abc/i, 'g').flags, 'gi');

new Set()遍历其参数,这意味着字符串被分割成码点。这些码点成为 Set 元素,从而消除了重复。在我们能够将它们连接成一个字符串之前,我们必须将它们转换成一个数组,并通过Array.from()来实现。

46.2 语法字符和转义

46.2.1 语法字符

在正则表达式的顶级,以下语法字符是特殊的。它们通过在前面加反斜杠(\)来转义。

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

这是一个例子:

> /\*/v.test('*')
true

在正则表达式字面量中,我们必须转义斜杠:

> /\//v.test('/')
true

new RegExp()的参数中,我们不需要转义斜杠:

> new RegExp('/', 'v').test('/')
true

46.2.2 非法顶级转义

使用 Unicode 标志(/u/v),在顶级转义非语法字符是一个语法错误。这允许使用像\p{···}这样的语法。

assert.throws(
  () => eval(String.raw`/\a/v`),
  {
    name: 'SyntaxError',
    message: 'Invalid regular expression: /\\a/v: Invalid escape',
  }
);
assert.throws(
  () => eval(String.raw`/\-/v`),
  {
    name: 'SyntaxError',
    message: 'Invalid regular expression: /\\-/v: Invalid escape',
  }
);

没有标志/u/v,顶级转义的非语法字符匹配自身:

> /\a/.test('a')
true

46.2.3 在字符类内部转义([···])

如果我们使用推荐的标志/v,则字符类内部转义的规则是不同的。我们在查看没有 Unicode 标志(既不是/u也不是/v)的正则表达式规则之前,先查看这些规则。

46.2.3.1 在字符类内部转义:标志/v

以下字符可以通过反斜杠转义:

有趣的是,我们并不总是需要转义这些字符。只有以下字符序列不匹配自身并且必须转义:

  • 单个^只有在它是第一个字符时才需要转义。

  • 类集语法字符必须始终转义:

    ( ) [ ] { } / - \ |
    
    
  • 类集保留双标点符号必须始终转义(至少有一个):

    && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~
    
    
46.2.3.2 在字符类内部转义:没有 Unicode 标志(既不是/u也不是/v
  • 我们总是必须转义:\]

  • 一些字符只在某些位置需要转义:

    • ^只有在它是第一个字符时才需要转义。

    • -只有在它不是第一个或最后一个字符时才需要转义。

46.3 正则表达式:字符的概念

在正则表达式的上下文中,“字符”意味着“文本的原子单位”:

  • 没有 Unicode 标志(/u/v),一个字符是 JavaScript 字符(一个 UTF-16 代码单元)。

  • 使用 Unicode 标志,一个字符是一个码点。

例如,一个点(.)匹配单个字符:

> '🙂'.match(/./g) // code point with 2 code units
[ '\uD83D', '\uDE42' ]
> '🙂'.match(/./gv)
[ '🙂' ]

46.4 语法:匹配单个字符

这些构造匹配单个字符:

  • 模式字符 是所有字符 除了 语法字符(^$ 等)。模式字符匹配自身。例如:A b % -

  • . 匹配任何字符。我们可以使用 标志 /s (dotAll) 来控制点是否匹配行终止符。

  • 字符转义(每个转义匹配一个固定的字符):

    • 控制转义(用于一些控制字符):

      • \f: 分页 (FF)

      • \n: 换行 (LF)

      • \r: 回车 (CR)

      • \t: 制表符

      • \v: 换行制表符

    • 控制字符:\cA (Ctrl-A), …, \cZ (Ctrl-Z)

    • 十六进制转义(前 256 个 Unicode 代码点):\x20 (空格)

    • Unicode 代码单元转义:\u00E4 (ä)

    • Unicode 代码点转义(需要 Unicode 标志 /u/v):\u{1F642} (🙂)

    • 标识符转义匹配转义字符(这些是带有 Unicode 标志 /u/v 的规则;没有它们,大多数字符都可以进行标识符转义):

      • 我们可以通过在它们前面加上反斜杠来转义以下 语法字符

        ^ $ \ . * + ? ( ) [ ] { } |
        
        
      • 我们也可以这样转义斜杠:\/

46.5 语法:字符类转义

字符类转义匹配代码单元集合、代码点集合或代码点序列集合。

46.5.1 基本字符类转义(代码单元集合):\d \D \s \S \w \W

以下字符类转义及其补集始终受支持:

转义 等价 补集
数字 \d [0-9] \D
“单词”字符 \w [a-zA-Z0-9_] \W
空白字符 \s \S

注意:

  • 空白字符:\s 匹配所有空白代码点:空格、制表符、行终止符等。它们都适合单个 UTF-16 代码单元。

  • “单词”字符与编程语言中的标识符相关。

示例:

> 'a7x4'.match(/\d/g)
[ '7', '4' ]
> 'a7x4'.match(/\D/g)
[ 'a', 'x' ]
> 'high - low'.match(/\w+/g)
[ 'high', 'low' ]
> 'hello\t\n everyone'.replaceAll(/\s/g, '-')
'hello---everyone'

46.5.2 Unicode 属性转义:\p{}\P{}

Unicode 属性转义看起来像这样:

  • 正转义:\p{UP} 匹配具有 Unicode 属性 UP 的 Unicode 字符或 Unicode 字符串。

  • 负转义:\P{UP} 匹配没有 Unicode 属性 UP 的 Unicode 字符。

有两种 Unicode 属性:

  • Unicode 字符属性 是代码点的属性。它们指定了一组代码点。

    • 示例:White_Space

    • 由标志 /u 和标志 /v 支持

    • 在 ES2018 中引入

  • Unicode 字符串属性 是一系列代码点的属性。它们指定了一组代码点字符串。字符串属性转义只能为正。

    • 示例:RGI_Emoji

    • 仅由标志 /v 支持

    • 在 ES2024 中引入

在我们更详细地查看所有内容之前,先快速看一下示例。这是如何转义 Unicode 字符属性 White_Space 的:

// Match all code points that are whitespace
assert.deepEqual(
  'a\tb c'.match(/\p{White_Space}/gv),
  ['\t', ' ']
);

// Match all code points that are not whitespace
assert.deepEqual(
  'a\tb c'.match(/\P{White_Space}/gv),
  ['a', 'b', 'c']
);

46.5.3 Unicode 字符属性转义 (ES2018)

使用标志 /u 或标志 /v,我们可以使用 \p{}\P{} 通过 Unicode 字符属性 指定代码点集(我们将在下一小节中了解更多关于这些内容)。这看起来是这样的:

  1. \p{prop=value}: 匹配所有其 Unicode 字符属性 prop 的值为 value 的字符。

  2. \P{prop=value}: 匹配所有没有 Unicode 字符属性 prop 的值为 value 的字符。

  3. \P{bin_prop}: 匹配所有其二进制 Unicode 字符属性 bin_prop 为 True 的字符。

  4. \P{bin_prop}: 匹配所有其二进制 Unicode 字符属性 bin_prop 为 False 的字符。

注释:

  • 形式 (3) 和 (4) 可以用作缩写,如果属性是 General_Category。例如,以下两个转义是等价的:

    \p{Uppercase_Letter}
    \p{General_Category=Uppercase_Letter}
    
    
  • 没有标志 /u/v\pp 相同。

示例:

  • 检查空白字符:

    > /^\p{White_Space}+$/v.test('\t \n\r')
    true
    
    
  • 检查希腊字母:

    > /^\p{Script=Greek}+$/v.test('μετά')
    true
    
    
  • 删除任何字母:

    > '1π2ü3é4'.replace(/\p{Letter}/gv, '')
    '1234'
    
    
  • 删除小写字母:

    > 'AbCdEf'.replace(/\p{Lowercase_Letter}/gv, '')
    'ACE'
    
    
46.5.3.1 Unicode 字符属性

在 Unicode 标准中,每个字符都有 属性 – 描述它的元数据。属性在定义字符性质方面发挥着重要作用。引用 Unicode 标准第 3.3 节,D3

字符的语义由其身份、规范性属性和行为决定。

这些是一些属性的例子:

  • Name: 一个唯一名称,由大写字母、数字、连字符和空格组成 – 例如:

    • A: Name = LATIN CAPITAL LETTER A

    • 🙂: Name = SLIGHTLY SMILING FACE

  • General_Category: 对字符进行分类 – 例如:

    • x: General_Category = Lowercase_Letter

    • 3: General_Category = Number

    • $: General_Category = Currency_Symbol

  • White_Space: 用于标记不可见空格字符,例如空格、制表符和换行符 – 例如:

    • \t: White_Space = True

    • π: White_Space = False

  • Age: 字符首次被引入的 Unicode 标准版本 – 例如:欧元符号 € 被添加到 Unicode 标准的 2.1 版本中。

    • €: Age = 2.1
  • Block: 一系列连续的代码点。块不重叠,并且它们的名称是唯一的。例如:

    • S: Block = Basic_Latin (范围 0x0000..0x007F)

    • 🙂: Block = Emoticons (范围 0x1F600..0x1F64F)

  • Script: 是一个或多个书写系统使用的字符集合。

    • 一些脚本支持多种书写系统。例如,拉丁字母脚本支持英语、法语、德语、拉丁语等书写系统。

    • 一些语言可以用多种由多种脚本支持的替代书写系统来书写。例如,土耳其语在 20 世纪初期过渡到拉丁字母脚本之前使用了阿拉伯字母脚本。

    • 示例:

      • α: Script = Greek

      • Д: Script = Cyrillic

进一步阅读:

46.5.4 Unicode 字符串属性转义^(ES2024)

只有标志/v允许我们使用\p{}通过Unicode 字符串属性指定代码点序列的集合(通过\P{}进行否定不支持)。例如,RGI_Emoji是一个 Unicode 字符串属性:

> /^\p{RGI_Emoji}$/v.test('⛔') // 1 code point (1 code unit)
true
> /^\p{RGI_Emoji}$/v.test('🙂') // 1 code point (2 code units)
true
> /^\p{RGI_Emoji}$/v.test('😵‍💫') // 3 code points
true

让我们看看 Unicode 字符属性Emoji如何处理这些输入:

> /^\p{Emoji}$/v.test('⛔') // 1 code point (1 code unit)
true
> /^\p{Emoji}$/v.test('🙂') // 1 code point (2 code units)
true
> /^\p{Emoji}$/v.test('😵‍💫') // 3 code points
false

如预期,它只匹配单个代码点。

46.5.4.1 Unicode 字符串属性

目前,JavaScript 只支持以下 Unicode 字符串属性:

  • Basic_Emoji:单个代码点

  • Emoji_Keycap_Sequence

  • RGI_Emoji_Modifier_Sequence

  • RGI_Emoji_Flag_Sequence

  • RGI_Emoji_Tag_Sequence

  • RGI_Emoji_ZWJ_Sequence

  • RGI_Emoji:上述所有集合的并集

Unicode 字符串属性的语义在文本文件中定义,该文件列举了如下代码点序列(\x{23}#):

0023 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: \x{23}
002A FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: *
0030 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 0
0031 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 1
0032 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 2
0033 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 3
0034 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 4
0035 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 5
0036 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 6
0037 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 7
0038 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 8
0039 FE0F 20E3 ; Emoji_Keycap_Sequence ; keycap: 9

进一步阅读:

46.6 语法:字符类

一个字符类类范围括在方括号中。类范围指定一组字符:

  • [«class ranges»]匹配集合中的任何字符。

  • [^«class ranges»]匹配集合中不存在的任何字符。

类范围规则:

  • 非语法字符代表自身:[abc]

  • 必须转义哪些字符取决于标志:

    • 标志/v:以下一些字符只有在出现两次时才需要转义,但始终转义它们更简单。有关详细信息,请参阅“字符类内的转义(§46.2.3)”。

    • 没有 Unicode 标志(既没有/v也没有/u):

      • 我们必须始终转义:\ ]

      • 一些字符在某些位置才需要转义:

        • ^只有在它是最前面的字符时才需要转义。

        • -只有在它不是第一个或最后一个字符时才需要转义。

  • 字符转义符(\n\x20\u{1F44D}等)具有通常的含义。

    • 注意:\b代表退格。在其他正则表达式中,它匹配单词边界。
  • 字符类转义符(\d\P{White_Space}\p{RGI_Emoji}等)具有通常的含义。

  • 通过连字符指定字符范围:[a-z]

46.6.1 字符类中的代码点序列^(ES2024)

使用标志 /v,我们可以使用 \q{} 向由字符类定义的集合中添加代码点序列。这种语法也称为:

这是一个使用 \q{} 的示例:

> /^[\q{😵‍💫}]$/v.test('😵‍💫')
true

没有使用 \q{},具有多个代码点的 图形群 仍然被视为多个字符:

> /^[😵‍💫]$/v.test('😵‍💫')
false
> /^[\u{1F635}\u{200D}\u{1F4AB}]$/v.test('😵‍💫') // equivalent
false
> /^[😵‍💫]$/v.test('\u{1F635}')
true

我们可以使用单个 \q{} 添加多个代码点序列——如果我们用管道符分隔它们:

> /^[\q{abc|def}]$/v.test('abc')
true
> /^[\q{abc|def}]$/v.test('def')
true

46.6.2 字符类的集合操作^(ES2024)

标志 /v 启用字符类的集合操作。

46.6.2.1 嵌套字符类

要启用字符类的集合操作,我们必须能够嵌套它们。字符类转义已经提供了一些嵌套功能:

> /^[\d\w]$/v.test('7')
true
> /^[\d\w]$/v.test('H')
true
> /^[\d\w]$/v.test('?')
false

使用标志 /v,我们还可以嵌套字符类(下面的正则表达式与上一个示例中的正则表达式等价):

> /^[[0-9][A-Za-z0-9_]]$/v.test('7')
true
> /^[[0-9][A-Za-z0-9_]]$/v.test('H')
true
> /^[[0-9][A-Za-z0-9_]]$/v.test('?')
false

46.6.2.2 通过 -- 的字符集减法

我们可以使用 -- 操作符从理论上减去由字符类或字符类转义定义的字符集:

> /^[\w--[a-g]]$/v.test('a')
false
> /^[\w--[a-g]]$/v.test('h')
true

> /^[\p{Number}--[0-9]]$/v.test('٣')
true
> /^[\p{Number}--[0-9]]$/v.test('3')
false

> /^[\p{RGI_Emoji}--\q{😵‍💫}]$/v.test('😵‍💫') // emoji has 3 code points
false
> /^[\p{RGI_Emoji}--\q{😵‍💫}]$/v.test('🙂')
true

单个代码点也可以用于 -- 操作符的两侧:

> /^[\w--a]$/v.test('a')
false
> /^[\w--a]$/v.test('b')
true

46.6.2.3 通过 && 的字符集交集

我们可以使用 && 操作符从理论上交集由字符类或字符类转义定义的字符集:

> /[\p{ASCII}&&\p{Letter}]/v.test('D')
true
> /[\p{ASCII}&&\p{Letter}]/v.test('Δ')
false

> /^[\p{Script=Arabic}&&\p{Number}]$/v.test('٣')
true
> /^[\p{Script=Arabic}&&\p{Number}]$/v.test('ج')
false

46.6.2.4 字符集的并集

要计算字符集的集合论并集,我们只需在字符类内部将它们的定义结构并排放置:

> /^[\p{Emoji_Keycap_Sequence}[a-z]]+$/v.test('a2️⃣c')
true

46.7 语法:捕获组

  • 编号捕获组: (a+)

    • 回溯引用:\1\2 等。
  • 命名捕获组^(ES2018): (?<as>a+)

    • 回溯引用:\k<as>
  • 非捕获组: (?:a+)

46.8 语法:量词

默认情况下,所有以下量词都是 贪婪的(它们尽可能多地匹配字符):

  • ?: 匹配零次或一次

  • *: 匹配零次或多次

  • +: 匹配一次或多次

  • {n}: 匹配 n

  • {n,}: 匹配 n 次或更多次

  • {n,m}: 至少匹配 n 次,最多匹配 m 次。

要使它们 犹豫不决(以便它们尽可能少地匹配字符),在它们后面放置问号(?):

> /X.*X/.exec('XabcXdefX')[0]  // greedy
'XabcXdefX'
> /X.*?X/.exec('XabcXdefX')[0] // reluctant
'XabcX'

46.9 语法:断言

  • ^ 仅匹配输入的开始

  • $ 仅匹配输入的结束

  • \b 仅在单词边界处匹配

    • \B 仅在非单词边界处匹配

可用前瞻断言概述:

模式 名称
(?=«pattern») 正向前视 ES3
(?!«pattern») 负向前视 ES3
(?<=«pattern») 正向后视 ES2018
(?<!«pattern») 负向后视 ES2018

46.9.1 向前视断言

正向前视匹配: (?=«pattern») 如果 pattern 匹配接下来的内容,则匹配。

示例:由 X 后缀的小写字母序列。

> 'abcX def'.match(/[a-z]+(?=X)/g)
[ 'abc' ]

注意,X 本身不是匹配子串的一部分。

负向前视匹配: (?!«pattern») 如果 pattern 不匹配接下来的内容,则匹配。

示例:由 X 后缀的小写字母序列。

> 'abcX def'.match(/[a-z]+(?!X)/g)
[ 'ab', 'def' ]

46.9.2 向后视断言 (ES2018)

正向后视匹配: (?<=«pattern») 如果 pattern 匹配前面的内容,则匹配。

示例:由 X 前缀的小写字母序列。

> 'Xabc def'.match(/(?<=X)[a-z]+/g)
[ 'abc' ]

负向后视匹配: (?<!«pattern») 如果 pattern 不匹配前面的内容,则匹配。

示例:由 X 前缀的小写字母序列。

> 'Xabc def'.match(/(?<!X)[a-z]+/g)
[ 'bc', 'def' ]

示例:将 “.js” 替换为 “.html”,但不在 “Node.js” 中替换。

> 'Node.js: index.js and main.js'.replace(/(?<!Node)\.js/g, '.html')
'Node.js: index.html and main.html'

46.10 语法:析取(|

注意:此操作符优先级低(绑定非常弱)。如有必要,请使用分组:

  • ^aa|zz$ 匹配所有以 aa 开头和/或以 zz 结尾的字符串。

    • 注意,| 的优先级低于 ^$
  • ^(aa|zz)$ 匹配两个字符串 'aa''zz'

  • ^a(a|z)z$ 匹配两个字符串 'aaz''azz'

46.11 正则表达式标志

文字标志 属性名 ES 描述
d hasIndices ES2022 开启匹配索引
g global ES3 多次匹配
i ignoreCase ES3 不区分大小写匹配
m multiline ES3 ^ 和 `
--- --- --- ---
d hasIndices ES2022 开启匹配索引
g global ES3 多次匹配
i ignoreCase ES3 不区分大小写匹配
每行匹配
s dotAll ES2018 点号匹配行终止符
u unicode ES6 Unicode 模式
v unicodeSets ES2024 Unicode 集合模式(推荐
y sticky ES6 匹配之间没有字符

表 46.1:这是 JavaScript 支持的正则表达式标志。

以下正则表达式标志在 JavaScript 中可用(表 46.1 提供了紧凑的概述):

  • /d.hasIndices):某些与 RegExp 相关的方法返回 match 对象,描述正则表达式在输入字符串中的匹配位置。如果此标志开启,每个 match 对象都包含 match 索引,告诉我们每个分组捕获的开始和结束位置。更多信息:“Match indices in match objects (ES2022)” (§46.14.1)。

  • /g.global)从根本上改变了后续方法的工作方式。

    • String.prototype.match()

    • RegExp.prototype.exec()

    • RegExp.prototype.test()

    解释见 “The flags /g and /y, and the property .lastIndex (advanced)” (§46.16)。简而言之:没有 /g,方法只考虑输入字符串中正则表达式的第一个匹配。有 /g,它们考虑所有匹配。

  • /i.ignoreCase)开启不区分大小写匹配:

    > /a/.test('A')
    false
    > /a/i.test('A')
    true
    
    
  • /m (.multiline): 如果此标志开启,^ 匹配每一行的开始,$ 匹配每一行的结束。如果关闭,^ 匹配整个输入字符串的开始,$ 匹配整个输入字符串的结束。

    > 'a1\na2\na3'.match(/^a./gm)
    [ 'a1', 'a2', 'a3' ]
    > 'a1\na2\na3'.match(/^a./g)
    [ 'a1' ]
    
    
  • /s (.dotAll): 默认情况下,点不匹配行终止符。使用此标志时,它会匹配:

    > /./.test('\n')
    false
    > /./s.test('\n')
    true
    
    

    解决方案:如果 /s 不受支持,我们可以用 [^] 代替点。

    > /[^]/.test('\n')
    true
    
    
  • 更好的 Unicode 支持(“Unicode 标志”):

    • /u (.unicode): 默认情况下,匹配的原子单位是 JavaScript 字符(Unicode 代码单元)。此标志将原子单位切换为 Unicode 代码点。这在“标志 /u:字符是代码点(ES6)”(§46.11.3)中有解释。

    • /v (.unicodeSets): 此标志改进并取代了标志 /u。它支持字符类中的多代码点图形群集和集合运算。这在“标志 /v:对多代码点图形群集的有限支持(ES2024)”(§46.11.4)中有解释。我建议对所有正则表达式使用标志 /v——因为它启用了所有这些功能。

  • /y (.sticky): 这个标志主要与 /g 结合使用才有意义。当两者都开启时,任何匹配必须直接跟在先前的匹配之后(即,它必须从正则表达式对象的 .lastIndex 索引处开始)。因此,第一个匹配必须位于索引 0。

    > 'a1a2 a3'.match(/a./gy)
    [ 'a1', 'a2' ]
    > '_a1a2 a3'.match(/a./gy) // first match must be at index 0
    null
    
    > 'a1a2 a3'.match(/a./g)
    [ 'a1', 'a2', 'a3' ]
    > '_a1a2 a3'.match(/a./g)
    [ 'a1', 'a2', 'a3' ]
    
    

    /y 的主要用例是标记化(在解析过程中)。有关此标志的更多信息:“标志 /g/y,以及属性 .lastIndex(高级)”(§46.16)。

46.11.1 如何排序正则表达式标志?

考虑以下正则表达式:/“([^”]+)”/vdg

在列出其标志的顺序中,有两种选择:

  1. 字母顺序:/dgv

  2. 按重要性排序(可以说 /v 是最基础的等):/vgd

由于(2)不明显,(1)是更好的选择。JavaScript 也将其用于 RegExp 属性 .flags

> /-/gymdivs.flags
'dgimsvy'

46.11.2 没有 Unicode 标志 /u/v:字符是 UTF-16 代码单元

没有 Unicode 标志 /u/v 时,大多数结构使用单个 UTF-16 代码单元,这在字符有多个代码单元时会有问题——例如 😃:

> '🙂'.length
2

没有 Unicode 标志时,我们可以通过一个代码单元转义来转义最大的字符,该转义由 \u 后跟四个十六进制数字组成:

> /^\uD83D\uDE42$/.test('🙂')
true

点操作符(.)匹配代码单元,这就是为什么我们得到两个匹配而不是一个:

> '🙂'.match(/./g)
[ '\uD83D', '\uDE42' ]

量词适用于代码单元,因此只会重复 😃 的后半部分:

> /^🙂{2}$/.test('\uD83D\uDE42\uDE42')
true
> /^\uD83D\uDE42{2}$/.test('\uD83D\uDE42\uDE42') // equivalent
true

字符类转义定义了一组代码单元。因此,表示“非十进制数字”的类转义 \D 会得到两个匹配:

> '🙂'.match(/\D/g)
[ '\uD83D', '\uDE42' ]

字符类定义了一组代码单元。因此,将 😃 放入字符类会有不直观的结果:

> /^[🙂]$/.test('🙂')
false
> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42') // equivalent
false
> /^[🙂]$/.test('\uD83D')
true

46.11.3 标志 /u:字符是代码点(ES6)

在前面的子节中,当我们想要匹配具有多个 UTF-16 代码单元的代码点时,我们遇到了问题 – 例如 🙂。标志 /u 启用了对代码点的支持并解决了这些问题。

我们可以通过 代码点转义 来转义代码点 – \u{},其中包含一到六个十六进制数字:

> /^\u{1F642}$/u.test('🙂')
true

点操作符 (.) 匹配代码点:

> '🙂'.match(/./gu)
[ '🙂' ]

量词适用于代码点:

> /^🙂{2}$/u.test('🙂🙂')
true

字符类转义定义了一组代码点:

> '🙂'.match(/\D/gu)
[ '🙂' ]

支持一种新的字符类转义 – Unicode 字符属性转义指定了一组代码点:

> /^\p{Emoji}$/u.test('⛔') // 1 code point (1 code unit)
true
> /^\p{Emoji}$/u.test('🙂') // 1 code point (2 code units)
true

字符类也定义了一组代码点:

> /^[🙂]$/u.test('🙂')
true
> /^[🙂]$/u.test('\uD83D')
false

46.11.4 标志 /v: 对多代码点图形簇的有限支持^(ES2024)

图标“提示”尽可能使用标志 /v

此标志改进了 JavaScript 正则表达式的许多方面,应默认使用。如果平台尚未支持,至少应使用 /u

  • 标志 /v 建立在标志 /u 带来的改进之上,并修复了其几个缺点。

  • 注意,标志 /v 和标志 /u 是互斥的 – 我们不能同时使用两者:

    assert.throws(
      () => eval('/-/uv'),
      SyntaxError
    );
    
    
46.11.4.1 标志 /u 的限制:处理包含多个代码点的图形簇

一些字体符号由包含多个代码点的 图形簇(代码点序列)表示 – 例如 😵‍💫:

> Array.from('😵‍💫').length // count code points
3

标志 /u 并没有帮助我们处理那些类型的图形簇:

// Grapheme cluster is not matched by single dot
assert.equal(
  '😵‍💫'.match(/./gu).length, 3
);

// Quantifiers only repeat last code point of grapheme cluster
assert.equal(
  /^😵‍💫{2}$/u.test('😵‍💫😵‍💫'), false
);

// Character class escapes only match single code points
assert.equal(
  /^\p{Emoji}$/u.test('😵‍💫'), false
);

// Character classes only match single code points
assert.equal(
  /^[😵‍💫]$/u.test('😵‍💫'), false
);

46.11.4.2 标志 /v: Unicode 字符串属性转义和字符类字符串字面量

标志 /v 与标志 /u 类似,但提供了对多代码点图形簇的更好支持。它不会在所有地方从代码点切换到图形簇,但它确实通过添加对多代码点图形簇的支持来修复了我们之前子节中遇到的最后两个问题 – 通过添加支持多代码点图形簇到:

  • 字符类转义:我们可以通过 \p{} 来引用 Unicode 字符串属性。

    > /^\p{RGI_Emoji}$/v.test('⛔') // 1 code point (1 code unit)
    true
    > /^\p{RGI_Emoji}$/v.test('🙂') // 1 code point (2 code units)
    true
    > /^\p{RGI_Emoji}$/v.test('😵‍💫') // 3 code points
    true
    
    
  • 字符类:\q{} 允许我们定义 代码点序列。

    > /^[\q{😵‍💫}]$/v.test('😵‍💫')
    true
    
    
46.11.4.3 标志 /v: 字符类集合操作

字符类可以通过集合操作减法和交集进行嵌套和组合 – 请参阅“字符类的集合操作^(ES2024)” (§46.6.2)。

46.11.4.4 标志 /v: 改进的忽略大小写匹配

标志 /u 在进行不区分大小写的匹配时有一个怪癖:使用 \P{···} 产生与 [^\p{···}] 不同的结果:

> /^\P{Lowercase_Letter}$/iu.test('A')
true
> /^\P{Lowercase_Letter}$/iu.test('a')
true

> /^[^\p{Lowercase_Letter}]$/iu.test('A')
false
> /^[^\p{Lowercase_Letter}]$/iu.test('a')
false

观察:

  • 两种否定方式应产生相同的结果。

  • 直观地讲,如果我们向正则表达式添加 /i,它应该匹配至少与之前一样多的字符串 – 而不是更少。

标志 /v 修复了那个怪癖:

> /^\P{Lowercase_Letter}$/iv.test('A')
false
> /^\P{Lowercase_Letter}$/iv.test('a')
false

> /^[^\p{Lowercase_Letter}]$/iv.test('A')
false
> /^[^\p{Lowercase_Letter}]$/iv.test('a')
false

进一步阅读:

46.12 模式修饰符(内联标志)^(ES2025)

模式修饰符允许我们将标志应用于正则表达式的一部分(而不是整个正则表达式)——例如,在以下正则表达式中,标志i仅应用于“HELLO”:

> /^x(?i:HELLO)x$/.test('xHELLOx')
true
> /^x(?i:HELLO)x$/.test('xhellox')
true
> /^x(?i:HELLO)x$/.test('XhelloX')
false

46.12.1 模式修饰符的语法

这是语法看起来像什么:

(?ims-ims:pattern)
(?ims:pattern)
(?-ims:pattern)

注意:

  • 后跟问号(?)的标志被激活。

  • 后跟连字符(-)的标志被去激活。

  • 标志不能同时出现在“激活部分”和“去激活部分”。

  • 没有任何标志的情况下,这个语法只是一个非捕获组:(?:pattern)

让我们改变之前的例子:现在整个正则表达式都是不区分大小写的——除了“HELLO”:

> /^x(?-i:HELLO)x$/i.test('xHELLOx')
true
> /^x(?-i:HELLO)x$/i.test('XHELLOX')
true
> /^x(?-i:HELLO)x$/i.test('XhelloX')
false

46.12.2 支持哪些标志?

以下标志可以在模式修饰符中使用:

文本标志 属性名称 ES 描述
i ignoreCase ES3 不区分大小写匹配
m multiline ES3 ^和`
--- --- --- ---
i ignoreCase ES3 不区分大小写匹配
按行匹配
s dotAll ES2018 点匹配行终止符

更多信息,请参阅“正则表达式标志” (§46.11)。

剩余的标志不受支持,因为它们要么会使正则表达式语义过于复杂(例如标志v),要么因为它们只有在应用于整个正则表达式时才有意义(例如标志g)。

46.12.3 用例:更改正则表达式部分的标志

有时更改正则表达式部分的标志是有用的。例如,Ron Buckton 解释说更改标志m有助于匹配文件开头的 Markdown 前缀块(我稍微编辑了他的版本):

const re = /(?-m:^)---\r?\n((?:^(?!---$).*\r?\n)*)^---$/m;
assert.equal(re.test('---a'), false);
assert.equal(re.test('---\n---'), true);
assert.equal(
  re.exec('---\n---')[1],
  ''
);
assert.equal(
  re.exec('---\na: b\n---')[1],
  'a: b\n'
);

这个正则表达式是如何工作的?

  • 默认情况下,标志m是开启的,锚点^匹配行的开始,锚点$匹配行的结束。

  • 首个^是不同的:它必须匹配字符串的开始。这就是为什么我们在那里使用模式修饰符并关闭标志m的原因。

这是正则表达式,格式化后带有不重要的空白和解释性注释:

(?-m:^)---\r?\n  # first line of string
(  # capturing group for the frontmatter
  (?:  # pattern for one line (non-capturing group)
    ^(?!---$)  # line must not start with "---" + EOL (lookahead)
    .*\r?\n
  )*
)
^---$  # closing delimiter of frontmatter

46.12.4 用例:内联标志

在某些情况下,标志位于实际正则表达式之外是不方便的。这时模式修饰符就派上用场了。例如包括:

  • 将正则表达式存储在配置文件中,例如 JSON 格式。

  • Regex+ 库 提供了一个模板字面量,使得创建正则表达式变得更加方便。指定标志的语法增加了一些杂乱,可以通过模式修饰符(如果它们支持所需的标志)来避免:

    regex('i')`world`
    regex`(?i:world)`
    
    

46.12.5 使用案例:改变标志的正则表达式片段

在复杂的应用程序中,如果你能够将较大的正则表达式由较小的正则表达式组成,这将很有帮助。上述 Regex+ 库支持这一点。如果较小的正则表达式需要不同的标志(例如,因为它想要忽略大小写),那么它可以通过模式修饰符实现(如果它们支持所需的标志):

46.13 正则表达式对象的属性

值得注意的是:

  • 严格来说,只有 .lastIndex 是一个真正的实例属性。所有其他属性都是通过 getter 实现的。

  • 因此,.lastIndex 是唯一的可变属性。所有其他属性都是只读的。如果我们想改变它们,我们需要复制正则表达式(有关详细信息,请参阅“克隆和不可破坏地修改正则表达式” (§46.1.3))。

46.13.1 标志作为属性

每个正则表达式标志都作为一个具有更长、更具描述性的属性存在:

> /a/i.ignoreCase
true
> /a/.ignoreCase
false

这是标志属性的完整列表:

  • .dotAll (/s)

  • .global (/g)

  • .hasIndices (/d)

  • .ignoreCase (/i)

  • .multiline (/m)

  • .sticky (/y)

  • .unicode (/u)

  • .unicodeSets (/v)

46.13.2 其他属性

每个正则表达式还具有以下属性:

  • .source (ES3):正则表达式模式

    > /abc/ig.source
    'abc'
    
    
  • .flags (ES6):正则表达式的标志

    > /abc/ig.flags
    'gi'
    
    
  • .lastIndex (ES3):当标志 /g 被打开时使用。有关详细信息,请参阅“标志 /g/y,以及属性 .lastIndex(高级)” (§46.16)。

46.14 匹配对象

几个与正则表达式相关的方法返回所谓的 匹配对象,以提供有关正则表达式匹配输入字符串位置的详细信息。这些方法是:

  • RegExp.prototype.exec() 返回 null 或一个单独的匹配对象。

  • String.prototype.match() 返回 null 或一个单独的匹配对象(如果未设置标志 /g)。

  • String.prototype.matchAll() 返回一个匹配对象的可迭代对象(必须设置标志 /g;否则,会抛出异常)。

这是一个示例:

assert.deepEqual(
  /(a+)b/d.exec('ab aaab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aaab',
    groups: undefined,
    indices: {
      0: [0, 2],
      1: [0, 1],
      groups: undefined
    },
  }
);

.exec() 的结果是第一个匹配的 匹配对象,具有以下属性:

  • [0]:正则表达式匹配的完整子串

  • [1]:编号组 1 的捕获(等等)

  • .index:匹配发生的位置在哪里?

  • .input:被匹配的字符串

  • .groups:命名组的捕获(请参阅“命名捕获组 (ES2018)” (§46.15.2.1))

  • .indices:捕获组的索引范围

    • 如果打开了标志 /d,则创建此属性。

46.14.1 匹配对象中的匹配索引 (ES2022)

匹配索引 是匹配对象的一个特性:如果我们通过正则表达式标志 /d(属性 .hasIndices)将其打开,它们将记录组被捕获的起始和结束索引。

46.14.1.1 编号组的匹配索引

这是我们如何访问编号组的捕获:

const matchObj = /(a+)(b+)/d.exec('aaaabb');
assert.equal(
  matchObj[1], 'aaaa'
);
assert.equal(
  matchObj[2], 'bb'
);

由于正则表达式标志 /dmatchObj 还有一个属性 .indices,它记录每个编号组在输入字符串中被捕获的位置:

assert.deepEqual(
  matchObj.indices[1], [0, 4]
);
assert.deepEqual(
  matchObj.indices[2], [4, 6]
);

46.14.1.2 命名组的匹配索引

命名组的捕获可以通过这种方式访问:

const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
assert.equal(
  matchObj.groups.as, 'aaaa'
);
assert.equal(
  matchObj.groups.bs, 'bb'
);

它们的索引存储在 matchObj.indices.groups 中:

assert.deepEqual(
  matchObj.indices.groups.as, [0, 4]
);
assert.deepEqual(
  matchObj.indices.groups.bs, [4, 6]
);

46.14.1.3 一个更实际的例子

匹配索引的一个重要用例是解析器,它指向语法错误的确切位置。以下代码解决了相关的问题:它指向引号内容的开始和结束位置(请参阅末尾的演示)。

const reQuoted = /“([^”]+)”/dgv;
function pointToQuotedText(str) {
  const startIndices = new Set();
  const endIndices = new Set();
  for (const match of str.matchAll(reQuoted)) {
    const [start, end] = match.indices[1];
    startIndices.add(start);
    endIndices.add(end);
  }
  let result = '';
  for (let index=0; index < str.length; index++) {
    if (startIndices.has(index)) {
      result += '[';
    } else if (endIndices.has(index+1)) {
      result += ']';
    } else {
      result += ' ';
    }
  }
  return result;
}

assert.equal(
  pointToQuotedText(
    'They said “hello” and “goodbye”.'),
    '           [   ]       [     ]  '
);

46.15 使用正则表达式的方法

46.15.1 默认情况下,正则表达式可以在字符串的任何位置进行匹配

默认情况下,正则表达式可以在字符串的任何位置进行匹配:

> /a/.test('__a__')
true

我们可以通过使用断言,如 ^(或者使用标志 /y)来改变这一点:

> /^a/.test('__a__')
false
> /^a/.test('a__')
true

46.15.2 没有 /gstring.match(regExp):获取第一个匹配的匹配对象

没有标志 /gstring.match(regExp) 返回 strregExp 的第一个匹配的 匹配对象:

assert.deepEqual(
  'ab aab'.match(/(a+)b/),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: undefined,
  }
);

46.15.2.1 命名的捕获组 (ES2018)

之前的例子包含一个编号组。以下例子演示了命名组:

assert.deepEqual(
  'ab aab'.match(/(?<as>a+)b/),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: { as: 'a' },
  }
);

.match() 的结果中,我们可以看到命名组也是一个编号组——它的捕获存在两次:

  • 一次作为编号捕获(属性 '1')。

  • 一次作为命名捕获(属性 groups.as)。

46.15.2.2 重复命名的捕获组 (ES2025)

自从 ECMAScript 2025 以来,我们可以使用相同的组名两次——只要它在不同的备选方案中出现:

const RE = /(?<a>a(?<xs>x+))|(?<b>b(?<xs>x+))/v;
assert.deepEqual(
  'axx'.match(RE).groups,
  {
    a: 'axx',
    xs: 'xx',
    b: undefined,
    __proto__: null,
  }
);
assert.deepEqual(
  'bxx'.match(RE).groups,
  {
    a: undefined,
    xs: 'xx',
    b: 'bxx',
    __proto__: null,
  }
);

以下是不允许的(组 xs 将会匹配两次):

assert.throws(
  () => eval('/(?<a>a(?<xs>x+))(?<b>b(?<xs>x+))/v'),
  /^SyntaxError:.* Duplicate capture group name$/
);

46.15.3 带有 /gstring.match(regExp):获取所有组 0 捕获 (ES3)

使用标志 /gstring.match(regExp) 返回 str 中与 regExp 匹配的所有子字符串:

> 'ab aab'.match(/(a+)b/g)
[ 'ab', 'aab' ]

如果没有匹配,.match() 返回 null

> 'xyz'.match(/(a+)b/g)
null

我们可以使用 空值合并运算符 (??) 来保护自己免受 null 的影响:

const numberOfMatches = (str.match(regExp) ?? []).length;

我们还可以使用 可选链 (?.) 并将其与空值合并运算符结合:

const numberOfMatches = str.match(regExp)?.length ?? 0;

46.15.4 string.matchAll(regExp): 获取所有匹配对象的迭代器 (ES2020)

这是调用 .matchAll() 的方式:

const matchIterable = str.matchAll(regExp);

给定一个字符串和一个正则表达式,.matchAll() 返回一个迭代器,遍历所有匹配项的匹配对象。

在以下示例中,我们使用 Array.from()(ch_arrays.html#Array.from)将可迭代对象转换为数组,以便我们可以更好地比较它们。

> Array.from('-a-a-a'.matchAll(/-(a)/gv))
[
  { 0:'-a', 1:'a', index: 0, input: '-a-a-a', groups: undefined },
  { 0:'-a', 1:'a', index: 2, input: '-a-a-a', groups: undefined },
  { 0:'-a', 1:'a', index: 4, input: '-a-a-a', groups: undefined },
]

必须设置标志 /g

> Array.from('-a-a-a'.matchAll(/-(a)/v))
TypeError: String.prototype.matchAll called with a non-global
RegExp argument

.matchAll() 不受 regExp.lastIndex 的影响,并且不会改变它。

图标“练习”练习:通过 .matchAll() 提取引号文本

exercises/regexps/extract_quoted_test.mjs

46.15.4.1 实现 .matchAll()

.matchAll() 可以通过 .exec() 如下实现:

function* matchAll(str, regExp) {
  if (!regExp.global) {
    throw new TypeError('Flag /g must be set!');
  }
  // Preserve and reset flags
  const localCopy = new RegExp(regExp, regExp.flags);
  let match;
  while (match = localCopy.exec(str)) {
    yield match;
  }
}

创建局部副本确保两件事:

  • regex.lastIndex 不会改变。

  • localCopy.lastIndex 为零。

使用 matchAll()

const str = '"fee" "fi" "fo" "fum"';
const regex = /"([^"]*)"/g;

for (const match of matchAll(str, regex)) {
  console.log(match[1]);
}

输出:

fee
fi
fo
fum

46.15.5 regExp.exec(str):捕获组(ES3)

46.15.5.1 regExp.exec(str) 没有设置 /g:获取第一个匹配对象的匹配对象

没有设置标志 /g 时,regExp.exec() 的行为类似于 string.match() – 它返回一个单独的匹配对象。

46.15.5.2 regExp.exec(str) 设置 /g:遍历所有匹配项

图标“提示”检索所有匹配项的更好替代方法:string.matchAll(regExp)(ES2020)

自从 ECMAScript 2020 以来,JavaScript 有另一种检索所有匹配项的方法:string.matchAll(regExp)(String.prototype.matchAll)。该方法更容易且更安全使用:它返回一个可迭代对象,不受 .lastIndex 的影响,如果缺少标志 /g,则抛出异常。

如果我们想要检索正则表达式的所有匹配项(而不仅仅是第一个),我们需要切换到标志 /g。然后我们可以多次调用 .exec() 并每次获取一个匹配项。在最后一个匹配项之后,.exec() 返回 null

> const regExp = /(a+)b/g;
> regExp.exec('ab aab')
{ 0: 'ab', 1: 'a', index: 0, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')
{ 0: 'aab', 1: 'aa', index: 3, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')
null

因此,我们可以如下循环遍历所有匹配项:

const regExp = /(a+)b/g;
const str = 'ab aab';

let match;
// Check for null via truthiness
// Alternative: while ((match = regExp.exec(str)) !== null)
while (match = regExp.exec(str)) {
  console.log(match[1]);
}

输出:

a
aa

46.15.6 string.match()string.matchAll()regExp.exec()

以下表格总结了三种方法之间的差异:

没有 /g /g
string.match(regExp) 第一个匹配对象 组 0 捕获的数组
string.matchAll(regExp) TypeError 匹配对象的迭代器
regExp.exec(string) 第一个匹配对象 下一个匹配对象或 null

46.15.7 使用 string.replace()string.replaceAll() 替换

两种替换方法都有两个参数:

  • string.replace(searchValue, replacementValue)

  • string.replaceAll(searchValue, replacementValue)

searchValue 可以是:

  • 一个字符串

  • 一个正则表达式

replacementValue 可以是:

  • 字符串:用此字符串替换匹配项。字符 $ 有特殊含义,允许我们插入组的捕获以及更多(详情将在后面解释)。

  • 函数:通过此函数计算替换匹配项的字符串。

这两种方法如下不同:

  • .replace() 替换字符串或正则表达式的第一个匹配项(没有 /g)。

  • .replaceAll() 替换字符串或正则表达式(带有 /g)的所有出现。

此表总结了它是如何工作的:

搜索:→ 字符串 没有带有 /g 的正则表达式 带有 /g 的正则表达式
.replace 第一个出现 第一个出现 (所有出现)
.replaceAll 所有出现 TypeError 所有出现

.replace() 的最后一列在括号中,因为此方法在 .replaceAll() 之前就已经存在,因此支持现在应该通过后者处理的功能。如果我们能改变这一点,.replace() 在这里将抛出 TypeError

我们首先探讨当 replacementValue 是一个简单字符串(没有 $ 字符)时,.replace().replaceAll() 如何单独工作。然后我们检查它们如何受到更复杂的替换值的影响。

46.15.7.1 string.replace(searchValue, replacementValue) (ES3)

.replace() 的操作方式受其第一个参数 searchValue 的影响:

  • 没有带有 /g 的正则表达式:替换此正则表达式的第一个匹配项。

    > 'aaa'.replace(/a/, 'x')
    'xaa'
    
    
  • 字符串:替换这个字符串的第一个出现(字符串按字面意思解释,而不是作为正则表达式)。

    > 'aaa'.replace('a', 'x')
    'xaa'
    
    
  • 带有 /g 的正则表达式:替换此正则表达式的所有匹配项。

    > 'aaa'.replace(/a/g, 'x')
    'xxx'
    
    

    推荐:如果可用 .replaceAll(),在这种情况下最好使用该方法——其目的是替换多个出现。

如果我们想要替换字符串的每个出现,我们有两种选择:

  • 我们可以使用 .replaceAll()(它是在 ES2021 中引入的)。

  • 在本章的后面部分,我们将遇到 工具函数 escapeForRegExp()),它将帮助我们将字符串转换为匹配该字符串多次的正则表达式(例如,'*' 变为 /\*/g)。

[46.15.7.2 string.replaceAll(searchValue, replacementValue) (ES2021)

.replaceAll() 的操作方式受其第一个参数 searchValue 的影响:

  • 带有 /g 的正则表达式:替换此正则表达式的所有匹配项。

    > 'aaa'.replaceAll(/a/g, 'x')
    'xxx'
    
    
  • 字符串:替换这个字符串的所有出现(字符串按字面意思解释,而不是作为正则表达式)。

    > 'aaa'.replaceAll('a', 'x')
    'xxx'
    
    
  • 没有带有 /g 的正则表达式:抛出 TypeError(因为 .replaceAll() 的目的是替换多个出现)。

    > 'aaa'.replaceAll(/a/, 'x')
    TypeError: String.prototype.replaceAll called with
    a non-global RegExp argument
    
    
46.15.7.3 .replace().replaceAll() 的参数 replacementValue

到目前为止,我们只使用了简单字符串的 replacementValue 参数,但它可以做更多。如果其值为:

  • A string,然后匹配项被替换为这个字符串。字符 $ 有特殊含义,允许我们插入组的捕获和更多(详情请继续阅读)。

  • 然后,函数将匹配项替换为通过此函数计算得到的字符串。

46.15.7.4 replacementValue 是一个字符串

如果替换值是一个字符串,美元符号有特殊含义 - 它插入由正则表达式匹配的文本:

文本 结果
`$ 文本
--- ---
单个 ` 文本
--- ---
` $$
$& 完整匹配
$` 匹配前的文本
$' 匹配后的文本
$n 编号组 n 的捕获 (n > 0)
$<name> 命名组 name 的捕获(ES2018)
` 完整匹配
`` 文本
--- ---
`` 匹配前的文本
`` 匹配后的文本
$n 编号组 n 的捕获 (n > 0)
$<name> 命名组 name 的捕获(ES2018)

示例:插入匹配子字符串之前、之内和之后的文本。

> 'a1 a2'.replaceAll(/a/g, "($`|$&|$')")
'(|a|1 a2)1 (a1 |a|2)2'

示例:插入编号组的捕获。

> const regExp = /^([A-Za-z]+): (.*)$/gv;
> 'first: Jane'.replaceAll(regExp, 'KEY: $1, VALUE: $2')
'KEY: first, VALUE: Jane'

示例:插入命名组的捕获。

> const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/gv;
> 'first: Jane'.replaceAll(regExp, 'KEY: $<key>, VALUE: $<value>')
'KEY: first, VALUE: Jane'

“练习”图标练习:通过 .replace() 和命名组更改引号

exercises/regexps/change_quotes_test.mjs

46.15.7.5 replacementValue 是一个函数

如果替换值是一个函数,我们可以计算每个替换。在以下示例中,我们将找到的每个非负整数乘以二。

assert.equal(
  '3 cats and 4 dogs'.replaceAll(/[0-9]+/g, (all) => 2 * Number(all)),
  '6 cats and 8 dogs'
);

替换函数获取以下参数。注意它们与匹配对象多么相似。这些参数都是位置参数,但我已经包括了如何命名它们:

  • all: 完整匹配

  • g1: 第 1 个编号组的捕获

  • 等等。

  • index: 匹配发生在哪里?

  • input: 我们要替换的字符串

  • groups (ES2018): 命名组的捕获(一个对象)。总是最后一个参数。

如果我们只对 groups 感兴趣,我们可以使用以下技术:

const result = 'first=jane, last=doe'.replace(
  /(?<key>[a-z]+)=(?<value>[a-z]+)/g,
  (...args) => { // (A)
    const groups = args.at(-1); // (B)
    const {key, value} = groups;
    return key.toUpperCase() + '=' + value.toUpperCase();
  });
assert.equal(result, 'FIRST=JANE, LAST=DOE');

由于行 A 中的 剩余参数,args 包含一个包含所有参数的数组。我们在行 B 中通过 数组方法 .at() 访问最后一个参数。

46.15.8 regExp.test(str): 是否有匹配? (ES3)

正则表达式方法 regExp.test(str) 如果 regExp 匹配 str 则返回 true

> /bc/.test('ABCD')
false
> /bc/i.test('ABCD')
true
> /\.mjs$/.test('main.mjs')
true

使用 .test() 我们通常应该避免使用 /g 标志。如果我们使用它,我们通常每次调用该方法时都不会得到相同的结果:

> const r = /a/g;
> r.test('aab')
true
> r.test('aab')
true
> r.test('aab')
false

结果是由于字符串中存在两个 /a/ 匹配。在找到所有这些匹配之后,.test() 返回 false。更多信息,请参阅“/g/y 标志,以及 .lastIndex 属性(高级)” (§46.16)。

46.15.9 string.search(regExp): 匹配发生在什么索引? (ES3)

字符串方法 .search() 返回 regExpstr 中匹配的第一个索引:

> '_abc_'.search(/abc/)
1
> 'main.mjs'.search(/\.mjs$/)
4

46.15.10 string.split(separator, limit?): 分割字符串 (ES3)

将字符串分割成由分隔符分隔的子字符串数组。

分隔符可以是一个字符串:

> 'a : b : c'.split(':')
[ 'a ', ' b ', ' c' ]

它也可以是一个正则表达式:

> 'a x:yyy b'.split(/x+:y+/)
[ 'a ', ' b' ]
> 'a x:yyy b'.split(/(x+):(y+)/)
[ 'a ', 'x', 'yyy', ' b' ]

最后一次调用演示了正则表达式中的组所做的捕获成为返回数组的元素。

如果我们希望分隔符成为返回的字符串片段的一部分,我们可以使用带有后视断言或前瞻断言的正则表达式。

> 'a: b: c'.split(/(?<=:) */)
[ 'a:', 'b:', 'c' ]
> 'a :b :c'.split(/ *(?=:)/)
[ 'a', ':b', ':c' ]

多亏了前瞻断言,用于分割的正则表达式将冒号留在片段中,并且只移除冒号前后或其间的空格。

陷阱.split('') 会按 JavaScript 字符分割,但通常我们希望按图形簇或至少 Unicode 代码点分割。因此,最好使用 Intl.SegmenterArray.from() 进行分割。更多信息,请参阅“文本的原子:代码点、JavaScript 字符、图形簇”(§22.7)。

46.16 正则表达式标志 /g/y,以及属性 .lastIndex(高级)

在本节中,我们将探讨正则表达式标志 /g/y 的工作方式以及它们如何依赖于正则表达式属性 .lastIndex。我们还将发现一个关于 .lastIndex 的有趣用例,你可能觉得它令人惊讶。

46.16.1 正则表达式标志 /g/y

每个方法对 /g/y 的反应都不同;这给我们一个大致的一般概念:

  • /g.global,ES3):正则表达式应该多次匹配,在字符串的任何位置。

  • /y.sticky,ES6):字符串内的任何匹配应立即跟在先前的匹配之后(匹配“粘”在一起)。

如果正则表达式既没有 /g 标志也没有 /y 标志,匹配只会发生一次,并从开始处开始。

无论使用 /g 还是 /y,匹配都是相对于输入字符串内部的“当前位置”进行的。该位置存储在正则表达式属性 .lastIndex 中。

有三组与正则表达式相关的方法:

  1. 字符串方法 .search(regExp).split(regExp) 完全忽略 /g/y(以及因此的 .lastIndex)。

  2. 当设置了 /g/y 时,RegExp 方法 .exec(str).test(str) 会发生变化。

    首先,我们通过重复调用一个方法来获取多个匹配,每次调用,它返回另一个结果(一个匹配对象或 true)或“结果结束”值(nullfalse)。

    第二,正则表达式属性 .lastIndex 用于遍历输入字符串。一方面,.lastIndex 决定了匹配的起始位置:

    • /g 表示匹配必须从 .lastIndex 或之后开始。

    • /y 表示匹配必须从 .lastIndex 开始。也就是说,正则表达式的开始被锚定到 .lastIndex

      注意,^$ 仍然像往常一样工作:它们将匹配锚定到输入字符串的开始或结束,除非设置了 .multiline。然后它们锚定到行首或行尾。

    另一方面,.lastIndex 被设置为上一个匹配的最后索引加一。

  3. 剩余的方法包括:

    • 字符串方法 .match(regExp).matchAll(regExp)(不使用 /g

    • 字符串方法 .replace(regExp, str).replaceAll(regExp, str)(如果没有 /g 则抛出异常)

    这些方法都受到影响如下:

    • /g 导致多个匹配项。

    • /y 导致一个必须从 .lastIndex 开始的单个匹配项。

    • /gy 导致没有间隔的多个匹配项。

这只是一个初步概述。接下来的章节将更深入地探讨。

46.16.2 /g 和 /y 如何具体影响方法?

以下方法不受 /g/y 的影响:

  • string.search(regExp)

  • string.split(regExp)

46.16.2.1 string.match(regExp) (ES3)

在没有 /g/y 的情况下,.match() 会忽略 .lastIndex 并始终返回第一个匹配项的匹配对象。

> const re = /#/; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]

使用 /y 时,匹配必须正好从 .lastIndex 开始。.lastIndex 将被更新。如果没有匹配项,则返回 null

> const re = /#/y; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> ['##-#'.match(re), re.lastIndex]
[null, 0]

使用 /g 时,我们得到一个数组中的所有匹配项(组 0)。.lastIndex 被忽略并重置为零。

> const re = /#/g; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#', '#']
> re.lastIndex
0

/gy 的行为与 /g 类似,但匹配项之间不允许有间隔:

> const re = /#/gy; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#']
> re.lastIndex
0

46.16.2.2 string.matchAll(regExp) (ES2020)

如果没有设置 /g.matchAll() 会抛出异常:

> const re = /#/y; re.lastIndex = 1;
> '##-#'.matchAll(re)
TypeError: String.prototype.matchAll called with
a non-global RegExp argument

如果设置了 /g,匹配将从 .lastIndex 开始,并且该属性不会改变:

> const re = /#/g; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
  { 0: '#', index: 1, input: '##-#' },
  { 0: '#', index: 3, input: '##-#' },
]
> re.lastIndex
1

/gy 的行为与 /g 类似,但匹配项之间不允许有间隔:

> const re = /#/gy; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
  { 0: '#', index: 1, input: '##-#' },
]
> re.lastIndex
1

46.16.2.3 regExp.exec(str) (ES3)

在没有 /g/y 的情况下,.exec() 会忽略 .lastIndex 并始终返回第一个匹配项的匹配对象:

> const re = /#/; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]

使用 /g 时,匹配必须从 .lastIndex 或之后开始。.lastIndex 将被更新。如果没有匹配项,则返回 null

> const re = /#/g; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 3, input: '##-#' }, 4]
> [re.exec('##-#'), re.lastIndex]
[null, 0]

使用 /y 时,匹配必须正好从 .lastIndex 开始。.lastIndex 将被更新。如果没有匹配项,则返回 null

> const re = /#/y; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex]
[null, 0]

使用 /gy 时,.exec() 的行为与使用 /y 时相同。

46.16.2.4 string.replace(regExp, str) (ES3)

在没有 /g/y 的情况下,只替换第一个出现:

> const re = /#/; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'x#-#'
> re.lastIndex
1

使用 /g 时,将替换所有出现。.lastIndex 被忽略但重置为零。

> const re = /#/g; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-x'
> re.lastIndex
0

使用 /y 时,只替换 .lastIndex 处的(第一个)出现。.lastIndex 将被更新。

> const re = /#/y; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'#x-#'
> re.lastIndex
2

/gy 的行为类似于 /g,但匹配项之间不允许有间隔:

> const re = /#/gy; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-#'
> re.lastIndex
0

46.16.2.5 string.replaceAll(regExp, str) (ES2021)

.replaceAll() 的行为类似于 .replace(),但如果未设置 /g,则会抛出异常:

> const re = /#/y; re.lastIndex = 1;
> '##-#'.replaceAll(re, 'x')
TypeError: String.prototype.replaceAll called
with a non-global RegExp argument

46.16.2.6 regExp.test(str) (ES3)

此方法的行为与 .exec() 相同,但返回 true 而不是匹配对象,返回 null 而不是 false

例如,如果没有 /g/y,结果总是 true

> const re = /#/; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 1]
> [re.test('##-#'), re.lastIndex]
[true, 1]

使用 /g 时,有两个匹配项:

> const re = /#/g; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 2]
> [re.test('##-#'), re.lastIndex]
[true, 4]
> [re.test('##-#'), re.lastIndex]
[false, 0]

使用 /y 时,只有一个匹配项:

> const re = /#/y; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 2]
> [re.test('##-#'), re.lastIndex]
[false, 0]

使用 /gy 时,.test() 的行为与 /y 相同。

46.16.3 /g 和 /y 的四个陷阱及其处理方法

我们将首先探讨 /g/y 的四个陷阱,然后探讨处理这些陷阱的方法。

46.16.3.1 陷阱 1:我们无法将带有 /g/y 的正则表达式内联

带有 /g 的正则表达式不能内联。例如,在下面的 while 循环中,每次检查条件时都会创建一个新的正则表达式。因此,其 .lastIndex 总是零,循环永远不会终止。

let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
  console.log(matchObj[0]);
}

使用 /y 时,问题相同。

46.16.3.2 陷阱 2:移除 /g/y 可能会破坏代码

如果代码期望一个带有 /g 的正则表达式,并且有一个遍历 .exec().test() 结果的循环,那么没有 /g 的正则表达式可能会导致无限循环:

function collectMatches(regExp, str) {
  const matches = [];
  let matchObj;
  // Infinite loop
  while (matchObj = regExp.exec(str)) {
    matches.push(matchObj[0]);
  }
  return matches;
}
collectMatches(/a+/, 'bbbaabaaa'); // Missing: flag /g

为什么会有无限循环?因为 .exec() 总是返回第一个结果,一个匹配对象,而不会返回 null

使用 /y 时,问题相同。

46.16.3.3 陷阱 3:添加 /g/y 可能会破坏代码

使用 .test() 时,还有一个需要注意的地方:它会受到 .lastIndex 的影响。因此,如果我们想精确地检查一次正则表达式是否与字符串匹配,那么正则表达式必须没有 /g。否则,我们通常每次调用 .test() 都会得到不同的结果:

> const regExp = /^X/g;
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ false, 0 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]

第一次调用会产生一个匹配并更新 .lastIndex。第二次调用找不到匹配并重置 .lastIndex 为零。

如果我们创建一个专门用于 .test() 的正则表达式,那么我们可能不会添加 /g。然而,如果我们使用相同的正则表达式进行替换和测试,那么遇到 /g 的可能性会增加。

再次强调,这个问题也存在于 /y 上:

> const regExp = /^X/y;
> regExp.test('Xa')
true
> regExp.test('Xa')
false
> regExp.test('Xa')
true

46.16.3.4 陷阱 4:如果 .lastIndex 不是零,代码可能会产生意外的结果

如果一个操作使用了受 .lastIndex 影响的正则表达式方法,那么我们必须确保 .lastIndex 在开始时为零。否则,我们可能会得到意外的结果:

function countMatches(regExp, str) {
  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(
  countMatches(myRegExp, 'babaa'), 1 // should be 3
);

通常,.lastIndex 在新创建的正则表达式中为零,我们不会像在例子中那样显式地更改它。但是,如果我们多次使用正则表达式,.lastIndex 仍然可能不是零。

46.16.3.5 如何避免 /g/y 的陷阱

作为处理 /g.lastIndex 的一个例子,我们回顾一下前一个例子中的 countMatches()。我们如何防止错误的正则表达式破坏我们的代码?让我们看看三种方法。

46.16.3.5.1 抛出异常

首先,如果 /g 没有设置或 .lastIndex 不是零,我们可以抛出一个异常:

function countMatches(regExp, str) {
  if (!regExp.global) {
    throw new Error('Flag /g of regExp must be set');
  }
  if (regExp.lastIndex !== 0) {
    throw new Error('regExp.lastIndex must be zero');
  }

  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}

46.16.3.5.2 克隆正则表达式

其次,我们可以克隆参数。这还有一个额外的优点,即 regExp 不会改变。

function countMatches(regExp, str) {
  const cloneFlags = regExp.flags + (regExp.global ? '' : 'g');
  const clone = new RegExp(regExp, cloneFlags);

  let count = 0;
  while (clone.test(str)) {
    count++;
  }
  return count;
}

46.16.3.5.3 使用不受 .lastIndex 或标志影响的操作

几个正则表达式操作不受 .lastIndex 或标志的影响。例如,如果存在 /g.match() 会忽略 .lastIndex

function countMatches(regExp, str) {
  if (!regExp.global) {
    throw new Error('Flag /g of regExp must be set');
  }
  return (str.match(regExp) ?? []).length;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(countMatches(myRegExp, 'babaa'), 3); // OK!

在这里,countMatches() 即使没有检查或修复 .lastIndex 也能正常工作。

46.16.4 使用 .lastIndex 的用例:从给定索引开始匹配

除了在多次匹配时存储当前位置外,.lastIndex 还可以用于:

  • 在给定位置精确匹配一次(通过标志 /y

  • 在给定位置或之后匹配一次(通过标志 /g

我们接下来将查看示例。

46.16.4.1 示例:检查正则表达式是否在给定索引处匹配

由于 .test()/y.lastIndex 的影响,我们可以用它来检查正则表达式 regExp 是否在给定的 index 处与字符串 str 匹配:

function startsWith(regExp, str, index) {
  if (!regExp.sticky || regExp.global) {
    throw new Error('Flag /y must be set. Flag /g must not be set.');
  }
  regExp.lastIndex = index;
  return regExp.test(str);
}
assert.equal(
  startsWith(/x+/y, 'aaxxx', 0), false
);
assert.equal(
  startsWith(/x+/y, 'aaxxx', 2), true
);

由于 /yregExp 锚定到 .lastIndex

注意,我们不得使用断言 ^,这将使 regExp 锚定到输入字符串的开始。

46.16.4.2 示例:从给定索引开始查找匹配位置

.search() 允许我们找到正则表达式匹配的位置:

> '#--#'.search(/#/)
0

可惜,我们无法更改 .search() 开始查找匹配的位置。作为替代方案,我们可以使用 .exec() 进行搜索:

function searchFrom(regExp, str, index) {
  if (!regExp.global || regExp.sticky) {
    throw new Error('Flag /g must be set. Flag /y must not be set.');
  }
  regExp.lastIndex = index;
  const match = regExp.exec(str);
  if (match) {
    return match.index;
  } else {
    return -1;
  }
}

assert.equal(
  searchFrom(/#/g, '#--#', 0), 0
);
assert.equal(
  searchFrom(/#/g, '#--#', 1), 3
);

46.16.4.3 示例:在给定索引处替换出现

当没有 /g 并且有 /y 时,.replace() 会进行一次替换——如果 .lastIndex 处有匹配项:

function replaceOnceAt(str, regExp, replacement, index) {
  if (!regExp.sticky || regExp.global) {
    throw new Error('Flag /y must be set. Flag /g must not be set.');
  }
  regExp.lastIndex = index;
  return str.replace(regExp, replacement);
}
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 0), 'X aaaa a')
;
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 3), 'aa X a'
);
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 8), 'aa aaaa X'
);

46.16.5 .lastIndex 的缺点和优点

正则表达式属性 .lastIndex 有两个显著的缺点:

  • 这使得正则表达式具有状态:

    • 我们现在必须注意正则表达式的状态以及我们如何共享它们。

    • 对于许多用例,我们也不能通过冻结来使它们不可变。

  • .lastIndex 在正则表达式操作中的支持不一致。

优点是,.lastIndex 也提供了额外的有用功能:我们可以指定匹配应该开始的位置(对于某些操作)。理想情况下,这种功能应该通过一个断言如 \G(与标志 /y 相比)以及作为各种正则表达式相关方法的参数的索引(与正则表达式属性 .lastIndex 相比)来提供。但总比没有好。

46.17 RegExp.escape():转义文本以便在正则表达式中使用 (ES2025)

对于字符串 textRegExp.escape(text) 创建一个与 text 匹配的正则表达式模式。

在正则表达式中具有特殊意义的字符不能直接使用,必须进行转义:

> RegExp.escape('(*)')
'\\(\\*\\)'

注意,我们看到的每个正则表达式反斜杠都出现了两次:其中一个是实际的反斜杠,另一个是在字符串字面量中将其转义:

> '\\(\\*\\)' === String.raw`\(\*\)`
true

没有特殊意义的字符不需要转义:

> RegExp.escape('_abc123')
'_abc123'

46.17.1 RegExp.escape() 的用例:替换所有文本的出现

转义的经典用例是搜索和替换文本:

function replacePlainText(str, searchText, replace) {
  const searchRegExp = new RegExp(
    RegExp.escape(searchText),
    'gu'
  );
  return str.replace(searchRegExp, replace)
}
assert.equal(
  replacePlainText('(a) and (a)', '(a)', '@'),
  '@ and @'
);

然而,自 ES2021 以来,我们有了 .replaceAll()

assert.equal(
  '(a) and (a)'.replaceAll('(a)', '@'),
  '@ and @'
);

46.17.2 RegExp.escape() 的用例:正则表达式的一部分必须匹配给定的文本

以下代码移除了 str 中所有未引用的 text 出现:

function removeUnquotedText(str, text) {
  const regExp = new RegExp(
    `(?<!“)${RegExp.escape(text)}(?!”)`,
    'gu'
  );
  return str.replaceAll(regExp, '•');
}
assert.equal(
  removeUnquotedText('“yes” and yes and “yes”', 'yes'),
  '“yes” and • and “yes”'
);

同样的方法也可以用来查找或计数未引用的文本。

图标“练习” 练习:搜索包含通配符的文本

exercises/regexps/includes-with-elision_test.mjs

46.18 匹配所有或无内容

有时,我们可能需要一个匹配所有或无内容的正则表达式 - 例如,作为默认值。

  • 匹配所有内容:/(?:)/

    空组 () 匹配所有内容。我们将其设置为非捕获组(通过 ?:),以避免不必要的操作。

    > /(?:)/.test('')
    true
    > /(?:)/.test('abc')
    true
    
    
  • 匹配无内容:/.^/

    ^ 仅匹配字符串的开始。点号将匹配扩展到第一个字符之外,现在 ^ 不再匹配。

    > /.^/.test('')
    false
    > /.^/.test('abc')
    false
    
    

正则表达式字面量不能为空,因为 // 开始了一个单行注释。因此,在当前情况下,使用前两个正则表达式中的第一个:

> new RegExp('')
/(?:)/

46.19 使用正则表达式更易于使用的技巧

在本节中,我们探讨我们可以使正则表达式更容易使用的方法。我们将使用以下正则表达式作为示例:

const RE_API_SIGNATURE =
  /^(new |get )?([A-Za-z0-9_.\[\]]+)/;

目前,它仍然相当晦涩。一旦我们到达“不重要的空白”,它将更容易理解。

46.19.1 提示:使用标志 /v

如果我们在正则表达式中添加标志 /v,我们会得到更少的怪异行为和更多功能:

const RE_API_SIGNATURE =
  /^(new |get )?([A-Za-z0-9_.\[\]]+)/v;

/v 在这个特定情况下没有改变任何东西,但它有助于我们在添加具有多个代码点的图形群集或想要字符类中的集合运算等功能时。

46.19.2 提示:按字母顺序排序标志

如果有多个标志,我们应该 按字母顺序排序标志 - 例如:

/pattern/giv

这样可以使顺序一致,这也是 JavaScript 显示正则表达式的方式:

> String(/pattern/vgi)
'/pattern/giv'

46.19.3 提示:使用命名捕获组

我们的正则表达式包含两个位置捕获组。如果我们给它们命名,它们将描述其目的,我们需要的额外文档更少:

const RE_API_SIGNATURE =
  /^(?<prefix>new |get )?(?<name>[A-Za-z0-9_.\[\]]+)/;

46.19.4 提示:通过 # 使用不重要的空白和行注释

到目前为止,正则表达式仍然相当难以阅读。我们可以通过添加空格和换行符来改变这一点。由于正则表达式字面量不允许我们这样做,我们使用库 Regex+,它为我们提供了模板标签 regex

import {regex} from 'regex';

const RE_API_SIGNATURE = regex`
 ^
 (?<prefix>
 new \x20  # constructor
 |
 get \x20  # getter
 )?
 (?<name>
 # Square brackets are needed for symbol keys
 [
 A-Z a-z 0-9 _
 .
 \[ \]
 ]+
 )
`;

正则表达式模式中忽略空白的功能被称为不重要的空白。此外,我们还使用了一个名为内联注释的功能——它以井号符号(#)开始。

两个观察结果:

  • 由于所有空格都被移除,我们使用十六进制转义符 \x20 来表示在 newget 之后有一个空格。

  • 很遗憾,行注释不允许在字符类内部。这就是为什么关于方括号的注释在字符类之前。

在未来,JavaScript 可能会通过标志 /xECMAScript 提案)内置对不重要的空白的支持。

使用 regex 模板标签,以下标志始终处于活动状态:

  • 标志 /v

  • 标志 /x(模拟)通过 # 启用不重要的空白和行注释。

  • 标志 /n(模拟)启用仅命名捕获模式,这防止了编号组进行捕获。换句话说:(模式)被处理成(?:模式)。

46.19.5 提示:为您的正则表达式编写测试

为了确保正则表达式按预期工作,我们可以为其编写测试。这些是针对 RE_API_SIGNATURE 的测试:

assert.deepEqual(
  getCaptures(`get Map.prototype.size`),
  {
    prefix: 'get ',
    name: 'Map.prototype.size',
  }
);
assert.deepEqual(
  getCaptures(`new Array(len = 0)`),
  {
    prefix: 'new ',
    name: 'Array',
  }
);
assert.deepEqual(
  getCaptures(`Array.prototype.push(...items)`),
  {
    prefix: undefined,
    name: 'Array.prototype.push',
  }
);
assert.deepEqual(
  getCaptures(`Map.prototype[Symbol.iterator]()`),
  {
    prefix: undefined,
    name: 'Map.prototype[Symbol.iterator]',
  }
);

function getCaptures(apiSignature) {
  const match = RE_API_SIGNATURE.exec(apiSignature);
  // Spread so that the result does not have a null prototype
  // and is easier to compare.
  return {...match.groups};
}

46.19.6 提示:在您的文档中提及示例

看到匹配的字符串,有助于理解正则表达式应该做什么:

/**
 * Matches API signatures – e.g.:
 * ```

* `get Map.prototype.size`

* `new Array(len = 0)`

* `Array.prototype.push(...items)`

* `Map.prototype[Symbol.iterator]()`

* ```js
 */
const RE_API_SIGNATURE = regex`
 ···
`;

一些文档工具允许我们在文档注释中引用单元测试,并在文档中显示它们的代码。这比我们上面所做的是一种好的替代方案。

46.19.7 奖励提示:使用插值来重用模式

Regex+ 库允许我们插值正则表达式片段(“模式”),这有助于重用。以下示例定义了一种简单的标记语法,它让人联想到 HTML:

import { pattern, regex } from 'regex';

const LABEL = pattern`[a-z\-]+`;
const ARGS = pattern`
 (?<args>
 \x20+
 ${LABEL}
 )*
`;
const NAME = pattern`
 (?<name> ${LABEL} )
`;

const TAG = regex`
 (?<openingTag>
 \[
 \x20*
 ${NAME}
 ${ARGS}
 \x20*
 \]
 )
 |
 (?<singletonTag>
 \[
 \x20*
 ${NAME}
 ${ARGS}
 \x20*
 / \]
 )
`;

assert.deepEqual(
  TAG.exec('[pre js line-numbers]').groups,
  {
    openingTag: '[pre js line-numbers]',
    name: 'pre',
    args: ' line-numbers',
    singletonTag: undefined,
    __proto__: null,
  }
);

assert.deepEqual(
  TAG.exec('[hr /]').groups,
  {
    openingTag: undefined,
    name: 'hr',
    args: undefined,
    singletonTag: '[hr /]',
    __proto__: null,
  }
);

正则表达式 TAG 使用了正则表达式片段 NAMEARGS 两次——这减少了冗余。

46.19.8 奖励提示:无需库的不重要空白

通过以下技巧,我们不需要库就可以编写具有不重要的空白格式的正则表达式:

const RE_API_SIGNATURE = new RegExp(
  String.raw`
 ^
 (?<prefix>
 new \x20
 |
 get \x20
 )?
 (?<name>
 [
 A-Z a-z 0-9 _
 .
 \[ \]
 ]+
 )
 `.replaceAll(/\s+/g, ''), // (A)
  'v'
);
assert.equal(
  String(RE_API_SIGNATURE),
  String.raw`/^(?<prefix>new\x20|get\x20)?(?<name>[A-Za-z0-9_.\[\]]+)/v`
);

这段代码是如何工作的?

  • String.raw 启用两件事:

    • 我们不需要为这种字符串字面量转义正则表达式反斜杠。

    • 正则表达式可以跨越多行。

  • .replaceAll() 移除所有空白字符(空格、制表符、换行符等),使得最终结果看起来几乎与正则表达式的初始版本相同。不过有一个区别:由于字面空格被移除,我们不得不找到一种不同的方式来指定 newget 后面有一个空格。一个选项是十六进制转义符 \x20:十六进制 20(十进制 32)是空格的代码点。

我们甚至可以像这样模拟内联注释:

// Template tag function
const cmt = () => '';
const RE = new RegExp(
 String.raw`
 a+ ${cmt`one or more as`}
 `.replaceAll(/\s+/g, ''),
 'v'
);
assert.equal(
 String(RE), '/a+/v'
);

很遗憾,它的语法比我想的要复杂。

46.19.9 结论:这是正则表达式应该被编写的方式

许多人不喜欢正则表达式的一个原因就是它们发现正则表达式难以阅读。然而,在不重要的空白和注释的情况下,这个问题就小得多。我认为这是编写正则表达式的正确方式:想想如果没有空白和注释,JavaScript 代码会是什么样子。

46.20 快速参考:正则表达式功能

46.20.1 总结:.global (/g) 和 .sticky (/y)

以下两个方法完全不受 /g/y 的影响:

  • String.prototype.search()

  • String.prototype.split()

此表解释了剩余的正则表达式相关方法如何受这两个标志的影响(如果既没有 /g 也没有 /y,则 regExp.lastIndex 总是会被忽略):

标志 尊重 .lastIndex 更新 .lastIndex
s.match /y
/g /gy 0
s.matchAll /g /gy
r.exec /g /y /gy
s.replace /y
/g /gy 0
s.replaceAll /g /gy 0
r.test /g /y /gy

图标“外部”更长的概述

我在网上发布了一个更长的表格(a longer table),该表格是通过 Node.js 脚本创建的。

46.20.2 String.prototype.*: 匹配和搜索

  • String.prototype.match(regExpOrString)

    ES3 | /y 尊重并更新 .lastIndex | /g /gy 忽略并重置 .lastIndex

    • (1 of 3) regExpOrString 是一个字符串。

      如果 regExpOrString 是一个字符串,它定义了一个没有 /g 的正则表达式模式(想想 new RegExp() 的参数)。该正则表达式将按下一项中所述使用。

    • (2 of 3) regExpOrString 是一个没有 /g 的 RegExp。

      match(
        regExpOrString: string | RegExp
      ): null | RegExpMatchArray
      
      interface RegExpMatchArray extends Array<string> {
        index: number;
        input: string;
        groups: undefined | {
          [key: string]: string
        };
      }
      
      

      如果 regExpOrString 是一个没有设置 /g 标志的正则表达式,那么 .match() 返回字符串中 regExpOrString 的第一个匹配项。如果没有匹配项,则返回 null

      • 编号捕获组成为数组元素(这就是为什么 RegExpMatchArray 扩展了 Array)。

      • 命名捕获组^(ES2018) 成为 .groups 的属性。

      示例:

      > 'ababb'.match(/a(b+)/)
      { 0: 'ab', 1: 'b', index: 0, input: 'ababb', groups: undefined }
      > 'ababb'.match(/a(?<bs>b+)/)
      { 0: 'ab', 1: 'b', index: 0, input: 'ababb', groups: { bs: 'b' } }
      > 'abab'.match(/x/)
      null
      
      
    • (3 of 3) regExpOrString 是带有 /g 的 RegExp。

      match(
        regExpOrString: RegExp
      ): null | Array<string>
      
      

      如果 regExpOrString/g 标志被设置,.match() 返回一个包含所有匹配项的数组或如果没有匹配项,则返回 null

      > 'ababb'.match(/a(b+)/g)
      [ 'ab', 'abb' ]
      > 'ababb'.match(/a(?<bs>b+)/g)
      [ 'ab', 'abb' ]
      > 'abab'.match(/x/g)
      null
      
      
  • String.prototype.matchAll(regExp)

    ES2020 | /g /gy 尊重并保留 .lastIndex

    matchAll(regexp: RegExp): Iterator<RegExpExecArray>
    interface RegExpMatchArray extends Array<string> {
      index: number;
      input: string;
      groups: undefined | {
        [key: string]: string
      };
    }
    
    
    • 如果没有设置标志 /g,则抛出异常。

    • 返回零个或多个匹配的迭代器。每个匹配项:

      • 编号捕获组成为数组元素(这就是为什么 RegExpMatchArray 扩展 Array)。

      • 命名捕获组 ^ (ES2018) 成为 .groups 的属性。

    示例:

    > 'yes'.matchAll(/(y|s)/gv).toArray()
    [
      { 0: 'y', 1: 'y', index: 0, input: 'yes', groups: undefined },
      { 0: 's', 1: 's', index: 2, input: 'yes', groups: undefined },
    ]
    
    
  • String.prototype.search(regExpOrString)

    ES3 | 忽略 .lastIndex

    返回 regExpOrString 在字符串中出现的索引。如果 regExpOrString 是一个字符串,它被用来创建一个正则表达式(想想 new RegExp() 的参数)。

    > 'a2b'.search(/[0-9]/)
    1
    > 'a2b'.search('[0-9]')
    1
    
    

46.20.3 String.prototype.*: 分割和替换

  • String.prototype.split(separator, limit?)

    ES3 | 忽略 .lastIndex

    split(separator: string | RegExp, limit?: number): Array<string>
    
    

    分隔符可以是一个字符串(它被解释为纯文本,而不是正则表达式模式)或正则表达式。

    示例:

    // Splitting with a string
    assert.deepEqual(
      'a.b.c'.split('.'),
      [ 'a', 'b', 'c' ]
    );
    
    // Splitting with a regular expression
    assert.deepEqual(
      'a x:yyy b'.split(/x+:y+/),
      [ 'a ', ' b' ]
    );
    
    // Group captures appear in the result
    assert.deepEqual(
      'a x:yyy b'.split(/(x+):(y+)/),
      [ 'a ', 'x', 'yyy', ' b' ]
    );
    
    

    如果我们希望分隔符成为返回的字符串片段的一部分,我们可以使用带有 向后查找断言 或 向前查找断言 的正则表达式:

    > 'a: b: c'.split(/(?<=:) */)
    [ 'a:', 'b:', 'c' ]
    > 'a :b :c'.split(/ *(?=:)/)
    [ 'a', ':b', ':c' ]
    
    

    陷阱.split('') 将分割成 JavaScript 字符,但通常我们希望分割成图形簇或至少 Unicode 代码点。因此,最好使用 Array.from()Intl.Segmenter 进行分割。更多信息,请参阅 “文本的原子:代码点、JavaScript 字符、图形簇”(§22.7)。

  • String.prototype.replace(searchValue, replaceValue)

    ES3 | /y 尊重并更新 .lastIndex | /g /gy 忽略并重置 .lastIndex

    关于此方法的更多信息,请参阅本章前面的 其部分。

    • (1 of 2) searchValue 是没有 /g 的字符串或 RegExp。

      replace(
        searchValue: string | RegExp,
        replaceValue: string | (...args: any[]) => string
      ): string
      
      

      .replaceAll() 的工作方式类似,但只替换第一个出现:

      > 'x.x.'.replace('.', '#') // interpreted literally
      'x#x.'
      > 'x.x.'.replace(/./, '#')
      '#.x.'
      
      
    • (1 of 2) searchValue 是带有 /g 的 RegExp。

      replace(
        searchValue: RegExp,
        replaceValue: string | (...args: any[]) => string
      ): string
      
      

      .replaceAll() 完全相同:

      > 'x.x.'.replace(/./g, '#')
      '####'
      > 'x.x.'.replace(/\./g, '#')
      'x#x#'
      
      
  • String.prototype.replaceAll(searchValue, replaceValue)

    ES2021 | /g /gy 忽略并重置 .lastIndex

    • (1 of 2) replaceValue 是一个字符串。

      replaceAll(
        searchValue: string | RegExp,
        replaceValue: string
      ): string
      
      

      searchValue 的所有匹配项替换为 replaceValue。如果 searchValue 是没有 /g 标志的正则表达式,则抛出 TypeError

      > 'x.x.'.replaceAll('.', '#') // interpreted literally
      'x#x#'
      > 'x.x.'.replaceAll(/./g, '#')
      '####'
      > 'x.x.'.replaceAll(/./, '#')
      TypeError: String.prototype.replaceAll called with
      a non-global RegExp argument
      
      

      replaceValue 中的特殊字符包括:

      • $$: 变成 $

      • $n: 变成编号组 n 的捕获(遗憾的是,$0 代表字符串 '$0',它不指向完整匹配)

      • $&: 变成完整匹配

      • $`: 变成匹配项之前的内容

      • $': 变成匹配项之后的内容

      • $<name> 变成命名组 name 的捕获

    • (2 of 2) replaceValue 是一个函数。

      replaceAll(
        searchValue: string | RegExp,
        replaceValue: (...args: any[]) => string
      ): string
      
      

      如果第二个参数是一个函数,则出现被其返回的字符串替换。其参数 args 是:

      • matched: string. 完整匹配

      • g1: string|undefined。编号组 1 的捕获

      • g2: string|undefined。编号组 2 的捕获

      • (等等。)

      • offset: number。匹配在输入字符串中的位置?

      • input: string。整个输入字符串

      const regexp = /([0-9]{2})\.([0-9]{4})/g;
      const replacer = (all, month, year) => `|${year}-${month}|`;
      assert.equal(
        'a 12.1995 b'.replaceAll(regexp, replacer),
        'a |1995-12| b'
      );
      
      

      命名捕获组 (ES2018) 也受支持。如果有,则会在末尾添加一个对象作为参数,该对象的属性包含捕获:

      const regexp = /(?<month>[0-9]{2})\.(?<year>[0-9]{4})/g;
      const replacer = (...args) => {
        const groups = args.at(-1);
        return `|${groups.year}-${groups.month}|`;
      };
      assert.equal(
        'a 12.1995 b'.replaceAll(regexp, replacer),
        'a |1995-12| b'
      );
      
      

46.20.4 RegExp.prototype.*

  • RegExp.prototype.test(string)

    ES3 | /g /y /gy 尊重并更新 .lastIndex

    test(string: string): boolean
    
    

    如果接收器与 string 匹配,则返回 true

    > /^# /.test('# comment')
    true
    > /^# /.test('#comment')
    false
    > /^# /.test('abc')
    false
    
    

    陷阱: 不要使用带有标志 /g 的正则表达式。然后 regExp.test() 将从 regExp.lastIndex 开始匹配,并更新该属性。

  • RegExp.prototype.exec(string)

    ES3 | /g /y /gy 尊重并更新 .lastIndex

    • (1 of 2) 接收器是一个没有 /g 的 RegExp。

      没有标志 /gregExp.exec(string) 的行为类似于 string.match(regExp)(#qref-String.prototype.match) - 它返回一个单独的匹配对象。

    • (2 of 2) 接收器是一个带有 /g 的 RegExp。

      exec(string: string): RegExpExecArray | null
      
      interface RegExpExecArray extends Array<string> {
        index: number;
        input: string;
        groups: undefined | {
          [key: string]: string
        };
      }
      
      

      如果 regExp 有标志 /g,则 regExp.exec(str) 返回一个对象,该对象从 regExp.lastIndex 开始的第一个匹配项 - 如果找不到匹配项,则返回 null。它还会更新 regExp.lastIndex,使其指向匹配之后的索引。

      • 编号捕获组成为数组元素(这就是为什么 RegExpExecArray 扩展了 Array)。

      • 命名捕获组 (ES2018) 成为 .groups 的属性。

      示例:

      > const regExp = /(a+)b/g, str = 'ab aab';
      
      > regExp.exec(str)
      {0: 'ab', 1: 'a', index: 0, input: 'ab aab', groups: undefined}
      > regExp.lastIndex
      2
      
      > regExp.exec(str)
      {0: 'aab', 1: 'aa', index: 3, input: 'ab aab', groups: undefined}
      > regExp.lastIndex
      6
      
      > regExp.exec(str)
      null
      > regExp.lastIndex
      0
      
      
posted @ 2025-12-12 18:01  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报