callcc.dev

JS 的函数既可以是 pure function,也可以是闭包。闭包相对于 pure function 来说,差别在于它捕获了当前函数域外的引用。那么,在 JS 里是如何实现闭包的呢?JS 是如何捕获外部变量的?答案是词法环境。

Δ词法环境(Lexical Environment)

JS 在每个域内,都有一个在语言层面看不到的东西,叫做词法环境(Lexical Environment),它负责对当前域内的引用绑定进行记录。比如这样一段代码
function foo() {
  let message = "oops"; // ① 在当前函数的词法环境中对*message*引用变量进行创建,然后用*opps*作为值对其初始化
  console.log(`${message}~`); // ② 在当前函数的词法环境中查找*message*引用的值
}
词法环境还会保留对上一层词法环境的引用,称为outer lexical environment,最后的结果是,词法环境之间关系是一棵多叉树的节点之间的结构关系。比如这样一段代码
function outer() {
  const r1 = "outer string";

  function inner1() {
    console.log("inner1", r1);
  }

  function inner2() {
    console.log("inner2", r1);
  }
}
inner1inner2outer lexical environment都是outerlexical environment。利用这样的结构,就能实现闭包了。引用的查找逻辑是:现在当前的词法环境中查找,找到直接返回对应的值;找不到时,就在外层的词法环境中找,直到外部词法环境为 null 时,返回 undefined。查找方法:ResolveBinding ( name, [env] )
还是以这个代码为例,在运行 inner1 时,会先在 inner1 的词法环境中查找名为 r1 的引用,当前的词法环境中不存在 r1,但是当前的词法环境的outer lexical environment存在,那么就在这个词法环境里找,在这个词法环境中有对名为 r1 的引用记录,因此返回它记录的值,也就是outer string
JS 里有四种词法环境,具体可以查阅Lexical Environments

Δ内存泄漏(Memory Leak)

内存泄漏在 C/C++里会经常听到,而在有 GC 的语言里少有听到,GC 语言里的内存泄漏是指对不需要的对象还持有它的引用,导致 GC 时不能被清理。在 JS 里,如果不小心使用闭包,那么也是有可能造成内存泄漏的,以下例子来自这里
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) {
      console.log("hi"); // ① 这里使得replaceThing需要保留originalThing的引用
    }
  };
  //originalThing = null; // ④
  theThing = {
    longArray: [...new Array(100000)].map((_) => "_"),
    someMethod: function () {
      // ② 这个函数的词法环境引用了replaceThing的词法环境
      console.log("someMethod");
    },
  };
};
setInterval(replaceThing, 1000); // ③
① 这里unused函数的声明导致 replaceThing 的词法环境必须包含 originalThing,然后在 ② 里,someMethod 的词法环境会对外层 replaceThing 函数的词法环境进行引用,导致的结果就是 theThing 现在间接地对 originalThing 进行了引用。如果对 replaceThing 进行重复调用(③),那么 originalThing 会引用上一次运行的 theThing 结果,然后又被新的词法环境捕获。重复这个过程,导致形成引用链,内存一直得不到释放,如图所示(LE -> Lexical Environment)
内存引用
内存引用
内存泄漏情况
内存泄漏
内存泄漏
解决方法是只要去掉 ④ 的注释即可,那么 replaceThing 函数的词法环境对 originalThing 的引用就会中断,整个引用链也就随之中断。
不泄露
不泄露

Δ参考资料