闭包的神秘面纱(上)

82 阅读3分钟

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

前言

想要了解闭包,首先要掌握词法环境执行上下文与调用栈词法作用域等。

名词解释

  • 闭包:闭包是指那些能够访问自由变量的函数,闭包=函数+函数能够访问的自由变量
  • 词法作用域:函数的的作用域在函数定义的时候就确定了。(静态作用域)
  • 词法环境:在代码编译阶段记录变量声明、函数声明、函数声明形参的合集。
  • 执行上下文:调用函数所带的所有信息。包括词法环境、变量环境、this。

我们以函数的创建及执行为例将这些概念串联起来:

  • 函数创建(编译阶段)

会发生变量提升,确定作用域,生成词法环境、变量环境。

词法环境:进行var、function变量提升、函数提升及添加函数形参、并初始化

变量环境:进行let、const、class等变量提升,但不会被初始化(在执行之前访问会报错)

  • 函数执行(执行阶段)

进入执行上下文(包括词法环境、变量环境和this以及确定作用域链),其中词法环境、变量环境在编译阶段就已经确定了。this及作用域链在函数调用时被确定。

  • 作用域链:当代码要访问一个变量时,首先会搜索自身的作用域(即内部词法环境)是否有此变量,再沿着 outer,去父作用域(外部环境),然后搜索更外部的环境,以此类推,直到全局词法环境,这被关系被称为作用域链,是在函数调用时被确认的

下面用一个例子来解释闭包

console.log(a) // undefined 变量提升及初始化
var a = 1
function fn(){
    var b = 2
    //console.log(c) 报错,变量未初始化
    let c = 3
    return function fn2(){
        console.log(b) //访问函数外部变量
    }
}
var fn2 = fn()
fn2()

以下为上述代码编译及执行过程

  1. 创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 创建全局词法环境,函数声明function fn 和变量声明 var a、var fn2
  3. 执行fn(),创建fn的执行上下文并压入栈。
  4. 创建fn()函数的词法环境,函数声明function fn2 和变量声明 var b、let c,并确定this及父作用域---全局作用域
  5. 调用完fn,fn出栈,变量c释放
  6. 调用函数fn2,创建fn2的执行上下文并压入栈。
  7. 创建fn2的词法环境,无内部变量,及函数,父作用域为fn
  8. 调用fn2函数,内部变量未查找到B,查找到父作用域fn的变量b,执行
  9. fn2出栈,由于fn2引用fn的变量b,变量b会一直存在

总结闭包的本质就是:内层函数引用外层函数的变量,而引用的外层函数的变量不会被释放,即使外层函数被销毁。

预告

本篇解释了闭包的形成过程,下篇将针对闭包的作用及应用进行说明,敬请期待!

参考资料