浏览器是否总是将 Javascript 中的字符串和数字视为不可变的?
Do browsers always treat strings and numbers in Javascript as immutable?
在 Javscript 中,浏览器运行时解释器总是将字符串和数字视为不可变的吗?
当然,在可证明无害的情况下,他们会优化并将它们视为可变的。如果没有,为什么不呢?
例如,考虑一下不起眼的 for 循环。
for (let i = 0; i < 1000000000000; i++) {
console.log(i)
}
由于 i
变量的作用域为循环,并且循环中的任何代码都不需要 i 变量的 "old values",因此浏览器简单地增加数字是有意义的符号 i
指向每次迭代。否则,新的内存字节流将被 i
的新值占用,原因不明 ("someone might need those old values of 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 开发人员——因此我对其他 browsers/engines 知之甚少。)
这个问题没有简单的答案;实现很复杂。
在 V8 中,字符串始终是不可变的(在创建之后)。一个原因是在堆上分配对象时,通常在对象后没有空闲的 space,因此我们不能只是将字符附加到现有字符串。另一个原因是,跟踪哪些字符串可以安全地发生变异会增加大量的复杂性(除了一些更容易检测的小众情况,但如果只支持这些情况,那么该机制将提供更少的价值)。
V8 确实有一些巧妙的字符串操作技巧:当您使用较大字符串的子字符串时,不会复制任何字符;新字符串只是一个引用 "I'm a slice of length X of that other string over there, starting at index Y"。同样,当像您的 completeWorks
示例那样连接两个字符串时,新字符串是一个表示 "I'm the concatenation of those two other strings" 的引用。 (为了完整起见,我会提到有最低字符数,低于该值这些技巧将不适用,因为简单地复制字符至少同样有效。)
数字比字符串对性能更敏感,也更容易处理。一般来说,堆分配的数字总是不可变的;但这还没有结束。 V8 大量使用 "Smis" ("small integers") 的特殊表示,因为 JavaScript 程序中的许多数字都属于该桶。 Smis 不是堆对象;创建一个新的和修改一个一样便宜,而且实际上无法区分(就像 C++ 中的 int
)。对于超出 Smi 范围的数字,优化编译器还会执行 "escape analysis" 并且可以 "unbox" 非转义数字,这意味着将它们保存在 CPU 寄存器中(作为普通的 64 位浮点数)而不是首先在堆上分配它们,这再次比改变其他不可变的堆对象更好。对于存储在对象属性中的数字的特殊情况,V8 也(在某些情况下)使用可变存储。
因此,您的问题的答案是 "yes"(例如,在生成未优化的代码时,V8 不会花时间执行分析,因此代码必须保守地假设某处需要任何旧值) 和 "no"(对于优化编译器,您的直觉是正确的,这应该是可以避免的;但这仍然并不意味着分配在堆上的任何数字都会在那里发生突变)。
Since the i
variable is scoped to the loop
JavaScript 中的范围界定很复杂。首先,没有 int i
。现在考虑一下:
for (var i = 0; i < 100; i++) {
// Use i here, or don't.
}
console.log(i); // Prints "100".
如果你的意思是 let i
,那么当然,你会有一个块范围的变量。在此示例中,性能将相同。
We will have an unnecessary race between the for loop (creating new values of i
in memory) and the garbage collector (killing off all the old values of i
), which the loop will generally win
没有。垃圾收集器具有高度自适应性,特别是当发生更多分配时它会做更多工作。没办法"outrun"了。如果需要,当垃圾收集器试图找到可以释放的内存时,程序会停止执行。
and we will have a stack overflow.
不,堆栈溢出与对象分配、垃圾回收或一般的堆内存无关。
在 Javscript 中,浏览器运行时解释器总是将字符串和数字视为不可变的吗?
当然,在可证明无害的情况下,他们会优化并将它们视为可变的。如果没有,为什么不呢?
例如,考虑一下不起眼的 for 循环。
for (let i = 0; i < 1000000000000; i++) {
console.log(i)
}
由于 i
变量的作用域为循环,并且循环中的任何代码都不需要 i 变量的 "old values",因此浏览器简单地增加数字是有意义的符号 i
指向每次迭代。否则,新的内存字节流将被 i
的新值占用,原因不明 ("someone might need those old values of 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 开发人员——因此我对其他 browsers/engines 知之甚少。)
这个问题没有简单的答案;实现很复杂。
在 V8 中,字符串始终是不可变的(在创建之后)。一个原因是在堆上分配对象时,通常在对象后没有空闲的 space,因此我们不能只是将字符附加到现有字符串。另一个原因是,跟踪哪些字符串可以安全地发生变异会增加大量的复杂性(除了一些更容易检测的小众情况,但如果只支持这些情况,那么该机制将提供更少的价值)。
V8 确实有一些巧妙的字符串操作技巧:当您使用较大字符串的子字符串时,不会复制任何字符;新字符串只是一个引用 "I'm a slice of length X of that other string over there, starting at index Y"。同样,当像您的 completeWorks
示例那样连接两个字符串时,新字符串是一个表示 "I'm the concatenation of those two other strings" 的引用。 (为了完整起见,我会提到有最低字符数,低于该值这些技巧将不适用,因为简单地复制字符至少同样有效。)
数字比字符串对性能更敏感,也更容易处理。一般来说,堆分配的数字总是不可变的;但这还没有结束。 V8 大量使用 "Smis" ("small integers") 的特殊表示,因为 JavaScript 程序中的许多数字都属于该桶。 Smis 不是堆对象;创建一个新的和修改一个一样便宜,而且实际上无法区分(就像 C++ 中的 int
)。对于超出 Smi 范围的数字,优化编译器还会执行 "escape analysis" 并且可以 "unbox" 非转义数字,这意味着将它们保存在 CPU 寄存器中(作为普通的 64 位浮点数)而不是首先在堆上分配它们,这再次比改变其他不可变的堆对象更好。对于存储在对象属性中的数字的特殊情况,V8 也(在某些情况下)使用可变存储。
因此,您的问题的答案是 "yes"(例如,在生成未优化的代码时,V8 不会花时间执行分析,因此代码必须保守地假设某处需要任何旧值) 和 "no"(对于优化编译器,您的直觉是正确的,这应该是可以避免的;但这仍然并不意味着分配在堆上的任何数字都会在那里发生突变)。
Since the
i
variable is scoped to the loop
JavaScript 中的范围界定很复杂。首先,没有 int i
。现在考虑一下:
for (var i = 0; i < 100; i++) {
// Use i here, or don't.
}
console.log(i); // Prints "100".
如果你的意思是 let i
,那么当然,你会有一个块范围的变量。在此示例中,性能将相同。
We will have an unnecessary race between the for loop (creating new values of
i
in memory) and the garbage collector (killing off all the old values ofi
), which the loop will generally win
没有。垃圾收集器具有高度自适应性,特别是当发生更多分配时它会做更多工作。没办法"outrun"了。如果需要,当垃圾收集器试图找到可以释放的内存时,程序会停止执行。
and we will have a stack overflow.
不,堆栈溢出与对象分配、垃圾回收或一般的堆内存无关。