不可变的 JS 字符串,操作起来有这么暴力吗

你以为 JS 中的字符串是不可变的,对于 JS 语言的使用者来说确实是这样,每次对字符串操作后,总是会创建一个新的字符串,占用新的内存空间。但是 v8 在底层实现中,对字符串的操作做了优化,字符串的概念发生了一些微妙的变化。

拼接操作

我们先从拼接两个字符串入手,逐步了解 v8 对字符串操作的优化方向,以及分析这种黑箱优化对开发的性能影响。

js 复制代码
const a = "a";
const b = "b";
const ab = a + b;

对我们开发者来说,ab 是一个全新的字符串。然而当 ab 都特别长的时候,如果直接暴力地给这个新字符串分配内存,会有很大的开销(算上原来的两个子字符串,总共占用了双倍的内存),V8 就对这个场景做了优化。参考 v8/src/objects 中对 ConString 的注释

The ConsString class describes string values built by using the addition operator on strings. A ConsString is a pair where the first and second components are pointers to other string values . One or both components of a ConsString can be pointers to other ConsStrings, creating a binary tree of ConsStrings where the leaves are non-ConsString string values.

v8 不管字符串长不长,只要是用 + 运算符拼接的,都用 ConString 来表示新字符串。同时 ConString 不是一个新的字符串,他记录了参与运算的两个字符串的指针。ConString 也可以由小的 ConString 和非 Constring 组合而成,最终的结构是一棵二叉树。

再回过头来看,ab 是最普通的字符串,他们不是 ConString,而 ab 是。这样字符串就出现了两个不同的种类(在底层的结构也不同)

这样是不是省下了不必要的内存开销?当然,拼接字符串也可以通过模板字符串来做,但是 v8 没有对这样的操作做类似的优化。可能未来会有。

切片操作

加法吃到的红利后,减法也想要引用原字符串来优化内存。现在讨论 slice 方法,如果也用引用来处理的话,我们可以保存原字符串,和切片的两端索引。访问子串的时候,只需要给出原字符串在切片范围内的内容就行了。v8 源码中关于 SlicedString 的解释在这里

A Sliced String is described as a pointer to the parent, the offset from the start of the parent string and the length.

看上去好像不错?实际上当我们对一个很长的字符串切片的时候,得到的是一个很小的字符串,我们保存了结果的引用。但是这个小字符串在底层还引用着大字符串,这样 GC 就无法正常回收占用大内存的原字符串。

js 复制代码
const longString = "#".repeat(10_000);
const subString = longString.slice(50, 60);

这个缺点在源码中也有提到

Currently missing features are:

  • truncating sliced string to enable otherwise unneeded parent to be GC'ed.

到这里总结一下,如果底层的操作都基于"可变"的原则来编写,那么结果是有利有弊的,在缩小的操作中,我们需要直接创建一个新的字符串,而不是复用原字符串。

字符串操作的性能优化

我们对创建两种不同字符串的方法做个命名

js 复制代码
const a = "a";
const b = "b";

const NotConString = `${a}${b}`; // Mutation

const ConString = a + b; // Concatenation

Concatenation 就是引用原字符串的方法,Mutation 则是创建一个完全新的字符串。我们希望在拼接的时候使用 Concatenation(当然 v8 默认就是这样实现,只要我们使用 + 来拼接),在切片的时候使用 Mutation(当我们调用 String.slice() 的时候,默认是 Concatenation,我们需要额外处理这个)

小字符串操作无所谓,怎么方便怎么来。在大字符串操作的场景下,对于拼接,尽量使用 +,对于切片,使用了 slice 方法之后,我们需要额外的释放掉原字符串的内存。可以使用其他的 Mutation 方法,比如说 replace

js 复制代码
const longString = "#".repeat(10_000);
let subString = longString.slice(50, 60);

// 替换掉一个完全不存在的子串
subString = subString.replace("*".repeat(subString.length + 1), "")

这样可以强行复制切片后的子串内容,删除掉对原字符串的引用。

参考

相关推荐
ZC跨境爬虫15 分钟前
跟着 MDN 学 HTML day_32:(AbstractRange 抽象接口与 DOM 范围操作)
前端·javascript·ui·html·音视频
十子木20 分钟前
设置把所有终端移动到最前端的快捷键
前端
陈老老老板22 分钟前
Bright Data Web Scraping 实战:用 MCP + Dify 构建 eBay 商品详情采集 AI 工作流(2026)
前端·人工智能
一渊之隔31 分钟前
uniapp蓝牙搜索连接展示蓝牙设备包含信号显示
前端·网络·uni-app·bluetooth
Cisyam^36 分钟前
Bright Data Web Scraper 实战:构建 TikTok 与 LinkedIn Web Scraping 自动化 Skill(2026)
运维·前端·自动化
李剑一1 小时前
开箱即用!Vue3+TS 视频组件完整代码,自动提取视频第一帧做封面。妈妈再也不用担心我手动截封面了
前端
盐多碧咸。。1 小时前
echarts折线图矩形选择 框选图表
前端·javascript·echarts
羽沢311 小时前
Canvas学习一
前端·css·学习·canvas
KaMeidebaby1 小时前
卡梅德生物技术快报|锦葵科植物遗传转化工程化优化:棉花胚尖农杆菌转化体系参数固化与效率提升
前端
invicinble1 小时前
前端框架使用vue-cli( 第二层:工程配置层--4.axios需要做的基础配置)
前端·vue.js·前端框架