前置知识: JavaScript

断言

00:00
4 min Intermediate 2026/6/14

正则表达式先行与后行断言

概述

正则表达式中的断言(Assertion)是一种零宽匹配机制,用于判断某个位置前后是否满足特定模式,但不会消耗字符。断言分为先行断言(Lookahead)和后行断言(Lookbehind),各有正向和负向两种形式。先行断言从 ES3 就已支持,后行断言则是 ES2018 引入的特性。断言在数据提取、格式验证和文本处理等场景中非常实用。

基础概念

先行断言(Lookahead):从当前位置向右查看,判断后面的字符是否匹配指定模式。正向先行 (?=pattern) 要求后面必须匹配,负向先行 (?!pattern) 要求后面不能匹配。

后行断言(Lookbehind):从当前位置向左查看,判断前面的字符是否匹配指定模式。正向后行 (?<=pattern) 要求前面必须匹配,负向后行 (?<!pattern) 要求前面不能匹配。

零宽特性:断言匹配的是位置而非字符,匹配结果不包含断言中的内容。这意味着断言不会消耗输入字符串中的字符,同一个位置可以被多次断言。

四种断言类型

类型语法说明
正向先行断言(?=pattern)后面必须匹配 pattern
负向先行断言(?!pattern)后面不能匹配 pattern
正向后行断言(?<=pattern)必须匹配 pattern
负向后断言(?<!pattern)不能匹配 pattern

快速上手

先行断言基础

// 正向先行断言:匹配后面跟着"元"的数字
const priceRegex = /\d+(?=元)/g;
const result1 = '苹果5元,香蕉3元'.match(priceRegex);
console.log(result1); // ['5', '3']
// 注意:匹配结果只有数字,"元"字不被包含

// 负向先行断言:匹配后面不跟"px"的数字
const noPxRegex = /\d+(?!px)/g;
const result2 = '12px 34em 56'.match(noPxRegex);
console.log(result2); // ['1', '34', '56']
// "12px" 中的 1 后面是 2 不是 px,所以 1 匹配
// 更精确的写法需要加边界

后行断言基础

// 正向后行断言:匹配"价格:"后面的数字
const afterLabelRegex = /(?<=价格:)\d+/g;
const result3 = '苹果价格:5,香蕉价格:3'.match(afterLabelRegex);
console.log(result3); // ['5', '3']

// 负向后行断言:匹配前面不是"不"的"喜欢"
const positiveVerbRegex = /(?<!不)喜欢/g;
const result4 = '我喜欢你,他不喜欢你'.match(positiveVerbRegex);
console.log(result4); // ['喜欢']
// 只匹配第一个"喜欢",因为第二个前面有"不"

详细用法

密码强度验证

// 使用正向先行断言实现多条件密码验证
// 要求:至少8位,包含小写字母、大写字母和数字
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;

console.log(strongPassword.test('Abc12345')); // true
console.log(strongPassword.test('abc12345')); // false(缺大写)
console.log(strongPassword.test('ABC12345')); // false(缺小写)
console.log(strongPassword.test('Abc1234')); // false(不足8位)

// 解析:
// ^ 从开头匹配
// (?=.*[a-z])  先行断言:后面必须有小写字母
// (?=.*[A-Z])  先行断言:后面必须有大写字母
// (?=.*\d)     先行断言:后面必须有数字
// .{8,}        实际匹配:至少8个任意字符
// $ 到结尾

// 更严格的密码:还要求包含特殊字符
const veryStrongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{12,}$/;

文件名过滤

// 负向先行断言:排除 .log 文件
const notLogRegex = /^(?!.*\.log$).+$/;
console.log(notLogRegex.test('app.log')); // false
console.log(notLogRegex.test('app.txt')); // true
console.log(notLogRegex.test('data.json')); // true

// 排除多种后缀
const excludeRegex = /^(?!.*\.(log|tmp|bak)$).+$/;
console.log(excludeRegex.test('file.tmp')); // false
console.log(excludeRegex.test('file.js')); // true

// 只匹配图片文件
const imageRegex = /(?=\.(jpg|png|gif|webp)$).+/i;
// 更简洁的写法:/\.(jpg|png|gif|webp)$/i

HTML 标签内容提取

// 组合使用先行和后行断言提取标签内文本
const innerTextRegex = /(?<=<\w+>)[^<]+(?=<\/\w+>)/g;
const html = '<div>Hello</div><span>World</span><p>Test</p>';
const texts = html.match(innerTextRegex);
console.log(texts); // ['Hello', 'World', 'Test']

// 提取 HTML 属性值
const attrValueRegex = /(?<=class=")[^"]+(?=")/g;
const htmlWithClass = '<div class="container"><span class="highlight">text</span></div>';
const classes = htmlWithClass.match(attrValueRegex);
console.log(classes); // ['container', 'highlight']

CSV 解析

// 匹配不在引号内的逗号(用于 CSV 分割)
// 这是一个经典的先行断言应用
const outsideQuotesRegex = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/g;

const csvLine = 'name,"Smith, John",age,"30",city';
const fields = csvLine.split(outsideQuotesRegex);
console.log(fields);
// ['name', '"Smith, John"', 'age', '"30"', 'city']
// 引号内的逗号不会被分割

// 解析原理:
// ,  匹配逗号
// (?=  先行断言开始
//   (?:[^"]*"[^"]*")*  零或多对引号对
//   [^"]*$  后面到行尾没有引号
// )  断言结束

常见场景

货币格式处理

// 提取金额数字
const extractAmount = /(?<=¥||\$)\s*[\d,]+\.?\d*/g;
const text = '商品A ¥1,299.00,商品B $99.50,商品C ¥3,500';
const amounts = text.match(extractAmount);
console.log(amounts); // ['1,299.00', '99.50', '3,500']

// 千分位格式化(使用先行断言)
function formatNumber(num) {
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
console.log(formatNumber(1234567)); // '1,234,567'
console.log(formatNumber(1234567.89)); // '1,234,567.89'

// 解析:
// \B 非单词边界(不在开头)
// (?=(\d{3})+) 后面是3的倍数个数字
// (?!\d) 后面不再有数字

文本清洗与替换

// 移除 HTML 标签但保留内容
function stripTags(html) {
  return html.replace(/(?<=<[^>]+>)|(?=<[^>]+>)/g, '');
  // 更简单的写法
  return html.replace(/<[^>]+>/g, '');
}

// 移除行首行尾空格(每行独立处理)
function trimLines(text) {
  return text.replace(/(?<=^|\n)[ \t]+|[ \t]+(?=\n|$)/g, '');
}

// 在数字和字母之间插入空格
function separateNumAndLetter(str) {
  return str.replace(/(?<=\d)(?=[a-zA-Z])/g, ' ').replace(/(?<=[a-zA-Z])(?=\d)/g, ' ');
}
console.log(separateNumAndLetter('abc123def456')); // 'abc 123 def 456'

URL 参数提取

// 提取 URL 中的查询参数值
function getQueryParam(url, param) {
  const regex = new RegExp(`(?<=${param}=)[^&]+`);
  const match = url.match(regex);
  return match ? match[0] : null;
}

const url = 'https://example.com/page?id=123&name=test&lang=zh';
console.log(getQueryParam(url, 'id')); // '123'
console.log(getQueryParam(url, 'name')); // 'test'
console.log(getQueryParam(url, 'page')); // null

// 提取所有参数键值对
function getAllParams(url) {
  const paramRegex = /(?<=\?|&)([^=]+)=([^&]+)/g;
  const params = {};
  let match;
  while ((match = paramRegex.exec(url)) !== null) {
    params[match[1]] = match[2];
  }
  return params;
}
console.log(getAllParams(url)); // { id: '123', name: 'test', lang: 'zh' }

注意事项

  • 浏览器兼容性:后断言(Lookbehind)是 ES2018 特性,在 IE 和旧版浏览器中不支持。如果需要兼容旧环境,应使用替代方案。
  • 性能考量嵌套或复杂的断言可能导致回溯爆炸,特别是在处理字符串时。简单匹配需求,优先考虑非断言方案。
  • 断言长限制:在部分旧版浏览器中,后断言中的模式必须是固定的(不能使用量词*+)。现代浏览器支持可变长的后断言。
  • 零宽理解断言是零宽匹配,不会消耗字符。这意味着在同一个位置可以使用断言,每个断言独立判断。
  • 转义特殊字符断言中的模式如果特殊字符(如 ?*),需要正确转义,否则可能导致正则表达式语法错误

进阶用法

条件替换

// 只替换不在引号内的关键字
function replaceOutsideQuotes(text, keyword, replacement) {
  // 先行断言确保后面有偶数个引号
  const regex = new RegExp(keyword + '(?=(?:[^"]*"[^"]*")*[^"]*$)', 'g');
  return text.replace(regex, replacement);
}

const code = 'const name = "old value"; // old comment';
const result = replaceOutsideQuotes(code, 'old', 'new');
console.log(result); // 'const name = "old value"; // new comment'
// 引号内的 "old" 未被替换,注释中的被替换了

平衡组模拟

// JavaScript 不支持 .NET 风格的平衡组
// 但可以用断言模拟简单的嵌套匹配

// 匹配最外层的括号内容
function matchOuterParens(str) {
  const regex = /\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\)/g;
  return str.match(regex);
}

const expr = '(a + (b * c)) + (d / e)';
console.log(matchOuterParens(expr));
// ['(a + (b * c))', '(d / e)']

自定义词法分析

// 使用断言实现简单的词法分析器
function tokenize(code) {
  const tokens = [];
  const regex = /(?<=^|\s)(\d+)|(?<=^|\s)([a-zA-Z_]\w*)|("(?:[^"\\]|\\.)*")|([+\-*/=])/g;
  let match;

  while ((match = regex.exec(code)) !== null) {
    if (match[1]) tokens.push({ type: 'NUMBER', value: match[1] });
    else if (match[2]) tokens.push({ type: 'IDENTIFIER', value: match[2] });
    else if (match[3]) tokens.push({ type: 'STRING', value: match[3] });
    else if (match[4]) tokens.push({ type: 'OPERATOR', value: match[4] });
  }

  return tokens;
}

const code = 'let x = 42 + "hello"';
console.log(tokenize(code));
// [
//   { type: 'IDENTIFIER', value: 'let' },
//   { type: 'IDENTIFIER', value: 'x' },
//   { type: 'OPERATOR', value: '=' },
//   { type: 'NUMBER', value: '42' },
//   { type: 'OPERATOR', value: '+' },
//   { type: 'STRING', value: '"hello"' }
// ]

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式