在我最初接触前端的时候,老师曾今和我说过在JS中,立即执行函数即是闭包

但这真的是对的吗?

带着疑问我们对IIFE和闭包进行一点探索,试图找到真相。

立即执行函数表达式

立即执行函数表达式 ( IIFE: Immediately Invoked Function Expression )

(function IIFE(){
    var a = 3
    console.log(a) // 3
})()

函数被包含在一对( )内部,因此成为了一个表达式,通过在末尾加上另外一个( )可以立即执行这个函数,即函数在声明时立刻被执行了,这就是立即执行函数(IIFE)。

我们观察一下这个例子中的变量,可以发现内部声明了一个变量aa只在函数内部可访问,这里涉及到一个函数作用域的知识。

什么是函数作用域?

每声明一个函数,就会产生一个函数作用域,你可以把它当作一个气泡,每个作用域气泡中声明的变量,都会附属于所处的作用域气泡:

function foo(){
    var a = 1
    function bar(){
        console.log(a, b)
    }// <- bar 的函数声明(Function Expression)
    var b = 2
    return bar
}// <- foo 的函数声明(Function Expression)
foo()() // 1 2

foo函数中声明了ab变量和bar函数,bar由于附属于foo气泡内,所以当bar执行时可以访问foo气泡内的变量ab

而上面IIFE的例子中:函数作用域仅仅在立即执行的时候有意义,且变量a只在函数内部可访问,执行完之后,变量a不再具有意义

闭包

上面这个例子中,bar执行时所在的是外部词法作用域,在外部词法作用域中并未声明ab变量,但是却可以访问foo内部的变量ab

function foo(){ 
    var a = 1
    function bar(){
        console.log(a, b)
    }
    var b = 2
    return bar
}
foo()()// <- 函数 bar 在这一行执行了
// 执行 即是函数调用,即: Function Involked

这个就是闭包的第一个特性,即:

函数在当前词法作用域之外执行时,也可以记住并访问其声明时所在的词法作用域,即保有其声明时所在词法作用域的引用。

也可以这么说:

当函数能记住并访问声明时所在的词法作用域时,就产生了闭包

我们回过头来看IIFE的例子:

(function IIFE(){
    var a = 3
    console.log(a)
})()

被认为是经典的闭包例子的IIFE,并没有在其所声明的词法作用域之外被执行,并且变量a的查询并非因为闭包而被发现,而是通过普通的RHS查询被找到的,从这一点看,IIFE已经不能算是一个经典的闭包了

那么闭包还有什么别的特性吗?

我们再回到上面这个例子,我们稍微改写一点点。

function foo(){ 
    var a = 1
    function bar(){
        console.log(a, b)
    }
    var b = 2
    return bar
}
var baz = foo()
baz() // <- bar 实际被执行

如果你了解垃圾回收器的话,我们执行到var baz = foo()这一行时,会期待函数foo已经被回收了,即foo内部的整个作用域会被销毁,因为我们知道它们不再有意义了(* 垃圾回收器: 引擎通过垃圾回收器来释放不再使用的内存空间)。

但是很显然,baz的执行告诉我们并不是,正因为其还保有对foo内部声明的变量ab的引用,所以变量ab并不会被释放,这就是闭包的第二个特性:

阻止垃圾回收器对闭包内部引用的回收

只要baz还在,引用就一直在。

根据这一特性,我们再反观刚刚的IIFE:

(function IIFE(){
    var a = 3
    console.log(a)
})()

执行完之后,a不再具有意义,因此会随着IIFE执行被销毁

结论

了解完什么是闭包之后我们做个总结闭包产生时,会继续保有声明词法作用域的引用,并且这些引用不会被垃圾回收机制回收,而IIFE在执行后并不会在外部作用域再次使用,且整个内部作用域都会被销毁。

因此IIFE并不算是一个经典的闭包案例,只能算是一个特殊的函数表达式执行