3、书写高性能的代码
1、对局部变量的使用
如果一个非局部变量在函数中的使用次数不止一次,那么最好使用局部变量进行存储。
2、作用域链的增长
不要使用 with,提升局部变量。
3、警惕闭包的使用
4、使用 switch(大于2个判断条件时)和 if-else(小于 2 个判断条件时)
通常对于多个离散值的取值条件判断,使用switch会比if-else具有更高的性能表现。
如果只有一两个条件的判断,通常if-else处理条件的时间会比switch更快,当判断条件多到两个以上时,因为在大多数时候,switch处理单个条件的时间比if-else更快,所以switch更加适合。
5、if-else 的优化
如果程序最终的执行路径是最后一个else if子语句,那么当执行到此处之前,其余所有条件判断必然都要经历一遍。这也就是说,匹配最后一个条件的情况,会比之前所有判断条件执行耗时都久。
基于此便有了两种优化方式,第一种优化方式是开发者可以预先估计条件被匹配到的频率,按照频率的降序顺序来排列if-else语句,可以让匹配频率高的条件更快执行,从而在整体上降低程序花费在条件判断上的时间。
第二种优化方式是利用二分法的思路,可能开发人员在编写相应的业务代码时,并不能预先估计出各种条件在多次执行时被匹配到的频率,但却能对取值区间的边界有明确的划分,那么便可以用二分取值范围来降低匹配条件的执行次数:
6、数组索引和对象属性(取值范围/条件非常大时)
除了if-else语句和switch语句,利用数组的索引查询或对象的属性查询也可以达到类似条件判断的目的,代码如下:
当匹配条件的数量较小时,并不适合使用这种基于数组或对象的查找方式,因为查找数组或映射对象属性值往往比执行少量的条件判断语句要慢,只有当取值范围变得非常大时,这种查找方式的性能优势才会凸显出来。
另外,这种基于对象属性查找的方式,就是应用了设计模式中的策略模式。
策略模式就是定义一系列的处理流程或算法,把它们分别封装起来,使得它们可以相互替代。其目的就是将算法的使用和实现分离
7、条件判断的使用建议
关于使用条件判断时,代码的书写建议如下:
●当所要匹配的条件仅为一两个离散值时,或者容易划分不同取值范围时,使用if-else语句。
●当所要匹配的条件超过一两个但少于十个离散值时,使用switch语句。
●当所要匹配的条件超过十个离散值时,使用基于数组索引或对象属性的查找方式。
8、使用 for 循环、while 循环 或 do-while 循环,而不是 forEach、for-in 等语法糖
由于该比较操作执行的过程中数组长度一般不会改变,且存取局部变量要比查找属性值更省时,所以提前将要遍历的数组长度声明为局部变量,然后将该局部变量进行循环结束的条件判断,效率会更高一些。
这在对包含较大规模DOM节点树的遍历过程中,效果会更加明显。
此外还有一种更简单地提升循环语句性能的方式:将循环变量递减到0,而非递增到数组总长度。
因为循环结束的判断是和常量0进行比较的,不存在对数组长度属性值的查找或局部变量的读取,其比较的运算速度会更快。
对通常的循环使用场景来说,由于它遍历属性的顺序不确定,循环的结束条件也无法改变,并且因为需要从目标对象中解析出每个可枚举的属性,即要检查对象的原型和整个原型链,所以其循环速度也会比其他循环方式要慢许多,如果循环性能有要求则尽量不要使用for-in循环。
这种方法使用起来的确会让数组元素的迭代看起来更加直观,但在通常情况下与三种基本的循环方法相比,其性能方面仅能达到后者的1/8,如果数组长度较大或对运行速度有比较严格的要求,则函数迭代的方式不建议使用。
同时还有一种for语句的变形,就是ES6加入的for-of循环,我们可以使用它来代替for-in和forEach循环,它不仅在性能方面比这二者更好,并且还支持对任何可迭代的数据结构进行遍历,比如数组、字符串、映射和集合,但与三种常规循环语句相比其性能还是稍逊色一些的。
9、降低对真实 DOM 的修改频率
所谓虚拟DOM就是将真实的DOM抽象为JavaScript对象,用户交互和数据运算可能会带来DOM频繁修改,但这其中大部分的修改操作可能对最终呈现给用户的页面来说都是中间过程,所以就将这些大量的中间过程交由JavaScript处理,处理完成后统一再去修改真实的DOM,这样便尽可能多的降低对真实DOM的修改频率。
10、不要用过深的递归或将递归改写成迭代
浏览器对JavaScript调用栈存在限制,超出限制递归执行便会失败,将递归改写成迭代能有效地避免此类问题。
任何递归函数都可以改写成迭代的循环形式,虽然循环会引入自身的一些性能问题,但相比于长时间执行的递归函数,其性能开销还是要小很多的。
11、使用 异步队列
JavaScript是单线程的,这就意味着浏览器的每个窗口或页签在同一时间内,要么执行JavaScript脚本,要么响应用户操作刷新页面,也就是说这二者的行为是相互阻塞的。例如JavaScript代码正在执行时,用户页面会处于锁定状态无法进行输入,如果JavaScript代码执行时间过长,显然会给用户带来糟糕的体验。。
既要处理运算又要响应与用户的交互,它是如何完成的呢?答案是异步队列。
当我们创建一个异步任务时,它其实并没有马上执行,而是被JavaScript引擎放置到了一个队列中,当执行完成一个任务脚本后,JavaScript引擎便会挂起让浏览器去做其他工作,比如更新页面,当页面更新完成后,JavaScript引擎便会查看此异步队列,并从中取出一个任务脚本去执行,只要该队列不为空,这个过程便会不断重复,当队列中的任务脚本执行完后,JavaScript引擎便处于空闲状态,直到有新的任务脚本进入该异步队列。
据此我们便有了对执行过长任务的一种优化策略,即将一个较长的任务拆分为多个异步任务,从而让浏览器给刷新页面留出时间,但过短的延迟时间也可能会让浏览器响应不及时,因为在几毫秒的时间里无法正确完成页面的更新与显示,通常可以使用定时器来控制一个100ms左右的延迟,同时定时器也是JavaScript中创建一个加入异步队列十分有效的方法:
这个例子只是为了说明起见,让每个异步任务仅处理数组中的一个数据集,对于较大规模的数组,可预先规定单次异步任务的处理数量,然后拆分数据集,依次加入异步队列进行处理。
12、使用位操作
几乎在所有编程语言中,位操作的执行速度都是十分快的,因为位操作通常发生在系统底层。在JavaScript中使用有符号的32位二进制来表示一个数字,位操作就是直接按照二进制方式进行计算的,这要比其他数学运算和布尔操作快得多。
JavaScript中支持6种位操作,分别是按位与、按位或、按位异或、按位取反及按位左移和按位右移,其计算示例如下:
两个使用位操作来提升JavaScript性能的场景:
一种场景是首先可以使用位操作来代替一些数学运算,比如需要对一个数组的奇偶位分别进行不同逻辑的处理场景。通常的做法就是在遍历数组时,将数组索引除以2取余数,看余数是否为0来判断奇偶。
当换用位操作时,我们发现奇数与偶数的差别其实就是判断其二进制最低位是1还是0,这样就可以简单地通过将遍历数组的索引值与1进行按位与来完成。偶数时的按位与结果是0,奇数时的按位与则是1。
仅修改了对奇偶判断的处理,但其带来的性能提升与之前的取余操作相比,通常是翻倍的,循环规模越大带来的提升越明显。
另一种场景是当需要判断某个选项值是否在备选集合中时,可以使用单个数字的每一位代表一个备选选项,然后使用按位与的方式进行判断。这种方式下每个选项的声明需要基数为2的不同幂:
使用位操作能够大幅度提升较大规模循环迭代中条件判断的数学运算性能,在实际开发中应注意使用。