☰
JavaScript字符串拼接5种方法详解:原理、性能与坑点
2026/9/29 1:22:38 网站建设 项目流程

字符串拼接这个话题,说基础它是真的基础,每个写 JavaScript 的人都用过;但说简单它又不简单——同一个功能,不同人写出来的代码差距可以非常悬殊。我在 code review 里见过直接把几十个变量用加号连成一条长龙的写法,也见过老代码里为了拼一行 SQL 反复新建临时变量的典型案例,每次都忍不住想把这块讲清楚:JS 里到底有哪几种字符串拼接方法,各自适合什么场景,踩过哪些坑。这篇文章就围绕 JS 字符串拼接这件事,把我这些年一直在用的 5 种方式一次性说透,包括+运算符、模板字符串、concat()、join(),以及封装好的占位符替换,把原理、性能、坑点都放到一起讲。不管是刚入门的新手,还是写了两三年 JS 想优化代码风格的同学,这 5 种方法都值得重新过一遍。

1. 最直觉的加号运算:+和+=

1.1 加号在 JS 中到底怎么工作

在 JavaScript 里,+是个比较特殊的运算符,它同时承担了“数字加法”和“字符串连接”两种职责。具体走哪条路,取决于两边的操作数类型:如果两边都是数字,就走加法逻辑;如果其中任意一边是字符串,JS 引擎就会尝试把另一边也转成字符串,然后进行拼接。

console.log(1 + 2); // 3 console.log("1" + 2); // "12" console.log(1 + "2"); // "12" console.log("" + 1); // "1"

这个行为可以用一句话概括:只要有一边是字符串,结果就是字符串。很直觉对吧,但这也是很多隐性 bug 的源头。比如表单里拿到的值经常是字符串,用户输入18,你直接用inputValue + 1,结果不是19而是"181"。这种问题在实际开发中出现的频率高得惊人,每次排查到最后都发现是类型转换在捣乱。

还有一种常见写法是+=,本质上是str = str + xxx的简写形式。

let title = "前端"; title += "入门"; title += "指南"; console.log(title); // "前端入门指南"

1.2 加号拼接最常见的几个坑

加号虽然简单,但有几个点需要格外留意。

第一个坑是运算顺序。+和+之间是左结合,也就是说从左往右依次算。如果你在一个表达式中混用了字符串拼接和数字加法,结果可能会出乎意料:

console.log(1 + 2 + "3"); // "33" 先算 1+2=3,再拼 "3" console.log(1 + "2" + 3); // "123" 先拼 "12",再拼 3

第二个坑是真值和假值的处理。用加号拼接null、undefined、NaN这些特殊值时,它们会先被转成对应的字符串文本,比如"null"、"undefined"、"NaN",这在日志输出和接口对接时很容易造成误解:

const user = null; console.log("当前用户是:" + user); // "当前用户是:null"

第三个坑是循环里拼大字符串。老式写法喜欢这样:

let result = ""; for (let i = 0; i < 10000; i++) { result += "item" + i + ","; }

字符串在 JS 中是不可变的,+=每次都会创建一个全新的字符串对象。循环 10000 次,就会产生 10000 个中间字符串,虽然现代引擎做了不少优化,但大循环里依然可能造成明显的性能损耗和 GC 压力。这个问题的替代方案后面会用join()来解。

1.3 什么时候该用加号

加号适合的场景非常明确:临时拼一两个字符串,代码量少、逻辑直白,比如拼个提示语、拼个简单的 URL 参数。它不需要额外学习成本,任何人一眼就能看懂。代码里用加号本身不丢人,过度封装反而会让简单的事情变复杂。只是不要让它出现在高频循环里,也不要让它承担太复杂的格式化逻辑。

2. 现代开发的主力:模板字符串

2.1 模板字符串为什么比加号好用

ES6 引入的模板字符串,用一对反引号(`)包裹内容,通过${}嵌入变量或表达式,直接解决了加号拼接在“可读性”和“多行文本”上的不足。

const name = "张三"; const age = 25; const message = `我叫${name},今年${age}岁`; console.log(message);

它最大的优势是可以直接放任意 JavaScript 表达式,而不只是变量:

const price = 99.5; const count = 3; console.log(`商品总价:${price * count} 元`); // "商品总价:298.5 元" console.log(`满减后:${price * count >= 300 ? "减50" : "不减"}`); // 可以直接写三元

模板字符串还支持多行文本,这对拼接 HTML 片段、SQL、纯文本邮件特别友好。以前用加号换行要手动加\n和一堆加号,现在反引号一包,换行就是换行:

const html = ` <div class="card"> <h3>${title}</h3> <p>${description}</p> </div> `;

这在渲染前端模板、拼接表格行时非常实用,代码结构清晰,不会再出现一行加号后面跟着\续行符的“蜈蚣代码”。

2.2 模板字符串的高级玩法:标签模板

模板字符串还有一个进阶用法叫标签模板(tagged template),本质上是一个函数调用。你可以在反引号前面放一个函数名,这个函数会接收到字符串片段和所有插值表达式的结果,然后你可以在函数内部自定义如何处理它们。

function highlight(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + (values[i] ? `<mark>${values[i]}</mark>` : ""); }, ""); } const name = "李四"; const result = highlight`用户:${name} 已登录`; console.log(result); // "用户:<mark>李四</mark> 已登录"

这个能力在实现富文本高亮、多语言模板、日志脱敏时非常有用。不过对大多数业务代码来说,直接用普通模板字符串就够了,标签模板属于锦上添花的技巧。

2.3 模板字符串的防坑要点

模板字符串确实好用,但有几个细节容易翻车。

第一是${}内部如果还包含反引号,需要转义,或者换一种写法:

// 错误,会提前结束模板字符串 // const bad = `这是一段 `code` 示例`; // 正确 const good = `这是一段 \`code\` 示例`;

第二是模板字符串里的缩进和空格会被原样保留。如果你想让多行模板的输出不带着乱七八糟的缩进,可以配合trim()或replace处理。比如上面 HTML 的例子,直接输出会发现每行行首都带着代码里的缩进空格,很多人刚用时不习惯。

第三是兼容性。模板字符串是 ES6 语法,在旧版浏览器或不支持转译的环境里会直接报语法错误。现在的工程环境基本都有 Babel 处理,问题不大,但如果写纯脚本给老环境跑,还是要确认一下。

3. 存在感不高但能打的:concat()方法

3.1concat()的用法与冷知识

String.prototype.concat()是字符串原型上自带的方法,功能是把一个或多个字符串拼接到原字符串后面,返回新字符串。

const a = "Hello"; const b = a.concat(" ", "World", "!"); console.log(b); // "Hello World!" console.log(a); // "Hello" 原字符串不变

这个方法接受任意多个参数,会按顺序拼接。和+一样,如果参数不是字符串,会先转为字符串再拼:

const txt = "数量:".concat(12, " 个"); console.log(txt); // "数量:12 个"

这里有一个冷知识:concat()遇到null和undefined时,会把它转成字符串"null"和"undefined",而不是给出空字符串或者报错。所以如果你不小心把一个可能为空的变量传进去了,输出里会出现莫名其妙的null字样。

3.2 为什么concat()平时用得少

说实话,我在实际项目中很少看到有人用concat()拼字符串,原因主要是三个。

第一,+运算符的可读性更直观。a + b比a.concat(b)写起来更短,读起来也更像“拼接”这个动作。第二,ESLint 的prefer-template规则默认会建议用模板字符串替代字符串拼接,concat()既不推荐也不禁止,属于“可以用但没必要”的中间地带。第三,性能上concat()并没有碾压+,在多数引擎里两者差距微乎其微。

不过它有一个独特的使用场景:当你需要延迟决定拼接内容时,concat()比+更容易做链式调用。比如在构建 HTML 类名时:

function buildClassName(base, ...modifiers) { return base .concat(" ") .concat(modifiers.filter(Boolean).join(" ")); }

这种写法虽然少见,但代码意图非常明确,就是“追加”。如果在团队里约定好用它处理“不断追加”的语义,也能避免+带来的隐式转换问题。

4. 数组思路的拼接:join()方法

4.1join()的正确打开方式

Array.prototype.join()严格来说是数组的方法,不是字符串的方法。它会把数组里的所有元素转为字符串,然后用你指定的分隔符连接起来,最终返回一个字符串。这个思路和前面几种完全不同:先收集片段,再一次性拼接。

const arr = ["2026", "01", "01"]; console.log(arr.join("-")); // "2026-01-01" const list = ["苹果", "香蕉", "橙子"]; console.log(list.join("、")); // "苹果、香蕉、橙子"

4.2 用join()解决真实项目里的拼接痛点

join()最典型的场景是把动态的数据列表转成字符串。比如后端返回了一个标签数组,前端要渲染成带分隔符的字符串:

const tags = ["JavaScript", "TypeScript", "Node.js"]; const tagText = tags.map(tag => `#${tag}`).join(" "); console.log(tagText); // "#JavaScript #TypeScript #Node.js"

再比如拼 CSS 类名、拼 URL 参数、拼 CSV 行,都是它的主场。

还有一个非常实用的场景,就是解决前面提到的“循环里+=拼接性能差”的问题。正确的姿势是把字符串片段丢进数组,循环结束后一次性join()出来:

const items = []; for (let i = 0; i < 10000; i++) { items.push(`item${i}`); } const result = items.join(",");

这样做的好处是,循环过程中只有数组在增长,不会反复创建大字符串;最终只做一次拼接操作。从内存角度来说,比+=稳得多。

4.3join()的边界行为与注意事项

join()有几个容易被忽略的边界行为。

如果没有传分隔符,默认使用逗号,,很多人会误以为默认是无分隔拼接。比如["a", "b"].join()返回的是"a,b"而不是"ab"。如果要无分隔拼接,需要显式传空字符串:["a", "b"].join("")。

空数组join()后返回空字符串;只有一个元素的数组不会加分隔符。这两点在做判断时用起来很顺手,比如判断arr.join("").length === 0,就不用再单独判断数组长度。

还有一点,如果数组元素中包含null或undefined,join()会把它们当作空字符串处理,而不会变成"null"或"undefined"。这和String()转换的行为不一样,很多人在数据清洗时并不知道这个差异。

5. 隐藏的第五种:占位符替换与模板格式化

5.1 一个通用的format函数

前面四种是语言本身提供的能力,第五种更像是一种模式:自己封装一个字符串格式化函数,通过占位符替换实现拼接。这种做法在复杂模板场景下非常有用,也让字符串拼接从“无脑连接”升级为“结构化渲染”。

一个经典实现是{name}风格占位符:

function format(template, data) { return template.replace(/\{(\w+)\}/g, (match, key) => { return data[key] !== undefined ? data[key] : match; }); } const text = format("您好,{name},您本月的账单金额是 {amount} 元。", { name: "王五", amount: 1299 }); console.log(text); // "您好,王五,您本月的账单金额是 1299 元。"

如果你更喜欢类 C 语言的%s风格,也可以封装成这样:

function sprintf(str, ...args) { return str.replace(/%s/g, () => args.shift()); } console.log(sprintf("%s 的博客发布了新文章:%s", "前端工程师", "JS 字符串拼接指南"));

5.2 为什么需要自己封装格式化函数

你可能会问:模板字符串都那么强了,为什么还要自己写格式化函数?这个问题问得好。

模板字符串适合“写代码时就确定好拼接结构”的场景,而占位符替换适合“拼接模板在运行时才确定”的场景。典型例子包括:

  • 多语言文案:{name} 已登录在不同语言下占位符位置不同,不能写死在代码里。
  • 日志模板:日志系统统一维护模板,参数动态传进来。
  • 邮件/短信通知:后台配置的通知文案需要动态替换变量。
  • 饼图标题、报表列名等带参数的业务文案。

在这些场景中,模板是动态数据,不是代码里的常量,用模板字符串反而很难处理。封装一个format函数,把模板和参数分离,可维护性会好很多。

5.3 补充一个容易被忽略的工具:String.raw

在字符串拼接相关的工具里,String.raw是个比较冷门但很好用的帮手。它是模板字符串的标签函数,会返回“原始字符串”,也就是反斜杠不会被解释成转义字符。

const path = String.raw`C:\Users\admin\Desktop`; console.log(path); // "C:\Users\admin\Desktop"

如果直接用普通字符串,\U、\a这类写法会被当成转义字符或者直接报错。在 Windows 路径拼接、正则表达式拼接等场景下,String.raw能省去双反斜杠的烦恼。

6. 选型策略与性能实测:哪种方法更快更稳

6.1 五种方法全方位对比

为了帮你快速决策,我把这 5 种方法从“可读性”“性能”“多行支持”“表达式支持”“适用场景”五个维度做了整理:

方法可读性多行支持表达式支持典型场景性能注意点
+/+=中不支持需先算好变量简单拼接、提示语大循环中会产生中间字符串
模板字符串高支持支持任意表达式日常开发首选、HTML 拼接性能优秀,引擎优化成熟
concat()中低不支持参数先求值链式追加、多参数拼接与+相当,无明显优势
join()中不支持需先构造数组数据列表、循环拼接、路径批量拼接效率高
format()封装高支持依赖实现国际化、日志模板、文案渲染多一次正则开销,可忽略

6.2 我亲手跑过的一次简单拼接耗时对比

为了不纸上谈兵,我在 Node.js 环境下对几种方案做了个简单的压力测试:循环 10 万次,拼一段包含用户名和年龄的字符串,结果方向基本符合预期。

const user = { name: "张三", age: 30 }; let str = ""; console.time("plus"); for (let i = 0; i < 100000; i++) { str = "用户:" + user.name + ",年龄:" + user.age; } console.timeEnd("plus");

如果分别换成模板字符串、数组push+join(),多次测试下来会发现两个结论:

第一,模板字符串和+在简单场景下差距很小,V8 引擎早就做过了“字符串连接”优化,不用太纠结谁快谁慢。第二,批量拼接场景(循环里拼多个元素)确实是join()更稳,因为中间字符串更少。

我个人的结论是:性能不是选型的第一依据,可读性和维护性才是。真正需要关注性能的场景,是那些高频执行的字符串处理,比如一秒钟跑几百次的日志格式化、报表生成、大文件解析,这种才值得用push+join()或者更精细的方案。

6.3 我的选型建议

根据不同的业务场景,我一般按下面的思路选:

  • 两个或三个字符串临时拼一下,直接用+,别纠结。
  • 字符串里有变量、有多行、有表达式,首选模板字符串。
  • 把一个数组转成带分隔符的字符串,直接join()。
  • 循环里拼接大量片段,先push进数组,最后join()。
  • 模板内容来自配置或接口,用自己封装的format()函数。
  • 涉及 Windows 路径或正则的原始字符串,用String.raw。

这套规则简单直接,团队成员也好记。

7. 常见问题与排查技巧实录

7.1 高频坑点速查表

字符串拼接的错误通常不是编译期报错,而是运行期结果不对。这里把常见的坑整理成表,方便你在排错时对照。

现象原因解决方案
"1" + 1结果是"11"字符串优先触发拼接,数字被转字符串先Number()转数字再相加
输出中莫名出现"null"变量为null时被拼接进字符串拼接前用??或 `
模板字符串多行输出带多余空格代码缩进被保留配合trim()或写在一行
["a", "b"].join()返回"a,b"join()默认分隔符是逗号显式传空字符串join("")
反引号模板里想再包反引号反引号需要转义使用 ``` 或嵌套用不同引号
数字保留小数位拼接后位数不对number.toFixed()返回字符串又被拼接把toFixed()结果参与计算后再拼

7.2 字符串包含判断:拼接之外最常用的字符串操作

在拼接字符串的日常工作中,判断一个字符串是否包含某个子串同样高频。很多初学者分不清includes()和indexOf()的取舍,这里顺手整理一下。

const url = "https://example.com/api/users"; console.log(url.includes("users")); // true,推荐 console.log(url.indexOf("users") !== -1); // true,老写法 console.log(url.startsWith("https")); // true console.log(url.endsWith("/users")); // true console.log(url.search(/api\/\w+/)); // 返回索引或 -1 console.log(url.match(/api\/\w+/)); // 返回匹配数组或 null

includes()语义最直白,返回布尔值,适合大多数判断场景。indexOf()适合需要拿索引位置的场景。search()和match()是正则派,适合复杂模式。做字符串包含判断时,不要用正则去匹配固定字符串,杀鸡用牛刀还容易写错转义。

7.3 排查字符串拼接问题的一个实用流程

如果你拼接出来的结果不对,别急着改代码,按下面的顺序查一遍,基本能定位 90% 的问题。

先检查变量类型。用typeof看看参与拼接的变量到底是什么类型,尤其注意接口返回的数据到底是number还是string,最好在拼接前打一条日志。再看是否有null或undefined混入。这类值拼接后会变成字符串"null"或"undefined",非常隐蔽。然后看隐式转换是否符合预期。必要的时候显式调用String()或toString()统一格式,别依赖${}的自动转换。

最后检查分隔符和换行。多行模板里的缩进、join()默认的逗号,都是容易看漏的地方。把这些过一遍,基本能解决绝大多数拼接异常。


我在实际开发中养成了一个习惯:小拼接用+,日常拼装用模板字符串,遇到“把一批数据变成字符串”的需求就想起join(),有模板化诉求就封装独立的格式化函数。最后再分享一个多年踩坑之后沉淀的小技巧:日志系统里如果需要不断追加内容,不要一直str = str + xxx,而是把片段推进数组,最后join()一次。字符串不可变性决定了这种写法在极端场景下能省下不少内存。字符串拼接看着简单,但用对了方法和场景,代码质量真的会肉眼可见地变好,希望这 5 种方法能让你少一点纠结,多一点从容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询