字符串拼接

将多个字符串连接成一个字符串的操作,可通过 + 运算符、模板字面量等方式实现。

字符串拼接 (String Concatenation) 是将多个字符串连接成一个字符串的操作。几乎所有编程语言都把它作为基本操作提供,广泛应用于用户界面构建、日志消息生成、SQL 查询组装等各种场景。虽然看似简单,但在处理大量数据时,性能特性会因语言和实现的不同而差异很大,因此理解各自的前提非常重要。

在 JavaScript 中,可以使用 + 运算符、concat() 方法或模板字面量 (`${}`) 这三种方式拼接字符串。流传很广的一种说法是:在循环中反复使用 += 会每次生成新的字符串对象,时间复杂度接近 O(n²)。但这一说法并不适用于 2026 年时点的 V8 (Chrome、Node.js)。V8 不会立即把拼接结果扁平化为一条字符串,而是保留为指向两个字符串的连接节点,直到实际读取内容时才首次扁平化。因此即使执行 100 万次 +=,耗时也与数组的 join('') 几乎相同。选择 join() 的理由并不是速度,而是可以在一处指定分隔符,并且能在数组状态下检查和加工中间元素。另外,像 [...items].join('') 这样插入展开运算符,会因整体复制数组而增加无谓开销,如果本来就是数组,直接写 items.join('') 即可。

在 Python 中,除 + 运算符外,还常用 f-string 和 ''.join()。官方文档的 Programming FAQ 说明:由于字符串是不可变的,大量连接的开销一般相对于总字符串长度呈二次增长,并推荐先累积到列表中,最后用 ''.join() 一次性连接的写法。不过 CPython 中存在一项依赖实现的优化:仅当连接目标的变量持有对该字符串的唯一引用时,才不创建新对象而是就地扩展空间。它是否生效会因一处写法而改变,同样是 20 万次的循环,只要把中间结果同时保存到另一个变量,耗时就会增加数十倍到数百倍 (按 Python 3.13 实测,每次追加 1 个字符约 50 倍、追加 10 个字符约 370 倍)。与其去判别优化生效的条件,把累积到列表再 join() 的写法作为默认更为安全。Java 提供 StringBuilder 类作为可变长度的字符串缓冲区,适用于循环内的拼接;Go 语言中 strings.Builder 承担相同的角色。

容易与字符串拼接混淆的概念是字符串插值 (String Interpolation)。字符串拼接是把多个字符串在物理上连接起来的操作,而字符串插值是在模板中嵌入变量或表达式值的语法。JavaScript 的模板字面量 `Hello, ${name}!` 是字符串插值的一种,内部执行的是字符串拼接,但在可读性方面更为优越。

字符编码的处理也需要注意。在字符串类型统一为 Unicode 的语言 (Java、JavaScript、Python 3 等) 中,拼接本身不会引起乱码。乱码产生于更前的阶段,即在把字节序列转换为字符串的边界上指定了错误的编码。当从数据库取得的值或从文件读入的文本出现损坏时,应当怀疑的不是拼接处理,而是连接时的字符集指定或读入时的编码指定。在 UTF-8 与 Shift_JIS 混用的环境中,先统一边界处的指定,然后再拼接。此外,直接拼接用户输入来生成 SQL 查询或 HTML 会导致 SQL 注入或 XSS 漏洞,因此必须同时使用参数化查询和转义处理。

从字符计数的角度来看,拼接后的字符数是否等于拼接前各字符数之和,取决于计数的单位。按码点计数时与总和一致,并会加上插入分隔符的部分。例如用逗号连接三个单词,就会增加两个分隔符的字符数。而按显示上相当于 1 个字符的书写素簇计数时,总和并不成立。把 "か" 与作为结合字符的浊点连接起来,合起来会成为 1 个字符的 "が";把两个区域指示符 (Regional Indicator) 连接起来会成为 1 个国旗;把人物表情符号与以 ZWJ (零宽连接符) 开头的序列连接起来会成为 1 个家庭表情符号。这些都是 1 + 1 或 1 + 2 的结果变成 1 的例子。跨越边界发生拼接的场合,是接续碎片化输入的场合,以及组装分割发送的文本的场合。像输入框的剩余字符数那样处理显示上的字符数时,可靠的做法不是把拼接前的值相加,而是对拼接后的最终字符串重新计数。

分享这篇文章