使用 JavaScript 字符串对象时的性能

本文旨在探讨 JavaScript 引擎对原始值字符串和对象字符串的性能表现。它展示了与 Kiro Risk 的优秀文章 包装器对象 相关的基准测试。在继续之前,我建议您先访问 Kiro 的页面,以了解此主题的介绍。

ECMAScript 5.1 语言规范(PDF 链接) 中,第 4.3.18 段关于字符串对象部分说明

字符串对象是对象类型的成员,它是标准内置字符串构造函数的实例。

注意 使用字符串构造函数的新表达式创建字符串对象,并提供字符串值作为参数。
生成的 object 具有一个内部属性,其值为字符串值。字符串对象可以强制转换为字符串值
通过将字符串构造函数作为函数调用(15.5.1)。

以及 David Flanagan 的优秀著作“JavaScript:权威指南”,在第 3.6 节中非常细致地描述了包装器对象。

但是,字符串不是对象,那么为什么它们具有属性呢?每当您尝试引用字符串 s 的属性时,JavaScript 会将字符串值转换为对象,就像通过调用 new String(s) 一样。[...] 一旦解析了属性,新创建的对象就会被丢弃。(实现不需要实际创建和丢弃此临时对象:但是,它们必须表现得好像这样做一样。

注意上面粗体字的内容非常重要。基本上,创建新字符串对象的不同方式是实现特定的。因此,一个显而易见的问题是“由于在尝试访问属性(例如 str.length)时,必须将原始值字符串强制转换为字符串对象,如果我们改为将变量声明为字符串对象,是否会更快?”。换句话说,将变量声明为字符串对象,即 var str = new String("hello"),而不是原始值字符串,即 var str = "hello",是否可以避免 JS 引擎动态创建新的字符串对象以访问其属性?

处理 ECMAScript 标准到 JS 引擎的实现的人员已经知道答案,但值得深入了解常见的建议“不要使用‘new’运算符创建数字或字符串”。

我们的展示和目标

对于我们的展示,我们将主要使用 Firefox 和 Chrome;但是,如果我们选择任何其他 Web 浏览器,结果也将类似,因为我们关注的不是两个不同浏览器引擎之间的速度比较,而是在每个浏览器上两个不同源代码版本之间的速度比较(一个版本具有原始值字符串,另一个版本具有字符串对象)。此外,我们有兴趣了解相同案例在同一浏览器的后续版本中的速度比较情况。第一批基准测试是在同一台机器上收集的,然后添加了其他具有不同操作系统/硬件规格的机器,以验证速度数据。

场景

对于基准测试,案例相当简单;我们将声明两个字符串变量,一个作为原始值字符串,另一个作为对象字符串,两者具有相同的值

  var strprimitive = "Hello";
  var strobject    = new String("Hello");

然后我们对它们执行相同类型的任务。(请注意,在 jsperf 页面中,strprimitive = str1,strobject = str2)

1. length 属性

  var i = strprimitive.length;
  var k = strobject.length;

如果我们假设在运行时,从原始值字符串strprimitive创建的包装器对象在性能方面与 JavaScript 引擎中的对象字符串strobject相同,那么我们应该期望在尝试访问每个变量的length属性时看到相同的延迟。然而,正如我们在下面的条形图中看到的,访问length属性在原始值字符串strprimitive上比在对象字符串strobject上快得多。


原始值字符串与包装器对象字符串 - length,在 jsPerf 上

实际上,在 Chrome 24.0.1285 上,调用strprimitive.length的速度比调用strobject.length2.5 倍,在 Firefox 17 上,速度快约2 倍(但每秒的操作次数更多)。因此,我们意识到相应的浏览器 JavaScript 引擎在处理原始字符串值时应用了一些“捷径”来访问 length 属性,每个案例都有特殊的代码块。

例如,在 SpiderMonkey JS 引擎中,处理“获取属性”操作的伪代码如下所示

  // direct check for the "length" property
  if (typeof(value) == "string" && property == "length") {
    return StringLength(value);
  }
  // generalized code form for properties
  object = ToObject(value);
  return InternalGetProperty(object, property);

因此,当您请求字符串原语上的属性,并且属性名称为“length”时,引擎会立即返回其长度,避免完全属性查找以及临时包装器对象的创建。除非我们像这样向 String.prototype 添加属性/方法以请求 |this|,

  String.prototype.getThis = function () { return this; }
  console.log("hello".getThis());

否则在访问 String.prototype 方法时不会创建包装器对象,例如 String.prototype.valueOf()。每个 JS 引擎都嵌入了类似的优化以产生更快的结果。

2. charAt() 方法

  var i = strprimitive.charAt(0);
  var k = strobject["0"];


原始值字符串与包装器对象字符串 - charAt(),在 jsPerf 上

此基准测试清楚地验证了前面的陈述,因为我们可以看到在 Firefox 20 中获取第一个字符串字符的值在strprimitive中比在strobject中快得多,性能提高了约70 倍。其他浏览器也适用类似的结果,尽管速度不同。此外,请注意增量 Firefox 版本之间的差异;这只是另一个指标,说明小的代码变化如何影响 JS 引擎在某些运行时调用的速度。

3. indexOf() 方法

  var i = strprimitive.indexOf("e");
  var k = strobject.indexOf("e");


原始值字符串与包装器对象字符串 - IndexOf(),在 jsPerf 上

同样在这种情况下,我们可以看到原始值字符串strprimitive可以用于比strobject更多的操作。此外,连续浏览器版本中的 JS 引擎差异会产生各种测量结果。

4. match() 方法

由于这里也有类似的结果,为了节省一些空间,您可以单击源链接查看基准测试。

原始值字符串与包装器对象字符串 - match(),在 jsPerf 上

5. replace() 方法

原始值字符串与包装器对象字符串 - replace(),在 jsPerf 上

6. toUpperCase() 方法

原始值字符串与包装器对象字符串 - toUpperCase(),在 jsPerf 上

7. valueOf() 方法

  var i = strprimitive.valueOf();
  var k = strobject.valueOf();

在这一点上,它开始变得更有趣了。那么,当我们尝试调用字符串最常用的方法,它的 valueOf() 时会发生什么?似乎大多数浏览器都有一种机制来确定它是否是原始值字符串或对象字符串,从而使用更快的速度获取其值;令人惊讶的是,Firefox 版本(最高到 v20)似乎更喜欢strobject的对象字符串方法调用,速度提高了7 倍


原始值字符串与包装器对象字符串 - valueOf(),在 jsPerf 上

还值得一提的是,Chrome 22.0.1229 似乎也更喜欢对象字符串,而在 23.0.1271 版本中,实现了一种获取原始值字符串内容的新方法。

在浏览器的控制台中运行此基准测试的更简单方法在 jsperf 页面的注释中进行了描述。

8. 添加两个字符串

  var i = strprimitive + " there";
  var k = strobject + " there";


原始字符串与包装器对象字符串 - 获取 str 值,在 jsPerf 上

现在让我们尝试使用原始值字符串添加两个字符串。如图表所示,Firefox 和 Chrome 在strprimitive方面分别提供了2.8 倍2 倍的速度提升,相比于使用对象字符串strobject与另一个字符串值相加。

9. 使用 valueOf() 添加两个字符串

  var i = strprimitive.valueOf() + " there";
  var k = strobject.valueOf() + " there";


原始字符串与包装器对象字符串 - str valueOf,在 jsPerf 上

在这里,我们再次看到 Firefox 更喜欢strobject.valueOf(),因为对于strprimitive.valueOf(),它会向上移动继承树,从而为strprimitive创建一个新的包装器对象。此事件链对性能的影响也可以在下一种情况下看到。

10. for-in 包装器对象

  var i = "";
  for (var temp in strprimitive) { i += strprimitive[temp]; }

  var k = "";
  for (var temp in strobject) { k += strobject[temp]; }

此基准测试将通过循环将字符串的值增量构建到另一个变量中。在 for-in 循环中,要计算的表达式通常是对象,但如果表达式是原始值,则此值会被强制转换为其等效的包装器对象。当然,这不是获取字符串值的推荐方法,但它是创建包装器对象的众多方法之一,因此值得一提。


原始字符串与包装器对象字符串 - 属性,在 jsPerf 上

正如预期的那样,Chrome 似乎更喜欢原始值字符串strprimitive,而 Firefox 和 Safari 似乎更喜欢对象字符串strobject。如果这看起来很典型,让我们继续进行最后一个基准测试。

11. 使用对象字符串添加两个字符串

  var str3 = new String(" there");

  var i = strprimitive + str3;
  var k = strobject + str3;


原始字符串与包装器对象字符串 - 2 个 str 值,在 jsPerf 上

在前面的示例中,我们已经看到,如果我们的初始字符串是对象字符串(如strobject),则 Firefox 版本提供了更好的性能,因此在使用另一个对象字符串添加strobject时,期望相同的结果似乎很正常,这基本上是同一件事。但是,值得注意的是,当使用对象字符串添加字符串时,如果我们使用strprimitive而不是strobject,它在 Firefox 中实际上会更快。这再次证明了源代码变化(例如修补错误)如何导致不同的基准测试数字。

结论

根据上面描述的基准测试,我们已经看到了关于字符串声明中的细微差异如何产生一系列不同性能结果的多种方式。建议您继续像往常一样声明字符串变量,除非您有非常具体的理由创建 String 对象的实例。此外,请注意,浏览器的整体性能,尤其是在处理 DOM 时,不仅仅基于页面的 JS 性能;浏览器中包含的内容远不止其 JS 引擎。

非常感谢您的反馈意见。谢谢 :-)

关于 Panagiotis Tsalaportas

Panagiotis 是 Mozilla 社区成员,也是 Mozilla 开发者网络和 Firefox 的贡献者。

更多 Panagiotis Tsalaportas 的文章…


31 条评论

  1. Nicholas C. Zakas

    精彩的文章。我想提个建议,只是为了使其更具可读性:如果您能将 str1 和 str2 重命名为 strprimitive 和 strobject,将会更容易理解。我发现自己经常向上滚动以仔细检查每个变量代表什么。

    2012 年 12 月 5 日 15:26

    1. Panagiotis Tsalaportas

      非常感谢 Nicholas!:-)
      我已根据建议应用了更改。
      您的文章一直是 JavaScript 的宝贵资源!

      2012 年 12 月 6 日 08:33

  2. Felipe N. Moura

    精彩的文章!
    我知道原始对象会更快,但从未真正理解原因!
    良好的数据和测试,以及不错的主题!

    我创建了另一个测试,将字符串添加到数字中,以便浏览器必须将该数字转换为字符串,并使用原始字符串和字符串对象进行了比较。

    测试在这里运行:http://jsperf.com/comparing-primitive-to-object-string-numbers

    2012 年 12 月 5 日 15:33

    1. Panagiotis Tsalaportas

      感谢 Felipe 的赞美之词!
      您的基准测试非常有趣;似乎所有浏览器在某种程度上都更喜欢字符串对象以进行操作。我建议您也看看 Number 对象。;-)

      2012 年 12 月 6 日 08:17

  3. Daniel Lewis

    尝试扩展 String.prototype 并查看从对象与原始值调用自定义方法是否存在很大差异,这可能也很有趣。

    同样,看看从对象与原始值调用的自定义方法中使用的“this”的操作速度是否存在任何差异,这也很有趣。

    2012年12月5日 18:13

    1. Panagiotis Tsalaportas

      Daniel说得太对了。还有很多其他方面可以与这个主题相关的基准测试;还有Number对象。:-)

      2012年12月6日 08:31

  4. Dave Herman

    我不理解.valueOf()测试的意义。我看不出在原始字符串上使用该方法的任何理由——它什么也没做!

    Dave

    2012年12月5日 23:32

    1. Karel Jára

      但是[str1 + ” there”]和[str1.valueOf() + ” there”]的速度差异相当奇怪。实际上,每秒操作数至少相差两个数量级……

      2012年12月6日 03:23

    2. Panagiotis Tsalaportas

      Karel说得对,感谢你指出这一点。
      Dave,当然你不会在原始字符串上使用它,也不会通过for-in循环读取字符串的值;这是一篇关于mozhacks的文章,这里发生了一些奇怪的事情……^^

      一句话,最终结果将使开发人员意识到在运行时访问String.prototype属性/方法有多少种方法,以及这些代码变化如何影响整体基准测试用例。;-)

      2012年12月6日 08:22

  5. Alberto Arena

    非常有趣的文章,感谢分享。

    2012年12月6日 01:59

    1. Panagiotis Tsalaportas

      谢谢Alberto!

      2012年12月6日 08:23

  6. Vyacheslav Egorov

    像1、2、6、7、8、9、11这样的微基准测试都可以被常量折叠掉。这在实际应用中不太可能发生。其中一些操作也已经受到循环不变代码移动的影响,因此你最终测量的是单个操作的成本加上jsperf围绕它生成的循环的成本。

    我建议生成一个字符串列表,并在循环中对它们应用相同的操作,并计时。

    2012年12月6日 02:42

    1. Panagiotis Tsalaportas

      好吧,生成一个字符串列表会引入另一组“运行时变量”,这些变量也会影响总操作时间。我认为在这篇文章中我们想要测量的是单个操作。请参阅我回复Dave的回复,了解更多信息。

      2012年12月6日 08:25

      1. Vyacheslav Egorov

        所以我坐下来写下了为什么这种简单的微基准测试没有用

        http://mrale.ph/blog/2012/12/15/microbenchmarks-fairy-tale.html

        2012年12月16日 10:41

        1. Panagiotis Tsalaportas

          Vyacheslav,首先我要说你的后续文章非常棒,能够为我们提供这些信息真的很好,因为我一直想在评论中看到这些信息。

          我认为创建jsperf网站的初衷是提供更多“洞察力”,以了解一些处理*DOM*的JavaScript库函数是否比其他函数更快,原因有很多,例如原生查询命令的存在。但是,再次强调,出于显而易见的原因(正如其他开发人员所表达的那样),这也值得怀疑。相反,这里的目的不是区分哪种变量声明会产生更快的结果,因此敦促开发人员使用一种而不是另一种,恰恰相反。这是一个展示,它将为大多数开发人员提供一些示例,以便了解(1)ECMAScript规范的JS引擎实现中存在一定程度的自由度,因此并非语言的每个方面都在所有引擎中都以完全相同的方式实现,以及(2)细微的代码变化最终将触发解释一些非常相似的代码段的不同方式。

          实际上,你的帖子可以作为感兴趣的开发人员的额外资源。正如文章上面提到的,浏览器中还有很多东西,不仅仅是它的JS引擎。感谢你的评论。:-)

          2012年12月16日 14:54

          1. Vyacheslav Egorov

            我既不否认jsperf的有用性也不否认它的重要性。

            我唯一想说的是:开发人员应该意识到微基准测试的陷阱,并避免从中得出错误的结论。就是这样:-)

            2012年12月16日 15:07

  7. Arun David

    非常有用且内容丰富的文章..谢谢

    2012年12月6日 03:47

    1. Panagiotis Tsalaportas

      谢谢Arun!

      2012年12月6日 08:26

  8. Tomas Corral

    这篇文章真的很棒,因为这是告诉我们的学生为什么不使用new String创建字符串的重要原因之一。

    2012年12月6日 06:18

    1. Panagiotis Tsalaportas

      很高兴看到这一点!

      2012年12月6日 08:26

  9. Dylan Schiemann

    不错的文章……我很想知道Opera和Internet Explorer在你的测试中的表现如何。过去,IE 6/7/8的表现与其他所有浏览器截然不同(这并不奇怪,但对于像字符串操作这样基本的东西来说,这令人失望)。

    我们在2008年写过几篇关于这个主题的文章(现在已经过时了,绝对需要更新,但它们涵盖了一些类似的要点):http://www.sitepen.com/blog/2008/05/09/string-performance-an-analysis/http://www.sitepen.com/blog/2008/06/09/string-performance-getting-good-performance-from-internet-explorer/

    2012年12月6日 06:29

    1. Panagiotis Tsalaportas

      谢谢!很想知道这些文章更新后的内容,Dylan。 :)

      2012年12月6日 08:28

    2. Panagiotis Tsalaportas

      实际上,在现代浏览器上进行基准测试是值得的,因为大多数JS引擎优化都发生在那里。

      2012年12月7日 14:24

  10. Blaise Kal

    @Dylan Schiemann

    你可以通过点击“……在jsPerf上”链接查看Opera和IE的表现。

    2012年12月6日 07:03

  11. Mauricio Samy Silva

    非常感谢Panagiotis,
    这篇有用的文章已被翻译成巴西葡萄牙语。
    翻译托管在:http://www.maujor.com/blog/2012/12/06/objetos-string-da-javascript-e-performance/

    2012年12月6日 12:28

    1. Robert Nyman – 编辑

      太棒了,非常感谢你的翻译!

      2012年12月7日 02:31

    2. Panagiotis Tsalaportas

      非常感谢!

      2012年12月7日 14:22

  12. Caesar

    这是一篇分析String的有用文章。示例代码和性能比较易于理解。谢谢。

    2012年12月6日 18:55

    1. Panagiotis Tsalaportas

      谢谢!

      2012年12月7日 14:22

  13. Swader

    精彩的文章,谢谢!已分享到Google Plus的Web性能社区,网址为gplus.to/webperf

    2012年12月17日 00:44

    1. Panagiotis Tsalaportas

      感谢Swader提供的链接!

      2012年12月17日 05:32

本文的评论已关闭。