JavaScript 中的字符串原语和字符串对象有啥区别?
Posted
技术标签:
【中文标题】JavaScript 中的字符串原语和字符串对象有啥区别?【英文标题】:What is the difference between string primitives and String objects in JavaScript?JavaScript 中的字符串原语和字符串对象有什么区别? 【发布时间】:2013-06-19 19:42:16 【问题描述】:取自MDN
字符串字面量(用双引号或单引号表示)和字符串 从非构造函数上下文中的 String 调用返回(即,没有 使用 new 关键字)是原始字符串。 javascript 自动 将原语转换为 String 对象,以便可以使用 原始字符串的字符串对象方法。在上下文中 方法将在原始字符串或属性查找上调用 发生时,JavaScript 将自动包装字符串原语并 调用方法或执行属性查找。
所以,我认为(逻辑上)对字符串原语的操作(方法调用)应该比对字符串对象的操作慢,因为在将method
应用于字符串之前,任何字符串原语都会转换为字符串对象(额外的工作)。
但在这个test case 中,结果却相反。 code block-1 比 code block-2 运行得更快,两个代码块如下:
代码块 1:
var s = '0123456789';
for (var i = 0; i < s.length; i++)
s.charAt(i);
代码块 2:
var s = new String('0123456789');
for (var i = 0; i < s.length; i++)
s.charAt(i);
结果因浏览器而异,但 code block-1 总是更快。谁能解释一下,为什么 code block-1 比 code block-2 快。
【问题讨论】:
使用new String
引入了另一个透明层的Object 包裹。 typeof new String(); //"object"
'0123456789'.charAt(i)
呢?
@YuriyGalanter,这不是问题,但我在问为什么 code block-1
更快?
字符串对象在现实生活中相对很少见,因此解释器优化字符串文字也就不足为奇了。如今,您的代码不仅仅是解释,还有许多优化层发生在幕后。
这很奇怪:revision 2
【参考方案1】:
JavaScript 有两个主要的类型类别,原语和对象。
var s = 'test';
var ss = new String('test');
单引号/双引号模式在功能上是相同的。除此之外,您尝试命名的行为称为自动装箱。所以实际发生的是,当调用包装器类型的方法时,原语被转换为其包装器类型。简单地说:
var s = 'test';
是一种原始数据类型。它没有方法,只不过是一个指向原始数据内存引用的指针,这解释了随机访问速度要快得多。
例如,当您执行s.charAt(i)
时会发生什么?
由于s
不是String
的实例,JavaScript 将自动将s
封装成其包装类型typeof string
,String
,typeof object
或更准确地说是s.valueOf(s).prototype.toString.call = [object String]
。
自动装箱行为根据需要将s
来回转换为其包装器类型,但标准操作速度非常快,因为您处理的是更简单的数据类型。但是自动装箱和Object.prototype.valueOf
有不同的效果。
如果您想强制自动装箱或将原语转换为其包装器类型,您可以使用Object.prototype.valueOf
,但行为不同。基于各种各样的测试场景,自动装箱仅应用“必需”方法,而不会改变变量的原始性质。这就是您获得更快速度的原因。
【讨论】:
【参考方案2】:这是相当依赖于实现的,但我会试一试。我将以 V8 为例,但我假设其他引擎使用类似的方法。
字符串原语被解析为v8::String
对象。因此,可以直接在其上调用方法,如 jfriend00 所述。
另一方面,String 对象被解析为扩展 Object
的 v8::StringObject
,并且除了是一个完整的对象之外,还用作 v8::String
的包装器。
现在这是合乎逻辑的,对new String('').method()
的调用必须在执行该方法之前将这个v8::StringObject
的v8::String
拆箱,因此它更慢。
在许多其他语言中,原始值没有方法。
MDN 提出的方式似乎是解释原语的自动装箱如何工作的最简单方法(在 flav 的回答中也提到过),即 JavaScript 的 primitive 如何-y 值可以调用方法。
但是,智能引擎不会在您每次需要调用方法时将字符串 primitive-y 转换为 String 对象。 Annotated ES5 spec. 中也提到了关于解析原始值的属性(和“方法”¹)的信息:
注意 可能在步骤 1 中创建的对象无法在上述方法之外访问。实现可能会选择避免实际创建对象。 [...]
在非常低的级别上,字符串最常被实现为不可变的标量值。示例包装结构:
StringObject > String (> ...) > char[]
离原语越远,到达它所需的时间就越长。在实践中,String
原语比StringObject
s 更频繁,因此引擎将方法添加到 String 原语的对应(解释)对象的类而不是在 String
之间来回转换并不奇怪和 StringObject
正如 MDN 的解释所暗示的那样。
¹ 在 JavaScript 中,“方法”只是解析为函数类型值的属性的命名约定。
【讨论】:
不客气。=]
现在我想知道 MDN 的解释是否只是因为它似乎是理解自动装箱的最简单方法,或者在 ES 规范中是否有任何引用它。现在阅读整个规范以检查,如果我找到参考资料,我会记得更新答案。
深入了解 V8 的实现。我要补充一点,拳击不仅仅是为了解决这个问题。它也可以将 this 引用传递给方法。现在我不确定 V8 是否会为内置方法跳过这个,但如果你添加自己的扩展名 String.prototype,你会在每次调用它时得到一个装箱版本的字符串对象。【参考方案3】:
如果是字符串文字,我们不能分配属性
var x = "hello" ;
x.y = "world";
console.log(x.y); // this will print undefined
而对于字符串对象,我们可以分配属性
var x = new String("hello");
x.y = "world";
console.log(x.y); // this will print world
【讨论】:
终于有人解释了为什么我们还需要String
对象。谢谢!
为什么会有人需要这样做?【参考方案4】:
字符串字面量:
字符串字面量是不可变的,这意味着一旦它们被创建,它们的状态就不能改变,这也使得它们是线程安全的。
var a = 's';
var b = 's';
a==b
结果将是 'true' 两个字符串都引用同一个对象。
字符串对象:
这里创建了两个不同的对象,它们有不同的引用:
var a = new String("s");
var b = new String("s");
a==b
结果将是错误的,因为它们有不同的引用。
【讨论】:
字符串对象也是不可变的吗? @YangWang 这是一种愚蠢的语言,对于a
和b
都尝试分配a[0] = 'X'
它将成功执行,但不会像你想象的那样工作期待
你写了“var a = 's'; var b = 's'; a==b 结果将是 'true' 两个字符串都引用同一个对象。”这是不正确的: a 和 b 不引用任何相同的对象,结果为真,因为它们具有相同的值。这些值存储在不同的内存位置,这就是为什么如果你改变另一个不会改变!
还有: var a = String("s"); var b = String("s");控制台.log(a == b); // 返回真
这个答案不正确。第一个示例中的a
和b
不是同一个对象。你正在做的是一个值比较,它是真实的,但是内存中的这两个不同的对象。【参考方案5】:
如果您使用new
,则您明确声明您要创建一个Object 的实例。因此,new String
正在生成一个包装 String 原语的 Object,这意味着对其进行的任何操作都涉及额外的工作层。
typeof new String(); // "object"
typeof ''; // "string"
由于它们的类型不同,您的 JavaScript 解释器也可以对它们进行不同的优化,as mentioned in comments。
【讨论】:
【参考方案6】:当您声明时:
var s = '0123456789';
您创建了一个字符串原语。该字符串原语具有允许您在其上调用方法而无需将原语转换为第一类对象的方法。因此,您认为这会更慢,因为必须将字符串转换为对象的假设是不正确的。它不必转换为对象。原语本身可以调用方法。
将其转换为完整的对象(允许您向其添加新属性)是一个额外的步骤,并且不会使字符串操作更快(实际上您的测试表明它会使它们变慢)。
【讨论】:
字符串原语怎么会继承包括自定义String.prototype
在内的所有原型属性?
var s = '0123456789';
是一个原始值,这个值怎么会有方法,我很困惑!
@SheikhHeera - 原语内置于语言实现中,因此解释器可以赋予它们特殊的权力。
@SheikhHeera - 我不明白你最后的评论/问题。字符串原语本身不支持您向其中添加自己的属性。为了实现这一点,javascript 也有一个 String 对象,它具有与字符串原语相同的所有方法,但它是一个成熟的对象,您可以在所有方面将其视为对象。这种双重形式似乎有点混乱,但我怀疑这是为了降低性能,因为 99% 的情况是使用原语,它们可能比字符串对象更快、内存效率更高。
@SheikhHeera “转换为字符串对象”是 MDN 如何表达它来解释基元如何能够调用方法。它们并没有真正转换为字符串对象。【参考方案7】:
对象的存在与 ECMAScript/JavaScript 引擎中字符串的实际行为几乎没有关系,因为根范围将只包含用于此的函数对象。所以字符串字面量的charAt(int)函数会被搜索并执行。
对于一个真实的对象,您可以再添加一层,在标准行为开始之前,还会在对象本身上搜索 charAt(int) 方法(与上面相同)。显然,在这种情况下完成了大量的工作。
顺便说一句,我不认为原语实际上被转换为对象,但脚本引擎会简单地将这个变量标记为字符串类型,因此它可以找到所有提供的函数,所以看起来你调用了一个对象。不要忘记这是一个脚本运行时,其工作原理与 OO 运行时不同。
【讨论】:
【参考方案8】:我看到这个问题早就解决了,字符串文字和字符串对象之间还有一个微妙的区别,因为似乎没有人涉及它,我想我只是为了完整性而写它。
基本上,两者之间的另一个区别是使用 eval 时。 eval('1 + 1') 给出 2,而 eval(new String('1 + 1')) 给出 '1 + 1',所以如果某个代码块可以“正常”或使用 eval 执行,它可以导致奇怪的结果
【讨论】:
感谢您的意见 :-) 哇,这真是奇怪的行为。您应该在评论中添加一个小的内嵌演示来展示这种行为 - 它非常令人大开眼界。 这很正常,如果您考虑一下的话。new String("")
返回一个对象,eval 只计算字符串,其他的都按原样返回【参考方案9】:
字符串原语和字符串对象的最大区别是对象必须跟在this rule for the ==
operator后面:
仅当操作数引用时,比较对象的表达式才为真 同一个对象。
因此,虽然字符串原语有一个方便的 ==
来比较值,但在使任何其他不可变对象类型(包括字符串对象)表现得像值类型时,您就很不走运了。
"hello" == "hello"
-> true
new String("hello") == new String("hello") // beware!
-> false
(其他人注意到字符串对象在技术上是可变的,因为您可以向其添加属性。但不清楚这有什么用处;字符串值本身是不可变的。)
【讨论】:
感谢您在很长一段时间后为这个问题添加价值 :-)【参考方案10】:代码在运行前由 javascript 引擎优化。 通常,微基准测试可能会产生误导,因为编译器和解释器会重新排列、修改、删除和对部分代码执行其他技巧,以使其运行得更快。 换句话说,书面代码说明目标是什么,但编译器和/或运行时将决定如何实现该目标。
Block 1 更快主要是因为: var s = '0123456789';总是比 var s = new String('0123456789'); 因为对象创建的开销。
循环部分不是导致减速的原因,因为chartAt() 可以由解释器内联。 尝试移除环路并重新运行测试,您将看到速度比与未移除环路时相同。换句话说,对于这些测试,执行时的循环块具有完全相同的字节码/机器码。
对于这些类型的微基准,查看字节码或机器码将提供更清晰的画面。
【讨论】:
感谢您的回答。【参考方案11】:在 Javascript 中,string 等原始数据类型是非复合构建块。这意味着它们只是值,仅此而已:
let a = "string value";
默认情况下没有内置方法,如 toUpperCase、toLowerCase 等...
但是,如果你尝试写:
console.log( a.toUpperCase() ); or console.log( a.toLowerCase() );
这不会引发任何错误,而是会正常工作。
发生了什么?
好吧,当您尝试访问字符串 a
的属性时,Javascript 会将字符串强制转换为 new String(a);
称为 wrapper object 的对象。
此过程与 JavaScript 中称为 函数构造函数 的概念相关联,其中函数用于创建新对象。
当您在此处键入 new String('String value');
时,String 是函数构造函数,它接受一个参数并在函数范围内创建一个空对象,这个空对象被分配给 this,在这种情况下,String 提供我们之前提到的所有已知的内置函数。并且一旦操作完成,例如做大写操作,包装对象就会被丢弃。
为了证明这一点,让我们这样做:
let justString = 'Hello From String Value';
justString.addNewProperty = 'Added New Property';
console.log( justString );
这里的输出将是未定义的。为什么 ? 在这种情况下,Javascript 创建包装器字符串对象,设置新属性 addNewProperty 并立即丢弃包装器对象。这就是你不确定的原因。伪代码如下所示:
let justString = 'Hello From String Value';
let wrapperObject = new String( justString );
wrapperObject.addNewProperty = 'Added New Property'; //Do operation and discard
【讨论】:
【参考方案12】:我们可以用三种方式定义字符串
-
var a = "第一种方式";
var b = String("第二种方式");
var c = new String("第三条路");
// 我们也可以使用创建 4. var d = a + '';
检查使用 typeof 运算符创建的字符串的类型
typeof //“字符串” typeof b //“字符串” typeof c // “对象”
当你比较 a 和 b var
a==b ( // yes)
当你比较 String 对象时
var StringObj = new String("third way")
var StringObj2 = new String("third way")
StringObj == StringObj2 // no result will be false, because they have different references
【讨论】:
以上是关于JavaScript 中的字符串原语和字符串对象有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章
打字稿:对象和原语之间的keyof typeof联合总是永远不会
JavaScript如何工作 3之 内存管理+如何处理4个常见的内存泄漏
JavaScript如何工作 3之 内存管理+如何处理4个常见的内存泄漏