前言
下面出现的代码只做效率测试,若运行,请修改其中存在命名冲突,再进行代码的执行。
代码优化
如何精准测试 JavaScript 性能
- 本质上就是采集大量的执行样本进行数学统计和分析。
- 使用基于 Benchmark.js 的 jsben.ch、jsbench.me、measurethat.net、jsperf.com(四者选其一即可)完成。
代码优化的方式
慎用全局变量
-
为什么慎用全局变量
1)全局变量定义在全局执行上下文,是所有作用域链的顶端;
2)全局执行上下文一直存在于上下文执行栈,直到程序退出;
3)如果某个局部作用域出现了同名变量则会遮蔽或污染全局。下面,我们使用 jsbench.me 来测试一下全局变量和局部变量两者中哪个的性能比较高。
代码示例如下:
// 全局变量 var i, str = ''; for (i = 0; i < 1000; i++) { str += i; } // 局部变量 for (let i = 0; i < 1000; i++) { let str = ''; str += i; }
jsbench.me 测试结果如下图所示:
通过上图,我们可以很明显的看到,局部变量的性能要远远高于全局变量。这也正反应了我们需要慎用全局变量的原因。
缓存全局变量
-
何时缓存变量
将使用中无法避免的全局变量缓存到局部。
下面,我们来看一个代码示例,看一下没有缓存和有缓存的性能对比:
代码示例如下:
<input type="button" value="btn" id="btn1"> <input type="button" value="btn" id="btn2"> <input type="button" value="btn" id="btn3"> <input type="button" value="btn" id="btn4"> <p>1111</p> <input type="button" value="btn" id="btn5"> <input type="button" value="btn" id="btn6"> <p>222</p> <input type="button" value="btn" id="btn7"> <input type="button" value="btn" id="btn8"> <p>333</p> <input type="button" value="btn" id="btn9"> <input type="button" value="btn" id="btn10"> <script> // 没有缓存 function getBtn1 () { let oBtn1 = document.getElementById('btn1') let oBtn3 = document.getElementById('btn3') let oBtn5 = document.getElementById('btn5') let oBtn7 = document.getElementById('btn7') let oBtn9 = document.getElementById('btn9') } function getBtn2 () { let obj = document // 通过局部变量,缓存document对象 let oBtn1 = obj.getElementById('btn1') let oBtn3 = obj.getElementById('btn3') let oBtn5 = obj.getElementById('btn5') let oBtn7 = obj.getElementById('btn7') let oBtn9 = obj.getElementById('btn9') } </script>
jsbench.me 测试结果如下图所示,很明显使用局部变量将document对象进行缓存,其性能远远的高于未缓存时的性能。
通过原型新增方法
-
在原型对象上新增实例对象需要的方法。
模拟直接在构造函数中添加方法,与通过原型对象添加方法对比。
代码示例如下:
var fn1 = function () { this.foo = function () { console.log(1111); } } let f1 = new fn1() // 通过原型对象添加方法 var fn2 = function () {} fn2.prototype.foo = function () { console.log(1111); } let f2 = new fn2()
jsbench.me 测试结果如下图所示,通过原型对象添加实例对象需要的方法,可以大大地提高性能。
避开闭包陷阱
-
闭包特点
1)外部具有指向内部的引用;
2)在 “外” 部作用域访问 “内” 部作用域的数据。代码示例如下:
function foo () { var name = 'lg' function fn () { console.log(name) // 外部具有指向内部的引用 } return fn } var a = foo() a()
-
关于闭包
1)闭包是一种强大的语法;
2)闭包使用不当很容易出现内存泄漏;
3)不要为了闭包而闭包。闭包陷阱代码示例:
function foo () { // el 所在的相当于外部作用域 var el = document.getElementById("btn") el.onclick = function () { // 内部作用域,内部作用域使用了外部作用域中的变量 // 此时 el.id 不会被回收,多个会导致内存泄漏 console.log(el.id); } }
避开闭包陷阱代码示例:
// 内存层面,避开闭包,防止内存泄漏 function foo () { var el = document.getElementById("btn") el.onclick = function () { console.log(el.id); } el = null // 释放变量 }
避免属性访问方法使用
-
JavaScript 中的面向对象
1)JS 不需要属性的访问方法,所有属性都是外部可见的;
2)使用属性访问方法只会增加一层重定义,没有访问的控制力。代码示例如下:
function Person () { this.name = 'icoder' this.age = 18 this.getAge = function () { // 属性访问方法,使用函数内属性 return this.age } } const p1 = new Person() const a = p1.getAge() function Person () { this.name = 'iconder' this.age = 18 } const p2 = new Person() const b = p2.age // 通过实例对象访问属性
jsbench.me 测试结果如下图所示,在第二段代码中,性能显然比第一段代码中的性能要高,两者不同的就是第二个段代码中没有采用属性访问方法使用属性的方式,这也说明了在实际开发中,应避免属性访问方法使用。
-
For 循环优化
代码示例如下:
<p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <p class="btn">add</p> <script> var aBtns = document.getElementsByClassName("btn") for (var i = 0; i < aBtns.length; i++) { console.log(i); } for (var i = 0, len = aBtns.length; i < len; i++) { console.log(i); } </script>
jsbench.me 测试结果如下图所示,在第二段中使用了变量 len 获取了数组的长度,使每次循环时,无需再去计算数组的长度,节省了运行时间和计算步骤,从而可以优化性能。
选择最优的循环方法
-
以 forEach、for、forin三种循环进行比较
代码示例如下:
var arrList = new Array(1, 2, 3, 4, 5) arrList.forEach(function (item) { console.log(item); }) for (var i = 0, len = arrList.length; i < len; i++) { console.log(arrList[i]); } for (var i in arrList) { console.log(i); }
jsbench.me 测试结果如下图所示,forEach的执行效率要远远高于其它两种,其次使优化过后的for循环,最后是forin循环。由此我们可以得出,forEach是这三者中最优的循环方法。你也可以将forEach与其它循环进行比较,在这里就不一一赘述了。
节点添加优化
-
节点的添加操作必然会有回流和重绘。
下面,我们来模拟创建 DOM 节点的两种方式。
代码示例如下:
for (var i = 0; i < 10; i++) { var oP = document.createElement('p') oP.innerHTML = i document.body.appendChild(oP) } // 文档碎片优化节点添加 const fragEle = document.createDocumentFragment() for (var i = 0; i < 10; i++) { var oP = document.createElement('p') oP.innerHTML = i fragEle.appendChild(oP) } document.body.appendChild(fragEle)
jsbench.me 测试结果如下图所示,当我们使用了 文档碎片 的形式进行节点的添加时,性能要远远大于直接添加节点时的性能。因此我们可以知道,文档碎片优化节点添加,可以大大提高代码的运行效率,提高性能。
克隆优化节点操作
-
下面以重新创建DOM节点和克隆DOM节点为例。
代码示例如下:
<p id="box1">old</p> <script> for (var i = 0; i < 3; i++) { var oP = document.createElement('p') oP.innerHTML = i document.body.appendChild(oP) } var oldP = document.getElementById("box1") for (var i = 0; i < 3; i++) { var newP = oldP.cloneNode(false) // 克隆节点 newP.innerHTML = i document.body.appendChild(newP) } </script>
jsbench.me 测试结果如下图所示,虽然两种结果的运行效率都很快,但是从运行的显示数值上,还是可以看到克隆节点的执行效率较高,这种会节省掉节点样式等一系列动作的执行。
直接量替换 Object 操作
-
使用对象、数组直接量替换 Object 操作
代码示例如下:
var a = [1, 2, 3] // 直接量 var a1 = new Array(3) // Object 操作 a1[0] = 1 a1[1] = 2 a1[2] = 3
jsbench.me 测试结果如下图所示,直接量的执行效率要远远高于 Object 操作,因此我们可以使用直接量替换 Object 操作的方式,来进行性能的优化。
堆栈中代码执行流程
-
下面,我们来看一下堆栈中,代码是如何执行的?
代码示例如下:
let a = 10 function foo (b) { let a = 2 function baz (c) { console.log(a + b + c); // 7 } return baz } let fn = foo(2) fn(3)
代码执行图表如下:
减少判断层级
-
一个程序的运行,f语句的嵌套可能会很多,在程序运行中的判断层级就会增加,从而影响性能。
下面,我们来模拟一个判断用户是否有VIP权限的例子。
代码示例如下:
function doSomething (part, chapter) { const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node'] if (part) { if (parts.includes(part)) { console.log('属于当前课程'); if (chapter > 5) { console.log('您需要提供 VIP 身份'); } } } else { console.log('请确认模块信息'); } } doSomething('ES2016', 6)
在上面的代码中,很明显存在if语句的嵌套,并且每个if语句都要进行判断,从而导致判断次数过多,影响性能。那么,该如何进行优化呢?
代码示例如下:
function doSomething (part, chapter) { const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node'] if (!part) { console.log('请确认模块信息'); return } if (!parts.includes(part)) return console.log('属于当前课程'); if (chapter > 5) { console.log('您需要提供 VIP 身份'); } } doSomething('ES2016', 6)
在上面的代码中,我们进行了与第一段代码不同的判断,我们利用了相反的判断以及return关键字,当程序不满足我们的条件时,将会直接return,结束当前代码运行,减少了不必要的条件判断,从而提高了性能。
上图是使用 jsbench.me 进行测试的结果,证明减少判断层级,是可以进行JavaScript的性能优化的。
减少作用域链查找层级
-
在使用变量时,如果他当前的作用域没有,会沿着原型链的方向,一级一级的往上查找,直到找到为止。往上查找的层级越多,代码的执行效率就会越低。
下面来看一个例子。
代码示例如下:
var name = 'zce' function foo () { name = 'zce666' // name 属于全局 function baz () { var age = 38 console.log(age); console.log(name); } baz() } foo()
在上面的代码中,在baz函数中引用了name变量,但是baz中的作用域中没有name变量,就会沿着原型链往上进入foo中的作用域中进行查找,在这个里面虽然可以找到,但是此时只是name变量的值的改变,因此,我们还要找到name的内存空间,可以看到name属于全局,此时会直接找到根上,查找层级较多。下面,我们来进行优化一下。
代码示例如下:
// 执行效率更高 var name = 'zce' function foo () { var name = 'zce666' function baz () { var age = 38 console.log(age); console.log(name); } baz() } foo()
在上面的代码中,我们在 foo函数中的name变量前面添加了 var 关键字,这说明我们在foo作用域中给name新开辟了一块内存空间,此时查找层级找到foo作用域时,就会结束,从而提高代码的执行效率,优化性能。
上图是使用 jsbench.me 进行测试的结果,证明减少作用域链查找层级,可以提高代码执行效率,缩短执行事件,提高性能。
减少数据读取次数
-
在开发中,我们会采用缓存的形式,去减少数据的读取,以便提高代码的执行效率。
下面我们来看一个简单的例子。
代码示例如下:
<div id="skip" class="skip"></div> <script> var oBox = document.getElementById('skip') function hasEle (ele, cls) { return ele.className == cls } // 建立在空间消耗的基础上,用空间换时间 function hasEle (ele, cls) { // 此时进行了缓存,减少了数据读取次数 var clsname = ele.className return clsname == cls } console.log(hasEle(oBox, 'skip')) </script>
jsbench.me 测试结果如下图所示,当我们将ele.className 使用clsname变量存储的时候,代码的执行效率明显比直接使用时要高。通过缓存,减少了数据的读取次数,以此提高性能。不过,因为新增了变量,会存在一定的空间消耗,也就是所谓的空间换时间。
字面量与构造式
JavaScript中的数据类型,分为引用数据类型和原始数据类型,下面我们分别来看一下这两种数据类型从字面量与构造式的定义方式上,执行效率有何不同。
-
引用数据类型
代码示例如下:
// new + 构造函数式 var test = () => { let obj = new Object() obj.name = 'zce' obj.age = 38 obj.slogan = '我为前端而活' return obj } // 字面量 var test = () => { let obj = { name: 'zce', age: 38, slogan: '我为前端而活' } return obj } console.log(test())
通过构造式定义的对象,在运行时会涉及到函数的调用,从而执行其他操作。而字面量定义的对象,会直接在内存中开辟空间,不会再进行其他的操作,从而节省运行的时间,提高执行效率,从而提高性能。
jsbench.me 测试结果如上图所示,字面量的代码执行效率要远远高于构造式,所以建议多数情况下使用字面量方式定义变量。但是,如果存在扩容的情况,建议使用构造式。 -
原始数据类型
代码示例如下:
var str = 'zce我为前端而活' var str = new String('zce我为前端而活') console.log(str)
在上面的代码中,通过字面量定义的字符串结果是一个单纯的字符串,而通过构造式定义的字符串,结果是一个对象,可以沿着原型链去调用一些方法
jsbench.me 测试结果如上图所示,原始数据类型使用字面量定义的执行效率要远远高于构造式,并且字面量定义的变量也可以使用同样的方法,因此,建议使用字面量方式定义变量,以此提高性能。
减少循环体活动
-
下面来看一组代码对比。
代码示例如下:
var test = () => { var arr = ['zce', 38, '我为前端而活'] var i for (i = 0; i < arr.length; i++) { console.log(arr[i]) } } var test = () => { var arr = ['zce', 38, '我为前端而活'] var i, len = arr.length for (i = 0; i < len; i++) { console.log(arr[i]) } }
上面的代码中,在第二个test函数中,我们将循环体中不变数据的获取,拿到for循环的外面进行,提前缓存了arr的长度,使其在循环时,减少了获取数据的活动。
jsbench.me 测试结果如上图所示,减少循环体内部活动次数的代码执行效率,要远远高于未减少之前的代码执行效率。下面,我们再来看一下,使用不同的循环语句,效率是否还会提高。
代码示例如下:
var test = () => { var arr = ['zce', 38, '我为前端而活'] var len = arr.length while(len--) { console.log(arr[len]); } }
上面代码中,我们使用了while循环,并采取了将数组元素从后往前的遍历方式。此时,当arr中有值时,会一直进行循环,并且这种会少做条件判断,以此来减少循环体的活动次数。
jsbench.me 测试结果如上图所示,表明在上述的情况中,while循环的执行效率要高于for循环,因为whlie中的活动次数更少,所以性能要高一点,但是还是要根据具体的问题来选择不同的循环语句。
减少声明及语句数
-
语句数:即表达式的多少
代码示例如下:
<div id="box" style="width: 100px; height: 100px;"></div> <script> var oBox = document.getElementById('box') var test = (ele) => { // 定义变量,也会增加内存的消耗 let w = ele.offsetWidth // 代码的数量要多于下面那种,表达式增多 let h = ele.offsetHeight return w * h } var test = (ele) => { // 在这里只需要对ele做词法、语法的分析、语法树的转化以及代码的生成 return ele.offsetWidth * ele.offsetHeight } console.log(test(oBox)); </script>
在代码的编译过程中,需要对语法进行拆分,然后做词法、语法的分析、语法树的转化以及代码的生成等操作,在第一个函数中,需要对w、h、ele三个做这些操作,也就是会执行三次,而在第二个函数中,只需要对ele做这些操作,执行一次,因此,第一个函数更要消耗时间。
jsbench.me 测试结果如上图所示,表达式较少的执行效率较高,也就是说,通过减少表达式的数量,可以提高程序的运行效率,提高性能。 -
减少声明数量
代码示例如下:
var test = () => { // 从代码结构的清晰程度来看,推荐这种 var name = 'zce' var age = 38 var slogan = '我为前端而活' return name + age + slogan } var test = () => { var name = 'zce', age = 38, slogan = '我为前端而活' return name + age + slogan } console.log(test())
在代码的编译过程中,需要对语法进行拆分,然后做词法、语法的分析、语法树的转化以及代码的生成等操作,在第一个test函数中,使用了三次var关键字,声明了三个变量,则需要执行三次从拆分到代码生成的操作,而在第二个test函数中,只有一个var关键字,就会只执行一次从拆分到代码生成的操作,节省时间,提高效率,从而提高性能。
jsbench.me 测试结果如上图所示,使用 var关键字较少的代码执行效率更高,因为他减少了声明的数量。不过,从代码结构的清晰程度来看,推荐第一种。
惰性函数与性能
-
下面,我们来看一个惰性函数与普通函数的例子。
代码示例如下:
<button id="btn">点击</button> <script> var oBtn = document.getElementById('btn') function foo () { console.log(this); } function addEvent (obj, type, fn) { if (obj.addEventListener) { obj.addEventListener(type, fn, false) } else if (obj.attachEvent) { obj.attachEvent('on' + type, fn) } else { obj['on' + type] = fn } } function addEvent (obj, type, fn) { if (obj.addEventListener) { addEvent = obj.addEventListener(type, fn, false) } else if (obj.attachEvent) { addEvent = obj.attachEvent('on' + type, fn) } else { addEvent = obj['on' + type] = fn } return addEvent } addEvent(oBtn, 'click', foo) addEvent(oBtn, 'click', foo) addEvent(oBtn, 'click', foo) addEvent(oBtn, 'click', foo) addEvent(oBtn, 'click', foo) </script>
通过上面的代码,我们可以看到惰性函数只是用了一个变量将结果进行了存储,并将这个变量进行了返回。
jsbench.me 测试结果如上图所示,惰性函数的执行效率要远远低于普通函数,但是惰性函数,会减少判断次数,当再次调用时,会使用上一次的结果进行判断,并且代码书写较高级,这种情况下,需要依据具体的情况进行选择。
采用事件委托
-
事件委托的好处:减少内存的占用,减少事件的注册。
代码示例如下:
<ul id="ul"> <li>ZCE</li> <li>28</li> <li>我为前端而活</li> </ul> <script> var list = document.querySelectorAll('li') function showTxt (ev) { console.log(ev.target.innerHTML) } for (let item of list) { item.onclick = showTxt } // 事件委托 var oUl = document.getElementById('ul') oUl.addEventListener('click', showTxt, true) function showTxt (ev) { var obj = ev.target if (obj.nodeName.toLowerCase() === 'li') { console.log(ev.target.innerHTML) } } </script>
在上面的代码中,我们分别采用了两种为li标签添加点击事件的方式:一种是利用for循环遍历每一个元素,为每一个li标签添加点击事件;另一种则是采用事件委托的方式,给li标签的父级ul添加了点击事件,从而达到为每个li添加点击事件的目的。事件委托可以减少内存的占用,可以减少事件的注册,因此,采用事件委托,可以通过减少内存消耗来提高性能。
总结
有的时候,虽然代码执行时间更短,但是代码本身可能并不是那么的健壮。因此,我们应该根据具体的情况,看一下是用时间换空间,还是用空间换时间,通过比较从而选出更适合的代码。