本文旨在探讨 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.length
快2.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 的贡献者。
31 条评论