闭包基本概念--从作用域方面理解闭包

150 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

1.如何产生闭包

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包

例如:以下代码片段中,fn2内部函数没有引用外部的变量b。

function fn1() {
  var a = 2
  var b = 'abc'
  function fn2() {
  console.log(a)
  }
}
fn1()

2.闭包是什么

理解一:闭包是嵌套的内部函数

理解二:内部函数中包含被引用变量(函数)的对象

注意:闭包存在于嵌套的内部函数中

3.产生闭包的条件

  1. 函数嵌套
  2. 内部函数引用外部函数的数据
  3. 执行外部函数,执行函数定义就会产生闭包,不需要调用内部函数

4.常见的闭包(闭包指的是一个函数和它周围状态的引用捆绑在一起的组合)

  1. 将函数作为另一个函数的返回值
function test() {
    const a =1
    return function () {
        console.log('a',a)
    }
}
const fn = test()
const a = 2
fn()

执行test的时候(test())就会返回return后面这个函数,将返回值保存在fn里面,执行fn的时候(fn()),这个时候就会返回function。

输出a的值是1。函数中打印的变量会在函数定义的地方去向上一层查找它的值。

  1. 将函数作为实参传递给另一个函数调用
function test(fn) {
    const a = 1
    fn()
}
const a =2
function fn() {
    console.log('a',a)
}
test(fn)

以上代码中,test(fn)将fn作为参数调用test函数,所以执行fn(),输出a=2,在定义fn函数的上级作用域中查找变量的值。

5.从作用域的角度理解闭包

闭包产生的本质是当前作用域中存在指向父级作用域的引用

 function foo() {
        var myName = "极客时间"
        let test1 = 1
        const test2 = 2
        var innerBar = {
            getName: function () {
                console.log(test1)
                return myName
            },
            setName: function (newName) {
                myName = newName
            }
        }
        return innerBar //该语句终止函数foo的执行,并返回innerBar给foo函数的调用者
    }
    var bar = foo()
    bar.setName("极客邦")
    bar.getName()
    console.log(bar.getName())

当执行到return innerBar的时候,该语句代表终止当前的foo函数,将innerBar返回给foo函数的调用者。此时的调用栈如下(通过var声明或function(){}声明的变量存在变量环境、通过let const with() try-catch创建的变量存在词法环境): 当执行到return innerBar的时候调用栈是什么样

image-20210807210534064.png

由代码可知innerBar是一个对象,包含了getName和setName两个方法,这两个方法都是在foo函数内部定义的,这两个方法使用了myName和test1这两个变量。

根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar的时候{var bar = foo(),bar即函数的调用者},虽然此时foo函数以及执行完毕,但是getName和setName函数还是可以使用foo函数中的变量myName和test1。所以当foo执行完毕后,调用栈变成了:

image-20210807211401970.png foo执行完毕后,虽然它的执行上下文被弹出栈中,但是setName与getName需要用到的两个变量还在栈中,相当于一个背包,无论在哪里调用了setName和getName,她们都会从这个背包中获取变量,除了这两个其他是无法访问的,这个包可以称为foo函数的闭包。

6. 闭包的使用

那闭包是如何使用的,当执行到bar.setName方法中的myName = "极客邦"的时候,JavaScript会沿着当前setName的执行上下文->foo的闭包->全局执行上下文的顺序来查找myName变量。此时调用栈如下图

当然getName也是从foo闭包中获取。

从开发者工具中也能查看到闭包的情况

Global代表全局执行上下文,Scope中体现的就是作用域链,从上到下。

结论:可以给闭包下一个定义了。在JavaScript中,根据词法作用域的规则,内部函数总是可以访问外部函数中声明的变量,当通过调用一个外部函数返回内部函数后,即使外部函数执行完毕,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量的集合称为闭包,比如外部函数是foo函数,那这些变量的集合就称为foo函数的闭包