在了解闭包之前,先要熟悉以下几点:
arguments
和其他命名参数的值来初始化函数的活动对象。在函数中,活动对象作为变量对象使用(作用域链是由每层的变量对象使用链结构链接起来的)。闭包的定义:指有权访问另一个函数作用域中的变量的函数,一般情况就是在一个函数中包含另一个函数。
闭包的作用:访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理
函数内部声明的变量是局部的,只能在函数内部访问到,但是函数外部的变量是对函数内部可见的。
子级可以向父级查找变量,逐级查找,直到找到为止或全局作用域查找完毕。
因此我们可以在函数内部再创建一个函数,这样对内部的函数来说,外层函数的变量都是可见的,然后我们就可以访问到他的变量了。
function foo() {let value = 1;function bar() {console.log(value);}return bar();}const baz = foo();// 这就是闭包的作用,调用 foo 函数,就会执行里面的 bar 函数,foo 函数这时就会访问函数外层的变量baz();
bar
包含 foo
内部作用域的闭包,使得该作用域能够一直存活,不会被垃圾回收机制处理掉,这就是闭包的作用,以供 bar
在任何时间进行引用。
我们通过一段代码仔细分析上述代码片段执行过程到底发生了什么:
function foo() {var a = 2;function bar() {console.log(a);}return bar;}var baz = foo();baz();
var baz = foo
,调用 foo
函数,此时执行流进入 foo
执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在两个执行环境,foo
函数为当前执行流所在执行环境。var a = 2;
,对 a
进行 LHS 查询,给 a
赋值 2。return bar
,将 bar
函数作为返回值返回。按理说,这时 foo
函数已经执行完毕,应该销毁其执行环境,等待垃圾回收。但因为其返回值是 bar
函数。bar
函数中存在自由变量 a
,需要通过作用域链到 foo
函数的执行环境中找到变量 a
的值,所以虽然 foo
函数的执行环境被销毁,但其变量对象不能被销毁,只是从活动状态变成非活动状态;而全局环境的变量对象则变成活动状态;执行流继续执行 var baz = foo
,把 foo
函数的返回值 bar
函数赋值给 baz
。baz
,通过在全局执行环境中查找 baz
的值,baz
保存着 foo
函数的返回值 bar
。所以这时执行 baz
,会调用 bar
函数,此时执行流进入 bar
函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在三个执行环境,bar
函数为当前执行流所在的执行环境。a
是个自由变量,需要通过 bar
函数的作用域链 bar -> foo -> 全局作用域
进行查找,最终在 foo
函数中找到 var a = 2;
,然后在 foo
函数的执行环境中找到 a
的值是 2,所以给 a
赋值 2。console.log(a)
,调用内部对象 console
,并从 console
对象中找到 log
方法,将 a
作为参数传递进去。从 bar
函数的执行环境中找到 a
的值是 2,所以,最终在控制台显示 2。bar
函数后,bar
的执行环境被弹出执行环境栈,并被销毁,等待垃圾回收,控制权还给全局执行环境。// 执行上下文栈ECStack = [globalContext]// 全局执行上下文global = {VO: [global],Scope: [globalContext.VO],this: globalContext.VO}// 函数foo被创建,保存作用域链到函数内部属性[[Scopes]]foo.[[Scopes]] = [globalContext.VO]
// foo函数执行上下文fooContext = {AO: {a: undefined,bar: function () {console.log(a);},arguments: [],},Scope: [AO, globalContext.VO],this: undefined,};
// bar 函数执行上下文barContext = {AO: {a: undefined,arguments: [],},Scope: [AO, globalContext.VO],this: undefined,};
当 bar
函数执行的时候,foo
函数上下文已经被销毁了(亦即从执行上下文栈中被弹出),怎么还会读取到 foo
作用域下的 a
值呢?
当我们了解了具体的执行过程后,我们知道 bar
函数执行上下文维护了一个作用域链:
barContext = {Scope: [AO, fooContext.AO, globalContext.VO],};
对的,就是因为这个作用域链,bar
函数依然可以读取到 fooContext.AO
的值,说明当 bar
函数引用了 fooContext.AO
中的值的时候,即使 fooContext
被销毁了,但是 JavaScript 依然会让 fooContext.AO
活在内存中,bar
函数依然可以通过 bar
函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
闭包的常见应用场景:
(function(){})()
这种格式function hoc(a, b) {return function () {console.log(a, b);};}const fn = hoc(1, 2);setTimeout(fn, 3000);
一般 setTimeout
的第一个参数是个函数,但是不能传值。如果想传值进去,可以调用一个函数返回一个内部函数的调用,将内部函数的调用传给 setTimeout
。内部函数执行所需的参数,外部函数传给他,在 setTimeout
函数中也可以访问到外部函数。
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其他函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。