javascript闭包详解

Table of Contents

闭包的两个知识点:

  1. 变量搜索方向:在javascript中,函数可以创造函数作用域。在函数作用域里面可以看到外面的变量,而函数的外面无法访问到函数里面的变量。因此,在作用域里面的变量搜索方向是:自内向外。比如,下面这段代码就验证了这个搜索方向:
var a = 1;

var func1 = function(){
    var b = 2;
    var func2 = function(){
        var c = 3;
        console.log(b);  // 输出:2
        console.log(a);  // 输出:1
    }
    func2();
    console.log(c);  // 输出:Uncaught ReferenceError: c is not defined
};

func1();
  1. 变量的生存的周期:全局变量的生存周期是永久的,局部变量随着函数调用的结束而销毁。
// 例一
var func = function(){
    var a = 1;  // 退出函数后局部变量a将被销毁
    console.log(a);
};

func();

// 例二
var func = function(){
    var a = 1;
    return function(){
        a++;
        console.log(a);
    }
}

var f = func();
f();  // 输出:2
f();  // 输出:3
f();  // 输出:4
f();  // 输出:5

闭包举例

<!DOCTYPE html>
<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++){
                (function(i){
                    nodes[i].onclick = function(){
                        alert(i);
                    };
                })(i);
            }
        </script>
    </body>
</html>
var Type = {};

for(var i=0, type; type=['String', 'Array', 'Number'][i++]; ){
    (function(type){
        Type['is' + type] = function(obj){
            return Object.prototype.toString.call(obj) === '[object '+ type +']';
        }
    })(type);
}

console.log(Type.isArray([]));  // 输出: true
console.log(Type.isString("str"));  // 输出: true

闭包的作用

  1. 封装变量:我们通过一个计算乘积的简单函数来介绍,这个计算函数加入缓存机制,来提高函数的而运行性能,然而cache处于全局作用域下面,但是只被mult函数使用。
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;
};

console.log(mult(1, 2, 3));  // 输出:6
console.log(mult(1, 2, 3));  // 输出:6

为了避免这个变量在其他地方被不小心修改而引发错误,我们把cache加入到函数作用域下面,用闭包封装起来,具体代码如下:

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;
    }
})();

mult(1, 2, 3);  // 输出:6

从上面我们可以看出,计算乘积的部分可以独立出来,提炼函数是代码重构的一种常见技巧,独立出来的小函数由于不需要在其他位置使用,我们可以使用闭包封装起来,代码如下:

var mult = (function () {
    var cache = {};
    var caculate = function () {  // 封闭caculate函数
        var a = 1;
        for (var i = 0, l = arguments.length; i < l; i++) {
            a = a * arguments[i];
        }
        return a;
    }

    return function () {
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }
        return cache[args] = caculate.apply(null, arguments);
    }
})();

console.log(mult(1, 2, 3));
console.log(mult(1, 2, 3));
  1. 延续局部变量的寿命: 我们通过一个数据上报的例子来说明这种情况。
var report = function(src){
    var img = new Image();
    img.src = src;
};

report('http://xxx.com/getUserInfo');

由于浏览器的兼容性问题,在report函数进行上报之前就会丢失30%左右的数据,由于report函数的http请求并不是每次都有效的。导致这个问题的根本原因在于img是局部变量,img变量随着report函数的结束而销毁。通过闭包可以解决这个bug。代码如下:

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

report('http://xxx.com/getUserInfo');

上面代码中的imgs依靠report这个全局变量而存在,而之前的report是一个函数变量,里面的变量随着函数结束而结束。

本篇博客参考自JavaScript设计模式与开发实践,[p35-p40]