从源头理解JavaScript中的闭包问题

1.定义

函数与对其状态即词法环境lexical environment)的引用共同构成闭包closure)。mdn

A closure is a pair consisting of the function code and the environment in which the function is created.链接

最开始看这个定义的时候,不知所云。当随着对闭包的理解,看这个定义就觉得简练而准确。这两个定义,是我现在比较认同的定义。

定义提到了两个东西 函数本身定义函数时的词法环境

函数本身,比较简单,就是定义函数时写的代码。 词法环境,也叫静态作用域,简单来说,就是你定义函数时,函数内部使用的变量是根据你书写代码的位置来确定的,而不是根据调用来确定的。函数的外部作用域之所以被闭包保存下来,是用于将来函数执行时的变量查找。

2.理解

js里函数是头等公民 First-Class ,意味着函数可以作为参数传递进另一个函数,可以作为函数的返回值返回。本来是没什么问题的,但当函数内部还存在自由变量时,这就会导致一个很经典的问题 Funarg problem

自由变量

除了以下两种,函数参数,函数内部定义的变量,之外的变量。

Funarg Problem

这个还细分为两个子类: 当函数作为参数传进另一个函数的时候。一般称为 downward funarg problem。 当函数作为返回值的时候。一般称为 upward funarg problem

1.downward funarg problem

let x = 10;
 
function foo() {
  console.log(x);
}
 
function bar(funArg) {
  let x = 20;
  funArg(); // 10, not 20!
}
 
// Pass `foo` as an argument to `bar`.
bar(foo);

对于函数 foo 来说,x 是自由变量。当函数调用的时候(通过 funArg ),x 应该如何解析呢,是定义时候的外部作用域里去找,还是执行时的作用域呢?此时变量的查找就会存在多义性。

JS 采用词法作用域来避免这个多义性,使用 [[Scope]] 来保存这个词法作用域的引用。 这一手段其实就是闭包的核心,在创建函数的时候,保存以词法作用域为准的父作用域的引用,用以将来函数调用时进行变量查找。

2.upward funarg problem

function foo() {
  let x = 10;
   
  // Closure, capturing environment of `foo`.
  function bar() {
    return x;
  }
 
  // Upward funarg.
  return bar;
}
 
let x = 20;
 
// Call to `foo` returns `bar` closure.
let bar = foo();
 
bar(); // 10, not 20!

正常来说,函数执行完毕后,函数会被回收掉。所以当 foo 执行完毕后, foo 就被回收掉了。此时bar的父环境作用域已经不存在了,如果 bar 函数中依赖父环境中的变量,那么函数执行结果就不会符合预期了。

由于 JS 中闭包的存在,foo 执行完毕后,并没有释放而是被 bar 函数引用,而保留下来了。所以这个问题也就迎刃而解了。

小结

那闭包到底是个什么东西呢? 首先你得明白两个名词 ,词法作用域和自由变量。 然后 JS 里函数是可以作为变量传递的,那就意味着1.函数可以作为参数传递进另一个函数,2.函数可以作为另一个函数的返回值返回出去。先抛开静态作用域和闭包,就可以发现函数式语言的经典问题,funarg problem。

针对第一种情况,作为参数传入另一个函数。那就意味着函数定义的地方和函数执行的地方不一定是一个作用域,很可能是两个作用域。那么这个时候如果函数内部存在自由变量,该去那个作用域里查找变量呢?这时便存在二义性。 JS里采用了词法作用域,来消除这个二义性,即自由变量的值由函数定义时的作用域查找而来。这只是确定了规则,还无法实施,既然要在定义时的作用域里查找,那么就需要把这个作用域的引用保存下来,如何保存呢?JS 里在定义函数的时候有一个隐藏属性叫 [[scope]] ,就是用来指向作用域的。这就是我理解的闭包背后所做的事情了,保留函数定义时的作用域用于自由变量的查找。

第二种情况,其实也差不多,函数作为另一个函数的返回值。正常来说函数执行完毕,会立即释放。那么要是返回的函数依赖于已经释放的变量,那么返回的函数执行的时候就会有问题。解决的方法同样是闭包 ,这种情况,由于作为返回值的函数持有定义时作用域的引用,所以函数执行完了,并没有释放,而是保留下来了。

所以闭包是什么,我个人认为说闭包是一种解决 funarg 的技术手段更合理一些,这也是导致我们经常感觉说不清楚 闭包是什么的原因。看完这乱起八糟的一团,我也觉得文章最开始给的那个定义也蛮合适的。闭包是由函数代码和其外部作用域的引用共同构成的。

综上所述,可以看出闭包和词法作用域,的确是解决了 funarg 问题。但为什么一定是闭包和词法作用域呢?可以是其它的手段么?

JS 里只有全局作用域和函数作用域(ES5),函数定义的时候,就持有了一个全局作用域的引用,这个意义上所有函数都使用了闭包。

3.作用

1.私有变量 2.模块化

使用 Discussions 讨论 Github 上编辑