本文适用于任何编程语言,但从JavaScript角度来讲解。
编码习惯
1. 尊重对象所有权
尊重对象所有权就意味着不要修改不属于你的对象。简单来说就是,如果你不负责创建和维护某个对象及其构造函数或方法,就不应该对其进行任何修改。具体来说就是遵循以下惯例:
- 不要给实例或原型添加属性
- 不要给实例或原型添加方法
- 不要重定义已有的方法
问题在于,假如有一个stopEvent()的方法用于取消某个事件的默认行为,你将其修改了,除了取消事件的默认行为还添加了其他行为,别人对于你添加的副作用并不知情,也使用了这个方法,就会导致别人出现错误或损失。
2. 不要声明全局变量、命名空间
最多可以创建一个全局变量作为其他函数或对象的命名空间。比如:
代码语言:javascript复制// 全局变量name
var name = "CODER-V";
// 全局变量sayName
function sayName(){
console.log(name);
};
上面代码声明了两个全局变量,我们可以像下面这样将其包含在一个对象中:
代码语言:javascript复制var MyApplicatioin = {
name: "CODER-V",
sayName: function(){
console.log(name);
}
};
重写后的版本值声明了一个全局对象MyApplicatioin 。该对象包含了name和sayName()。这样就避免了变量name会覆盖window.name属性,而且还可能会影响其他功能。其次,有助于分清功能都集中在哪里。
这样一个全局对象可以扩展为命名空间的概念。命名空间涉及创建一个对象,然后通过这个对象来暴露能力。关于命名空间,最重要的是确定一个大家都同意的全局对象名称。这个名称要足够独特,不能与其他人的冲突。大多数情况下会选择使用公司名。下面的示例演示了coder最为命名空间来组织功能:
代码语言:javascript复制// 创建全局对象
var Coder = {};
// 为CODER-V创建命名空间
Coder.Coder-v = {};
// 为CODER-V添加用到的对象
Coder.Coder-v.EventUtil = {};
Coder.Coder-v.CookieUtil = {};
...
以上代码以Coder作为全局命名空间,然后它的下面又创建了命名空间,这样将相应的变量放到相应的命名空间下,就可以避免命名冲突的问题,因为它们在不同的命名空间下。虽然命名孔家需要多写一点代码,但是从可维护性角度来看,这个代价还是非常值得的。命名空间还可以保证代码不与页面上的其他代码互不干扰。
3. 不要比较null
JavaScript不会自动做任何类型检查,因此就需要开发者来承担这个责任。最常见的类型检查就是看值是不是null。然而,与null进行比较的代码太多了,其中很多因为类型检查不够而频繁引发错误。来看下面的例子:
代码语言:javascript复制function sortArray(values){
if(values != null){// 不要这样比较
values.sort(comparator);
}
}
这个函数的目的是使用给定的比较函数对数组进行排序。为保证函数正常执行,values必须是数组。但是,if语句在这里只是简单的检查了这个值是不是null。实际上,字符串、数值还是有很多其他类型都可以通过这里的检查,结果就会导致错误。
注意:类型检查要检查的是它的类型,而不是检查它不能是什么!。比如前面的values应该检查它到底是不是数值,而不是检查它是不是null,应该这样做:
代码语言:javascript复制function sortArray(values){
if(values instanceof Array){// 检查类型
values.sort(comparator);
}
}
如果看到null值比较的代码,可以使用以下技术替换它:
- 如果值应该是引用类型,则使用 instanceof 操作符检查其构造函数。
- 如果值应该是原始类型,则使用 typeof 检查其类型。
- 如果希望值是有特定方法名的对象,则使用 typeof 操作符确保对象上存在给定名称的方法。
注:
- typeof():返回参数类型
- instanceof:返回boolean,检查一个对象是否某个类的实例,会查找原型链
4. 使用常量
依赖常量的目标是从应用程序逻辑中分离数据,以便修改数据时不会引发错误。将一些可能会变的数值,字符串,url等提取出来放在单独定义的常量中,以实现逻辑和数据分离,方便后期维护,同时也避免了魔法数字或魔法值(对于魔法值不了解的可以看一下我的另一篇文章:代码优化通用准则)。
作用域意识
在《执行上下文与作用域》一文中,我们了解了作用域的工作原理。随着作用域链中作用域数量的增加,访问当前作用域外部变量所需的时间也会增加。访问全局变量始终比访问局部变量要慢,因为必须遍历作用域链。任何可以缩短遍历作用域链的时间的措施都会提升代码的性能。
1. 避免全局查找
全局变量或函数相比于局部值,始终是最费时间的,因为许需要遍历作用域链来查找,看以下代码:
代码语言:javascript复制function updateUrl(){
let imgs = document.getElementsByTagName("img");
for(let i=0,len=img.length; i<len; i ){
imgs[i].title = '${document.title} image ${i}';
}
let msg = document.getElementById("msg");
msg.innerHTML = "Update complete."
}
这个函数看起来没什么问题,但是其中有三个地方引用了全局对象 document 。如果网页的图片非常多,那么每次 for 循环都需要遍历作用域链是十分耗时的。
解决方案就是:通过在局部作用域中保存 document 对象的引用,可以将全局查找的数量限制为1个来提升这个函数的性能。
代码语言:javascript复制function updateUrl(){
let doc = document;// 用一个引用来保存这个全局对象,将全局查找次数限制为1次
let imgs = doc.getElementsByTagName("img");
for(let i=0,len=img.length; i<len; i ){
imgs[i].title = '${doc.title} image ${i}';
}
let msg = doc.getElementById("msg");
msg.innerHTML = "Update complete."
}
2. 不要使用with语句
可能很多人都不知道with语句,不知道那就更不会使用了。这里来就简单介绍一下。
with语句会创建自己的作用域,因此也会增长作用域链(在作用域链前端增加)。在with语句中执行的代码一定比其他外部作用域执行的更慢,因为它多了异步作用域查找。
选择正确的方法
1. 避免使用对象属性查找
在计算机科学中,算法复杂度使用大 O 表示法来表示。最简单最快的算法可以表示为 常量值 或 O(1)。时间长一点的由以下方式表示
表示法 | 名称 | 说明 |
---|---|---|
O(1) | 常量 | 无论多少值,执行时间都不变。表示简单值和保存在变量中的值。 |
O(logn) | 对数 | 执行时间随着值的增加而增加,但算法完成不需要读取每个值。比如:二分查找 |
O(n) | 线性 | 执行时间与值的数量直接相关。比如:迭代数组中的所有元素。 |
O(n2) | 二次方 | 执行时间随着值的增加而增加,而且每个值至少需要读取n次,比如:插入排序 |
查找效率从高到底排列: 常量 、O(1) > 变量、数组 > 对象属性
另外,如果某个需求既可以是使用数组的数字索引,又可以使用命名属性,那么推荐使用 数值索引。
对象属性查找慢,是因为查找属性名要查找原项链。解决方案就是将对象的属性保存在变量中,这样查找的时间复杂度就是O(1)。比如:
代码语言:javascript复制let url = window.location.href;
let query = url.xxx;
只要对象属性的访问超过了1次,就应该这样做来提升性能。
2. 优化循环
优化循环是性能优化的重要内容,因为循环会多次运行相同的代码,所以运行期间会自动增加。优化循环的基本步骤如下:
- 简化终止条件。因为每次循环都会计算终止条件,所以应该让他尽可能的快。这意味着要避免属性查找或其他O(n)操作。
- 简化循环体。循环体是最花时间的。因此要尽可能优化。要确保其中不会包含轻松转移到循环外部的密集计算。
- 使用后测试循环do-while。最常见的循环就是for循环和while循环,这两种循环都属于先测试循环。do-while 就是后测试循环,避免了对终止条件的初始评估,因此会更快,本人实测有效。
来看一下这个示例:
代码语言:javascript复制for(let i=0; i<values.length; i ){
console.log(i);
}
使用 do-while 优化:
代码语言:javascript复制let i = 0;
do{
console.log(valuse[i]);
}while( --i >= 0 );// 注意这里是 --i,而没有使用i <value.length自己想想为什么,不懂的评论区评论
可以自行测试一下,博主自测使用后测试循环执行时间比for循环快了一半。
3. 展开循环
如果循环的次数是有限的,那么通常抛弃循环,直接多次调用函数会更快,以前面的数组为例,如果数组的长度始终一样,则可能对每一个元素都调用一次console.log(values[i]);
效率更高。
console.log(valuse[0]);
console.log(valuse[1]);
console.log(valuse[2]);
console.log(valuse[3]);
console.log(valuse[4]);
假设这个数组始终只有5个元素,像这样展开循环可以节省创建循环、计算终止条件的消耗,从而让代码运行更快。
如果不能提前预知循环的次数,也可以使用一种叫做**达夫设备(Duff’s Device)**的技术,达夫设备的基本思路是:以8的倍数作为迭代次数从而将循环展开为一系列语句。下面介绍一种基于达夫设备的优化,其效率约比原始达夫设备高40%。
代码语言:javascript复制let iterations = Math.floor(values.length / 8);//迭代次数,主循环中只能有8个元素
let leftover = values.length % 8;//leftover以为剩下的,也就是除主循环中剩下的元素
let i = 0;
if(leftover > 0){//先处理剩下的
do{ //前面提到了,使用后测试循环会更快
console.log(values[i]);
}while( --leftover > 0);
}
do{//再执行主循环
console.log(values[i ]);
console.log(values[i ]);
console.log(values[i ]);
console.log(values[i ]);
console.log(values[i ]);
console.log(values[i ]);
console.log(values[i ]);
console.log(values[i ]);//主循环只能有8个语句
}while( --iterations > 0 );
这个达夫设备实现,首先通过用values数组的长度除以8计算需。要多少次循环,floor()保证取得的数据是整数,leftover(剩余的、额外的)中保存着不会在主循环中处理,因而需要在第一个循环中处理的次数。处理完这些额外的次数之后进入主循环,每次循环调用八次console.log()。
展开循环对于大型数据集可以节省很多时间,但对于小型数据而言,则可能不值得。因为实现同样的任务需要写很多代码,所以,如果处理的数据量不大,那么显然没有必要。
4. 尽量使用原生方法
原生方法都是使用c或c 等编译型语言写的,因此比JavaScript写的代码运行要快得多。
5. 尽量使用switch语句
如果代码中有复杂得if-else语句,将其转换成switch语句可以变得更快。然后,通过重组分支,将最可能得放前面,不太可能的放后面,进一步提升代码性能。
6. 尽量使用 位操作运算符
在执行数学运算操作时,位操作一定比任何布尔值或数字计算更快。像求模、逻辑与AND、逻辑或OR都很适合使用位操作代替。甚至某些计算可以考虑使用位移操作符代替。