在JavaScript中,将一个字符串推入数组或将其设置为对象属性的值,会复制字符串还是保留引用?

4
在v8 / Node.js中,特别是当你将一个原始类型(字符串、数字、布尔值)推入数组时,它是克隆字符串还是存储引用?
我知道你不能这样做并改变字符串:
let array = []
let x = 'foo'
array.push(x)
x = 'bar'
console.log(array) //=> ['foo']

但是如果我这样做,会不会多次复制字符串(从而增加内存占用)?
let array = []
let x = 'foo'
array.push(x)
array.push(x)
array.push(x)
...

同样的问题也适用于对象键,如果我这样做,它会克隆字符串吗?
let object = {}
let x = 'foo'
object.a = x
object.b = x
object.c = x

我在周围搜索了一下,但没有找到这个问题的直接答案。 在JavaScript中,将对象推入数组中是深拷贝还是浅拷贝? 这篇博客文章说:

对象和数组被推入时是指向原始对象的指针。内置的原始类型,如数字或布尔值,被推入时是拷贝。

但我不确定这是否正确(没有备份)。我需要运行一系列彻底的测试来真正检查并确定在将元素推入数组时内存是否增长。我不太确定最简单的方法是什么,也许v8工程师或其他精通编译器理论的人知道这是如何实现的。
我想使用Buffer.byteLength(text, 'utf8')来计算我添加到trie中的每个字符串的大小,然后跟踪trie的大致大小(将其使用的字符串大小相加,并粗略估计用于存储n个对象属性和x长度数组的字节数)。所以第一步是理解,当我将字符串推入多个位置时,我的字符串会被复制,还是同一个引用会在每个位置传递? 我希望那篇博客是错误的,并且它推送的是一个引用,只是当你将一个变量发送到另一个函数后,就无法修改它。但是字符串仍然是一个引用,直到你尝试改变变量,类似于这样的情况。

js值是克隆的,因为它是一个原始值。底层表示通常是对文本字节的引用,只需要复制该引用,因为引用的字节永远不会改变。 - undefined
@Bergi 关键是字符串没有被克隆。我不认为重复的问题完全回答了这个问题。想要重新打开这个问题,这样我就可以发布我的答案了吗? - undefined
@jmrk 哎呀,你说得对,这些问题中都没有明确说明(尽管我认为这是强烈暗示/可以轻易推断出来的)。请随意重新打开。 - undefined
2个回答

2
(这里是V8开发者。)
当将字符串存储在数组(或任何地方)时,字符串不会被复制。布尔值也是如此。
至于数字,情况取决于:通常它们也被存储为引用,除非在某些优化情况下有更高效的替代方案。
原因有两个: (1)没有必要克隆字符串。 (2)不克隆字符串更简单、更快速。
你引用的代码片段在实现细节方面是完全错误的。从可观察的语义角度来看,可以说它并不完全不正确:你的程序行为无法判断字符串是否被复制,或者只是对它的另一个引用。(但当然,这使得整个陈述毫无意义:如果对象被存储为引用,对于原始类型我们无法区分,为什么不简单地假设一切都被存储为引用呢?)
作为一个经验法则:对于像JavaScript这样的动态语言,虚拟机将一切都视为引用,除非它们选择优化的特殊情况(通常是某种数字的定义;如果你想深入了解,请搜索“smi-tagging”和“nan-boxing”这些术语)。一个值是否是“原始值”只影响它是否具有对象标识。
{foo: 42} === {foo: 42}  // false, objects have identity
42 === 42                // true, numbers have no identity
"foo" === "foo"          // true, strings have no identity

作为原始类型并不影响值在数组/对象/变量/任何地方的存储方式,也不影响它的分配位置(有时我看到的一个相关的错误观点是“原始类型在堆栈上分配”——不,它们并不是)。
在@Bergi的要求下,对于“添加”进行了澄清:
当你反复调用array.push(x)时,数组的后备存储的大小会增长,因为它需要存储越来越多对字符串的引用。因此,虽然字符串不会被复制,但整体内存使用量会增加(平均每次push增加一个指针,但实际上是以块的形式发生)。

你引用的那段代码在实现细节方面是完全错误的” - 嗯,那篇博客文章并不关注实现细节,它试图向初学者解释原始值和引用值(对象)之间的区别。 - undefined
字符串不会被复制。布尔值也不会被复制。 - 你可能首先想要明确你所指的“字符串”或“布尔值”的具体含义。在将其存储到数组/对象中时,必须有一些东西会被复制。 - undefined
@Bergi “它试图解释原始值和引用值之间的区别” 嗯,我不认为它做得很好。甚至可以说,“原始值没有属性”更有意义。 “必须有东西会被复制” 是的,对这个东西的引用(无论这个“东西”是布尔值、字符串还是对象)会被复制。a[0] = foovar x = foo非常相似:xa[0]之后都将持有对foo引用的值。这个值可以是对象,也可以是不可变的原始值。 - undefined
请将这一点添加到你的回答中——当添加新属性时,数组/对象的大小将会增加引用的大小。顺便问一下,你不认为引用就是值吗?我通常将“被引用的东西”称为“值的内容”。但也许这只是JavaScript的观点,而不是引擎的观点。 - undefined
@Bergi: “你难道不认为引用是值吗?”不,我不这样认为。当然,引用是一种特殊类型的值,但只有在你不将值和引用等同起来时,才有意义地区分“按引用传递”和“按值传递”这样的概念。不过,我很乐意同意“值”是一个多义词,通常用于不同的概念,例如,根据所讨论语句的抽象级别而定 :-) - undefined

1
JavaScript虚拟机在尽可能的情况下不会复制字符串。在这种情况下,不复制字符串是微不足道的。
如果你真的想要复制字符串,你需要进行一些花招,比如将它们转换为其他编码再转回来,或者将它们分割然后再拼接起来。如果我记得没错的话,上次我检查的时候,一旦字符串被复制,虚拟机就不会尝试去重复它们。
来源:曾在SpiderMonkey工作过。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接