7.彻底搞懂javascript-闭包

1,085 阅读3分钟

关于闭包的讨论的太多太多了,这里不讲晦涩难懂的概念。

我们来想一个问题,

如何把函数的运行上下文保存下来?

啥?

function print() {
    var inFunction = 'inFunction';
    console.log(inFunction);
}

print()

我们知道在进入函数运行的时候,会创建函数的运行上下文,并放在栈顶:

当函数运行完毕,把函数运行上下文从运行栈中弹出:

大家会发现,对上次运行的函数时创建的词法环境的访问链路,断掉了,我们找不到了,无法再引用函数词法环境里的变量(上图中红色框框所示)。

那有没有办法,把这个对这个词法环境的引用保存下来呢?

答案是有的。

我们回忆一下:函数创建时会把创建函数时候的运行上下文的词法环境保存在函数对象的[[scope]]中。因此,如果我们在函数运行的上下文创建一个函数,这个函数的[[scope]]就可以保存父级函数运行的时的词法环境了。像这样:

function print() {
    var inFunction = 'inFunction';
    console.log(inFunction);
    function inner() {
        var inFunction = inFunction;
    }
}

print()

如上图,发现虽然可以通过内部函数的[[scope]]其访问 "失联"的词法环境,我们发现,内部函数由于外部函数运行上下文的退出,也处于“失联”状态,我们在全局上下文上无法访问内部函数。

其实很直观的,如果在全局上创建一个变量,并把内部函数返回给它,它不就可以通过全局变量访问内部函数了,进而可以访问外部函数那些“失联的词法环境”。

function print() {
    var inFunction = 'inFunction';
    console.log(inFunction);
    function inner() {
        var inFunction = inFunction;
    }
    
    return inner
}

var inner = print();
inner();

如下图所示:

我们发现,即使函数运行上下文退出了,我们在全局环境中还是可以访问到所退出的函数的词法环境。

更近一步思考:既然可以在函数内部返回一个函数对象给全局变量,可以保持对已经退出运行栈的运行上下文的词法环境的访问,如果我们在函数内返回一个普通对象呢?把需要被全局环境访问的“东西”复制到对象上,然后把对象返回,我们也可以在全局变量通过对象找到那些“失联”的信息。

function print() {
  var inFunction = 'inFunction';
  console.log(inFunction);
  function inner() {
      var inFunction = inFunction;
  }
  
  return {
    inFunction:inFunction,
    inner:inner
  }
}

var obj = print();
console.log(obj.inFunction);

这就是闭包。

所以闭包,其实就是通过某种方式把把“失联”词法环境的引用引到全局环境来。因为这词法环境现在只有外围函数返回时提供的引用作为入口,只有一个洞可以进去,是一个密闭的空间,故而叫做闭包吧。

闭包,可以让函数在运行完毕后,其运行时的上下文的词法环境仍然能被访问。