“闭包和高阶函数“,拿来吧你

·  阅读 755
“闭包和高阶函数“,拿来吧你

1.闭包

闭包的形成与变量的作用域以及变量的生存周期密切相关

1.1变量的作用域

指变量的有效范围。常见的是在函数中声明的变量作用域。

当在函数中声明一个变量的时候,如果该变量前面没有加关键字var,这个变量就是全局变量(容易造成命名冲突)。

var在函数中声明变量,是局部变量,只有在该函数内部才能访问到这个变量,在函数外面是访问不到的。

示例:

var func = function () {
    var a = 1;
    alert(a); // 输出:1
}
func();
alert(a) // 输出: Uncaught ReferenceError: a is not defined
复制代码

在JS中,函数可以用来创造函数作用域。在函数里面可以看到外面的变量,而在函数外面则无法看到函数里面的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。变量的搜索是从内到外而非从外到内的。

示例:

var a = 1;

var func1 = function() {
    var b = 2;
    var func2 = function() {
        var c = 3;
        alert(b); // 输出:2
        alert(c); // 输出:3
    }
    func2();
    alert(c); // 输出: Uncaught ReferenceError: c is not defined
}

func1();
复制代码

1.2 变量的生命周期

除了变量的作用域之外,另外一个跟闭包有关的概念是变量的生命周期。

对于全局变量,全局变量的生命周期是永久的,除非主动销毁这个全局变量。

而对于在函数内用var关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁:

var func = function(){
    var a = 1; // 退出函数后局部变量a将被销毁
    alert(a);
}

func();
复制代码

看下面这段代码:

var func = function(){
    var a = 1;
    return function(){
        a++;
        alert(a);
    }
}

var f = func();

f(); // 输出:2
f(); // 输出:3
f(); // 输出:4
f(); // 输出:5
复制代码

跟之前的推论相反,当退出函数后,局部变量a并没有消失,而是似乎一直在某个地方存活着。这是因为当执行var f = func();时,f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起来被延续了。

介绍一个闭包的经典应用:假设页面上有5个div节点,通过循环来给每个div绑定onclick事件,按照索引顺序,点击第1个div时弹出0,点击第2个div弹出1,以此类推

<html>
    <body>
        <div>1</div>
        <div>2</div>
        <div>3</div>
        <div>4</div>
        <div>5</div>
    <script>
        var nodes = document.getElementsByTagName('div');
        
        for(var i=0,len=nodes.length; i<len; i++) {
            nodes[i].onclick = function() {
                alert(i);
            }
        }
    </script>
    </body>
</html>
复制代码

测试这段代码发现,无论点击哪个div, 最后弹出的结果都是5。这是因为div节点的onclick事件是被异步触发的,当事件被触发的时候,for循环早已结束,此时变量i的值已经是5,所以在div的onclick事件函数中顺着作用域链从内到外查找变量i时,查找到的值总是5。

解决方法是在闭包的帮助下,把每次循环的i值都封闭起来。当在事件函数中顺着作用域链中从内到外查找变量i时, 会先找到被封闭在闭包环境中的i,如果有5个div,这里的i就分别是0,1,2,3,4:

for(var i=0,len=nodes.length; i<len; i++) {
    (function(i){
      nodes[i].onclick = function() {
        alert(i);
      }  
    })(i)
}
复制代码

1.3闭包的更多作用

1.3.1 封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘机的简单函数:

var mult = function() {
    var a = 1;
    for (var i=0, l = arguments.length; i<l; i++) {
        a = a * arguments[i];
    }
    return a;
}
复制代码

mult函数接收一些number类型的参数,并返回这些参数的乘机。对于那些相同的参数来说,每次都进行计算是一种浪费,可以加入缓存机制提高函数的性能。

var cache = {};
var mult = function() {
    var args = Array.prototype.join.call(arguments, ',')
    if (cache[args]) {
        return cache[args];
    }
    var a = 1;
    for (var i=0, l = arguments.length; i<l; i++) {
        a = a * arguments[i];
    }
    return  cache[args] = a;
}

alert(mult(1,2,3)); // 输出: 6
alert(mult(1,2,3)); // 输出: 6
复制代码

变量cache 仅仅在mult函数中被使用,应该把它封闭在mult函数内部,这样可以减少页面中的全局变量, 以避免这个变量在其它地方被不小心修改而引发错误。

var mult = (function() {
    var cache = {};
    return function(){
        var args = Array.prototype.join.call(arguments, ',')
        if (args in cache) {
            return cache[args];
        }
        var a = 1;
        for (var i=0, l = arguments.length; i<l; i++) {
            a = a * arguments[i];
        }
        return  cache[args] = a;
    }
})();
复制代码

1.3.2 延续局部变量的寿命

img对象经常用于进行数据上报

var report = function(src) {
    var img = new Image();
    img.src = src;
}

report('http://xxx.com/getUSerInfo');
复制代码

因为一些低版本的浏览器的实现存在bug,在这些浏览器下使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功发起了HTTP请求。丢失数据的原因是 img 是report函数中的局部变量,当report函数调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求就会丢失掉。

变量img用闭包封闭起来,便能解决请求丢失的问题:

var report = (function(src) {
    var imgs = [];
    return function(src) {
      var img = new Image();
      imgs.push(img);
      img.src = src;  
    }
})()
复制代码

1.4 闭包和面向对象设计

过程与数据的结合是形容面向对象中的“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现。反之亦然。

闭包相关的代码

var extent = function(){
    var value = 0;
    return {
        call: function(){
            value++;
            console.log(value);
        }
    }
};

var extent = extent();

extent.call(); //输出:1
extent.call(); //输出:2
extent.call(); //输出:3
复制代码

换成面向对象的写法

var extent = {
    value: 0,
    call: function(){
        this.value++;
        console.log(this.value);
    }
};

extent.call(); //输出:1
extent.call(); //输出:2
extent.call(); //输出:3
复制代码

或者

var Extent = function(){
    this.value = 0;
};

Extent.prototype.call = function(){
    this.value++;
    console.log(this.value);
}

var extent = new Extent();

extent.call(); //输出:1
extent.call(); //输出:2
extent.call(); //输出:3
复制代码

1.5 用闭包实现命令模式

先用面向对象的方式来编写一段命令模式的代码

<html>
    <body>
        <button id="execute">点击我执行命令</button>
        <button id="undo">点击我执行命令</button>
        <script>
            var Tv = {
                open: function(){
                    console.log('打开电视机');
                },
                close: function(){
                    console.log('关闭电视机');
                }
            };
            
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open(); //执行命令,打开电视机
            }
            
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close(); //撤销命令,关闭电视机
            }
            
            var setCommand = function(command){
                document.getElementById('execute').onclick = function(){
                    command.execute();
                }
                
                document.getElementById('undo').onclick = function(){
                    command.undo();
                }
            };
            
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
复制代码

命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。

在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:

var Tv = {
    open: function(){
        console.log('打开电视机');
    },
    close: function(){
        console.log('关闭电视机');
    }
};

var createCommand = function(receiver){
    var execute = function(){
        return receiver.open(); //执行命令,打开电视机
    }
    var undo = function(){
        return receiver.close(); //撤销命令,关闭电视机
    }
    return {
        execute: execute,
        undo: undo
    }
};

var setCommand = function(command){
    document.getElementById('execute').onclick = function(){
        command.execute();
    }

    document.getElementById('undo').onclick = function(){
        command.undo();
    }
};

setCommand(createCommand(Tv)); 
复制代码

1.6 闭包与内存管理

误解: 闭包会造成内存泄露

局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直存活下去。从这个意义上看,闭包的确会使一些数据无法被及时销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为null。

跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非JavaScript的问题。在IE浏览器中,由于BOM和DOM中的对象是使用C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用技术策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。

同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null即可。将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。

2.高阶函数

高级函数是指至少满足下列条件之一的函数。

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

应用场景

2.1 函数作为参数传递

可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。

2.1.1 回调函数

在ajax异步请求的应用中,回调函数的使用非常频繁。当我们想在ajax请求返回之后做一些事情,但又不知道请求返回的确切时间时,最常见的方案就是把callback函数当作参数传入发起ajax请求的方法中,待请求完成之后执行callback函数:

var getUserInfo = function( userId, callback ) {
    $.ajax('http://xxx.com/getUserInfo?' + userId, function(data){
        if (typeOf callback === 'function' ) {
            callback(data);
        }
    });
}

getUserInfo(111, function(data) {
    alert(data.userName);
})
复制代码

回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数,并把它作为参数传递给另一个函数,“委托”给另一个函数来执行。

比如: 在页面中创建100个div节点,然后把这些div节点都设置为隐藏。

var appendDiv = function(){
    for(var i =0; i<100; i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
        div.style.display = 'none';
    }
};

appendDiv();
复制代码

把 div.style.display = 'none' 的逻辑硬编码在appendDiv里显然是不合理的,函数难以复用, 并不是每个人创建了节点之后就希望它们立刻被隐藏。

把 div.style.display = 'none' 抽出来,用回调函数的形式传入 appendDiv

var appendDiv = function(callback){
    for(var i =0; i<100; i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
        if (typeof callback === 'function') {
            callback(div)
        }
    }
};

appendDiv(function(node) {
     node.style.display = 'none';
});
复制代码

可以看到,隐藏节点的请求实际上是有客户发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放在回调函数中,“委托”给appendDiv方法。 appendDiv 方法当然知道节点什么时候创建好,所以在节点创建好的时候,appendDiv会执行之前客户传入的回调函数。

2.1.2 Array.prototype.sort

Array.prototype.sort 接受一个函数当作参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可以看到,目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使Array.prototype.sort方法成为了一个非常灵活的方法

// 从小到大排序
[1,4,3].sort(function(a, b) {
    return a - b;
})
// 输出: [1,3,4]

// 从大到小排序
[1,4,3].sort(function(a, b) {
    return b - a;
})
// 输出: [4,3,1]
复制代码

2.2 函数作为返回值输出

相比把函数当作参数传递,函数作为返回值输出的应用场景更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。

2.2.1 判断函数的类型

用 Object.prototype.toString 来计算。Object.prototype.toString.call(obj)返回一个字符串,比如 Object.prototype.toString.call([1,2,3])总是返回“[object Array]”,而 Object.prototype.toString.call("str")总是返回"[object String]"。

var isString = function( obj ) {
    return Object.prototype.toString.call(obj) === '[object String]';
}

var isArray = function( obj ) {
    return Object.prototype.toString.call(obj) === '[object Array]';
}

var isNumber = function( obj ) {
    return Object.prototype.toString.call(obj) === '[object Number]';
}
复制代码

发现这些函数的大部分实现都是相同的,不同的只是 Object.prototype.toString.call(obj)返回的字符串。为了避免多余的代码,把这些字符串作为参数提前植入isType函数:

var isType = function(type) {
    return function (obj) {
       return Object.prototype.toString.call(obj) === '[object' + type + ']'; 
    }
}

var isString = isType('String');
var isArray = isType('Array');
var isNumber = isType('Number');

console.log(isArray([1,2,3])) // 输出:true
复制代码

2.2.2 getSingle

下面是一个单例模式的例子

var getSingle = function(fn) {
    var ret;
    return function() {
        return ret || (ret = fn.apply(this, arguments));
    }
}

var getScript = getSingle(function() {
    return document.createElement('script');
});

var script1 = getScript();
var script2 = getScript();

alert(script1 === script2); // 输出: true
复制代码

即把函数当作参数传递, 又让函数执行后返回了另外一个函数。

2.3 高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过‘动态植入’的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。

在JavaScript中实现AOP, 都是指把一个函数“动态植入”到另一个函数之中,实现方式有很多,这里我们通过扩展Funtion.prototype来实现

Function.prototype.before = function(beforefn) {
    var __self = this; //保存原函数的引用
    return function() { //返回包含了原函数和新函数的“代理”函数
        beforefn.apply(this, arguments); //执行新函数,修正this
        return __self.apply(this, arguments); //执行原函数
    }
}

Function.prototype.after = function(afterfn) {
    var __self = this;
    return function() {
        var ret = __self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}

var func = function(){
    console.log(2)
}

func = func.before(function(){
    console.log(1)
}).after(function(){
    console.log(3)
});

func();

// 输出:
1
2
3
复制代码

把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func函数。

使用AOP的方式来给函数添加职责,也是JavaScript语言中一种非常特别和巧妙的装饰者模式实现。

2.4 高阶函数的其他应用

2.4.1 currying

函数柯里化(function currying)。又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

例子: 编写一个计算每月开销的函数。在每天结束前,记录今天花掉了多少钱。

var monthlyCost = 0;

var cost = function(money) {
    monthlyCost += money;
};

cost(100); //第1天开销
cost(200); //第2天开销
cost(300); //第3天开销

alert(monthlyCost) //输出:600
复制代码

通过这段代码可以看到,每天结束后都会记录并计算到今天为止花掉的钱。但我们其实并不太关心每天花掉了多少钱,而只想知道到月底的时候会花掉多少钱。也就是说,实际上只需要在月底计算一次。

如果在每个月的前29天,我们都只是保存好当天的开销,知道第30天才进行求值计算,这样就达到了我们的要求。虽然下面的cost函数还不是一个currying函数的完整实现,但有助于我们了解其思想:

var cost = (function(){
    var args = [];
    
    return function(){
        if (arguments.length === 0) {
            var money = 0;
            for (var i=0, l = args.length; i<l; i++ ) {
                money += args[i];
            }
            return money;
        } else {
            [].push.apply(args, arguments);
        }
    }
})();

cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值

console.log(cost()); //求值并输出: 600
复制代码

编写一个通用的function currying(){},function currying(){}接受一个参数,即将要被currying的函数。这个函数的作用遍历本月每天的开销并求出它们的总和。

var currying = function(fn) {
    var args = [];
    return function(){
        if (arguments.length === 0) {
            return fn.apply(this, args)
        } else {
            [].push.apply(args, arguments);
            return arguments.callee;
        }
    }
};

var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0, l=arguments.length; i<l; i++) {
            money += arguments[i];
        }
        return money;
    }
})();

var cost = currying(cost);

cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值

console.log(cost()); //求值并输出: 600
复制代码

当调用cost()时,如果明确的带上了一些参数,表示此时并不进行真正的求值计算,而是把这些参数保存起来,此时让cost函数返回另外一个函数。只有以不带参数的形式执行cost()时,才利用前面保存的所有参数,真正开始进行求值计算。

2.4.2 uncurrying

在JavaScript中,当我们调用对象的某个方法时,其实不用关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点,也是常说的鸭子类型思想。

同理,一个对象也未必只能使用它自身的方法,那么有什么办法可以让对象去借用一个原本不属于它的方法呢?

call和apply都可以完成这个需求:

var obj1 = {
    name: 'xiaoming'
}

var obj2 = {
    getName: function(){
       return this.name;
    }
}

console.log(obje2.getName.call(obj1)); //输出: xiaoming
复制代码

我们常常让类数组对象去借用Array.prototype的方法,这是call和apply最常见的应用场景之一:

(function(){
    Array.prototype.push.call(arguments, 4); // arguments借用Array.prototype.push方法
    console.log(arguments); //输出:[1,2,3,4]
})(1,2,3)
复制代码

在我们的预期中, Array.prototype上的方法原本只能用来操作array对象。但用call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。

有没有办法把泛化this的过程提取出来呢? uncurrying就是用来解决这个问题的。

Function.prototype.uncurrying = function(){
    var self = this;
    return function() {
        var obj = Array.prototype.shift.call(arguments);
        return self.apply(obj, arguments);
    }
}
复制代码

在讲解这段代码的实现原理之前, 先来分析下它有什么作用。

在类数组对象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call这句代码转换为一个通用的push函数:

var push = Array.prototype.push.uncurrying();

(function(){
    push(arguments, 4);
    console.log(arguments); // 输出: [1,2,3,4]
})(1,2,3);
复制代码

通过uncurrying的方式,Array.prototype.push.call变成了一个通用的push函数。这样一来,push函数的作用域就跟 Array.prototype.push一样了, 同样不仅仅局限于只能操作array对象。而对于使用者而言,调用push函数的方式也显得更加简洁和意图明了。

我们还可以一次性把 Array.prototype上的方法“复制”到array对象上,同样这些方法可以操作的对象也不仅仅只是array对象:

for(var i=0, fn, ary = ['push', 'shift', 'forEach']; fn = ary[i++];) {
    Array[fn] = Array.prototype[fn].uncurrying();
};

var obj = {
    "length": 3,
    "0": 1,
    "1": 2,
    "2": 3
}

Array.push(obj, 4); //向对象中添加一个元素
console.log(obj.length);  //输出:4

var first = Array.shift(obj); //截取第一个元素
console.log(first); //输出
console.log(obj); // 输出:{0:2,1:3,2:4,length: 3}

Array.forEach(obj, function(i, n) {
    console.log(n) // 分别输出: 0,1,2
})
复制代码

甚至 Function.prototype.call 和Function.prototype.apply本身也可以被 uncurrying,不过这没有实用价值, 只是使得对函数的调用看起来更像JavaScript语言的前身Scheme:

var call = Function.prototype.call.uncurrying();
var fn = function(name) {
    console.log(name);
}
call(fn, window,'xiaoming'); //输出:xiaoming

var apply = Function.prototype.apply.uncurrying();
var fn = function(name) {
    console.log(this.name); //输出:xiaoming
    console.log(arguments); //输出: [1,2,3]
}
call(fn, {name: 'xiaoming'}, [1,2,3]);
复制代码

现在来分析下调用Array.prototype.push.uncurrying()这句代码时发生了什么事情:

Function.prototype.uncurrying = function(){
    var self = this; //self此时是Array.prototype.push
    return function() {
        var obj = Array.prototype.shift.call(arguments);
        // obj是 {
        //  "length": 1,
        //    "0": 1
        // }
        // arguments对象的第一个元素被截去,剩下[2]
        return self.apply(obj, arguments);
        // 相当于Array.prototype.push.apply(obj, 2)
    }
}

var push = Array.prototype.push.uncurrying();
var obj = {
    "length": 1,
    "0": 1
}

push(obj, 2);
console.log(obj); //输出: {0:1,1:2,length: 2}
复制代码

uncurrying 的另外一种实现方式:

Function.prototype.uncurrying = function() {
    var self = this;
    return funtion() {
        return Function.prototype.call.apply(self, arguments);
    }
}
复制代码

2.4.3 函数节流

JavaScript中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则一般不会遇到跟性能相关的问题。但在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁的调用,而造成大的性能问题。下面列举一些场景。

(1) 函数被频繁调用的场景

  • window.onresize事件。给window对象绑定了resize事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果我们在window.onresize事件函数里做了一些跟DOM节点相关的操作,会非常消耗性能的,可能会造成浏览器卡顿现象。

  • mousemove事件。 同样,给一个div节点绑定了拖拽事件(主要是mousemove),当div节点被拖动的时候,也会频繁地触发该拖拽事件函数。

(2) 函数节流的原理

可以借助setTimeout

(3) 函数节流的代码实现

函数节流的代码实现有许多种,下面的throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。throttle函数接受2个请求,第一个参数为需要被延迟执行的函数,第二个参数为延迟执行的时间。

var throttle = function(fn, interval){
    var __self = fn, //保存需要被延迟执行的函数引用
        timer, //定时器
        firstTime = true; //是否是第一次调用
        
    return function() {
        var args = arguments,
            __me = this;
            
        if (firstTime) { // 如果是第一次调用,不需要延迟执行
            __self.apply(__me, args);
            return firstTime = false;
        }
        
        if (timer) { //如果定时器还在,说明前一次延迟执行还没有完成
            return false;
        }
        
        timer = setTimeout(function() { // 延迟一段时间执行
            clearTimeout(timer);
            timer = null;
            __self.apply(__me, args);
        }, interval || 500);
    };    
}

window.onresize = throttle(function(){
    console.log(1)
}, 500)
复制代码

2.4.4 分时函数

某些函数确实是由用户主动调起的,但因为一些原因, 这些函数会严重的影响页面性能。

举个例子: 创建一个好友列表。列表中有成百上千个好友,如果一个好友用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次往页面中创建成百上千个节点。

在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,造成浏览器卡顿甚至假死。

var ary = [];

for(var i=1; i<=1000; i++) {
    ary.push(i);
};

var renderFriendList = function(data) {
    for (var i=0, l= data.length; i<l; i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
}

renderFriendList(ary);
复制代码

解决方案之一: 让创建节点的工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。

timeChunk函数接受3个参数,第一个参数是创建节点时需要用到的数据,第二个参数是封装了创建节点的逻辑,第三个参数表示每一批创建的节点数量。

var timeChunk = function(ary, fn, count) {
    var obj,
        t;
    
    var len = ary.length;
    
    var start = funciton(){
        for(var i=0; i<Math.min(count || 1, array.length); i++) {
            var obj = ary.shift();
            fn(obj);
        }
    }
    
    return function(){
        t = setInterval(function(){
            if (ary.length === 0) { // 如果全部节点都已经被创建好
                return clearInterval(t);
            }
            start();
        }, 200) // 分批执行的时间间隔, 也可以用参数的形式传入
    };
}
复制代码
var ary = [];

for(var i=1; i<=1000; i++) {
    ary.push(i);
};

var renderFriendList = timeChunk(ary, function(n) {
  var div = document.createElement('div');
  div.innerHTML = n;
  document.body.appendChild(div);      
}, 8)

renderFriendList();
复制代码

2.4.5惰性加载函数

在Web开发中, 因为浏览器之间的实现差异, 一些嗅探工作总是不可避免。比如我们需要一个在各个浏览器中能够通用的事件绑定函数addEvent,常见的写法如下:

var addEvent = function(elem, type, handler) {
    if (window.addEventListener) {
        return elem.addEventListener(type, handler, false);
    }
    if (window.attachEvent) {
        return elem.attachEvent('on'+type, hanlder);
    }
}
复制代码

这个函数的缺点是,当它每次被调用的时候都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。

第二种方案:把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让addEvent返回一个包裹了正确逻辑的函数。

var addEvent = (function(){
    if (window.addEventListener) {
        return function(elem, type, handler){
            elem.addEventListener(type, handler, false);
        } 
    }
    if (window.attachEvent) {
        return function(elem, type, handler){
            elem.attachEvent('on'+type, hanlder);
        } 
    }
})();
复制代码

目前的addEvent函数依然有个缺点, 业务我们从头到尾都没有使用过addEvent函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面ready的时间。

第三种方案: 惰性载入函数。此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,在下一次进入addEvent函数的时候,addEvent函数里不再存在条件分支语句:

var addEvent = function(elem, type, handler) {
    if (window.addEventListener) {
        addEvent = function(elem, type, handler){
            elem.addEventListener(type, handler, false);
        } 
    }
    if (window.attachEvent) {
        addEvent = function(elem, type, handler){
            elem.attachEvent('on'+type, hanlder);
        } 
    }
    
    addEvent(elem, type, handler);
}

var div = document.getElementById('div');

addEvent(div, 'click', function(){
    alert(1)
})

addEvent(div, 'click', function(){
    alert(2)
})
复制代码

总结:在JavaScript开发中,闭包和高阶函数的应用极多。就设计模式而言,因为JavaScript这门语言的自身特点,许多设计模式在JavaScript之中的实现跟在一些传统面向对象语言中的实现相差很大。在JavaScript中,很多设计模式都是通过闭包和高阶函数实现的。

分类:
前端