5. 函数优化
5.1 提炼函数
在javascript开发中,大部分时间都在与函数打交道,所以希望这些函数有着良好的命名,函数体内包含的逻辑清晰明了。
如果一个函数过长,不得不加上若干注释才能让这个函数显得易读一些,那这些函数就很有必要进行重构
如果在函数中有一段代码可以被独立出来,那最好把这些代码放进另外一个独立的函数中,这是一种很常见的优化工作。
项目越大,必须拆分得越细,好维护(函数也是如此)
千万不要一个函数从奶奶家写到外婆家去了
这样做的好处主要有以下几点:
避免出现超大函数
独立出来的函数有助于代码复用
独立出来的函数更容易被覆写
独立出来的函数如果拥有一个良好的命名,它本身就起到了注释的作用
比如在一个负责取得用户信息的函数里面,还需要打印跟用户信息有关的信息,那么打印的语句就可以被封装在一个独立的函数里:
let getUserInfo = function(){ ajax( 'http:// xxx.com/userInfo', function( data ){ console.log( 'userId: ' + data.userId ); console.log( 'userName: ' + data.userName ); console.log( 'nickName: ' + data.nickName ); }); }; //改成: let getUserInfo = function(){ ajax( 'http:// xxx.com/userInfo', function( data ){ printDetails( data ); }); }; let printDetails = function( data ){ console.log( 'userId: ' + data.userId ); console.log( 'userName: ' + data.userName ); console.log( 'nickName: ' + data.nickName ); };
5.2 尽量减少参数
调用一个函数时需要传入多个参数,那这个函数是让人望而生畏的(甚至调用起来还会骂娘~)
必须搞清楚这些参数代表的含义,必须小心翼翼地把它们按照顺序传入该函数。
在实际开发中,向函数传递参数不可避免,但应该尽量减少函数接收的参数数量
比如我们需要封装一个CSS操作库:
第一版的代码
function cssTransform(ele, attr, val){ // ele 要操作的元素 // attr 运动属性 缩放,旋转 // val 具体值 if(!ele.transform){ ele.transform = {}; } if(typeof val === "undefined"){ // 取值阶段 // 取不到 设置默认值 if(typeof ele.transform[attr] === "undefined"){ switch(attr){ case "scale": case "scaleX": case "scaleY": case "scaleZ": // attr是scale 默认值是1 其它都是0 ele.transform[attr] = 1; break; default: ele.transform[attr] = 0; } } // 取值完毕 返回该值 return ele.transform[attr]; }else{ // 赋值阶段 ele.transform[attr] = val; // 设置属性 方便取值 let transformVal = ""; for(var s in ele.transform){ switch(s){ case "scale": case "scaleX": case "scaleY": case "scaleZ": transformVal += " " + s + "("+(ele.transform[s])+")"; break; case "rotate": case "rotateX": case "rotateY": case "rotateZ": case "skewX": case "skewY": transformVal += " " + s + "("+(ele.transform[s])+"deg)"; break; default: transformVal += " " + s + "("+(ele.transform[s])+"px)"; } ele.style.WebkitTransform = ele.style.transform = transformVal; } } }
- 我们需要传入三个参数,这就导致了传参的复杂性
- 可以将它绑定到HTML元素上,减少传参
HTMLElement.prototype.cssTransform = function (prop, value){ var transform, transformValue = ""; if(this.transform === undefined) { this.transform = transform = Object.create(null); } if(value !== undefined){ // 赋值阶段 this.transform[prop] = value; transform = this.transform; for(var name in transform){ switch(name){ case "scale": case "scaleX": case "scaleY": case "scaleZ": transformValue += " " + name + "("+ transform[name]+")"; break; case "rotate": case "rotateX": case "rotateY": case "rotateZ": case "skewX": case "skewY": transformValue += " " + name + "("+ transform[name] + "deg)"; break; default: transformValue += " " + name + "("+ transform[name] + "px)"; } this.style.WebkitTransform = this.style.transform = transformValue; } }else{ // 取值 return this.transform[prop]; } }
- 现在就只需要两个参数了
- 核心代码没变,事件直接绑定在元素上,每一个html都拥有这个原型方法
- 调用更加方便,传参更加简单
6.条件优化
6.1 合并条件判断
如果一个函数体内有一些条件分支语句,而这些条件分支语句内部散布了一些重复的代码,那么就有必要进行合并去重工作。
假如有一个分页函数paging(),该函数接收一个参数currPage,currPage表示即将跳转的页码。
在跳转之前,为防止currPage传入过小或者过大的数字,要手动对它的值进行修正,详见如下代码
let paging = function( currPage ){ if ( currPage == 0 ){ currPage = 0; jump( currPage ); // 跳转 }else if ( currPage == totalPage ){ currPage = totalPage; jump( currPage ); // 跳转 }else{ jump( currPage ); // 跳转 } };
- 可以看到,负责跳转的代码jump(currPage)在每个条件分支内都出现了,所以完全可以把这句代码独立出来
let paging = function( currPage ){ if ( currPage == 0 ){ currPage = 0; }else if ( currPage == totalPage ){ currPage = totalPage; } jump( currPage ); // 把jump 函数独立出来 };
6.2 条件语句过多,提炼成函数
- 在程序设计中,复杂的条件分支语句是导致程序难以阅读和理解的重要原因,而且容易导致一个庞大的函数
- 假设现在有一个需求是编写一个计算商品价格的 getPrice(),商品的计算只有一个规则:如果当前正处于夏季,那么所有冬装将以5折出售
let getPrice = function( price ){ let date = new Date(); if ( date.getMonth() > 6 && date.getMonth() < 10 ){ // 冬天 return price * 0.5; } return price; };
- 判断是否处于夏季
if ( date.getMonth() > 6 && date.getMonth() < 10 )
这句代码要表达的意思很简单,就是判断当前是否正处于夏天(7 - 9月)。
尽管这句代码很短小,但代码表达的意图和代码自身还存在一些距离,阅读代码的人必须要多花一些精力才能明白它传达的意图。
其实可以把这句代码提炼成一个单独的函数,既能更准确地表达代码的意思,函数名本身又能起到注释的作用
let isSummer = function(){ let date = new Date(); return date.getMonth() > 6 && date.getMonth() < 10; }; let getPrice = function( price ){ if ( isSummer() ){ // 夏天 return price * 0.5; } return price; };
7. 循环优化
7.1 高效循环
- 在函数体内,如果有些代码实际上负责的是一些重复性的工作
- 合理利用循环不仅可以完成同样的功能,还可以使代码量更少
- 以for循环为例,最常规写法
for (let i = 0; i < arr.length; i++) { // do something... }
- 大多数人都是这种写法,这种写法的缺点在于,每次循环都要去读取一次数组的长度,性能上并不友好
- 变量情况的优化写法
for (let i = 0, j = arr.length; i < j; i++) { // do something... }
- 将长度进行存储,之后循环无需再去读取长度
- 这只是上面写法的一种变体,另一种写法而已,谈不上优化。因为无块级作用域,所以和上面的效果是一样的
- 优化版
for (let i = arr.length - 1; i >= 0; i--) { // do something... }
推荐的写法,它在第上面的基础上节约了一个变量
8. 如何提升js性能
8.1 性能至关重要
性能是创建网页或应用程序时最重要的一个方面,特别是加载性能,如果让用户等久了,别人干脆不等了,会造成用户流失
用户对于应用的体验普遍要求提高了(现在来说,2s 内打不开网页的都是不合格的网站)
应用的性能瓶颈,依然是在js上
8.2 优化方向
1. 删除未使用的js代码
未使用的功能性代码以及与之相关的代码(它一样的会编译,运行)
多余的依赖(不需要的及时删除)
2. 数组与对象避免使用构造函数
构造函数是啥?
new Array(), new Object()等
demoObj = () => { let obj = new Object() obj.name = '朱小明' obj.age = 10 obj.sex = '男' return obj }
- 推荐写法
demoObj = () => { let obj = { name: '朱小明', age: 10, sex: '男' } return obj }
3. 避免全局变量
js 的垃圾回收机制是不会销毁全局变量的
所以它会常驻内存
4. 合理使用闭包
闭包可以提供面向对象编程的便利,有私有属性和方法
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存
过度使用闭包可能会导致内存占用过多的问题。
所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
5. 减少循环中的活动
循环本来就耗性能(特别是数据量大的)
方法尽量不要放在循环中执行(执行多次,特别是逻辑复杂的方法,后果可想而知)