javascript - 浏览器是否总是将 Javascript 中的字符串和数字视为不可变的?
问题描述
在 JavaScript 中,浏览器运行时解释器是否总是将字符串和数字视为不可变的?
当然,如果它被证明是无害的,他们会优化并将它们视为可变的。如果没有,为什么不呢?
例如,考虑不起眼的 for 循环。
for (let i = 0; i < 1000000000000; i++) {
console.log(i)
}
由于变量的作用域是循环,并且循环中的任何代码都不需要 i 变量的“旧值”,因此浏览器只需增加符号指向每次迭代i
的数字是有意义的。i
否则,一个新字节的内存流将被 的新值占用,这将是i
不可想象的原因(“有人可能需要i
! 的旧值”)。我们将在 for 循环(i
在内存中创建新值)和垃圾收集器(杀死所有旧值i
)之间进行不必要的竞争,循环通常会获胜,并且我们将发生堆栈溢出。
哦,就是这样,不是吗。如果是这样,为什么浏览器在以其他方式优化代码方面如此聪明,却以这种方式愚蠢?
字符串也有类似的情况。考虑以下。
{
let completeWorks = "This string dictates the complete works of William Shakespeare. To be or not to be that is the question whether it is nobler in the mind..."
completeWorks += "The End." // <-- what happens here?
}
该字符串completeWorks
是块范围的,并且可以证明只存在于该块中。所以当浏览器遇到指令时,completeWorks += "The End"
它肯定会发生变化completeWorks
。如果不是,为什么不呢?他们不这样做可能有一个很好的理由,我想学习它。
解决方案
(这里是 V8 开发人员——因此我对其他浏览器/引擎知之甚少。)
对此没有简单的答案。实现很复杂。
在 V8 中,字符串始终是不可变的(在创建之后)。一个原因是,在堆上分配对象时,对象后面通常没有可用空间,因此我们不能只将字符附加到现有字符串。另一个原因是,跟踪哪些字符串可以安全地被突变会增加非常多的复杂性(除了一些更容易检测的小众案例,但如果只支持这些,那么该机制提供的价值就会少得多)。
V8 确实有一些巧妙的字符串操作技巧:当您获取较大字符串的子字符串时,不会复制任何字符;新字符串只是一个引用,上面写着“我是那里另一个字符串的长度为 X 的切片,从索引 Y 开始”。同样,当像您的completeWorks
示例一样连接两个字符串时,新字符串是一个引用,上面写着“我是其他两个字符串的连接”。(为了完整起见,我会提到有最小字符数低于这些技巧不会应用,因为简单地复制字符至少同样有效。)
与字符串相比,数字对性能更敏感,也更容易处理。一般来说,堆分配的数字总是不可变的。但这不是故事的结局。V8 大量使用“Smis”(“小整数”)的特殊表示,因为 JavaScript 程序中的许多数字都属于该类别。Smis 不是堆对象;创建一个新的和修改一个一样便宜,而且实际上无法区分(就像一个int
在 C++ 中)。对于超出 Smi 范围的数字,优化编译器还执行“转义分析”并可以“拆箱”非转义数字,这意味着将它们保存在 CPU 寄存器中(作为普通的 64 位浮点数)而不是在堆上分配它们首先,这再次比改变其他不可变的堆对象更好。对于存储在对象属性中的数字的特殊情况,V8 也(在某些情况下)使用可变存储。
因此,您的问题的答案是“是”(例如,在生成未优化的代码时,V8 不会花时间执行分析,因此代码必须保守地假设某处需要任何旧值)和“否”(对于优化编译器,您的直觉是正确的,这应该是可以避免的;但这并不意味着在堆上分配的任何数字都会在那里发生变异)。
由于
i
变量的范围是循环
JavaScript 中的作用域很复杂。首先,没有int i
. 现在考虑一下:
for (var i = 0; i < 100; i++) {
// Use i here, or don't.
}
console.log(i); // Prints "100".
如果您的意思是let i
,那么可以肯定的是,您将拥有一个块范围的变量。在此示例中,性能将是相同的。
我们将在 for 循环(
i
在内存中创建新值)和垃圾收集器(杀死所有旧值i
)之间进行不必要的竞争,循环通常会获胜
不会。垃圾收集器是高度自适应的,尤其是在发生更多分配时它会做更多的工作。没有办法“超越”它。如果需要,程序执行会在垃圾收集器尝试查找可以释放的内存时停止。
我们将有一个堆栈溢出。
不,堆栈溢出通常与对象分配、垃圾收集或堆内存无关。
推荐阅读
- python - 使用 Python 和 NetFilterQueue 进行 DNS 欺骗
- node.js - socket.io 无法在 React 应用上的 Heroku 上运行
- java - 配置 Springboot Jaeger 和 RSocket
- json - 使用 jq 通过非唯一键将数组转换为对象
- scala - Scala 中 Derived DataFrame 的持久化工作原理及其对性能的影响
- reactjs - 反应路由器循环并返回
- python - 使用部分文本获取标签的值
- ios - 每场比赛的游戏中心排行榜
- assembly - nasm 键盘驱动程序 I/O
- vuetify.js - 如何使用 Vuetify 将 v-card 放在页面中心