JavaScript代码优化 (By ES5/ES6/ES7) ---长期更新

自2015年6月17日(ECMAScript 2015(ES6)正式版发布至今已快三年,众多新特性使得JavaScript不再是一门“简陋”的语言,Node端在6.4.0之后的版本对ES6的原生支持已达到95%(参考:node各版本ES6支持程度表),而前端借助babel的编译也使得我们几乎可以随心所欲的在代码中使用各种ES6及之后版本的新特性而不用考虑不同浏览器(版本)的支持情况,我相信各位前端从业者or爱好者肯定也已经将一些新特性应用到了自己的项目中用于提升开发效率等。本文依据自己的体验与借鉴他人成果(会附原地址)记录了一些在JavaScript代码层面的优化,我会尽可能写得详尽,文章长期更新,内容之间可能会较为混乱敬请见谅!

1. 尾调用优化:

来源:阮一峰ES6入门-函数扩展-7
定义:尾调用指函数的最后一步调用另一个函数
前情概要: V8引擎对JavaScript函数调用会形成一个“调用记录”,又称”调用帧“,用于保存调用位置和内部变量信息等,其中一个典型就是“闭包”。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧,等到B运行结束将结果返回A,B的调用帧才会消失,如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推···所有的调用帧就形成了一个”调用栈“。 while(!kandong) {readAgain()}
调用栈示例:

function A() {
    const x = 1;
    const y = 2;
    return B(x + y);
}
A();

//上述函数调用栈等同于
function A() {
    return B(3);
}

//上述函数调用栈等同于
B(3);

如果函数A最后是const z = B(x + y);return z,那么A就要保存内部变量x和y的值与B的调用位置等信息,但如代码中这样写,调用B之后函数A的工作就结束了,同时A的调用帧就可以不再保存只留下B的,因此最终的调用帧只剩下B(3)。
意义:减少调用栈的长度,节省内存。
优化示例:部分人在前端笔试面试或其他渠道应该会遇到用JavaScript实现一个Fibonacci数列,实现方式很多,代码大部分人都能写出来,网上常见的一种实现方式为:

function Fibonacci(n) {
    if (n <= 1) return 1;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

这种方式简明扼要,代码比较容易看懂,不过因为每次递归都会向内存中保存新的调用帧,所以当n比较大时浏览器就会由于堆栈溢出引发崩溃,这种时候就需要使用到”尾调用优化了“,即将return的内容重构为单个函数。
大佬写法:

'use strict';
function newFibonacci(n, sum1 = 1, sum2 = 1) {
    if (n <= 1) return sum2;
    return newFibonacci(n - 1, sum2, sum1 + sum2)
}

这样当n比较大时主要影响计算力,内存方面就不会存在问题了。

注:使用尾调用优化需要开启严格模式,ES6类和模块的内部时默认开启严格模式的。

2.Boolean的另类表示

来源:同事之间交谈,源出处不详。
方式: JavaScript使用Boolean类型时可以考虑使用!1代替false,!0代替true,优化效果通常不很明显,但当文件中Boolean使用较多时就可以减少一定大小的体积,算是一个小优化,,弊端是表达不清晰,不易阅读。
使用情景: 如果打包过程开启了压缩,那么这个替换过程会由压缩工具自动完成,所以正常项目开发还是好好的用false和true吧~~。
效果模拟:这里写图片描述这里写图片描述这里写图片描述,从模拟结果可以看到当Boolean数量到一定大小时优化效果还是很明显的,当然在实际项目中不会存在这么多使用Boolean的地方,但蚂蚱也是肉,优化就是毫厘之间的积累,比如现在webpack配置uglify压缩打包后生成的js文件中就不会存在显式的true和fasle,都用!1和!0替代了:这里写图片描述

3.空对象作键值对时创建优化

来源: Hash maps without side effects
详解: 有时我们习惯创建一个“{}”来保存键值对信息,比如数组去重啊之类的,虽然ES6出了Map专门用来做键值对存储,但毕竟{}习惯了用着顺手,不过用{}当键值对它的_proto_属性就不发挥作用而占用内存,不是很合理,因此可以使用const map = Object.create(null)来创建类{}效果,这样创建的对象是默认没有任何属性的,特别纯,用法也没有差异。
效果:
这里写图片描述
使用场景: 不用到prototype便可以考虑用此法替代,当然键值对还是首推专业的Map,至于习惯嘛,该改还是得改。

4.减少作用域向上查找次数

来源: 基本操作,来源已不详。
概念: 当在某下层作用域多次用到一个上层对象或其某些属性时,在当前作用域创建一个变量暂存,可减少查找耗时。
例子: 看过jQuery源码的同学都知道jQuery刚开始就是(function(window,undefine){…})(window,undefined),这个undefined有另类作用此处不详说,大概就用来保证undifined一定是undefined,想了解的同学可自行查阅jQuery源码分析相关内容,另一个window的作用就是进行作用域降级,将全局作用域window编程局部作用域,这样当内部需要添加全局变量或使用到window时就能至少少访问一层,加快代码运行效率。
使用场景: 这个优化方式比较常用的,类似操作的还有css style、const tempState = Object.assign({},this.state),之类的,少一次访问,多一次速度优化,平时注意点大部分地方都可以用到,属于牺牲内存优化速度的范畴。

5.使用位运算替代一些计算方式

来源: 运算符——阮一峰
概念: 数值和字符串在内存中都是以0和1的形式存储,每个0或1称为一个‘位’,位运算符包括与(&)、或(|)、非(~)、亦或(^)、左移(<<)、右移(>>)、带符号右移(>>>),使用位运算符进行计算操作称为位运算。
优劣: 位运算是很底层的计算,速度很快,但不容易理解。
常用场景: 由于是二进制操作,因此在某些涉及2的计算就可以考虑是否能使用位运算符,常用场景有:

  1. 判断奇偶性:n & 1 ? 'odd ' : 'even'
  2. 2的n次方:1 << n
  3. 向下取整:~~decimals
  4. 交换两个整数的值:a ^= b;b ^= a;a ^= b;
  5. x乘以2的n次方:x << n
  6. x除以2的n次方: x >> n
    注意事项: 可读性不强,毕竟前端了解这个的不多,然后就是位运算符的计算优先级不高,所以别忘了加小括号。

6. cloneNode()拷贝节点取代反复新建

来源: 写项目的时候想到优化,然后去搜了搜。cloneNode()–w3school
概念: 当项目中涉及某dom元素反复创建使用删除(如果不删除可以考虑只更改css),那么每次都createElement消耗是较大的,而且代码量也多,这时可以考虑先初始化一个节点,之后调用该节点的cloneNode()进行节点拷贝,速度上会有较大的提升(莫不是亘古不变的复制比新建块?)。
实测效果: 阿西吧,万万没想到,创建节点比拷贝节点快难道cloneNode()的实现是createElement加属性拷贝吗·····各位,勇敢的追逐create吧。。。。。

后期附:根据这个问题我查阅了一些相关书籍(如高性能JavaScript),使用cloneNode和createElement在不同浏览器上速度有所差异,而且也和所要拷贝(创建)的节点复杂度有关,所以没有绝对的谁快谁慢,大家可以根据自己开发场景进行选择。

7. 判断条件多时使用Array/Object/Map/WeakMap代替if else和switch

来源: 基本操作and高性能JavaScript
概念: 如果有很长的条件控制,用if else和switch就要经过多次判断,最坏的结果要走过所有条件而且if else语句会不够直观switch代码会特长,这时候就可以考虑来一份Array/Object/Map/WeakMap套餐。
套餐优劣: 四者速度相当,Array可以代码最少,但是由于用下标获取,在代码可读性、扩展性、鲁棒性上都有一定的劣势;Object、Map和WeakMap都是键值对形式存储,优点兼具直观明了,Object使用最简单但不够专业,Map够专业但使用麻烦,前面这三者都会造成对象引用导致内部变量无法被回收(V8的引用计数回收机制),WeakMap弱引用避免内存泄漏但key只能是对象,用起来相对麻烦一些。
代码示例:

/* 
** 需求:我做了一个toC的服务盈利型app,已收集大量用户身份数据,现在准备
** 根据不同身份的用户进行不同的服务定价,假设基本价格为10元,人群价格划分
** 如下:小萝莉2元、古风系女子4元、可爱的coser6元、常规女性8元、常规男性
** 10元、苦逼程序员12元、邪恶资本家99元、死肥宅不卖(用不上)。
** 提供性别(1男0女)、身份信息(string),假设女性用户多(条件排序)。
*/
// if else写法
let price = 10;
if (sex === 0) {
    if (identity === '常规女性') {
        price = 8;
    } else if (identity === '小萝莉') {
        price = 2;
    } else if (identity === '古风系女子') {
        price = 4;
    } else {
        price = 6;
    }
} else {
    if (identity === '常规男性') {
        price = 10;
    } else if (identity === '苦逼程序员') {
        price = 12;
    } else if (identity === '邪恶资本家') {
        price = 99;
    } else {
        price = NaN;
    }
}
// switch写法
let price = 10;
switch (identity) {
    case '常规女性':
        price = 8;
        break;
    case '常规男性':
        price = 10;
        break;
    case '苦逼程序员':
        price = 12;
        break;
    case '死肥宅':
        price = NaN;
        break;
    case '邪恶资本家':
        price = 99;
        break;
    case '小萝莉':
        price = 2;
        break;
    case '古风系女子':
        price = 4;
        break;
    case '可爱的coser':
        price = 6;
        break;
    default:
        break;
}
// Array写法
const arr = [2, 4, 6, 8, 10, 12, 99, NaN];
const price = arr[i];

// Object写法
const obj = {
    '小萝莉': 2,
    '古风系女子': 4,
    '可爱的coser': 6,
    '常规女性': 8,
    '常规男性': 10,
    '苦逼程序员': 12,
    '邪恶资本家': 99,
    '死肥宅': NaN,
}
const price = obj[identity];
// Map写法
const map = new Map([['小萝莉', 2], ['古风系女子', 4],
    ['可爱的coser', 6], ['常规女性', 8], ['常规男性', 10],
    ['苦逼程序员', 12], ['邪恶资本家', 99], ['死肥宅', NaN]]);
const price = map.get(identity);

最终用谁可以自己根据实际情况抉择。

猜你喜欢

转载自blog.csdn.net/qq_39300332/article/details/79739855