概述

这里我们来看一下关于代码优化的内容介绍。

首先我们要去考虑一下该如何去精准测试我们js代码的性能,那这个精准的测试本质上来说我们做的事情就是,经过大量的数据采集,然后去执行样本的一个数学统计的分析,从而去得出一个比对的结果来,证明什么样的脚本他的执行效率更高。

而这样的一个过程对于我们的编码者来说呢显得就有些麻烦,因为我们可能更多的只是关注该如何使用脚本去实现某一个功能,而不是去做大量的数学统计。

所以在这个地方我们去采用一个基于Benchmark.js的一个perf的网站,进行一个在线的js脚本的性能测试。

那么接下来我们就来看一下,这样的测试该如何去执行,也就是哦们的jsperf这样一个网站该如何去使用。

后续我们会具体演示这样一个过程,在这我们先以文字的形式列一下这样一个执行步骤。

Jsperf使用流程

  1. 使用GitHub账号登录

  2. 填写个人信息(非必须)

  3. 填写详细的测试用例信息(title, slug)

这里我们比较关注的是当天测试用例的title和slug,他会用它去生成一个短连接,用于我们去在其他的地方访问,这样的一个测试用例。

  1. 填写准备代码(DOM操作时经常使用)

  2. 填写必要的setup和teardown代码

setup可以理解为是当前要做的一个前值准备工作,比如说我们要使用手机就要先打开手机,而teardown就是所有代码执行完之后要做的一个销毁的操作,比如我们在时候数据库的时候,链接完之后用完了这个过程我们应该把当前的链接资源释放掉。从而让我们的内存得到一个空间的释放。

  1. 填写测试代码片段

这里我们可以填一个片段也可以填多个片段,取决于我们到底想要测试几个片段,有了这些操作以后我们就可以直接在浏览器当中去运行我们当前的脚本。

最终当我们这样一个脚本在这样一个网站上执行完成之后就会给出数据上的提现,通过数据我们就可以得出,什么样的类型的js脚本会具有更高的执行效率。

我们可以在浏览器当中测试使用一下。

首先我们打开jsperf(www.jsperf.com)网站,使用登录完成之后,我们进入到填写个人信息界面,这里是非必填的,我们向下看,可以发现Test case details栏目。

这里有两个是必填的,一个是title,一个是slug,这里我们需要注意的是当前这个slug必须是唯一的,因为他会去生成一个空间,利于我们去访问自己的测试用例。

往下看有个叫做Preparation code html栏目,这里就是准备代码,也就是我们需要用到一些DOM操作或者说我们需要引入一些第三方的资源库的时候,可以在这个里边提那些代码。

在下边就是setup和teardown的填写区域,这里我们可以在后续的测试中有所应用,现在我们先放在这里。

再往下就是当前代码片段的填写,这里有几个是必填的,第一个是测试标题,紧接着就是我们要测试的代码片段,我们可以直接把代码贴在这里面,然后这里有多个片段,还可以自己添加。

准备好这些内容之后,我们可以直接保存,保存之后我们会跳转到新的界面。

慎用全局变量

这里我们来看一下关于慎用全局变量,相关的内容介绍,首先我们先从字面意思做一个解释,关于慎用全局变量我们就认为是在程序执行过程中如果针对于某些数据需要进行存储,那么我们可以尽可能的把他放置在局部作用域当中,变成一个局部变量。

至于说我们为什么要这样做?我们从以下的几点进行分析,当我们在一个全局的范围内定义完变量之后,那么他其实是存在于全局的执行上下文之中,那么这个上下文也就是我们在后续程序查找数据过程中所有作用域链的最顶端。

那如果我们按照这种层级向上查找的一个过程来说,下面某些局部作用域没有找到的变量最终都会查找到顶端的全局作用域。

所以在这种情况下我们查找的一个时间消耗是非常大的,这样一来也就降低了我们当前代码的执行效率,除此以外我们再当前全局上下文当中去定义的变量他一直是存活于我们的上下文执行栈。

而这个上下文执行栈是直到我们当前程序退出之后才会消失的,所以这对于我们当前的GC工作来说是非常不利的,因为只要GC发现这样的变量处于存活状态我们就不会把他当做垃圾对象来进行回收。

因此这样的做法也会降低当前程序运行过程中对于内存的一个使用,除此以外,如果说我们再某个局部作用域当中去定义了一个同名的全局变量,这个时候就有可能造成当前全局变量的命名污染,或者将当前全局的数据进行了遮蔽。

总归来说我们再使用全局变量的时候就需要考虑更多的事情,否则就会给我们带啦一些意想不到的情况。

那在这里我们主要讨论的是针对一个具体的场景去判断一下我们在使用全局变量的时候他的执行效率,和我们在使用局部变量的时候他的执行效率进行一个对比。

首先我们要明确要实现的效果是一样的,只不过一个是通过全局变量来进行存储,另外一个通过局部变量来进行存储,最后我们借助jsperf来观察一下两者执行的性能差异。

这里我们通过一段对比代码来完成。

首先这里我们定义两个全局变量,一个是i一个是str,str我们做一个基础赋值,给个空的字符串。

接下来我们通过一个for循环来生成一个很长的字符串,这个字符串我们建议小一些,不然机器可能会出现问题。在循环内部我们拼接上字符串的长度。这是我们采用全局作用域的方式来实现我们要做的事情。

var i, str = '';
(function() {
    for (i = 0; i < 1000; i++) {
        str += i;
    }
})()

另外一个和第一个类似,不过这里我们使用的是局部变量i和str

(function() {
    let str = '';
    for (let i = 0; i < 1000; i++) {
        str += i;
    }
})()

那这个时候我们就相当是用两份不同的代码去完成了一个相同的效果。接下来我们就把他们放在jsperf当中,将他们大量执行,从而得出谁的执行效率更高。

在jsperf中分别贴入这两段代码,然后运行当前的脚本,等待一会他会把我们的脚本进行一个大量的执行,然后给出一个最终的性能结果。

执行完成以后可以发现,我们采用全局变量和采用局部变量之间的差距是非常大的,所以这里我们就验证了,当我们去完成一个相同的时间结果过程中,如果我们采用全局变量来完成的话他的效率是远低于局部变量的。

这也就验证了我们应该慎用全局变量,从而提高js的执行效率。

缓存全局变量

这里我们来看一下,如何通过缓存全局变量的方式,让我们的js代码在执行的时候会有更高的执行性能。

关于所谓的缓存全局变量,他其实值得就是在我们程序执行过程中,个别全局变量的使用是无法避免的,例如当我们想要查找DOM元素的时候,这种情况下我们就必须使用到document,而document就是一个全局变量。

所以在在这种情况下我们就可以选择将这种需要大量使用的全局变量放置在某一个局部作用域中。从而达到一种缓存效果。

下面我们就来实现一下,当我们查找DOM元素时,使用缓存和不使用缓存得到的一个性能差异到底有多大。

为了避免元素单一,我们可以在input标签之间穿插一些p标签。

<body>
    <input value="btn" id="btn1" />
    <input value="btn" id="btn2" />
    <input value="btn" id="btn3" />
    <input value="btn" id="btn4" />
    <p>111</p>
    <input value="btn" id="btn5" />
    <input value="btn" id="btn6" />
    <p>222</p>
    <input value="btn" id="btn7" />
    <input value="btn" id="btn8" />
    <p>333</p>
    <input value="btn" id="btn9" />
    <input value="btn" id="btn10" />
    <script>
        function getBtn() {
            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;
            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>
</body>

这里我们回到jsperf里面分别执行这两个获取函数,在html里面贴入body中的html,不包含body,然后再js里面分别放入两个函数然后开始测试。

结果可以发现,使用缓存的效果比没使用缓存会有一些优势。

通过原型对象添加附加方法

接下来我们看一下js中可以如何使用原型链的方式提升我们的性能。首先呢我们都知道,在js中存在三种概念,第一个就是构造函数,第二就是原型对象,第三就是我们的实例对象。

而在这个过程中我们的实例对象和构造函数都是可以指向原型对象的,所以在这个过程中如果某个构造函数的内部具有成员方法,让我们后续的实例对象都需要频繁的去进行调用,那这里就可以直接把他添加在原型对象上。而不需要放在构造函数内部。

这样两种不同的实现方式在性能上也会有所差异,所以在这个地方我们还是通过代码的方式来演示一下。

var fn1 = function() {
    this.foo = function() {
        console.log(11111);
    }
}

let f1 = new fn1();

var fn2 = function() {}
fn2.prototype.foo = function() {
    console.log(11111);
}

let f2 = new fn2();

我们在jsperf中对比发现,通过构造函数添加的方法相比于通过原型添加的方法,效率差别还是比较大的。

避开闭包陷阱

这里我们来看一下如何避开闭包陷阱,从而让我们js代码在执行过程中可以具有更高的执行效率。这里我们先来介绍一下闭包。

我们都知道闭包在js中是一个非常强大的语法,在有些情况下可以给我们的使用带来非常大的便捷,不过我们也要知道,闭包如果使用不当的情况下很容易造成内存泄漏。这里为了表达的就是,不要为了闭包而闭包。

接下来我们去对比一下使用闭包和使用函数重用的方式实现相同的功能谁的执行效率更高。

首先我们这里定义一个函数,他接收另外一个函数作为参数,从而将来他在调用的时候可以让另外函数内部的代码得到执行,这个过程我们分别采用两种方式实现,使用闭包和不使用闭包。

function test(func) {
    console.log(func());
}

function test2() {
    var name = 'yd';
    return name;
}

test(function() { // 使用闭包
    var name = 'yd';
    return name;
});

test(test2) // 不使用闭包

我们将test和test2放在setup中,因为他们是我们每轮都需要使用的部分,我们没必要在每轮测试时都去重新定义。

可以发现使用闭包效率低于使用函数的方式,所以我们这里建议,如果非不要,不建议使用闭包。

避免属性访问方法使用

这里我们看一下如何通过避免属性访问方法的使用,去提高我们js在执行过程中的一些性能。

关于属性访问方法,我们知道这是跟面向对象语法相关的,为了更好的实现这个封装性,所以在更多的时候我们可能会将一些对象的成员属性和方法放在一个函数内部,然后我们对外部暴露一个方法对这些属性进行增删改查。

但是这个特性在我们的js中并不是那么的适用,因为在js里面是不需要属性的访问方法的,所有的属性他其实在外部都是可见的,而且在我们使用属性访问方法的时候他就相当是增加了一层重定义,对于当前的访问控制来说,没有太多的意义。所以在这里我们就不推荐属性访问方法的使用。

function Person() {
    this.name = 'icoder';
    this.age = 18;
    this.getAge = function() {
        return this.age;
    }
}
const p = new Person();
const a = p.getAge();

function Person() {
    this.name = 'icoder';
    this.age = 18;
    this.getAge = function() {
        return this.age;
    }
}
const p = new Person();
const b = p.age;

通过对比发现通过属性访问方法访问属性比直接访问属性效率要低很多。

For循环优化

这里我们来看一下关于for循环的优化,对于js编码来说,通常会遇到一组数据结构,这个时候我们就可以采用for循环对其进行遍历,而在使用for循环遍历的过程中呢,我们同样可以从小的细节方面进行优化。

不要直接使用数组的属性,最好缓存下来。

var arrList = [];
arrList[10000] = 'icoder';
for (var i = 0; i < arrList.length; i++) {
    console.log(arrList[i])
}

for (var i = arrList.length; i; i--) { // 缓存数组长度
    console.log(arrList[i])
}

选择最优的循环方法

这里我们要介绍forEach,for, for … in, 这里我们要做的就是当我们遍历一组相同的对象时,这三种方式他们在实现上谁的效率更高一些。

var arrList = new Array(1, 2, 3, 4, 5)

arrList.forEach(function(item) {
    console.log(item);
})

for (var i = arrList.length; i; i--) { // 缓存数组长度
    console.log(arrList[i])
}

for (var i in arrList) { // 缓存数组长度
    console.log(arrList[i])
}

比对发现, foreach效率最高,for…in…效率最低。

文档碎片优化节点添加

这里我们来看一下关于节点添加的优化操作, 针对于我们web开发来说,DOM节点的操作是非常频繁的,针对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);

采用文档碎片添加速度要快于直接append的方式的。

克隆优化节点操作

这里我们通过克隆的方式优化节点操作,从而让我们js代码在执行的时候具有更高的使用性能。

针对于我们当前的DOM节点操作,我们可以认为有新增这样一个节点,第二我们新增完成之后呢后续可能还会有很多属性和方法的添加,我们这里主要以节点的新增去配合克隆完成一个优化的操作。

例如我们想向界面当中添加一个p标签,当页面渲染完成之后其实页面中应该存在很多p标签,其中有一个p标签和我们想要创建的p标签他们在属性和外观的样式上和我们当前的p应该有很多相似的地方,所以我们的克隆就是当我们要去新增节点的时候,可以先去找到一个与他类似的已经存在的节点,把他克隆一下然后再去把克隆好的节点直接添加到界面当中。

这样优化的内容是本身已经具有的样式和属性就不需要后续再执行添加了,这就是所谓的优化。

<body>
    <p id="box1">old</p>
    <script>
        // 创建方式
        for (var i = 0; i < 10; i++) {
            var oP = document.createElement('p');
            oP.innerHTML = i;
            document.body.appendChild(oP);
        }
        // 克隆方式
        var oldP = document.getElementById('box1');
        for (var i = 0; i < 10; i++) {
            var newP = oldp.cloneNode(false);
            newP.innerHTML = i;
            document.body.appendChild(newP);
        }
    </script>
</body>

测试后可以发现,克隆的方式要快于创建的方式。

直接量替换newObject

在这里我们来看一下通过直接量去天幻Object的操作,对我们当前js代码进行一个优化,从而去提高他的执行性能。

这个操作相对来说比较简单,我们直接在代码中进行测试和编写。

当我们定义对象和数组的时候,我们有两种不同的形式,比如可以使用new的方式获取相应的数据,但是也可以直接采用字面量,这里我们以数组为例来进行一个测试。

var a = [1, 2, 3];

var a1 = new Array(3)
a1[0] = 1;
a1[1] = 2;
a1[2] = 3;