JavaScript性能优化

0. 提要

  自从JavaScript诞生以来,用这门语言编写网页的开发人员有了极大的增长。与此同时,JavaScript代码的执行效率也越来越受到关注。因为JavaScript最初是一个解释型语言,执行速度要比编译型语言慢得多。Chrome是第一款内置优化引擎,将JavaScript变异成本地代码的浏览器。此后,主流浏览器纷纷效仿,陆续实现了JavaScript的编译执行。

  即使到了编译执行JavaScript的新阶段,仍然会存在低效率的代码。不过,还是有一些方式可以改进代码的整体性能的。

  主要有以下四个方面:

  1. 作用域方面。
  2. 选择方法方面。
  3. 最小化语句数方面。
  4. 优化DOM交互方面。

1.作用域方面

1.1避免全局查找

  可能优化脚本性能最重要的就是注意全局查找。使用全局变量和函数肯定要比局部的开销更大,因为要涉及作用域链上的查找。请看以下函数:

function updateUI(){
var imgs = document.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}

  该函数可能看上去完全正常,但是它包含了三个对于全局document对象的引用。如果在页面上有多个图片,那么for循环中的document引用就会被执行多次甚至上百次,每次都会要进行作用域链查找。通过创建一个指向document对象的局部变量,就可以通过限制一次全局查找来改进这个函数的性能:

function updateUI(){
var doc = document;
var imgs = doc.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}

  这里,首先将 document 对象存在本地的 doc 变量中;然后在余下的代码中替换原来的 document。与原来的的版本相比,现在的函数只有一次全局查找,肯定更快。
  将在一个函数中会用到多次的全局对象存储为局部变量总是没错的。

1.2避免with语句

  在性能非常重要的地方必须避免使用with语句。和函数类似,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度。由于额外的作用域链查找,在with语句中执行的代码肯定会比外米娜执行的代码要慢。

  对比一下以下两段作用相同的代码:

function updateBody(){
with(document.body){
alert(tagName);
innerHTML = "Hello world!";
}
}
function updateBody(){
var body = document.body
alert(body.tagName);
body.innerHTML = "Hello world!";
}

  第一个使用了with语句,第二个没有使用。第二个代码虽然稍微长了点,但是阅读起来比with语句版本更好,它确保让你知道tagName和innerHTML是属于哪个对象的。同时,这段代码通过将document.body存储在局部变量中省去了额外的全局查找。

2.选择方法方面

2.1避免不必要的属性查找

var values = [5, 10];
var sum = values[0] + values[1];
alert(sum);
var values = { first: 5, second: 10};
var sum = values.first + values.second;
alert(sum);

  对比一下上述两个代码,第一段代码使用数组相加来得到sum值,第二段代码使用访问对象的属性方法相加来得到sum值,前者为O(1),后者为O(n)。

  对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。如第二段代码进行一两次属性查找并不会导致显著的性能问题,但是进行成百上千次则肯定会减慢执行速度。

  简而言之,属性查找越多,执行时间就越长。

2.2优化循环

  1. 减值迭代——大多数循环使用一个从0开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
  2. 简化终止条件——由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或其他O(n)的操作。
  3. 简化循环体——循环体是执行最多的,所以要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
  4. 使用后测试循环——最常用for循环和while循环都是前测试循环。而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。

2.3展开循环

  当循环的次数是确定的,消除循环并使用多次函数调用往往更快。

for (var i=values.length -1; i >= 0; i--){
process(values[i]);
}
//消除循环
process(values[0]);
process(values[1]);
process(values[2]);

  如果数组的长度总是一样的,对每个元素都调用process()可能更优。这个例子假设values数组里面只有3个元素,直接对每个元素调用process()。这样展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行得更快。

2.4性能的其他注意事项

  • 原生方法较快——只要有可能,使用原生方法而不是自己用JavaScript重写一个。
  • switch语句较快
  • 位运算符较快——当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。选择性地用位运算替换算数运算可以极大提升复杂运算的性能。诸如取模,逻辑与和逻辑或都可以考虑用位运算来替换。

3.最小化语句数方面

3.1多个变量声明

//4 个语句—— 很浪费
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();

改为

//一个语句
var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();

  此处,变量声明只用了一个var语句,之间由逗号隔开。在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明快很多。

3.2插入迭代值

var name = values[i];
i++;

改为

var name = values[i++];

3.3使用数组和对象字面量

  有两种创建数组和对象的方法:构造函数和字面量。使用构造函数总是要用到更多的语句来插入元素或者定义属性,而字面量可以将这些操作在一个语句中完成。

//用 4 个语句创建和初始化数组——浪费
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//用 4 个语句创建和初始化对象——浪费
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
};

改为

//只用一条语句创建和初始化数组
var values = [123, 456, 789];
//只用一条语句创建和初始化对象
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};

  重写后的代码只包含两条语句,一条创建和初始化数组,另一条创建和初始化对象。之前用了八条语句的东西现在只用了两条,减少了75%的语句量。在包含成千上万行JavaScript的代码库中,这些优化的价值更大。

4.优化DOM交互

  在JavaScript各个方面中,DOM毫无疑问是最慢的一部分。DOM操作与交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一部分。进一步说,看似细微的操作也可能要花很久来执行,因为DOM要处理非常多的信息。理解如何优化与DOM的交互可以极大得提高脚本完成的速度。

4.1最小化现场更新

  一旦你需要访问的DOM部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。

  要修正这个性能瓶颈,需要减少现场更新的数量。一般有2种方法。

  1. 将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置。这个方法不是非常理想,因为每次页面更新的时候它会不必要的闪烁。
  2. 使用文档片段来构建DOM结构,接着将其添加到List元素中。这个方式避免了现场更新和页面闪烁问题。
var list = document.getElementById("myList"),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}

改为

var list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);

在这个例子中只有一次现场更新,它发生在所有项目都创建好之后。文档片段用作一个临时的占位符,放置新创建的项目。然后使用appendChild()将所有项目添加到列表中。

  一旦需要更新DOM,请考虑使用文档片段来构建DOM结构,然后再将其添加到现存的文档中。

4.2使用innerHTML

  有两种在页面上创建DOM节点的方法:

  1. 使用诸如createElement()和appendChild()之类的DOM方法。
  2. 使用innerHTML。

  对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。

var list = document.getElementById("myList"),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}

改为

var list = document.getElementById("myList"),
html = "",
i;
for (i=0; i < 10; i++) {
html += "<li>Item " + i + "</li>";
}
list.innerHTML = html;

 

参考文献:《JavaScript高级程序设计(第三版)》。

转载于:https://www.cnblogs.com/huahai/p/6351472.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值