JavaScript性能优化:(五)优化字符串操作和正则表达式

几乎所有的 JavaScript 程序都与字符串操作密切相关。一个典型的应用程序通常需要大量类似合并、分隔、重新排序、搜索、遍历等字符串操作。

对于复杂的文本匹配,正则表达式是必不可少的,我们有必要学习如何编写高性能的正则表达式。

合并字符串

拼接字符串是编程中最常见的操作了,我们先来总结一下 JavaScript 中连接字符串的方法:

  • +
  • +=
  • Array.prototype.join()
  • String.prototype.concat()
  • ES6 新增的模板字符串

当拼接少量较短字符串,以上这些方法速度都差不多。但随着需要连接的字符串的长度和数量的增加,一些方法开始展现出优势。

待连接字符串是变量

如果要拼接的字符串中有以变量形式存在的字符串,优先使用 ES6 中的模板字符串。

将变量名包裹在 ${} 中,然后使用反引号(` )包裹变量和普通字符串:

1
`${username} is from ${country} .`

++=

我们首先必需弄明白一点,在 JavaScript 中字符串一经初始化值便是不可改变的。

下面是一个拼接字符串的常见操作:

1
str += 'one' + 'two';

上面代码在运行时,会历经以下四个步骤:

  1. 在内存中创建一个临时字符串
  2. 拼接后的字符串 onetwo 被赋值给该临时字符串
  3. 临时字符串与 str 当前的值进行连接
  4. 将上一步的拼接结构赋值给 str

为了避免产生临时字符串造成额外的性能开销,我们可以这样做:

1
2
str += 'one';
str += 'two';

如果使用下面这种方式,我们能获得更显著的性能提升:

1
2
str = str + 'one' + 'two';
// 等价于 str = ((str + 'one') + 'two');

注意,上面代码中如果赋值号右侧表达式中的 str 不是处在最左侧,那么将得不到优化效果,这与浏览器合并字符串时分配内存的方法有关。大多数浏览器都会尝试为表达式左侧的字符串分配更多的内存,然后简单地将第二个字符串拷贝至它的末尾。如果在一个循环中,基础字符串位于最左侧的位置,就可以避免重复拷贝一个逐渐变大的基础字符串。

合并数组项

Array.prototype.join() 方法将数组中地所有元素合并成一个字符串,它接收一个参数作为每项之间的分隔符。如果传入参数为空字符,那么我们就可以使该方法将所有数组项连接为一个字符串。

然而,现实情况是,在大多数浏览器中,数组项合并比其他字符串连接方法更慢。

String.prototype.concat()

包装类型 String 的原生方法 String.prototype.concat() 能够接受任意数量的参数,并将每一个参数附加到所调用的字符串上,这是最灵活的字符串合并方法。

遗憾的是,在多数情况下,使用 cancat() 方法比使用简单的 ++= 稍慢。

总结:如果要合并的字符串中存在变量,那么使用模板字符串;否则,仅使用简单的 + 操作符合并字符串,并将基础字符串置于表达式最左侧。

正则表达式优化

回溯失控

回溯失控的正则表达式可能会导致浏览器假死数秒甚至更长时间,为了避免出现回溯失控,编写正则表达式时可以考虑如下方案:

  • 尽可能具体化分隔符之间的字符串匹配模式
  • 使用预查和反向引用的模拟原子组
  • 保证正则表达式的两个部分不能对字符串的相同部分进行匹配
  • 尽可能保持正则表达式简洁易懂

提高正则表达式效率

  • 关注如何让匹配更快:
    正则表达式慢的原因是匹配失败的过程慢而不是匹配成功的过程慢

  • 以简短、必需的元字符开头:
    尽可能地避免以分组或分支或选择性元字符开头,这样会造成性能损失

  • 使用量词模式,使它们后面的元字符互斥:
    具体化匹配模式,避免出现字符与元字符相邻或子表达式能够重叠匹配的情况

  • 减少分支数量,缩小分支范围:
    字符集比分支更快;如果不可避免地使用分支,应将概率最大的分支放在靠前的位置

  • 尽量避免使用捕获分组:
    捕获组消耗时间和内存来记录反向引用,并使它保持最新

  • 只捕获需要的文本以减少处理:
    如果需要引用匹配,应该采取一切手段捕获那些片段,再使用反向引用来处理

  • 暴露必需的元字符:
    尽可能地让引擎判断那些元字符是必需的

  • 使用合适的量词:
    贪婪和惰性量词的匹配过程有较大区别,使用更合适的量词类似可以显著提升性能,尤其是在处理长字符时

  • 使用局部变量缓存正则表达式并重用

  • 拆分复杂的正则表达式为简单片段: 避免在一个正则表达式中处理太多任务

不使用正则表达式

当仅仅是搜索某个字符串特定位置上的值时,我们没必要动用正则表达式,因为那样不但性能低下,反而可能会弄巧成拙。

包装类型 String 拥有的 charAt()slice()substr()substring()indexOf()lastIndexOf() 等原生方法都非常适合查找特定字符串的位置,或者判断它们是否存在。

对字符串进行操作,在使用正则表达式之前,先考虑一下这些原生的 String 方法,它们有助于避免正则表达式带来的性能开销。


参考

  • 《高性能JavaSript》,[美]Nicbolas C.Zakas 著,丁琛 译,电子工业出版社 2015
  • 《精通JavaSript开发》,[英]Den Odell 著,邝健威 厉海洋 译,人民邮电出版社 2015