128

取自MDN

字符串文字(用双引号或单引号表示)和从非构造函数上下文中的 String 调用返回的字符串(即,不使用 new 关键字)是原始字符串。JavaScript 自动将原语转换为 String 对象,因此可以将 String 对象方法用于原语字符串。在原始字符串上调用方法或发生属性查找的上下文中,JavaScript 将自动包装字符串原始并调用方法或执行属性查找。

所以,我认为(逻辑上)对字符串原语的操作(方法调用)应该比对字符串对象的操作慢,因为任何字符串原语在method应用于字符串之前都会转换为字符串对象(额外的工作)。

但在这个测试用例中,结果却相反。代码块 1代码块 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);
}

结果因浏览器而异,但代码块 1总是更快。谁能解释一下,为什么code block-1code block-2快。

4

12 回答 12

163

JavaScript 有两个主要的类型类别,原语和对象。

var s = 'test';
var ss = new String('test');

单引号/双引号模式在功能上是相同的。除此之外,您尝试命名的行为称为自动装箱。所以实际发生的是当调用包装器类型的方法时,原语被转换为其包装器类型。简单地说:

var s = 'test';

是原始数据类型。它没有任何方法,它只不过是一个指向原始数据内存引用的指针,这解释了随机访问速度快得多的原因。

那么当你这样做时会发生什么s.charAt(i)

由于s不是 的实例String,JavaScript 将自动装箱stypeof string其包装器类型为 ,Stringtypeof object或更准确地说s.valueOf(s).prototype.toString.call = [object String]

自动装箱行为s根据需要来回转换为其包装器类型,但标准操作非常快,因为您正在处理更简单的数据类型。不过自动装箱又有Object.prototype.valueOf不同的效果。

如果要强制自动装箱或将原语转换为其包装器类型,可以使用Object.prototype.valueOf,但行为不同。基于各种各样的测试场景,自动装箱仅应用“必需”方法,而不会改变变量的原始性质。这就是为什么你会获得更好的速度。

于 2013-06-22T23:34:19.763 回答
36

这是相当依赖于实现的,但我会试一试。我将以 V8 为例,但我假设其他引擎使用类似的方法。

字符串原语被解析为v8::String对象。因此,可以直接在其上调用方法,如jfriend00 所述

另一方面, String 对象被解析为v8::StringObject扩展的 a ,Object并且除了是一个完整的对象之外,还用作v8::String.

现在它只是合乎逻辑的,调用必须在执行方法之前new String('').method()取消包装 this ,因此它更慢。v8::StringObjectv8::String


在许多其他语言中,原始值没有方法。

MDN 提出的方式似乎是解释原语的自动装箱如何工作的最简单方法(正如flav的回答中提到的那样),即 JavaScript 的原语-y值如何调用方法。

但是,智能引擎不会在您每次需要调用方法时将字符串原始 y转换为 String 对象。Annotated ES5 规范中也提到了这一点。关于解析原始值的属性(和“方法”¹):

注意可能在步骤 1 中创建的对象在上述方法之外是不可访问的。实现可能会选择避免实际创建对象。[...]

在非常低的级别上,字符串最常被实现为不可变的标量值。示例包装器结构:

StringObject > String (> ...) > char[]

你离原始人越远,到达它所需的时间就越长。在实践中,String原语比 s 更频繁,因此引擎将方法添加到 String 原语的相应(解释)对象的 Class 而不是像 MDN 的解释所暗示的那样在和StringObject之间来回转换也就不足为奇了。StringStringObject


¹ 在 JavaScript 中,“方法”只是解析为函数类型值的属性的命名约定。

于 2013-06-22T23:45:51.723 回答
19

如果是字符串文字,我们不能分配属性

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
于 2016-04-14T06:01:15.390 回答
14

字符串字面量:

字符串文字是不可变的,这意味着一旦它们被创建,它们的状态就不能改变,这也使它们成为线程安全的。

var a = 's';
var b = 's';

a==b结果将是“真”两个字符串引用的相同对象。

字符串对象:

在这里,创建了两个不同的对象,它们有不同的引用:

var a = new String("s");
var b = new String("s");

a==b结果将是错误的,因为它们有不同的引用。

于 2014-06-20T05:53:56.563 回答
10

如果使用new,则明确声明要创建Object的实例。因此,new String正在生成一个包装String原语的Object,这意味着对它的任何操作都涉及额外的工作层。

typeof new String(); // "object"
typeof '';           // "string"

由于它们的类型不同,您的JavaScript解释器也可能对它们进行不同的优化,如评论中所述

于 2013-06-22T23:16:50.653 回答
7

当您声明:

var s = '0123456789';

您创建一个字符串原语。该字符串原语具有允许您在其上调用方法而无需将原语转换为第一类对象的方法。因此,您认为这会更慢,因为必须将字符串转换为对象的假设是不正确的。它不必转换为对象。原语本身可以调用方法。

将其转换为成熟的对象(允许您向其添加新属性)是一个额外的步骤,并且不会使字符串操作更快(实际上您的测试表明它会使它们变慢)。

于 2013-06-22T23:15:14.407 回答
3

对象的存在与 ECMAScript/JavaScript 引擎中字符串的实际行为几乎没有关系,因为根范围将只包含用于此的函数对象。因此,将搜索并执行字符串文字情况下的 charAt(int) 函数。

对于一个真实的对象,您可以再添加一层,在标准行为开始之前,还会在对象本身上搜索 charAt(int) 方法(与上面相同)。显然,在这种情况下完成了大量的工作。

顺便说一句,我不认为原语实际上被转换为对象,但脚本引擎会简单地将这个变量标记为字符串类型,因此它可以找到所有提供的函数,所以看起来你调用了一个对象。不要忘记这是一个脚本运行时,它的工作原理与 OO 运行时不同。

于 2013-06-22T23:30:13.980 回答
3

我可以看到这个问题很久以前就已经解决了,字符串文字和字符串对象之间还有另一个微妙的区别,因为似乎没有人触及它,我想我只是为了完整性而写它。

基本上,两者之间的另一个区别是使用 eval 时。eval('1 + 1') 给出 2,而 eval(new String('1 + 1')) 给出 '1 + 1',所以如果某个代码块可以“正常”或使用 eval 执行,它可以导致奇怪的结果

于 2018-06-03T09:18:15.690 回答
3

字符串原语和字符串对象之间的最大区别在于对象必须遵循运算符的以下规则==

仅当操作数引用相同的对象时,比较对象的表达式才为真。

因此,虽然字符串原语具有==比较值的便利,但在使任何其他不可变对象类型(包括字符串对象)表现得像值类型时,您就不走运了。

"hello" == "hello"
-> true
new String("hello") == new String("hello") // beware!
-> false

(其他人已经注意到字符串对象在技术上是可变的,因为您可以向它添加属性。但不清楚它有什么用处;字符串值本身是不可变的。)

于 2019-05-19T22:58:04.467 回答
1

代码在由 javascript 引擎运行之前进行了优化。一般来说,微基准测试可能会产生误导,因为编译器和解释器会重新排列、修改、删除和对部分代码执行其他技巧,以使其运行得更快。换句话说,书面代码告诉目标是什么,但编译器和/或运行时将决定如何实现该目标。

块 1 更快主要是因为: var s = '0123456789'; 总是比 var s = new String('0123456789'); 因为对象创建的开销。

循环部分不是导致速度变慢的部分,因为 chartAt() 可以由解释器内联。尝试移除环路并重新运行测试,您将看到速度比与未移除环路时相同。换句话说,对于这些测试,执行时的循环块具有完全相同的字节码/机器码。

对于这些类型的微基准,查看字节码或机器码将提供更清晰的画面。

于 2020-03-27T17:10:24.750 回答
0

在 Javascript 中,诸如字符串之类的原始数据类型是一个非复合构建块。这意味着它们只是值,仅此而已: let a = "string value"; 默认情况下没有内置方法,如 toUpperCase、toLowerCase 等......

但是,如果您尝试编写:

console.log( a.toUpperCase() ); or console.log( a.toLowerCase() );

这不会引发任何错误,而是会按应有的方式工作。

发生了什么 ?好吧,当您尝试访问字符串的属性时,aJavascript 通过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
于 2020-05-07T17:58:56.350 回答
0

我们可以用 3 种方式定义 String

  1. var a = "第一种方式";
  2. var b = String("第二种方式");
  3. var c = new String("第三条路");

// 我们也可以使用 4 创建。 var d = a + '';

检查使用 typeof 运算符创建的字符串的类型

  • typeof a // “字符串”
  • 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
于 2020-06-18T13:32:31.643 回答