手记

JavaScript闭包详解

基本概念

闭包与变量的作用域以及变量的生命周期密切相关:

1、变量的作用域
用 var 关键字在函数中声明的变量拥有函数作用域,只有在该函数内部才能访问到这个变量。
2、变量的生命周期
对于用 var 关键字在函数中声明的变量来说,当退出函数时,这些变量就失去了它们的价值,它们都会随着函数调用的结束而被销毁。

如果一个拥有函数作用域的变量,能够被外界访问,并且没有随着函数调用的结束而被销毁,在这里就产生了一个闭包。
闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式就是在一个函数内部创建另一个函数。

例子:

function demo() {
    var a = 0;
    return function(n) {
        a+=n;
        console.log(a);
    }
}
var fn = demo();
fn(1); //输出:1
fn(1); //输出:2
fn(1); //输出:3

*通过例子可以看到:由于闭包的存在( 函数 demo 内部的匿名函数就是一个闭包 ),我们可以在函数 demo 外部访问并修改变量 a。

闭包的原理

在一个函数内部定义的子函数会将其父函数的作用域添加到它的作用域链中。当父函数执行完毕之后,由于子函数的作用域链依然在引用它的某些变量或函数,所以父函数的作用域不会被销毁。

经典的闭包问题

例子:

<button>0</button>
<button>1</button>
<button>2</button>

var btns = document.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
    btns[i]`.onclick` = function() {
        console.log(i);
    }
}
//输出:3 3 3

用 var 声明的变量 i 在全局执行环境都有效,全局只有一个 i,每一次循环,变量 i 的值都会增加。而函数内部引用的变量 i 在取值时,只能取得 i 的最后一个值,而不是某个特殊的值。因此,当函数执行时并没有返回自己的索引值,而是变量 i 的最终值 3。例子中的 for 循环结束之后,可以理解为下面的形式:

//伪代码
var i = 3;
btns[0]`.onclick` = function() {
    console.log(i);
}
btns[1]`.onclick` = function() {
    console.log(i);
}
btns[2]`.onclick` = function() {
    console.log(i);
}

根据作用域链的搜索机制,当点击事件发生时,函数内部引用的变量 i 会在全局作用域中找到它定义的位置,并赋值为 3,因此三次点击事件都返回 3。如果像下面这样,写一个立即执行的匿名函数,形成一个闭包结构,就可以返回各自不同的索引值。

例子:

<button>0</button>
<button>1</button>
<button>2</button>

var btns = document.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
    (function(i) {
        btns[i]`.onclick` = function() {
            console.log(i);
        }
    })(i)
}
//输出:1,2,3

立即执行的函数会把每次循环的 i 值以参数的形式保存在函数内部,可以理解为下面的形式:

var i = 3;
(function(i) {
    btns[i]`.onclick` = function() {
        console.log(i);
    }
})(0)
(function(i) {
    btns[i]`.onclick` = function() {
        console.log(i);
    }
})(1)
(function(i) {
    btns[i]`.onclick` = function() {
        console.log(i);
    }
})(2)

我们知道,函数的参数就相当于在函数内部定义的变量,所以每一次循环并被保存在函数内部的变量 i 都是一个全新的变量,我们可以继续把上面的代码拆解成下面的形式进行理解:

var i = 3;
(function() {
    var i = 0;
    btns[i]`.onclick` = function() {
        console.log(i);
    }
})()
(function() {
    var i = 1;
    btns[i]`.onclick` = function() {
        console.log(i);
    }
})()
(function() {
    var i = 2;
    btns[i]`.onclick` = function() {
        console.log(i);
    }
})()

当用户点击的时候,子函数会优先搜索到父函数中定义的变量 i,因此实现了返回各自的索引值。

闭包的意义

1、闭包可以把一些不想要暴露在全局的变量或方法封装在函数内部。

例子:

var plusNum = (function() {
    //私有变量和私有函数
    var obj = {};
    var plusFn = function(arr) {
        var n = 0;
        for (var i = 0; i < arr.length; i++) {
            n = n + arr[i];
        }
        return n;
    }

    //特权/公有方法和属性
    return function() {
        var array = Array.prototype.slice.call(arguments, 0);
        var objName = array.join("");
        if (objName in obj) {
            console.log("输出缓存");
            return obj[objName];
        } else {
            obj[objName] = plusFn(array);
            return obj[objName];
        }
    }
})()
plusNum(1,2); //输出:3
plusNum(1,2); //输出:输出缓存,3

2、通常用面向对象思想能实现的功能,用闭包都能实现。

例子:

面向对象的写法:

function $set() {
    this.items = {};
    this.has = function(value) {
        return value in this.items;
    }
}

$set.prototype = {
    constructor: $set,
    add: function(value) {
        if (!this.has(value)) {
            this.items[value] = value;
            console.log(Object.keys(this.items));
            return "添加成功";
        } else {
            console.log(Object.keys(this.items));
            return "数据已经存在";
        }
    },
    clear: function() {
        this.items = {};
        console.log(Object.keys(this.items));
        return "清除成功";
    }
}

var set = new $set();
set.add(1); //输出:["1"],"添加成功"
set.add(2); //输出:["1", "2"],"添加成功"
set.add(2); //输出:["1", "2"],"数据已经存在"
set.clear(); //输出:[],"清除成功"

闭包的写法:

var $set = (function() {

    //私有变量和私有函数
    var items = {};
    var has = function(value) {
        return value in items;
    }

    //公有方法和属性
    return {
        add: function(value) {
            if (!has(value)) {
                items[value] = value;
                console.log(Object.keys(items));
                return "添加成功";
            } else {
                console.log(Object.keys(items));
                return "数据已经存在";
            }
        },
        clear: function() {
            items = {};
            console.log(Object.keys(items));
            return "清除成功";
        }
    }
})()

$set.add(1); //输出:["1"],"添加成功"
$set.add(2); //输出:["1", "2"],"添加成功"
$set.add(2); //输出:["1", "2"],"数据已经存在"
$set.clear(); //输出:[],"清除成功"

上述有关于闭包意义的两个例子,在 JavaScript 的设计模式中有一个统一的称呼:模块模式。我们知道,函数的参数、在函数中定义的变量或子函数,都是不能在函数的外部进行访问的,所以我们可以称它们是函数的私有变量。但如果在这个函数内部创建一个闭包( 用于访问私有变量的公有方法 ),就可以在函数外部访问这些变量。

创建一个“ 模块模式 ”也很简单:首先利用匿名函数创建私有作用域,然后在这个匿名函数内部,定义私有变量和函数。最后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。具体可以参考上面两个例子。

闭包与性能

由于闭包会使一些数据无法被及时销毁,所以很多文章都会强调要尽量减少闭包的使用,其实我们在程序开发中,会选择主动把一些变量封闭在闭包中,主要考虑的是在不污染全局环境的前提下,以后还会需要使用这些变量。把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的。
从提高性能的角度考虑,一旦全局和闭包中的数据不再有用,最好通过将其值设置为 null 来释放其引用,以便垃圾收集器下次运行时将其回收。

真正理解闭包

在 JavaScript 中,闭包一直是一个非常重要但又难以掌握的概念。闭包可以说是基于作用域所产生的自然结果,所以要想真正的理解闭包,就需要深入的理解作用域。由于篇幅有限,大家可以参考我的另一篇手记:JavaScript作用域与闭包


如有错误,欢迎指出

7人推荐
随时随地看视频
慕课网APP