跳到主要内容

正则✅

JavaScript 支持哪些正则能力?

答案

核心概念

  • 标志位:i/g/m/s/u/y 控制匹配行为(大小写、全局、多行、dotAll、Unicode、黏连)
  • 常用元字符与字符类:. \d \w \s 以及自定义 [ ]、排除 [^ ]
  • 边界:^ $ \b,多行模式下行首行尾
  • 分组与捕获:()、命名捕获 (?<name>pattern)、反向引用 \1$<name>
  • 断言:前瞻 (?=…) / (?!…),后顾 (?<=…)/(?!<…)
describe('RegExp flags 基础能力', () => {
  test('i - 忽略大小写', () => {
    expect(/abc/i.test('AbC')).toBe(true);
  });

  test('g - 全局匹配影响 lastIndex 与 exec 行为', () => {
    const re = /a/g;
    const input = 'a a';
    const m1 = re.exec(input);
    expect(m1 && m1.index).toBe(0);
    expect(re.lastIndex).toBe(1);

    const m2 = re.exec(input);
    expect(m2 && m2.index).toBe(2);
    expect(re.lastIndex).toBe(3);
  });

  test('m - 多行模式使 ^/$ 匹配行首/行尾', () => {
    const str = 'hello\nworld';
    expect(/^world$/m.test(str)).toBe(true);
  });

  test('s - dotAll 让 . 可匹配换行符', () => {
    expect(/a.b/s.test('a\nb')).toBe(true);
  });

  test('u - 正确解析 Unicode 转义与码点', () => {
    // U+1F680 ROCKET
    expect(/\u{1F680}/u.test('🚀')).toBe(true);
  });

  test('y - 黏连(sticky)从 lastIndex 位置开始匹配', () => {
    const re = /\d/y;
    const s = 'a1b2';

    re.lastIndex = 1; // 指向 '1'
    const m1 = re.exec(s);
    expect(m1 && m1[0]).toBe('1');
    expect(re.lastIndex).toBe(2);

    re.lastIndex = 2; // 指向 'b',不是数字,无法从此处黏连匹配
    const m2 = re.exec(s);
    expect(m2).toBeNull();
  });
});

Open browser consoleTests

说一下 JS RegExp 与 String 的常用方法及差异

答案

核心要点

  • RegExp:test/exec;String:match/matchAll/replace/replaceAll/split
  • g 标志影响 match/matchAll 返回值与 exec 的 lastIndex
  • replace 支持分组回填 $1、命名捕获 $<name>
describe('RegExp 与 String 常用方法差异', () => {
  test('test vs exec', () => {
    const re = /a/;
    expect(re.test('cat')).toBe(true);
    const m = re.exec('cat');
    expect(m && m[0]).toBe('a');
    expect(m && m.index).toBe(1);
  });

  test('String.match 与 全局标志', () => {
    expect('a1b2c3'.match(/\d/g)).toEqual(['1', '2', '3']);
    // 无 g 标志返回包含捕获组的第一个匹配
    const m = '2024-12-31'.match(/(\d{4})-(\d{2})-(\d{2})/);
    expect(m && m[0]).toBe('2024-12-31');
    expect(m && m[1]).toBe('2024');
  });

  test('String.matchAll 返回迭代器(需 g)', () => {
    const iter = 'a1b2c3'.matchAll(/(\d)/g);
    const arr = Array.from(iter, m => m[1]);
    expect(arr).toEqual(['1', '2', '3']);
  });

  test('replace 与 replaceAll', () => {
    expect('foo foo'.replace('foo', 'bar')).toBe('bar foo');
    expect('foo foo'.replaceAll('foo', 'bar')).toBe('bar bar');
    // 使用捕获组重排
    const s = '2024-12-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1');
    expect(s).toBe('12/31/2024');
  });

  test('split 支持正则与捕获组(保留分隔符)', () => {
    // 捕获组会出现在结果中
    const parts = 'a1b2'.split(/(\d)/);
    expect(parts).toEqual(['a', '1', 'b', '2', '']);
  });
});

Open browser consoleTests

分组/命名捕获/反向引用怎么用?

答案

核心要点

  • 位置分组 () 通过下标访问;命名捕获通过 groups 访问
  • 反向引用用于去重、格式校验与替换重排
describe('分组 / 命名捕获 / 反向引用', () => {
  test('位置分组提取', () => {
    const m = /(\d{4})-(\d{2})-(\d{2})/.exec('2024-12-31');
    expect(m && m[1]).toBe('2024');
    expect(m && m[2]).toBe('12');
    expect(m && m[3]).toBe('31');
  });

  test('命名捕获组', () => {
    const m = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/.exec('2024-12-31');
    expect(m && m.groups && m.groups.y).toBe('2024');
    expect(m && m.groups && m.groups.m).toBe('12');
    expect(m && m.groups && m.groups.d).toBe('31');
  });

  test('反向引用与去重字符', () => {
    expect(/(.)\1/.test('book')).toBe(true); // oo
    expect(/(.)\1/.test('abc')).toBe(false);
  });

  test('replace 使用命名捕获重排', () => {
    const s = 'Doe, John'.replace(/(?<last>\w+), (?<first>\w+)/, '$<first> $<last>');
    expect(s).toBe('John Doe');
  });
});

Open browser consoleTests

边界/多行/单词边界如何工作?

答案

核心要点

  • ^/$ 在默认模式匹配整个字符串的首尾;m 模式作用于每行
  • \b 用于单词边界匹配(字母数字与非字母数字之间)
describe('边界与锚点:^ $ \\b,以及多行 m', () => {
  test('^ 和 $ 匹配整个字符串的开头与结尾', () => {
    expect(/^hello$/.test('hello')).toBe(true);
    expect(/^hello$/.test('hello\n')).toBe(false);
  });

  test('m 使 ^/$ 匹配每一行', () => {
    const s = 'foo\nbar';
    expect(/^bar$/m.test(s)).toBe(true);
  });

  test('\\b 单词边界', () => {
    expect(/\bcat\b/.test('black cat ')).toBe(true);
    expect(/\bcat\b/.test('concatenate')).toBe(false);
  });
});

Open browser consoleTests

前瞻/后顾断言的应用场景?

答案

核心要点

  • 不消费字符的条件匹配:(?=…)/(?!…)(?<=…)/(?<!…)
  • 常用于前后缀定位、金额提取、复杂条件过滤
describe('前瞻 / 后顾(lookaround)', () => {
  test('正向前瞻 (?=...) 与 负向前瞻 (?!...)', () => {
    const s = 'file.txt other';
    const m = /\w+(?=\.)/.exec(s);
    expect(m && m[0]).toBe('file');
    expect(/^\d+(?!\.)/.test('123a')).toBe(true);
    expect(/^\d+(?!\.)/.test('123.45')).toBe(false);
  });

  test('正向后顾 (?<=...) 与 负向后顾 (?<!...)', () => {
    const price = 'Pay $100 now';
    const m1 = /(?<=\$)\d+/.exec(price);
    expect(m1 && m1[0]).toBe('100');

    expect(/(?<!\$)\d+/.test('Pay 100')).toBe(true);
    expect(/(?<!\$)\d+/.test('Pay $100')).toBe(false);
  });
});

Open browser consoleTests

数字千分位

答案

核心要点

  • 使用零宽断言模式:\B(?=(\d{3})+(?!\d)) 对整数部分分组
  • 注意负号与小数部分保留
function thousandSeparator (input) {
  const s = String(input)
  const negative = s.startsWith('-')
  const core = negative ? s.slice(1) : s
  const [intPart, fracPart] = core.split('.')
  const intFormatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
  return (negative ? '-' : '') + intFormatted + (fracPart !== undefined ? '.' + fracPart : '')
}

module.exports = thousandSeparator

Open browser consoleTests

url 查询字段解码

答案

核心要点

  • 解析 ? 之后的查询串,支持重复 key → 数组
  • + 视为空格,配合 decodeURIComponent 解码
function decodeQuery (url) {
  const queryIndex = url.indexOf('?')
  if (queryIndex === -1) return {}
  const query = url.slice(queryIndex + 1)
  if (!query) return {}
  const result = {}
  // 支持 key=value&key2=value2,支持重复 key → 数组
  query.split('&').forEach(pair => {
    if (!pair) return
    const [k, v = ''] = pair.split('=')
    const key = decodeURIComponent(k.replace(/\+/g, '%20'))
    const value = decodeURIComponent(v.replace(/\+/g, '%20'))
    if (Object.prototype.hasOwnProperty.call(result, key)) {
      const exist = result[key]
      result[key] = Array.isArray(exist) ? [...exist, value] : [exist, value]
    } else {
      result[key] = value
    }
  })
  return result
}

module.exports = decodeQuery

Open browser consoleTests