闭包 (“背包”)

274 阅读6分钟

关于闭包的理解,我们看到过很多种说法:

  • 内部函数引用内部变量并被外部所引用称之为闭包;
  • 函数在定义时的词法作用域以外的地方被调用就会形成闭包;
  • 闭包就是指有权访问另一个函数作用域中的变量的函数;
  • 闭包可以延长作用域链;
  • ...

看过这么多解释以后,我们可能都明白:“通过闭包,我们可以访问到其他作用域中的变量”。但是至于为什么,好像并没有完全理解。

最近在学习闭包的过程中,看到一篇很好的文章(需要翻墙访问),此文的末尾有这么一段话:

    The way I will always remember closures is through the backpack
    analogy. When a function gets created and passed around or returned 
    from another function, it carries a backpack with it. And in the 
    backpack are all the variables that were in scope when the function 
    was declared.
    ‘我理解闭包的方式是通过背包进行类比。当函数被创建并传递或者从其他函数
    返回时,会携带一个背包。并且在这个背包中装有的是函数声明时所在作用
    域中的所有变量。’

上段话中,有几点很重要的点:函数声明、作用域、所有变量,我们接下来对闭包的分析也会围绕这几个关键词展开。

“背包” --> 函数声明时所在作用域中的所有变量

在具体理解闭包之前,我们先理清楚下面代码的执行逻辑(清楚的可以自行跳过):

var x = 10;
function fn(){
    console.log(x); // 10?20?
}
function show(callback){
    var x =20;
    (function (){
        callback();
    })();
}
show(fn);

以上代码执行的结果是10还是20呢?我们从定义和执行两个阶段分析一下:

定义时各作用域中可访问的变量:

  • 全局作用域:x、fn、show
  • fn函数作用域:可访问所在作用域中的所有变量:x和show
  • show函数作用域:x、可访问所在作用域中的所有变量:x和fn

执行时:

  • show函数创建新的执行上下文 --> 进执行栈(作用域链:show-->window),执行函数fn
  • fn函数创建新的执行上下文--> 进执行栈(作用域链fn-->window),执行代码console.log(x):fn发现自己的函数作用域中没有x,因此沿着作用域链查找发现x。因此最终执行结果是10,而不是20。

理解清楚上面代码的执行结果,我们对上述代码稍作修改,继续分析:

var x = 10;
function fn(){
    return x; 
}
function show(){
    var x = 20;
    return function (){
        console.log(x+fn()); // 20?30? 40?
    }
}
var a = show();
a(); // show函数执行结束,变量都会清空,因此无法访问其中的变量x

这段代码的执行结果只与x和函数fn的返回值相关,因此:

  • 函数fn的返回值是多少呢?这个问题可能看过一开始的例子,就会知道答案是10;
  • “函数show执行结束,变量都会清空,其中的x=20这个变量应该就无法访问”这句话乍一看很有道理,但实际因为闭包的存在,恰恰相反x刚好就是20;

那么新的问题就会产生了:“为什么闭包的存在就可以访问到x=20这个变量,这其中的原理是什么呢?”

带着疑问,我们慢慢来分析上述代码的执行过程:

定义时各作用域中可访问的变量:

  • 全局作用域:x、fn、show、a
  • fn函数作用域:全局作用域中的x、a和函数show
  • show函数作用域:x、全局作用域中的x、a和函数fn
    执行时:
  • 执行函数show,创建新的执行上下文并入执行栈,返回匿名函数的定义和它的闭包给变量a(注意:不仅返回函数定义,还返回了函数的闭包,而“背包”中装的恰恰就是函数声明时所在作用域中的所有变量:这里仅有x --> 20)。show执行上下文从执行栈中推出;
  • 执行函数a,创建新的执行上下文并入执行栈,发现新的函数fn,继续;
  • 执行函数fn,创建新的执行上下文并入执行栈,根据其作用域链fn-->window,将x的值10返回(如果不理解为什么是10,再去看下本文开始的代码示例分析)。fn执行上下文从执行栈中推出;
  • 继续执行函数a,a中并没有变量x的定义,但因为闭包的存在,此时a会先去闭包中查找,发现闭包中有x,因此执行打印结果30;如果闭包中没有,则会延着作用域链继续向上查找。

代码分析结束,疑问也就此解答:(“为什么闭包的存在就可以访问到x=20这个变量,这其中的原理是什么呢?”

  • 因为返回函数时,不仅返回函数的定义,还会返回函数声明时所在作用域中的所有变量,这些变量由闭包承载;
  • 返回的函数被调用时,对于其中变量的访问:会首先去它的闭包中查找,然后是父级作用域,直到全局作用域;
  • 因此,返回的函数可以访问到其声明时所在作用域中的变量。

闭包 -- “通过闭包可以访问其他作用域中的变量” 原理你懂吗?

虽然上面的分析看起来内容很多很复杂,但其实把闭包理解成是“背包”,就会很容易理解闭包的作用和其原理:

  • 作用:通过闭包可以访问其他作用域中的变量
  • 原理:为什么通过闭包可以访问其他作用域中的变量?

因为函数返回时会返回函数定义和它的闭包。而闭包中“承载”的是函数声明时所在作用域中的所有变量。因此我们可以通过闭包访问其他作用域中的变量。

最后,我们再分析一个例子,看看你是否真的懂得了闭包 ~ 背包:

function example(){
    var count = 1;
    return function (){
        count += 1;
        console.log(count);
    }
}
var increase = example();
var r1 = increase(); // ?
var r2 = increase(); // ?
var r3 = increase(); // ?
  • 执行example函数,返回匿名函数的定义和它的闭包(count = 1);
  • 执行increase函数,首先在闭包中找到count的值为1,将count加1,并将新的结果2赋值给count;
  • 执行increase函数,首先在闭包中找到count的值为2,将count加1,并将新的结果3赋值给count;
  • 执行increase函数,首先在闭包中找到count的值为3,将count加1,并将新的结果4赋值给count;
  • 因此打印的结果分别是2,3,4。

看完此篇文章,我希望你也会和我一样有所启发,可以从“背包”的角度去理解闭包,可以在理解闭包的原理时想到下面这两句话:

  • 当函数被创建并传递//或者函数被返回时,不仅返回该函数的定义,还包括它的闭包
  • 闭包“承载”的就是函数声明时所在作用域中的所有变量

关于闭包的应用场景,本文暂不分析,持续学习再持续更新。

以上就是对闭包这部分知识的学习和总结,如有不妥之处,还请指正。