关于前端:温故而知新-重新认识JavaScript的Closure

32次阅读

共计 4332 个字符,预计需要花费 11 分钟才能阅读完成。

闭包的重温。

定义:什么是 Closure?

MDN: Closures 的定义如下:

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

再来看看,来自 专业性 的解释:

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.

简而言之,closure是由一个函数以及申明该函数的词法环境组成。

须要留神几个点:

  • a closure is a record

    闭包是一个构造体,存储了一个函数(通常是其入口地址)和一个申明该函数的环境(相当于一个符号查找表)。

  • free variable

    自在变量(在函数内部定义但在函数内被援用的变量)。闭包跟函数最大的不同在于,当捕获闭包的时候,它的自在变量会在捕获时被确定,这样即使脱离了捕获时的上下文,它也能照常运行。

来看上面的代码:

var a = 1;
function test() {
  const b = 1;
  function inner1() {
    var c = a + 2;

    function inner2() {
      var d = a + b;

      return c;
    };

    inner2()};

  inner1();}
test();

应用 Chrome,别离在inner1()inner2()return c 三处进行断点调试:

  • Step1: 程序运行到inner1()

    能够在控制台看到 Scope 外面有 LocalGlobal两项

  • Step2: 程序运行到inner2()

    能够在控制台看到 Scope 外面有 LocalClosure(test)Global 三项,Closure(test)外面有函数 test 中定义的变量b

  • Step3: 程序运行到return c

    能够在控制台看到 Scope 外面有 LocalClosure(test)Closure(inner1)Global 四项,Closure(test)外面有函数 test 中定义的变量 bClosure(inner1) 外面有函数 inner1 中定义的变量c

若外部函数不应用内部函数申明的变量,来批改实例调试看看:

var a = 1;
function test() {
  const b = 1;
  function inner1() {
    var c = a + 2;

    function inner2() {
      var d = a + c;

      return c;
    };

    inner2()};

  inner1();}
test();

持续按之前步骤调试,能够发现程序运行到 inner2() 时,Scope外面没有 Closure(test) 了。出现的后果:外部函数不援用任何内部函数中的变量,不会产生闭包。

MDN 外面提到

In JavaScript, closures are created every time a function is created, at function creation time.

再回顾 closure 概念,其包含函数入口地址和关联的环境。这跟函数创立时相似的,从这角度看,函数是闭包 。那么按实践来讲,上述例子应该是会产生闭包,而理论在chrome 调试发现并没有。猜想是不是这里进行了优化,当没有自在变量的时候,就不进行闭包解决?找了找材料,发现这的确是编译技巧,叫Lambda lifting

至此,总结 闭包产生的必要条件

  • 函数存在外部函数,即函数嵌套
  • 外部函数存在援用任何内部函数中的变量,即存在自在变量
  • 外部函数被执行

剖析:为什么 closure 会常驻内存?

先来论断:闭包之所以会常驻在内存,在于闭包的 lexical environment 没有被开释。

function test() {
  var a = 0;

  return {increase: function() {a++;},
    getValue: function() { return a;},
  };
}

var obj = test();

obj.getValue();
obj.increase();
obj.getValue();

尝试剖析下执行过程:

  1. 初始执行,创立全局lexical environment

    GlobalExecutionContext = {
      LexicalEnvironment: {
        EnvironmentRecord: {
          Type: "Object",
          test: < func >
        }
        outer: <null>,
        ThisBinding: <Global Object>
      },
      VariableEnvironment: {
        EnvironmentRecord: {
          Type: "Object",
          // Identifier bindings go here
          obj: undefined,
        }
        outer: <null>, 
        ThisBinding: <Global Object>
      }
    }
  2. 执行 test 函数,进入 test 函数的lexical environment

    FunctionExecutionContextOfTest = {
      LexicalEnvironment: {
        EnvironmentRecord: {Type: "Object",}
        outer: <null>,
        ThisBinding: <Global Object>
      },
      VariableEnvironment: {
        EnvironmentRecord: {
          Type: "Object",
          a: 0,
        }
        outer: <null>, 
        ThisBinding: <Global Object>
      }
    }
  3. 返回一个对象,外面蕴含两个函数,两个函数的lexical environment

    FunctionExecutionContextOfIncrease、FunctionExecutionContextOfGetValue:{
      LexicalEnvironment: {
        EnvironmentRecord: {Type: "Object",}
        outer: <LexicalEnvironmentOfTest>,
        ThisBinding: <Global Object or undefined>
      },
      VariableEnvironment: {
        EnvironmentRecord: {Type: "Object",}
        outer: <LexicalEnvironmentOfTest>, 
        ThisBinding: <Global Object or undefined>
      }
    }
  4. 调用 objgetValue函数,进入 getValue 函数的 lexical environment,按标准,通过GetIdentifierReference(lex, name, strict) 来解析 a,在increase 函数的 lexical environment 中并不能解析 a,便转到outer 对应的内部 lexical environment 中持续解析
  5. 调用 objincrease函数,过程相似调用 objgetValue函数。

大体过程是如此。标准中对于 FunctionInitialize,能够看到 4.Set F.[[Environment]] to Scope.,看下标准对于[[Environment]] 的阐明

Type Description
Lexical Environment The Lexical Environment that the function was closed over. Used as the outer environment when evaluating the code of the function.

那么,调用 objgetValue函数,转到 outer 对应的内部 lexical environment 中持续解析,这里的lexical environment,应该是指函数的外部属性[[Scopes]]

在之前 chrome 中调试的例子,能够看到 Scope 中呈现 Closure(test),外面保留的是test 函数 enviroment record 中被其余 lexical environment 援用的那局部。

此外,obj.getValue()是在 Global lexical environment 中被调用,被执行时,其 outer 却不是Global lexical environment,这是因为内部词法环境援用指向的是逻辑上突围外部词法环境的词法环境:

The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.

总结

closure,搜查相干材料重新认识,浏览标准,了解业余定义。原来很多货色并不是外表上那么简略!

参考资料

  1. 万维百科 Closure)
  2. MDNClosures
  3. ECMAScript® 2019 Language Specification
  4. 解读闭包,这次从 ECMAScript 词法环境,执行上下文说起
  5. 所有的函式都是閉包:談 JS 中的作用域與 Closure

正文完
 0