JavaScript 的 String.prototype.replace() 和 replaceAll() 是最常用的文本处理方法。结合正则表达式,它们可以实现强大的模式匹配替换:捕获组、命名组、回调函数等。本指南涵盖从基础到 15 个可直接复制使用的生产级示例。
1. replace() 与 replaceAll() — 核心区别
根本区别:当传入字符串参数时,replace() 只替换第一个匹配项,而 replaceAll() 替换所有匹配项。当使用带 /g 标志的正则时,两者行为完全一致。
// replace() — only replaces the FIRST match with a string argument
const str = 'foo bar foo baz foo';
console.log(str.replace('foo', 'qux'));
// Output: "qux bar foo baz foo"
// replaceAll() — replaces EVERY match (ES2021)
console.log(str.replaceAll('foo', 'qux'));
// Output: "qux bar qux baz qux"
// replace() with regex /g — also replaces every match
console.log(str.replace(/foo/g, 'qux'));
// Output: "qux bar qux baz qux"// Quick comparison table:
// ┌──────────────────────────────┬────────────────────┬──────────────────┐
// │ Method │ String arg │ Regex /g arg │
// ├──────────────────────────────┼────────────────────┼──────────────────┤
// │ str.replace(search, rep) │ First match only │ All matches │
// │ str.replaceAll(search, rep) │ All matches │ All matches │
// └──────────────────────────────┴────────────────────┴──────────────────┘2. 基本正则替换:/g 标志
要用 replace() 替换所有匹配项,需传入带全局标志(g)的正则表达式。这是自 ES3 以来的标准做法,在所有 JavaScript 环境中都可用。
// Replace all digits with '#'
const masked = 'Order 12345, ID 67890'.replace(/\d/g, '#');
console.log(masked);
// Output: "Order #####, ID #####"
// Replace all vowels (case-insensitive with 'i' flag)
const noVowels = 'Hello World'.replace(/[aeiou]/gi, '_');
console.log(noVowels);
// Output: "H_ll_ W_rld"
// Replace all whitespace sequences with a single space
const cleaned = 'too many spaces here'.replace(/\s+/g, ' ');
console.log(cleaned);
// Output: "too many spaces here"
// Remove all non-alphanumeric characters
const alphaOnly = 'Hello, World! @2024'.replace(/[^a-zA-Z0-9]/g, '');
console.log(alphaOnly);
// Output: "HelloWorld2024"
// Case-insensitive replacement
const result = 'Hello HELLO hello'.replace(/hello/gi, 'hi');
console.log(result);
// Output: "hi hi hi"3. 捕获组:替换字符串中的 $1, $2
捕获组 (...) 允许你在替换字符串中使用 $1、$2 等来引用匹配的子表达式。完整匹配可通过 $& 获取。
// Swap first and last name using $1 and $2
const name = 'John Smith';
const swapped = name.replace(/(\w+) (\w+)/, '$2, $1');
console.log(swapped);
// Output: "Smith, John"
// Reformat a date: MM/DD/YYYY → YYYY-MM-DD
const date = '12/25/2024';
const isoDate = date.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2');
console.log(isoDate);
// Output: "2024-12-25"
// Wrap each word in <strong> tags using $&
const bold = 'hello world'.replace(/\w+/g, '<strong>$&</strong>');
console.log(bold);
// Output: "<strong>hello</strong> <strong>world</strong>"
// Use $` (before match) and $' (after match)
const demo = 'abc-def-ghi'.replace(/-def-/, '[$`|$&|$\']');
console.log(demo);
// Output: "abc[abc|-def-|ghi]ghi"
// Multiple capture groups: restructure a log entry
const log = '[ERROR] 2024-01-15 Connection timeout';
const parsed = log.replace(
/\[(\w+)\] (\d{4}-\d{2}-\d{2}) (.+)/,
'Date: $2 | Level: $1 | Message: $3'
);
console.log(parsed);
// Output: "Date: 2024-01-15 | Level: ERROR | Message: Connection timeout"4. 命名捕获组:(?<name>...)
命名捕获组(ES2018)通过为每个组分配名称来提高可读性。在替换字符串中使用 $<name> 来引用它们。
// Named capture groups with $<name> syntax (ES2018+)
const dateStr = '2024-12-25';
const formatted = dateStr.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
);
console.log(formatted);
// Output: "12/25/2024"
// Named groups in a callback function
const url = 'https://example.com:8080/path';
const parts = url.replace(
/(?<protocol>https?):\/\/(?<host>[^:]+):(?<port>\d+)/,
(match, _p1, _p2, _p3, _offset, _str, groups) => {
return `Protocol=${groups.protocol} Host=${groups.host} Port=${groups.port}`;
}
);
console.log(parts);
// Output: "Protocol=https Host=example.com Port=8080/path"
// Combining named and numbered groups
const input = 'John Doe, age 30';
const output = input.replace(
/(?<first>\w+) (?<last>\w+), age (?<age>\d+)/,
'$<last>, $<first> (age: $<age>)'
);
console.log(output);
// Output: "Doe, John (age: 30)"5. 回调函数:replace(regex, fn)
对于动态替换,将函数作为第二个参数传入。回调函数接收完整匹配、每个捕获组、偏移量和原始字符串。
// Callback signature:
// replace(regex, (match, p1, p2, ..., offset, string, groups) => newString)
// Uppercase every word
const title = 'the quick brown fox'.replace(
/\b\w/g,
(char) => char.toUpperCase()
);
console.log(title);
// Output: "The Quick Brown Fox"
// Double every number found in a string
const doubled = 'I have 3 cats and 12 fish'.replace(
/\d+/g,
(match) => String(Number(match) * 2)
);
console.log(doubled);
// Output: "I have 6 cats and 24 fish"
// Convert hex color to rgb
const hexToRgb = '#ff8800'.replace(
/#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i,
(_match, r, g, b) => {
return `rgb(${parseInt(r, 16)}, ${parseInt(g, 16)}, ${parseInt(b, 16)})`;
}
);
console.log(hexToRgb);
// Output: "rgb(255, 136, 0)"
// Use offset parameter: show match positions
const positions: string[] = [];
'abcabc'.replace(/b/g, (match, offset) => {
positions.push(`"${match}" at index ${offset}`);
return match;
});
console.log(positions);
// Output: ['"b" at index 1', '"b" at index 4']
// Conditional replacement based on capture groups
const env = 'DB_HOST=localhost DB_PORT=5432 DB_PASS=secret123';
const redacted = env.replace(
/(\w+)=(\S+)/g,
(_match, key, value) => {
return key.includes('PASS') ? `${key}=****` : `${key}=${value}`;
}
);
console.log(redacted);
// Output: "DB_HOST=localhost DB_PORT=5432 DB_PASS=****"6. 15 个实用示例
以下每个示例都是独立可运行的代码片段。复制粘贴到浏览器控制台或 Node.js REPL 中即可立即测试。
示例 1:camelCase 转 kebab-case
function camelToKebab(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
console.log(camelToKebab('backgroundColor')); // "background-color"
console.log(camelToKebab('fontSize')); // "font-size"
console.log(camelToKebab('borderTopWidth')); // "border-top-width"
console.log(camelToKebab('XMLHttpRequest')); // "xmlhttp-request"示例 2:移除 HTML 标签
function stripHtml(html) {
return html.replace(/<[^>]*>/g, '');
}
console.log(stripHtml('<p>Hello <b>world</b></p>'));
// Output: "Hello world"
console.log(stripHtml('<div class="test">Content <br/> here</div>'));
// Output: "Content here"
// Also remove &entities;
function stripHtmlFull(html) {
return html
.replace(/<[^>]*>/g, '')
.replace(/&[a-zA-Z]+;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
console.log(stripHtmlFull('<p>Price: $100&up</p>'));
// Output: "Price: $100 up"示例 3:遮蔽邮箱地址
function maskEmail(email) {
return email.replace(
/^(.)(.*)(@.+)$/,
(_match, first, middle, domain) => {
return first + '*'.repeat(middle.length) + domain;
}
);
}
console.log(maskEmail('alice@example.com')); // "a****@example.com"
console.log(maskEmail('bob@gmail.com')); // "b**@gmail.com"
console.log(maskEmail('charlie@company.io')); // "c******@company.io"示例 4:格式化电话号码
function formatPhone(phone) {
// Strip all non-digits first
const digits = phone.replace(/\D/g, '');
// Format as (xxx) xxx-xxxx
return digits.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
console.log(formatPhone('1234567890')); // "(123) 456-7890"
console.log(formatPhone('123-456-7890')); // "(123) 456-7890"
console.log(formatPhone('(123) 456 7890')); // "(123) 456-7890"
console.log(formatPhone('+1-234-567-8901')); // "(123) 456-7890" (first digit stripped)
// International format variant
function formatPhoneIntl(phone) {
const digits = phone.replace(/\D/g, '');
return digits.replace(/(\d{1})(\d{3})(\d{3})(\d{4})/, '+$1 ($2) $3-$4');
}
console.log(formatPhoneIntl('12345678901')); // "+1 (234) 567-8901"示例 5:修剪多余空白
function trimWhitespace(str) {
return str
.replace(/^\s+|\s+$/g, '') // trim leading & trailing
.replace(/\s+/g, ' '); // collapse internal spaces
}
console.log(trimWhitespace(' hello world '));
// Output: "hello world"
console.log(trimWhitespace('\t line1 \n line2 \t'));
// Output: "line1 line2"
// Remove blank lines from multiline text
function removeBlankLines(text) {
return text.replace(/^\s*\n/gm, '');
}
console.log(removeBlankLines('line1\n\n\nline2\n\nline3'));
// Output: "line1\nline2\nline3"示例 6:生成 URL Slug
function slugify(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // remove special chars
.replace(/\s+/g, '-') // spaces to hyphens
.replace(/-+/g, '-') // collapse multiple hyphens
.replace(/^-|-$/g, ''); // trim leading/trailing hyphens
}
console.log(slugify('Hello World!'));
// Output: "hello-world"
console.log(slugify(' JavaScript String Replace --- with Regex! '));
// Output: "javascript-string-replace-with-regex"
console.log(slugify('10 Tips & Tricks for React.js'));
// Output: "10-tips--tricks-for-reactjs"示例 7:每个单词首字母大写(Title Case)
function titleCase(str) {
return str
.toLowerCase()
.replace(/\b\w/g, (char) => char.toUpperCase());
}
console.log(titleCase('hello world')); // "Hello World"
console.log(titleCase('JAVASCRIPT IS FUN')); // "Javascript Is Fun"
console.log(titleCase('the quick brown fox')); // "The Quick Brown Fox"
// Smarter version: skip small words (a, an, the, in, on, etc.)
function smartTitleCase(str) {
const small = new Set(['a', 'an', 'the', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'but', 'or']);
return str
.toLowerCase()
.replace(/\b\w+/g, (word, index) => {
if (index > 0 && small.has(word)) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
});
}
console.log(smartTitleCase('the lord of the rings'));
// Output: "The Lord of the Rings"示例 8:交换两个单词
function swapWords(str, word1, word2) {
const regex = new RegExp(`\\b(${word1}|${word2})\\b`, 'gi');
return str.replace(regex, (match) => {
return match.toLowerCase() === word1.toLowerCase() ? word2 : word1;
});
}
console.log(swapWords('Hello World', 'Hello', 'World'));
// Output: "World Hello"
console.log(swapWords('left is right and right is left', 'left', 'right'));
// Output: "right is left and left is right"
// Swap using capture groups only (no function needed)
const swapped = 'foo bar'.replace(/(\w+) (\w+)/, '$2 $1');
console.log(swapped);
// Output: "bar foo"示例 9:移除连续重复单词
function removeDuplicateWords(str) {
return str.replace(/\b(\w+)\s+\1\b/gi, '$1');
}
console.log(removeDuplicateWords('the the quick brown fox'));
// Output: "the quick brown fox"
console.log(removeDuplicateWords('hello hello world world'));
// Output: "hello world"
console.log(removeDuplicateWords('I I am am very very happy'));
// Output: "I am very happy"
// Remove all consecutive duplicates (even more than 2)
function removeAllDuplicateWords(str) {
return str.replace(/\b(\w+)(?:\s+\1)+\b/gi, '$1');
}
console.log(removeAllDuplicateWords('no no no no duplicates'));
// Output: "no duplicates"示例 10:数字添加千分位逗号
function addCommas(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
console.log(addCommas(1234567)); // "1,234,567"
console.log(addCommas(1000)); // "1,000"
console.log(addCommas(100)); // "100"
console.log(addCommas(1234567890)); // "1,234,567,890"
// Handle decimals too
function addCommasDecimal(numStr) {
const [integer, decimal] = numStr.split('.');
const formatted = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return decimal ? `${formatted}.${decimal}` : formatted;
}
console.log(addCommasDecimal('1234567.89')); // "1,234,567.89"
console.log(addCommasDecimal('1000000.5')); // "1,000,000.5"示例 11:从 URL 提取域名
function extractDomain(url) {
return url.replace(/^(?:https?:\/\/)?(?:www\.)?([^\/:]+).*$/, '$1');
}
console.log(extractDomain('https://www.example.com/path'));
// Output: "example.com"
console.log(extractDomain('http://blog.example.com:8080/post'));
// Output: "blog.example.com"
console.log(extractDomain('https://sub.domain.co.uk/page?q=1'));
// Output: "sub.domain.co.uk"
// Extract just the root domain (without subdomains)
function extractRootDomain(url) {
const domain = url.replace(/^(?:https?:\/\/)?(?:www\.)?([^\/:]+).*$/, '$1');
const parts = domain.split('.');
return parts.slice(-2).join('.');
}
console.log(extractRootDomain('https://blog.shop.example.com'));
// Output: "example.com"示例 12:只替换第 N 次出现
function replaceNth(str, pattern, replacement, n) {
let count = 0;
return str.replace(new RegExp(pattern, 'g'), (match) => {
count++;
return count === n ? replacement : match;
});
}
console.log(replaceNth('a-b-c-d-e', '-', ' | ', 3));
// Output: "a-b-c | d-e"
console.log(replaceNth('foo bar foo baz foo', 'foo', 'QUX', 2));
// Output: "foo bar QUX baz foo"
// Replace every Nth occurrence
function replaceEveryNth(str, pattern, replacement, n) {
let count = 0;
return str.replace(new RegExp(pattern, 'g'), (match) => {
count++;
return count % n === 0 ? replacement : match;
});
}
console.log(replaceEveryNth('a,b,c,d,e,f', ',', ' | ', 2));
// Output: "a,b | c,d | e,f"示例 13:转换模板变量
function interpolate(template, data) {
return template.replace(
/\{\{\s*(\w+)\s*\}\}/g,
(_match, key) => data[key] ?? `{{${key}}}`
);
}
const tpl = 'Hello {{ name }}, welcome to {{ city }}!';
const data = { name: 'Alice', city: 'Tokyo' };
console.log(interpolate(tpl, data));
// Output: "Hello Alice, welcome to Tokyo!"
// With missing key fallback
console.log(interpolate('Hi {{name}}, your role is {{role}}', { name: 'Bob' }));
// Output: "Hi Bob, your role is {{role}}"
// Dollar-sign template syntax
function interpolateDollar(template, data) {
return template.replace(
/\$\{(\w+)\}/g,
(_match, key) => data[key] ?? ''
);
}
console.log(interpolateDollar('User: ${user}, ID: ${id}', { user: 'admin', id: '42' }));
// Output: "User: admin, ID: 42"示例 14:转义 HTML 实体
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => map[char]);
}
console.log(escapeHtml('<script>alert("XSS")</script>'));
// Output: "<script>alert("XSS")</script>"
console.log(escapeHtml('Price: $5 < $10 & "free" > 'nothing''));
// Output: "Price: $5 < $10 & "free" > 'nothing'"
// Reverse: unescape HTML entities
function unescapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
''': "'",
};
return str.replace(/&(?:amp|lt|gt|quot|#039);/g, (entity) => map[entity]);
}
console.log(unescapeHtml('<p>Hello</p>'));
// Output: "<p>Hello</p>"示例 15:用星号屏蔽敏感词
function censorWords(text, wordList) {
const pattern = new RegExp(
'\\b(' + wordList.join('|') + ')\\b',
'gi'
);
return text.replace(pattern, (match) => '*'.repeat(match.length));
}
console.log(censorWords('This is a damn bad example', ['damn', 'bad']));
// Output: "This is a **** *** example"
console.log(censorWords('Foo and Bar walked into a baz', ['foo', 'bar', 'baz']));
// Output: "*** and *** walked into a ***"
// Partial censoring: keep first and last letter
function partialCensor(text, wordList) {
const pattern = new RegExp('\\b(' + wordList.join('|') + ')\\b', 'gi');
return text.replace(pattern, (match) => {
if (match.length <= 2) return '*'.repeat(match.length);
return match[0] + '*'.repeat(match.length - 2) + match[match.length - 1];
});
}
console.log(partialCensor('That was a damn bad move', ['damn', 'bad']));
// Output: "That was a d**n b*d move"7. replaceAll 的字符串与正则用法
replaceAll() 同时接受字符串和正则表达式。使用正则时,g 标志是必需的,否则会抛出 TypeError。使用纯字符串时不需要特殊语法。
// ✅ replaceAll with a string — simple and effective
const str1 = 'foo.bar.baz';
console.log(str1.replaceAll('.', '/'));
// Output: "foo/bar/baz"
// ✅ replaceAll with regex + /g flag — works fine
const str2 = 'foo123bar456baz';
console.log(str2.replaceAll(/\d+/g, '#'));
// Output: "foo#bar#baz"
// ❌ replaceAll with regex WITHOUT /g — throws TypeError!
try {
'hello'.replaceAll(/hello/, 'world');
} catch (e) {
console.log(e.message);
// "String.prototype.replaceAll called with a non-global RegExp argument"
}
// String replaceAll does NOT interpret regex special chars
const str3 = 'price is $100.00 (USD)';
console.log(str3.replaceAll('$100.00', '€85.50'));
// Output: "price is €85.50 (USD)"
// No need to escape $ or . — they are treated as literal characters
// But replace() with string also only matches literally
const str4 = 'a.b.c';
console.log(str4.replace('.', '-')); // "a-b.c" (first only)
console.log(str4.replaceAll('.', '-')); // "a-b-c" (all)8. 性能优化建议
正则替换在大多数场景下性能良好,但在性能敏感的代码中有一些需要避免的陷阱。
- 在循环外预编译正则:在循环内创建
new RegExp()是浪费的。声明一次并复用。 - 避免灾难性回溯:像
(a+)+b这样的模式在非匹配输入上可能导致指数级执行时间。使用原子式模式或更精确的字符类。 - 能用字符串方法就用字符串方法:如果是替换字面量字符串(无模式),
replaceAll(string, string)比正则更快。 - 优先使用非捕获组:不需要反向引用时使用
(?:...)而非(...),可减少内存开销。 - 用真实数据做基准测试:使用
performance.now()来测量实际输入规模下的替换耗时。
// ❌ Bad: regex created inside loop
function processItemsBad(items) {
return items.map(item => item.replace(new RegExp('\\s+', 'g'), '-'));
}
// ✅ Good: regex pre-compiled outside loop
const WHITESPACE_RE = /\s+/g;
function processItemsGood(items) {
return items.map(item => item.replace(WHITESPACE_RE, '-'));
}
// ⚠️ Catastrophic backtracking example
// This pattern can hang the browser on non-matching input:
// const badRegex = /^(a+)+b$/;
// badRegex.test('aaaaaaaaaaaaaaaaaaaaaaaaaac'); // extremely slow!
// ✅ Safe equivalent:
const goodRegex = /^a+b$/;
// Benchmark template
function benchmark(fn, iterations = 100000) {
const start = performance.now();
for (let i = 0; i < iterations; i++) fn();
const end = performance.now();
console.log(`${iterations} iterations: ${(end - start).toFixed(2)}ms`);
}
// Compare string vs regex performance
const testStr = 'hello world hello world hello world';
benchmark(() => testStr.replaceAll('hello', 'hi')); // string method
benchmark(() => testStr.replace(/hello/g, 'hi')); // regex method9. TypeScript 中 replace 的类型
TypeScript 完全支持 replace() 和 replaceAll() 并提供正确的类型推断。以下是类型安全替换的关键类型签名和模式。
// TypeScript signatures for replace and replaceAll:
//
// interface String {
// replace(searchValue: string | RegExp, replaceValue: string): string;
// replace(searchValue: string | RegExp,
// replacer: (substring: string, ...args: any[]) => string): string;
// replaceAll(searchValue: string | RegExp, replaceValue: string): string;
// replaceAll(searchValue: string | RegExp,
// replacer: (substring: string, ...args: any[]) => string): string;
// }
// Type-safe replacement function
function safeReplace(
input: string,
pattern: string | RegExp,
replacement: string
): string {
return input.replace(pattern, replacement);
}
// Type-safe callback replacement
function mapReplace(
input: string,
pattern: RegExp,
fn: (match: string, ...groups: string[]) => string
): string {
return input.replace(pattern, fn);
}
// Usage examples
const result1: string = safeReplace('hello world', /world/g, 'TypeScript');
console.log(result1); // "hello TypeScript"
const result2: string = mapReplace('3 + 5 = 8', /\d+/g, (match) => {
return String(Number(match) * 10);
});
console.log(result2); // "30 + 50 = 80"
// Generic template literal type (TypeScript 4.1+)
type Replace<
S extends string,
From extends string,
To extends string
> = S extends `${infer Before}${From}${infer After}`
? `${Before}${To}${After}`
: S;
// Compile-time string replacement:
type Result = Replace<'Hello World', 'World', 'TS'>;
// type Result = "Hello TS"10. 常见问题
JavaScript 中 replace() 和 replaceAll() 有什么区别?
当传入字符串参数时,replace() 只替换第一个匹配项,replaceAll() 替换所有匹配项。当使用带 /g 标志的正则时,两者结果相同。replaceAll() 在 ES2021 引入,使用正则参数时必须带 /g 标志。
如何在 JavaScript replace 中使用捕获组?
将正则的一部分用圆括号包裹来创建捕获组:/(\w+)@(\w+)/。在替换字符串中,$1 表示第一个组,$2 表示第二个组,以此类推。完整匹配可用 $& 获取。在回调函数中,捕获组作为第二个、第三个等参数传入。
可以在 replace() 中使用函数作为替换值吗?
可以。将函数作为第二个参数传入:str.replace(/pattern/g, (match, p1, p2, offset, string) => { ... })。每次匹配都会调用该函数,其返回值用作替换内容。这是 replace() 最强大的形式,对于动态转换至关重要。
为什么 replaceAll() 在使用正则时会抛出 TypeError?
当传入正则参数时,replaceAll() 要求必须带 /g(全局)标志。如果传入不带 /g 的正则,JavaScript 会抛出 TypeError:"String.prototype.replaceAll called with a non-global RegExp argument"。这是为了防止意外的单次替换行为而设计的。
如何在 JavaScript 中不使用正则进行字符串替换?
使用 replaceAll(searchString, newString) 替换所有字面量字符串匹配项,或在旧环境中使用 split(search).join(replacement) 作为兼容方案。单次替换用 replace(searchString, newString)。这些方法不会解析特殊正则字符,因此对用户输入更安全。
收藏本指南,方便下次字符串处理时查阅。要进行交互式正则测试,请试试下面的工具。