为什么在JavaScript中字符串字面量被视为原始类型?

5
官方文档以及互联网上的大量文章都说'some string'是一个原始值,这意味着每次将其分配给变量时都会创建一个副本。然而,这个问题(以及其中的答案)如何强制JavaScript深度复制字符串?表明,实际上V8甚至在substr方法中也不会复制字符串。每次将字符串传递到函数中进行复制也是不合理的,也没有意义。在像C#,Java或Python这样的语言中,字符串数据类型肯定是引用类型。
此外,这个链接展示了层次结构,我们可以看到HeapObject在最后。 https://thlorenz.com/v8-dox/build/v8-3.25.30/html/d7/da4/classv8_1_1internal_1_1_sliced_string.html enter image description here

最后,在检查之后。
let copy = someStringInitializedAbove

Devtools 中可以清楚地看到,该字符串的新副本并未被创建!因此,我非常确定字符串在赋值时不会被复制。但我仍然不明白为什么像 JS Primitives vs Reference 这样的文章会说它们会被复制。

语言规范要求字符串是一个原始类型,并在JS用户界面方面表现为这样。它可能在底层不由原始类型表示,但这是一个抽象的实现细节,JS用户界面并不关心。 - Lennholm
是因为在设计时,JS 设计师们并没有考虑到与本地计算机内存的接近程度吗?想象一下 JS 在您的浏览器中运行(Firefox 是用 C/C++ 编写的),而不必担心它将如何在某个内存系统中实现最佳性能。如果当时有 Node.js 的概念,那么我相信同样的设计师在规划 JS 时会有不同的想法。 - nabster
1个回答

10
基本上,因为规范是这样规定的

字符串值

有限的、有序的零个或多个16位无符号整数值的原始值

规范还定义了String对象,与原始字符串不同。 (类似地,还有原始的numberbooleansymbol类型,以及Number和Boolean和Symbol对象。)

原始字符串遵循其他原始类型的所有规则。 在语言层面上,它们的处理方式与原始数字和布尔值完全相同。 就所有目的而言,它们就是原始值。 但是正如您所说,如果a = b字面上复制b中的字符串并将该副本放入a将是疯狂的。 实现不必这样做,因为原始字符串值是不可变的(就像原始数字值一样)。 您无法更改字符串中的任何字符,只能创建新字符串。 如果字符串是可变的,则实现必须在执行a = b时进行复制(但如果它们是可变的,则规范将编写不同)。

请注意,原始字符串和String对象确实是不同的东西:

const s = "hey";
const o = new String("hey");

// Here, the string `s` refers to is temporarily
// converted to a string object so we can perform an
// object operation on it (setting a property).
s.foo = "bar";
// But that temporary object is never stored anywhere,
// `s` still just contains the primitive, so getting
// the property won't find it:
console.log(s.foo); // undefined

// `o` is a String object, which means it can have properties
o.foo = "bar";
console.log(o.foo); // "bar"

因此,为什么需要原始字符串?你可能要问 Brendan Eich(他在 Twitter 上会比较负责),但我认为这是为了避免等价操作符(==, ===, !=, 和 !==)的定义既可以被对象类型用于自身目的重载,也可以为字符串特殊处理。
那么为什么需要字符串对象?有了 String 对象(以及 Number 对象、Boolean 对象和 Symbol 对象),并规定何时创建基本类型的临时对象版本,就可以在基本类型上定义方法。当你执行以下操作时:
console.log("example".toUpperCase());

从规范方面来说,一个字符串对象是通过GetValue操作 创建的,然后在该对象上查找并调用了toUpperCase属性(如上所述)。因此,原始字符串从String.prototypeObject.prototype获得它们的toUpperCase(和其他标准方法)。但是创建的临时对象对代码不可访问,除非在一些边缘情况下,¹JavaScript引擎可以避免在这些边缘情况之外直接创建对象。好处是新方法可以添加到String.prototype中并在原始字符串上使用。


¹ “什么是边缘情况?”我听到你问道。我能想到的最常见的情况是在松散模式下的代码中向String.prototype(或类似对象)中添加自己的方法:

Object.defineProperty(String.prototype, "example", {
    value() {
        console.log(`typeof this: ${typeof this}`);
        console.log(`this instance of String: ${this instanceof String}`);
    },
    writable: true,
    configurable: true
});

"foo".example();
// typeof this: object
// this instance of String: true

在这种情况下,JavaScript引擎被迫创建String对象,因为在宽松模式下,this不能是原始类型。

严格模式可以避免创建对象,因为在严格模式下,this不需要是对象类型,它可以是一个原始类型(在这种情况下,是一个原始字符串):

"use strict";
Object.defineProperty(String.prototype, "example", {
    value() {
        console.log(`typeof this: ${typeof this}`);
        console.log(`this instance of String: ${this instanceof String}`);
    },
    writable: true,
    configurable: true
});

"foo".example();
// typeof this: string
// this instanceof String: false


非常感谢您为这个话题提供了光明和让人们参考这个问题的可能性。 - Sagid
主要问题在于通常(在大多数语言中),本地变量的引用类型的实际值位于堆上,而原始值位于堆栈上。因此,有一种误解认为字符位于堆栈上。但对我来说,规范可以指定如此基本的事情并不是很清楚。 - Sagid
1
@Sagid - 当然,规格书决定了比那更基本的事情。但是JavaScript的规格书没有这样做。它详细定义了语言的语义和行为,但只要实现按照规格书所说的方式运行,实现可以进行任何优化。值得注意的是,JavaScript的规格书并没有说明除了类型数组的底层数据之外的任何东西都分配在哪里。它没有理由这样做,因为这是一个实现细节。 - T.J. Crowder
@Sagid - 顺便说一下,“引用类型(对象)存储在堆中”的想法并不总是正确的。例如,现代JIT系统可以进行逃逸分析,并在对象不总是在函数调用终止时存活且不大于可用堆栈空间时将其分配到堆栈上。Oracle的JVM就是这样做的,一些JavaScript引擎也是如此。(当然,如果一个原始类型是对象内部的字段,并且该对象在堆上,那么该原始类型也在堆上。) - T.J. Crowder

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