javascript提高性能

  目录

javascript提高性能的方式总结

javascript提高性能

  javascript如何提高性能,这个在面试中被问到的几率也是蛮高的,之前我也就是想到哪里说到哪里,面试官估计也都是菜鸟(我面的也都是小公司),也觉得我说的对,但是,有空冷静下来,觉得自己说的每次都不是很全面,所以,我想把平时能想到的记录下来,这样再去面试的时候,呵呵,一定把面试官说蒙。

4种内存泄漏

  提高JavaScript性能,首先就是避免4种内存泄漏,这里只是简单列出。

1.意外的全局变量

JavaScript的目标是开发一种看起来像Java但足够自由的被初学者使用的语言。JavaScript自由的其中一种方式是它可以处理没有声明的变量:一个未声明的变量的引用在全局对象中创建了一个新变量。在浏览器的环境中,全局对象是window。也就是说:

1
2
3
4
5
6
7
function foo(arg) {
bar = "this is a hidden global variable";
}
实际上是:
function foo(arg) {
window.bar = "this is an explicit global variable";
}

如果bar是仅在foo函数作用域内承载引用,并且你忘记用var来声明的变量,一个意外的全局变量就被创建了。在这个例子中,泄漏一个单一字符串不会有太大害处,但这的确是不好的。

另一种意外全局变量被创建的方式是通过this:

1
2
3
4
5
6
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

为了阻止这种错误发生,在你的Javascript文件最前面添加’use strict;’。这开启了解析JavaScript的阻止意外全局的更严格的模式。

全局变量的一个注意事项:

即使我们谈了不明的全局变量,仍然存在很多代码被显式的全局变量填充的情况。这是通过定义不可收集的情况(除非清零或重新赋值)。特别的,用来临时存储和处理大量信息的全局变量会引起关注。如果必须用全局变量来存储很多数据,在处理完之后,确保对其清零或重新赋值。 一个在与全局连接上增加内存消耗常见的原因是缓存)。 缓存存储重复被使用的数据。为此,为了有效,缓存必须有其大小的上限。飙出限制的缓存可能会因为内容不可被回收,导致高内存消耗。

2.被遗忘的计时器或回调

在JavaScript中setInterval的使用相当常见。其他库提供观察者和其他工具以回调。这些库中大多数,在引用的实例变成不可访问之后,负责让回调的任何引用也不可访问。在setInterval的情况下,这样的代码很常见:

1
2
3
4
5
6
7
8
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);

这个例子表明了跳动的计时器可能发生什么:计时器使得节点或数据的引用不再被需要了。代表node的对象将来可能被移除,使得整个块在间隔中的处理不必要。然而,处理函数,由于间隔仍然是活跃的,不能被回收(间隔需要被停掉才能回收)。如果间隔处理不能被回收,它的依赖也不能被回收。那意味着可能存储着大量数据的someResource,也不能被回收。

观察者情况下,一旦不被需要(或相关的对象快要访问不到)就创建明确移除他们的函数很重要。在过去,这由于特定浏览器(IE6)不能很好的管理循环引用(下面有更多相关信息),曾经尤为重要。现如今,一旦观察对象变成不可访问的,即使收听者没有明确的被移除,多数浏览器可以并会回收观察者处理函数。然而,它保持了在对象被处理前明确的移除这些观察者的好实践。例如:

1
2
3
4
5
6
7
8
9
10
11
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.

一条关于对象观察者及循环引用的笔记

观察者和循环引用曾经是JavaScript开发者的祸患。这是由于IE垃圾回收的一个bug(或者设计决议)出现的情况。IE的老版本不能检测到DOM节点和JavaScript代码间的循环引用。 这是一个通常为观察到的保留引用(如同上面的例子)的观察者的典型。 也就是说,每次在IE中对一个节点添加观察者的时候,会导致泄漏。这是开发者在节点或空引用之前开始明确的移除处理函数的原因。 现在,现代浏览器(包括IE和MS Edge)使用可以剪裁这些循环和正确处理的现代垃圾回收算法。换言之,在使一个节点不可访问前,调用removeEventLister不是严格意义上必须的。

像Jquery一样的框架和库做了在处置一个节点前(当为其使用特定的API的时候)移除监听者的工作。这被在库内部处理,即使在像老版本IE一样有问题的浏览器里面跑,也会确保没有泄漏产生。

3. 超出DOM引用

有时存储DOM节点到数据结构中可能有用。假设你想要迅速的更新一个表格几行内容。存储每个DOM行节点的引用到一个字典或数组会起作用。当这发生是,两个对于同个DOM元素的引用被留存:一个在DOM树中,另外一个在字典中。如果在将来的某些点你决定要移除这些行,需要让两个引用都不可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}

对此的额外考虑,必须处理DOM树内的内部节点或叶子节点。假设你在JavaScript代码中保留了一个对于特定的表格内节点(一个td标签)的引用。在将来的某个点决定从DOM中移除这个表格,但是保留对于那个节点的引用。直观的,会假设GC会回收除那个节点之外的每个节点。在实践中,这不会发生的:这个单节点是那个表格的子节点,子节点保留对父节点引用。换句话说,来自JavaScript代码的表格元素的引用会引起在内存里存整个表格。当保留DOM元素的引用的时候,仔细考虑下。

4.闭包

一个JavaScript开发的关键点是闭包:从父级作用域捕获变量的匿名函数。很多开发者发现,由于JavaScript runtime的实现细节,有以一种微妙的方式泄漏的可能,这种特殊的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);

这个代码片段做了一件事:每次replaceThing被调用的时候,theThing获取到一个包括一个大数组和新闭包(somMethod)的新对象。同时,变量unused保留了一个有originalThing(theThing从之前的对replaceThing的调用)引用的闭包。已经有点疑惑了,哈?重要的是一旦一个作用域被在同个父作用域下的闭包创建,那个作用域是共享的。这种情况下,为闭包somMethod创建的作用域被unused共享了。unused有一个对originalThing的引用。即使unused从来没被用过,someMethod可以通过theTing被使用。由于someMethod和unused共享了闭包作用域,即使unused从来没被用过,它对originalThing的引用迫使它停留在活跃状态(不能回收)。当这个代码片段重复运行的时候,可以看到内存使用稳步的增长。GC运行的时候,这并不会减轻。本质上,一组关联的闭包被创建(同unused变量在表单中的根节点一起),这些闭包作用域中每个带了大数组一个非直接的引用,导致了大型的泄漏。

垃圾回收的直观行为

即使垃圾回收很方便,他们有自己的一套权衡方法。其中一个权衡是nondeterminism。也就是说,GC是不可预期的。通常不能确定什么时候回收器被执行。这意味着在一些情况下,需要比程序正在使用的更多的内存。其他情况下,短的暂停在特别敏感的应用中很明显。即使不确定性意味着不能确定回收什么时候执行,大多数GC实现共享在分配期间,普通的回收通行证模式。如果没有执行分配,大多数CG停留在休息状态。考虑下面的方案:

1.执行一组大型的分配。

2.多数元素(或所有)被标记为不可访问(假设我们置空了一个指向不再需要的缓存的引用)。

3.没有进一步的分配执行了。

在这个方案中,大多GC不会运行任何进一步的回收通行了。换言之,即使有可用于回收的,不可访问的引用,回收器不会要求他了。这不是严格的泄漏,但是也会导致比平常更高的内存使用率。

Google在 JavaScript Memory Profiling docs, example #2.文章中,提供了一个优秀的例子。

编写javascript代码时的优化

1.从JavaScript的作用域谈起

当JavaScript代码执行时,JavaScript引擎会创建一个执行环境,又叫执行上下文。执行环境定义了变量或函数有权访问的其他数据,决定了它们的行为,每个执行环境都有一个与它关联的变量对象,环境中定义的所有函数、变量都保存在这个对象中。在页面加载的时候,JavaScript引擎会创建一个全局的执行环境,所有全局变量和函数都是作为window对象(浏览器中)的属性和方法创建的。在此之后,每执行一个函数,JavaScript引擎都会创建一个对应的执行环境,并将该环境放入环境栈中,所以当前正在执行的函数的执行环境是在环境栈的最顶部的,当函数执行完毕之后,其执行环境会弹出栈,并被销毁,保存在其中的变量和函数定义也会被销毁。

当代码在一个执行环境中执行时,JavaScript引擎会创建变量对象的一个作用域链,它可以保证对执行环境有权访问的变量和函数的有序访问。作用域链的前端始终是当前执行的代码所在的环境的变量对象。全局环境的作用域链中只有一个变量对象,它定义了所有可用的全局变量和函数。当函数被创建时,JavaScript引擎会把创建时执行环境的作用域链赋给函数的内部属性[[scope]];当函数被执行时,JavaScript引擎会创建一个活动对象,最开始时这个活动对象只有一个变量,即arguments对象。该活动对象会出现在执行环境作用域链的顶端,接下来是函数[[scope]]属性中的对象。

当需要查找某个变量或函数时,JavaScript引擎会通过执行环境的作用域链来查找变量和函数,从作用域链的顶端开始,如果没找到,则向下寻找直至找到为止。若一直到全局作用域都没有找到,则该变量或函数为undefined。

举个栗子:

1
2
3
4
function add(a,b) {
return a + b;
}
var result = add(2,3);

代码执行时,add函数有一个仅包含全局变量对象的[[scope]]属性,add函数执行时,JavaScript引擎创建新的执行环境以及一个包含this、arguments、a、b的活动对象,并将其添加到作用域链中。如下图所示:
img

2.使用局部变量

了解了作用域链的概念,我们应该知道在查找变量会从作用域链的顶端开始一层一层的向下找。显然,查找的层数越多,花费的时间越多。所以为了提高查找的速度,我们应该尽量使用 局部变量(到目前为止,局部变量是JavaScript中读写最快的标识符)。

例如:

1
2
3
4
5
6
7
function createEle() {
document.createElement("div");
}
function createEle() {
var doc = document;
doc.createElement("div");
}

当document使用次数比较少时,可能无所谓,可是如果在一个函数的循环中大量使用document,我们可以提前将document变成局部变量。

来看看jquery怎么写的:

1
2
3
4
5
(function(window, undefined) {
var jQuery = function() {}
// ...
window.jQuery = window.$ = jQuery;
})(window);

这样写的优势:

1、window和undefined都是为了减少变量查找所经过的scope作用域。当window通过传递给闭包内部之后,在闭包内部使用它的时候,可以把它当成一个局部变量,显然比原先在window scope下查找的时候要快一些。(原来的window处于作用域链的最顶端,查找速度慢)

2、在jquery压缩版本jquery.min.js中可以将局部变量window替换成单个字母,减小文件大小,提高加载速度

img
3、undefined也是JavaScript中的全局属性。将undefined作为参数传递给闭包,因为没给它传递值,它的值就是undefined,这样闭包内部在使用它的时候就可以把它当做局部变量使用,从而提高查找速度。undefined并不是JavaScript的保留字或者关键字。

4、undefined在某些低版本的浏览器(例如IE8、IE7)中值是可以被修改的(在ECMAScript3中,undefined是可读/写的变量,可以给它赋任意值,这个错误在ECMAScript5中做了修正),将undefined作为参数并且不给它传值可以防止因undefined的值被修改而产生的错误。

3.避免增长作用域链

在JavaScript中,有两种语句可以临时增加作用域链:with、try-catch

with可以使对象的属性可以像全局变量来使用,它实际上是将一个新的变量对象添加到执行环境作用域的顶部,这个变量对象包含了指定对象的所有属性,因此可以直接访问。

这样看似很方便,但是增长了作用域链,原来函数中的局部变量不在处于作用域链的顶端,因此在访问这些变量的时候要查找到第二层才能找到它。当with语句块之行结束后,作用域链将回到原来的状态。鉴于with的这个缺点,所以不推荐使用。

try-catch中的catch从句和with类似,也是在作用域链的顶端增加了一个对象,该对象包含了由catch指定命名的异常对象。但是因为catch语句只有在放生错误的时候才执行,因此影响比较少。

4.字符串链接优化

由于字符串是不可变的,所以在进行字符串连接时,需要创建临时字符串。频繁创建、销毁临时字符串会导致性能低下。

当然,这个问题在新版本浏览器包括IE8+中都得到了优化,所以不需要担心

在低版本浏览器(IE6、IE7)中,我们可以种数组的join方法来代替。

1
2
3
4
5
6
var temp = [];
var i = 0;
temp[i++] = "Hello";
temp[i++] = " ";
temp[i++] ="everyone";
var outcome = temp.join("");

5.条件判断

当出现条件判断时,我们采用什么样的结构才能使性能最优?

1
2
3
4
5
6
7
8
9
10
11
if(val == 0) {
return v0;
}else if(val == 1) {
return v1;
}else if(val == 2) {
return v2;
}else if(val == 3) {
return v3;
}else if(val == 4) {
return v4;
}

当条件分支比较多时,我们可以斟酌哪种条件出现的概率比较大,并将对应的语句放在最上面,这样可以减少判断次数。

使用switch语句,新版的浏览器基本上都对switch做了优化,这样层数比较深时,性能比if会更好

使用数组:

1
2
var v = [v0,v1,v2,v3,v4];
return v[valeue];

要求:对应的结果是单一值,而不是一系列操作

另外,其他方面的优化,譬如

1
2
3
4
5
6
7
8
9
10
11
if(condition1) {
return v1;
}
else {
return v2
}
// 改成
if(condition1) {
return v1;
}
return v2;

6.快速循环

1、循环总次数使用局部变量

1
2
3
4
5
6
7
8
for( var i = 0;i < arr.length;i++) {

}
// 改成
var len = arr.length;
for( var i = 0;i < len;i++) {

}

这样就避免了每次循环的属性查找。这点尤其重要,因为在进行dom操作时,很多人会这样写:

1
2
3
4
var divList = document.getElementsByTagName("div");
for( var i = 0;i < divList.length;i++) {

}

查找DOM元素的属性是相对耗时的,所以应该避免这种写法。

2、如果可以,递减代替递增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for(var i = 0;i < arr.length;i++) {

}
// 改成
for(var i = arr.length - 1;i--;) {

}

var i = 0;
while(i < arr.length) {
i++;
}
// 改成
var i = arr.length - 1;
while(i--) {

}

i=0的时候会直接跳出,循环次数比较多时还是很有用的。

7.展开循环

1
2
3
4
var i = arr.length - 1;
while(i--) {
dosomething(arr[i]);
}

遇到这样的情况时,执行一次循环的时候我们可以选择不止执行一次函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var interations = Math.floor(arr.length / 8);
var left = arr.length % 8;
var i = 0;

if(left) {
do {
dosomething(arr[i++]);
} while(--left);
}
do {
dosomething(arr[i++]);
dosomething(arr[i++]);
dosomething(arr[i++]);
dosomething(arr[i++]);
dosomething(arr[i++]);
dosomething(arr[i++]);
dosomething(arr[i++]);
dosomething(arr[i++]);
} while(--interations);

当遇到大数组,减少循环的开销,性能不就提上去了嘛。(至于为什么是每次循环,调8次函数,大牛测出来的,这样达到最佳)

8.高效存取数据

JavaScript中4种地方可以存取数据:

字面量值;变量;数组元素;对象属性

字面量值和变量中存取数据是最快的,从数组元素和对象属性中存取数据相对较慢,并且随着深度增加,存取速度会越来越慢,譬如obj.item.value就比obj.item慢。

某些情况下我们可以将对象、数组属性存成局部变量来提高速度,譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for( var i = 0;i < arr.length;i++) {

}
// 改成
var len = arr.length;
for( var i = 0;i < len;i++) {

}

var divList = document.getElementsByTagName("div");
for( var i = 0;i < divList.length;i++) {

}
// 改成
//
var divList = document.getElementsByTagName("div");
for( var i = 0,len = divList.length;i < len;i++) {

}

9.事件委托

事件委托就是利用冒泡的原理,将原本应该添加在某些元素身上的监听事件,添加到其父元素身上,来达到提高性能的效果。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
</div>
<script>
window.onload = function() {
var ul = document.getElementsByTagName('ul')[0];
var liList = document.getElementsByTagName('li');

for(var i = 0,len = liList.length;i < len;i++) {
liList[i].onclick = function() {
alert(this.innerHTML);
}
}
}
</script>

这样我们就为每个li添加了监听事件了。

显然,我们通过循环为每个li添加监听事件是不优化的。这样不仅浪费了内存,在新的li加入的时候我们还要重新为它添加监听事件。

我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
</div>
<script>
window.onload = function() {
var ul = document.getElementsByTagName('ul')[0];
var liList = document.getElementsByTagName('li');

ul.onclick = function(e) {
var e = e || window.event;
var target = e.target || e.srcElement;

if(target.nodeName.toLowerCase() == "li") {
alert(target.innerHTML);
}
}
}
</script>

这样写的好处:

只添加一个监听事件,节省了内存;新加入li的时候我们也不用为它单独添加监听事件;在页面中添加事件处理程序所需的时候更少,因为我们只需要为一个DOM元素添加事件处理程序。

使用typescript

使用typescript来提高性能我实在一本关于nodejs的书里看到的,主要还是用在chrome的V8引擎上,这本书里写的比如

1
2
3
4
5
6
7
// 两个数相加的函数
function add(a, b) {
return a + b;
}
for(let i=0; i<10000; i++) {
console.log(add(i + i*3));
}

书里说,如果V8引擎发现一个函数使用频率很高,而且参数传入的都是同一种类型的,好像是在内存寻址的时候书都很快,因为引擎认为传入的参数就是int型。

再来看一个例子

1
2
3
4
5
6
7
8
9
10
11
// 两个数相加的函数
function add(a, b) {
return a + b;
}
for(let i=0; i<10000; i++) {
if(i % 8 === 0) {
console.log(add(i + i*0.3)); // 这里出现了浮点数
}else {
console.log(add(i + i*3)); // 这里是整数
}
}

上边的代码可以看到,在使用整形参数的时候突然传入浮点数,V8引擎寻址机制突然变化,丢弃之前的函数,使用新的来计算,性能下降。

所以,如果使用typescript,类型固定,这样性能就会提高哦。