高性能 JavaScript - 字符串和正则表达式

这几天分享一下我看《高性能 JavaScript》的学习笔记,希望能对大家有所帮助。

字符串连接

字符串的合并是通过循环来完成的,所以在性能上不同的写法性能是有差别的。

str += 'one' + 'two'
str = str + 'one' + 'two'
str = 'one' + str + 'two'

上面三种写法中:

  • 第一种写法会先创建一个零时字符串 'onetwo' 然后再合并到 str 上去。
  • 第二种写法会直接将字符串 'one' 和 'two' 添加到 str 后面,不产生零时字符串。
  • 第三种写法也会创建临时字符串再将三个字符串依次加到临时字符串后面,最后将临时字符串赋值给 str。

性能上第二种写法最优。

注意:在 Firefox 中字符串合并在编译器环境下就完成了,而其他浏览器会在运行时合并字符串。

一种通过字符串数组合并为字符串的方式为 strArr.join('')。这种方式很方便的合并了数组中所有字符串。而性能方面,在 IE7 及其之前数组合并字符串的方式更快,而最新的浏览器使用遍历的方式合并字符串更快。

还有一种合并字符串的方式是通过 String.prototype.concat() 方法来合并多个字符串,并返回一个合并后的新字符串。但是 String.prototype.concat() 方法的性能要比 + 和 += 慢。

所以,字符串合并尽量使用 + 和 += 来实现,避免出现临时字符串。在合并数组字符串时可以使用 join('') 方法来快速合并。

正则表达式

正则表达式是非常强大的和好用的。那么正则的本质是什么呢?其实正则是一种遍历行为算法。关于这点在 LeetCode 中有不少实现正则语句的算法题大家可以了解下~

既然正则匹配行为是一种遍历,那么当遇到正则语句或者匹配内容非常长的情况下,匹配就需要花费更长的时间。

工作原理

  1. 编译正则表达式
  2. 设置起始位置 startIndex
  3. 从前往后进行正则表达式字元匹配
  4. 如果全部匹配成功,返回结果;如果匹配失败,则回溯到上一个匹配成功的点上继续匹配。
  5. 如果当前起始位置所有匹配全部失败,起始位置 startIndex 向后移动一位开始重新匹配。回到步骤 2。

回溯

我理解正则的匹配过程就像是一个 DFS (深度优先)算法,需要逐步走到头去看看是否能够匹配上。如果匹配失败,就回到最后一个匹配成功的节点上重新开始下一种可能性的匹配。这种回到上一个匹配成功节点的行为就是回溯。

回溯行为在遇到正则表达式太笼统、匹配的字符串更好近似与目标字符串时等条件是表现很差。比如像重复回溯问题:

var str = "<p>Para 1.</p>" +
    "<img src='smiley.jpg'>" +
    "<p>Para 2.</p>" +
    "<div>Div.</div>";

/<p>.*<\/p>/i.test(str);

由于正则表达式写的太笼统,正则匹配会很慢,并且引起浏览器的假死。这被称为回溯失控。

如何避免回溯失控

  • 具体化 —— 使用更加具体的正则表达式描述替代宽泛的描述,越具体越好。
  • 使用模拟原子组
  • 做基准测试 —— 为正则构建一组近似但是不完全匹配的字符串来测试正则表达式。

提高正则表达式效率的 10 个方法

  1. 关注让匹配更快失败。因为正则表达式匹配成功很快,而匹配失败需要尝试所有可能性,会很慢。
  2. 以简单、必需的字元开头。
  3. 使用量词模式,使用互斥字元。
  4. 减少条件分支数量,缩小分支范围。因为 DFS 算法需要遍历所有的分支。
  5. 使用非捕获组
  6. 只捕获感兴趣的文本,以减少后处理。
  7. 正则中暴露必要的字元,写正则不要太笼统。
  8. 使用合适的词量,贪婪或者惰性。
  9. 把正则表达式赋值给变量并重用它,因为正则表达式每次定义都需要编译。
  10. 将复杂正则表达式拆分为简单的片段。避免在一个表达式中处理太多任务。这样可以减小回溯的问题。

如何去除字符串前后的空白

有两个思路,遍历或者正则。这里使用正则看似很方便,但是正则匹配的算法需要匹配的次数很多,尤其是处理字符串末尾的空格。下面是两种我认为不错的实现方式:

if (!String.prototype.trim) {
    String.prototype.trim = function () {
        return this.replace(/^\s+/, "").replace(/\s+$/, "");
    }
}

书上说,使用首尾两次正则要比一次检查两类正则效率要高。

if (!String.prototype.trim) {
    String.prototype.trim = function () {
        var str = this.replace(/^\s+/, ""),
            end = str.length - 1,
            ws = /\s/;

        while (ws.test(str.charAt(end))) {
            end--;
        }

        return str.slice(0, end + 1);
    }
}

以上方式是正则和遍历的混合用法,字符串头部的空格由正则检查,而尾部空格通过反向遍历来来实现。这种方式优化了尾部空格的处理方式,挺不错的想法。而且它的性能非常好。

小结

相比于字符串合并,正则相关知识对我收获更大。特别是在做算法题的时候明白了正则的原理后不会再滥用、乱用正则了。

  1. 字符串凭借最好的方式还是使用 + 和 += 来实现,避免产生临时字符串。
  2. 正则匹配其实是一种 DFS 算法,它匹配过程中一直会用到回溯。
  3. 回溯在一些情况下需要处理很多的条件而导致性能问题,会让浏览器一段时间内处于假死状态。
  4. 书中给出了 10 条优化正则匹配的建议。
  5. 去除字符串首尾空格的方式让理解了正则并不是万能的,在性能方面循环更加的灵活。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 最近在阅读这本Nicholas C.Zakas(javascript高级程序设计作者)写的最佳实践、性能优化类的书...
    undefinedR阅读 6,423评论 0 30
  • 初衷:看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印...
    DCbryant阅读 9,484评论 0 20
  • 一、字符串在C#中,字符串是一系列不可修改的Unicode字符,创建字符串后,就不能修改它。要创建字符串,最常用的...
    CarlDonitz阅读 5,052评论 0 2
  • 9.19--9.23 第7章 正则表达式 正则表达式是一个拆分字符串并查询相关信息的过程。 推荐练习网站: js ...
    如201608阅读 4,678评论 0 4
  • 原来认为是重大如天的事,面对得久了也就觉得不那么重大了,最起码是可以部分忽视的了。 我决定后天去一个装修队干小工,...
    许蚀阅读 806评论 0 0