1. 什么是闭包?
闭包是指有权访问另一个函数作用域中变量的函数,具有以下特点:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
- 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
//fn函数可以访问变量a,但是a既不是fn函数的局部变量,也不是fn函数的参数,所以a就是自由变量
var a = 1;
function fn() {
console.log(a);
}
fn(); //1
在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。如下:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var fn = checkscope();
fn(); //'local scope'
首先我们分析一下这段代码中执行上下文栈和执行上下文的变化情况,执行过程可阅读《JavaScript深入之执行上下文》这篇文章。
这里直接给出简要的执行过程:
(1)进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
(2)全局执行上下文初始化
(3)执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
(4)checkscope 执行上下文初始化,创建变量对象、作用域链、this等
(5)checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
(6)执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
(7)f 执行上下文初始化,创建变量对象、作用域链、this等
(8)f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
了解到这个过程,我们应该思考一个问题,那就是:
当 fn 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
答案就是 fn在 执行上下文维护了一个作用域链,就是因为这个作用域链,fn 函数依然可以读取到 checkscopeContext的局部变量值,说明当 fn 函数引用了 checkscopeContext 中的局部变量值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext的局部变量活在内存中,fn 函数依然可以通过 fn 函数的作用域链找到它,该变量相对于 fn 函数来说就是一个自由变量,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
2. 闭包的特点
- 让外部访问函数内部变量变成可能
- 变量会常驻在内存中
- 可以避免使用全局变量,防止全局变量污染;
3. 闭包的用途
闭包可以用在许多地方。它的最大用处有两个:
- 在函数外部访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
经典面试题:循环中使用闭包解决 var 定义函数的问题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); //3
data[1](); //3
data[2](); //3
当执行 data[0] 函数时,data[0] 函数的作用域里并没有 i 值,就会顺着作用域链往上找,找到全局变量 i = 3, 所以打印结果为3, data[1], data[2] 同理。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0](); //0
data[1](); //1
data[2](); //2
改成闭包后,当执行 data[0] 函数时,data[0] 的作用域链就发生了变化,data[0] 函数的作用域里也没有 i 值, 所以会沿着作用域链从匿名函数的变量中查找,这时候就会找 i 为 0,找到了就不会往全局中查找了,即使全局变量也有 i 的值(值为3),所以打印的结果就是0。data[1], data[2] 同理。
4.使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。