原来你是这样的闭包啊

175 阅读6分钟

你们肯定知道小笼包叉烧包、大连太子包,今天我们就来尝尝 “闭包

前言

提到闭包,那肯定就要和执行上下文、作用域、作用域链挂钩,只有理解了这个东西才能更好吃透闭包

上一篇文章我们已经讲述了什么是作用域、作用域链、执行上下文,我们来简单回顾一下

1、执行上下文

我们先来当一段代码

function foo1() {
    console.log(namer);
}

function foo2() {
    var namer = '张三'
    foo1()
}

var namer = '李四'
foo2()

js引擎在编译上面代码的时候,会生成3个执行上下文,分别是全局执行上下文、foo2执行上下文、foo1执行上下文

每个上下文都包含变量环境和词法环境

  • 变量环境:存储var定义的变量
  • 词法环境:存储letconst定义的变量

20210409223053.png

调用栈就是用来管理这些执行上下文的,是一个栈结构,遵循先进后出

查找变量的时候,js引擎首先会在当前执行上下文中查找,如果没有找到,就根据outer的指向到下一个执行上下文中去查找

记住:js引擎只会去上一级执行上下文中查找

foo1foo2执行完毕后,就会弹出调用栈,等待垃圾回收;而全局执行上下文只会陪伴整个程序走到最后

2、作用域和作用域链

作用域就是变量和函数可以发挥作用的区域,控制着变量和函数的可见性和声明周期

分为全局作用域函数作用域,ES6新增了块级作用域,由letconst命令来体现

注意:用letconst声明的变量会直接存储到词法环境中

块级作用域最大的好处就是避免变量污染全局,所以我们更推荐使用let来声明变量

这也是为什么像JQuery的源码,都会放在立即执行函数(function() {...}) 中,因为函数作用域中的所有变量都不会外泄和暴露到全局作用域中

作用域和执行上下文最大的区别是:执行上下文在运行的时候,随时可能改变;作用域在定义的时就确定,并不会改变

js引擎查找变量时,会在当前执行上下文中先查找,找不到就继续往上一级执行上下文查找,找到了返回,找不到就只能继续往上一级查找,直到找到了全局执行上下文,如果找到了就返回变量,否则就会报错

从局部作用域一直找到全局的作用域的这个查找链条(路径)我们就称之为作用域链

作用域链.png

3、词法作用域

词法作用域就是指作用域是由代码中函数的声明位置来决定的,由它可以知道变量在什么位置,是如何进行查找变量的

一句话:作用域链是由词法作用域决定的

并且,词法作用域是你在敲下代码的那一刻就决定好了,这个变量是在这个函数的作用域声明的,那个函数定义在全局作用域下

闭包

现在我们可以愉快地来学习闭包了

闭包:指的是那些引用了另一个函数作用域中变量的函数 —— 《js高级程序设计》

就是说一个函数中使用了另外一个函数里面的变量,这个函数就叫做闭包,而且通常情况下,这两个函数是相互嵌套的

function foo() {
    var namer = '张三'
    let test = 100
    let demo = 101

    function bar() {
        console.log(test);
        return namer;
    }
    return bar;
}

var f = foo();
f();  // 张三

现在这个bar函数就是闭包了,我老是听说,闭包是会长时间逗留在内存中,导致内存泄露,就是怎么回事?

不急,我们来看一下它的执行上下文:

调用栈1_爱奇艺.jpg

根据词法作用域的规则,内部函数bar总是可以访问到其外部函数中的变量。当bar对象返回给全局变量f时,foo函数已经执行结束,按照游戏规则,foo函数应该退出战场(出栈)

但是返回的函数bar现在已经全局变量f,它会长期保存在内存中,直到程序结束。更要命的是它使用了foo函数内部的变量,foo函数它也没办法,只能把被引用的变量留在内存中,自己出局(出栈)。

闭包.png

现在testnamer这两个变量就成了bar函数的专属背包,哪里调用了bar函数,它就会引用这两个变量,背着foo函数的专属背包;并且,除了bar函数之外,其它任何方法、函数都无法访问该背包

现在我们可以来明确一下闭包:调用外部函数返回内部函数,内部函数引用外部函数的变量,这些变量的集合就称之为闭包

这时候你肯定要说:不是说闭包是一个函数,而且是那个内部函数;其实不需要去刻意斟酌闭包的概念,你只需要清楚明白闭包是怎样形成的

闭包形成有两个条件:

  1. 首先内部函数要引用外部函数中的变量
  2. 其次调用外部函数返回的内部函数

这两句话死记硬背都要把它牢记起来

闭包的特点就是:即使外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在中

打开chrome开发者工具,可以看到,在作用域(Scope)下面可以看到

开发者工具.png

我们称这两个变量的集合为foo函数的闭包

实际上,无论通过何种手段将内部函数传递到所在的词法作用域以外的地方,只要它持有对原来作用域的引用,那么无论在何处执行这个函数都会使用闭包

function foo() {
    var test = '张三'

    function bar() {
        console.log(test);
    }
    func(bar)
}

// 调用内部函数fn()时,它引用外部外部函数的变量test,故在此形成闭包
function func(fn) {
    fn();
}

foo(); // 张三

最后,引用《你不知道的JavaScript》的一段话来结束这篇文章

如果将访问他们各自词法作用域的函数当做第一级的值并到处传递,你就会看到闭包在这些函数中的应用。定时器、事件监听器、Ajax请求、跨域通信、任何其他的异步(或同步)任务中,只要使用回调函数,实际上就是在使用闭包