ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript字符串拼接5种方法详解:性能对比与工程选型

JavaScript字符串拼接5种方法详解:性能对比与工程选型 做前端或者 Node 的同学日常工作里恐怕没有一个项目能绕开字符串拼接。拼一条接口参数、拼一段 HTML 模板、拼一个 SQL 的 in 条件、拼一条日志甚至面试题里也总爱问“你有哪几种方式把几个字符串合并成一个”。这个标题看起来基础可我见过不少写了三四年 JavaScript 的人翻来覆去只会一招遇到循环里拼一千个片段时性能崩了也不知道为什么然后反过来怪 JS 慢。这篇就把合并拼接字符串的 5 种方法彻底摊开包括每种写法的背后规则、类型转换的坑、不同数据量级下的性能表现以及我在真实项目里踩过的几个雷。新同学可以按顺序通读老手可以直接跳到性能实测和选型那两节。1. 五种字符串拼接方式逐项拆解1.1 “” 运算符最老牌也最容易想当然应该是绝大多数人学会的第一种拼接方式写法最直白const str1 Hello; const str2 World; const result str1 str2; console.log(result); // Hello World规则只有一条只要两侧任意一侧是字符串JavaScript 就会把另一边也转成字符串然后合并。听起来简单但实际执行时有个从左到右的问题特别容易让人翻车console.log(1 2 3); // 33 console.log(3 1 2); // 312第一个例子最有迷惑性。1 2先被当成数学加法算出了3然后3 3因为遇到了字符串才转为拼接最终结果是33。第二个例子则完全反过来3 1率先进入拼接模式结果是31继续 2也在拼接于是得到312。这里没有任何弯弯绕就是“从左到右、逐段判断”的规则在起作用。如果你想把所有参数都当成字符串来拼最稳妥的办法是先把数字部分用String()或者模板字符串处理掉而不是依赖隐式转换。别小看这行规则很多线上 bug 就是这么来的。1.2运算符循环场景里的高频用法是result result right的语法糖本质和是一套规则但使用场景非常固定循环里面逐条追加内容。let result ; const list [苹果, 香蕉, 橘子]; for (const item of list) { result item 、; } // 去掉末尾多余的分隔符 result result.slice(0, -1); console.log(result); // 苹果、香蕉、橘子这里有一个很常见的小技巧叫“先拼后切”循环体内统一在后面加分隔符循环结束后用slice(0, -1)把最后一个多余的分隔符去掉。比起在循环里写if (i list.length - 1)判断要不要加分隔符这种写法可读性高很多也不容易出错。使用时要注意一个反直觉的点字符串本身是不可变的。str x并不是在原来的字符串尾巴上追加字符而是创建了一个全新的字符串然后把变量指向它。老教程里说这样很慢在早期浏览器里确实如此但现代的 V8 引擎内部对字符串拼接做了不少优化尤其是“短字符串 短字符串”的场景引擎会用一种类似链表的结构暂存拼接片段等到真正需要取值时才合并成完整字符串。所以你在日常数据量下使用并不会有什么问题真正需要担心的场景我在后面性能部分再展开。1.3 concat() 方法被很多人遗忘的正规 APIString.prototype.concat()是 JavaScript 内置的字符串方法可以接收多个参数一次性全部拼进去const str Hello; const result str.concat( , World, !); console.log(result); // Hello World!它的特点是不会修改原字符串而是返回一个拼接后的新字符串。这一点和一样字符串本身不可变任何拼接操作都会产生新值。不过说句实话日常业务代码里我很少看到有人用concat()。主要原因有两个一是写起来更短二是模板字符串和join()在很多场景的可读性更好。但这并不代表concat()没有价值至少有两类场景它比顺手第一当你已经有一个数组想用类似“流式拼接”的方式把多个值串起来的时候concat()接收不定长参数的特性就体现出来了const parts [2026, 03, 25]; const dateStr .concat(...parts, 00:00:00); console.log(dateStr); // 20260325 00:00:00第二面试题偶尔会问和concat()的区别。这里有个容易混淆的点运算符只要有一边是字符串就走拼接而concat()方法一定会把参数转成字符串再拼接两者最终行为其实非常接近最大区别是concat()传数组进来时会把数组转成字符串结果和数组本身的toString()一致。1.4 数组 join() 方法把“拼一个字符串”变成“拼一组字符串”前面几种方法都是一个一个把字符串粘起来而Array.prototype.join()的思路完全不同先把所有片段放进一个数组然后用一个分隔符统一合并。const parts [2026, 03, 25]; console.log(parts.join(-)); // 2026-03-25join()的语法是数组.join(分隔符)如果不传分隔符默认用逗号如果传空字符串那就是无缝拼接。这个方法真正厉害的地方在于它把“拼接”和“动态决定是否加入某个片段”彻底解耦了。比如说你要拼一个查询字符串有的参数可能为空需要在拼接前先过滤掉const params { keyword: 手机, page: 1, size: 20, tag: }; const queryString Object.entries(params) .filter(([, value]) value ! value ! undefined value ! null) .map(([key, value]) ${key}${encodeURIComponent(value)}) .join(); console.log(queryString); // keyword%E6%89%8B%E6%9C%BApage1size20这种用filter map join的写法非常干净如果用来拼你会写出一大堆条件判断代码立刻变得又臭又长。另外join()和split()是一对黄金搭档。把一个字符串按某个分隔符拆开、处理后再拼回去这种“split 转换 join”的模式在处理 CSV、日志、URL path 时非常常见。比如去掉字符串里所有数字里的逗号1,234,567.split(,).join()得到1234567。1.5 模板字符串开发效率最高的现代写法ES6 引入的模板字符串Template Literals是现在我最推荐的拼接方式。它用反引号包裹整个字符串通过${}插入变量或表达式const user { name: 李雷, age: 18 }; const text 姓名${user.name}年龄${user.age}; console.log(text); // 姓名李雷年龄18模板字符串最大的优势不只是省去一堆加号和引号而是它天然支持多行文本。以前拼多行 HTML 需要写一堆\n和加号现在直接原样书写const html div classcard h2${user.name}/h2 p年龄${user.age}/p /div ;这在拼 HTML 模板、表格行、邮件正文等场景下可读性提升是质变级别的。${}里面也不限于变量可以是任意表达式甚至嵌套模板字符串const items [苹果, 香蕉]; const listHtml ul ${items.map(item li${item}/li).join()} /ul ;这里我用了两个小技巧一是.map()返回的数组用.join()无缝合并成字符串避免了输出数组默认的逗号二是内层的反引号模板字符串可以正常使用嵌套模板在动态生成 DOM 时几乎是刚需。性能方面需要说句公道话模板字符串更多是语法层面的便利底层仍然要走字符串拼接逻辑并不存在“模板字符串一定更快”的说法。真正决定快慢的是你拼了多少次、每次内容多长具体见后面性能章节。为了方便对照五种方法的基本特性我整理成了表格方法语法原字符串是否改变适合场景备注a b否简单拼接存在隐式转换陷阱a b否重新赋值循环内逐条追加注意初始值concat()a.concat(b, c)否不定长参数拼接日常用得少join()arr.join(sep)否批量拼接、列表转字符串天然支持数组模板字符串${a}${b}否插值、多行文本现代项目首选2. 拼接背后的类型转换逻辑很多人拼接时踩坑问题不是出在方法本身而是没搞懂 JavaScript 的隐式类型转换规则。这一节专门把这条“暗线”拉出来讲清楚你会少踩很多坑。2.1 “” 到底是加法还是拼接是 JavaScript 里唯一一个身兼数职的运算符它既能做数学加法又能做字符串拼接。具体执行哪一种取决于操作数的类型。规则是这样的如果两侧有任何一侧是字符串那就走拼接如果两侧都是数字或者能转成数字的布尔值等就走数学加法。这条规则本身不难难的是它会在表达式从左到右执行的过程中反复变化console.log(1 2 3); // 33先加后拼 console.log(1 2 3); // 123全程拼接 console.log(1 2 3); // 123先拼再拼理解了这一点再看这类题就不会再晕了。处理混合类型的正确思路是如果你需要的是字符串结果先把所有非字符串内容显式转成字符串如果你需要的是数字结果避免让字符串混进加法表达式。2.2 对象、数组和 null/undefined 参与拼接时发生什么当一个对象参与拼接时JavaScript 会尝试把它转成原始值。转换顺序大致是先调用valueOf()如果返回的不是原始值再调用toString()。大多数普通对象最终会落到toString()所以结果通常是[object Object]。console.log(obj: {}); // obj: [object Object] console.log(arr: [1, 2, 3]); // arr: 1,2,3 console.log(emptyArr: []); // emptyArr: 数组的情况更有意思[1, 2, 3]转字符串得到的是1,2,3空数组得到空字符串。这里有个非常经典的坑console.log([] ); // console.log({} ); // [object Object]至于null和undefined它们转字符串很直白一个变成null一个变成undefined。但问题在于这个结果往往不是你想要的const user { name: 李雷, age: null }; console.log(年龄${user.age}); // 年龄null如果你拼的是用户可见文案把null原样输出去是明显不合适的。正确做法是先做默认值处理const ageText user.age ?? 未知; console.log(年龄${ageText}); // 年龄未知2.3 String() 与 toString() 的差异处理显式转换时很多同学用value.toString()但这个方法不是万能的const value null; console.log(value.toString()); // TypeError: Cannot read properties of nullnull和undefined没有toString()方法一调用就抛错。相比之下String(value)是全局函数它内部会处理原始值转换console.log(String(null)); // null console.log(String(undefined)); // undefined所以我的习惯是在拼接外部数据时只要拿不准类型就用String()保底如果确定是数字或普通对象再用toString()也不迟。另外数字转字符串还有个常用技巧 数字也能完成转换但可读性不如String(数字)直观。3. 性能实测不同数据量级下的真实差距说到拼接方法网上文章最喜欢渲染“哪种最快”。我想直接说结论在你我日常写业务代码的绝大多数场景里这几种方法的性能差距可以忽略不计。真正拉开差距的是拼接次数和单次长度而不是方法本身。但为了让结论有依据我给出可以自己复现的测试思路。3.1 可复现的对比测试测试环境是 Node.js 18你可以直接复制到本地跑。思路很简单准备一个数组分别用、join()、concat()、模板字符串去把数组元素拼成一个大字符串然后用console.time记录耗时。const list Array.from({ length: 100000 }, (_, i) item- i); // 方式一 console.time( ); let result1 ; for (const item of list) { result1 item ,; } console.timeEnd( ); // 方式二join console.time(join); const result2 list.join(,); console.timeEnd(join); // 方式三concat console.time(concat); let result3 ; for (const item of list) { result3 result3.concat(item, ,); } console.timeEnd(concat); // 方式四模板字符串 console.time(template); let result4 ; for (const item of list) { result4 ${result4}${item},; } console.timeEnd(template);我本地跑了一次结果大致是join最快和template次之concat最慢。但说实话差距也就是几十毫秒级别而且不同引擎、不同数据长度下排名还会变。3.2 老说法“数组 join 最快”现在还成立吗这个说法在十几年前的 IE 时代是成立的那时字符串拼接每次都会创建新字符串循环一千次就是一千次内存分配join()先攒后拼的策略优势明显。但现在 V8 等现代引擎引入了对字符串拼接的优化在大多数场景下并不慢。那是不是说join()就没用了不是。它真正的价值不是性能而是语义。当你需要拼的是一组动态片段每个片段还要经过过滤、映射处理时join()配合数组方法比一串要清晰得多。性能只是它顺带的一个好处没必要把它神化。3.3 什么情况下才需要认真关心拼接性能只有两类场景我会认真做性能考量第一类超大数据量的日志或上报内容拼接。比如一次要拼几万条记录、每条还带时间戳和上下文这时候我倾向于用数组收集后统一join()或者直接交给专门的序列化工具处理而不是在循环里不断。第二类高频执行路径里的字符串拼接。比如requestAnimationFrame回调里每帧都要拼一次 UI 文案或者 WebSocket 消息处理里对大量短消息做格式化。这种场景要尽量减少创建大字符串的次数能直接输出就输出能分段就分段。// 不推荐每帧创建新字符串 function renderBad(count) { let text ; for (let i 0; i count; i) { text 第${i}个 ; } return text; } // 推荐先收集再拼接 function renderGood(count) { const parts []; for (let i 0; i count; i) { parts.push(第${i}个); } return parts.join( ); }这里的核心思想是减少大字符串的重复创建而不是纠结用哪个方法。只要这个原则记住了性能一般不会出大问题。4. 真实项目里的选型逻辑与踩坑记录4.1 我的日常选型标准讲完原理和性能说说我平时写代码时实际怎么选。我把规则浓缩成三条单条简单拼接变量不超过两个用就够比如拼个带单位的值price 元。超过两个变量、需要换行、或涉及表达式直接用模板字符串别犹豫。要拼接的内容是一个列表且每个元素需要单独处理用map join()这是最稳的组合。concat()在我日常代码里确实很少出现在业务逻辑中更多是在写工具函数、处理可变参数时出现。它不是不好而是有更好的替代品。不要因为某个方法“存在”就在所有场景硬套代码是给人读的可读性优先。4.2 拼 URL 参数时的经典错误这是我在 Code Review 里见过最多的问题。拿拼 URL 时最典型的两处坑一是 base 地址末尾是否带?或没判断二是参数值没有编码。// 错误示范 const keyword 手机 华为; const url /api/search?keyword keyword page1; // 空格、中文等特殊字符直接出现在 URL 里很容易出问题 // 正确示范 const params new URLSearchParams({ keyword, page: 1 }); const url /api/search?${params.toString()}; console.log(url); // /api/search?keyword%E6%89%8B%E6%9C%BA%E5%8D%8E%E4%B8%BApage1如果不想用URLSearchParams至少要记得对每个参数值调用encodeURIComponent()。凡是拼接外部输入到 URL 里的场景都要先编码不然你会踩到中文乱码、特殊字符截断参数等一系列问题。4.3 拼 SQL 和 HTML 时的安全提醒拼 SQL 是所有后端开发最早学会、也最应该警惕的场景。字符串拼接拼 SQL 看起来方便但只要你把外部参数直接用拼进 SQL就等于给 SQL 注入开了大门// 极其危险的示范 const sql SELECT * FROM users WHERE name ${userInput};正确做法是使用数据库驱动提供的参数化查询或预编译语句让值通过占位符传递而不是拼进 SQL 文本。这条红线不能破不管项目多小、多急都不能把用户输入直接拼进 SQL。拼 HTML 时也有类似问题。用模板字符串拼 HTML 片段如果变量里有用户生成的内容很容易产生 XSS 注入风险。正确做法是把变量经过转义处理后再插入或者使用框架自带的安全插值机制而不是裸奔innerHTML 模板字符串。4.4 浮点数精度在拼接时的隐藏问题这个坑可能很多人没注意到。浮点数运算有精度问题一旦你把计算结果拼成字符串精度问题就会直接暴露出来console.log(0.1 0.2 ); // 0.30000000000000004 console.log(${0.1 0.2}); // 0.30000000000000004 console.log(String(0.1 0.2)); // 0.30000000000000004不管用哪种拼接方式结果都一样难看。如果你要展示金额或百分比务必先做精度处理const total (0.1 * 10 0.2 * 10) / 10; // 先转成整数运算 console.log(${total}元); // 0.3元 // 或者用 toFixed console.log((0.1 0.2).toFixed(2)); // 0.30toFixed()返回的是字符串可以直接用于展示但要注意它返回的是四舍五入后的结果而不是精确的十进制金融场景下还是要用专门的十进制库。4.5 多行模板字符串的缩进问题模板字符串的多行能力是优点但也带来一个容易忽视的副作用代码里的缩进空格会被原样保留在结果字符串中。const html div phello/p /div ; console.log(JSON.stringify(html)); // \n div\n phello/p\n /div\n输出结果里包含了换行和每行开头的空格。这在大部分展示场景下没问题但如果你拿这个字符串去做精确匹配校验或生成文件多出来的空白字符就可能引发 bug。我的习惯是如果对字符串内容要求干净就加一行.trim()const trimmed html.trim();或者把整个模板字符串左对齐书写虽然缩进难看但输出干净。这两种方案里trim()更省事推荐优先使用。4.6 不要忽略 Unicode 编码问题最后补一个偏门但真实存在的坑。String.prototype.length统计的是 UTF-16 码元数量而不是用户感知的“字符数”。当你拼接用户输入或特殊 emoji 时可能看到奇怪的长度const emoji .length; console.log(emoji); // 2这不会直接影响拼接但如果你把字符串按索引切分拼接就可能把一个完整字符从中间截断产生乱码。处理含 emoji 的字符串时建议用Array.from()或for...of遍历而不是普通下标切分。我个人在实际项目里的体会是模板字符串和数组join()这两招已经能覆盖九成以上的拼接需求在理解类型转换规则后也完全够用concat()更像是压箱底的备用技能。最后再分享一个小技巧如果你经常要在几种写法之间来回改说明你的函数边界可能没划清楚与其纠结字符串怎么拼不如先想清楚哪些内容应该由函数返回、哪些应该由调用方拼接代码会清爽很多。字符串拼接这件事本身不难难的是在正确的地方用正确的方式希望这篇能帮你把这块补完整。
返回列表