最近看了一些javascript相关的知识,打算整理一下

闭包

在javascript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包

看以下示例代码,在bar执行的时候,访问了foo的内部变量a和b,所以产生了闭包

function foo() {
    var a = 20
    var b = 30

    function bar() {
        return a + b
    }

    return bar;
}
var bar = foo();
bar();

在chrome里面断点调试,Call Stack表示当前的函数调用栈,Scope为当前正在被执行的函数的作用域链,Local为当前活动对象。可以看出来产生了一个名为foo的闭包

chrome断点调试以上代码-1

chrome断点调试以上代码-1

再来看一段示例代码,可以看到也产生了闭包

function foo() {
    var a = 1
    let b = 2
    const c = 3
    var innerBar = {
        getA : function() {
            console.log(b)
            return a
        },
        setA : function(value) {
            a = value
        }
    }
    return innerBar
}
var bar = foo()
bar.setA(100)
console.log(bar.getA())

打印结果为:

2
100

也用chrome断点调试一下,看下具体情况

chrome断点调试以上代码-3

chrome断点调试以上代码-4

下面来分析以下调用栈情况,在执行return innerBar的时候的调用栈如下:

执行到return bar时候的调用栈

foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setA和getA方法使用了foo函数的变量a和b,所以产生了foo函数的“专属背包”。整个调用栈的状态如下:

foo函数执行完成之后

当执行到bar.setA方法的时候,会创建setA函数的执行上下文,其中访问了b和a两个变量,javascript引擎会沿着当前执行上下文 -> foo函数闭包 -> 全局执行上下文 的顺序来查找b和a变量。调用栈状态如下:

执行到bar.setA方法的时候

同理,在调用bar.getA方法的时候,可以在chrome里面打断点,情况如下图所示,由于之前已经调用过bar.setA(100),foo函数闭包里的a变量已经变成了100,查找a变量的时候,沿着Local -> Closure(foo) -> Global作用域链来查找。

chrome断点调试以上代码-getA-5

chrome断点调试以上代码-getA-6

看下面的示例代码,长得很像产生了闭包的样子,实际并没有产生闭包╮( ̄▽ ̄)╭

var bar = {
    myName : "test",
    printName : function() {
        console.log(myName)
    }
}
function foo() {
    let myName = 'blabla'
    return bar.printName
}
let myName = 't'
let _printName = foo()
_printName()
bar.printName()

打印结果是:

t
t

可以用chrome断点调试一下,看下执行_printName()时候的情况,可以看到Scope里面并没有Closure出现

chrome断点调试以上代码-printName-7

chrome断点调试以上代码-printName-8

闭包什么时候回收

通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量