闭包和词法环境

闭包是个老生常谈的问题,所以写个记录下什么是闭包,闭包怎么来的,这里就从代码块开始说起。

代码块

  • {...} 花括号中声明的变量对于外部是不可见的。
    • 其中函数 function () {...}; if(){...};for(let i;;){...};while{...}
    // 例 1
    if (true) {
      const testA = 1;
    }
    console.log(testA); //testA is not defined;
    
    // 例 2
    {
      const testB = 2;
    };
    console.log(testAB); //testB is not defined;
    
    // 例 3 
    for (let i = 0; i < 5; i++) {
      console.log(i);
    }
    console.log(i); // i is not defined;
    
    let testC;
    console.log(testC); // undefined
    
    • for循环从视觉上看,let i 位于 {...} 之外。但是 for 构造在这里很特殊:在其中声明的变量被视为块的一部分

块级作用域

  • var 没有块级作用域且会进行变量声明提升,但赋值不会。
    function ku() {
      // 变量提升
      // var phrase; // 第一行(a)
      phrase = 'hello'; 
      console.log(phrase); 
      var phrase; // hello 因为会变量提升 提升到第一行(a)
    }
    ku();
    // 没有块级作用域 无视代码块
    say = 'world';
    if (false) {
      var say;
      const habit = 1;
    }
    console.log(say); // world ---> 没有块级作用域
    // const let 有块级作用域
    console.log(habit); // habit is not a defined;
    
  • 模拟块级作用域:使用自执行函数;
    • 所有function开头都会被认为是函数声明:所以可以使用(), +, !形式写函数表达式。
    !function(){}( console.log(1) );
    +function(){}( console.log(2) );
    (function(){ console.log(3) }());
    (function(){ console.log(4) })();
    

词法环境

  • 在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。

  • 词法环境对象由两部分组成:

    • 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
    • 对 外部词法环境 的引用,与外部代码相关联。
  • 一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。

  • 函数声明

    • 一个函数其实也是一个值,就像变量一样。
    • 不同之处在于函数声明的初始化会被立即完成
    • 当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)
    • 这就是为什么我们可以在(函数声明)的定义之前调用函数声明。
    • 一个变量从技术的角度来讲是存在的,但是在 let 之前还不能使用
      • 从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let死区”语句。
    let x = 1;
    
    function func() {
      // 引擎从函数开始就知道局部变量 x,
      // 但是变量 x 一直处于“未初始化”(无法使用)的状态,直到结束 let(“死区”)
      // 因此答案是 error
      console.log(x); // ReferenceError: Cannot access 'x' before initialization
      let x = 2;
    }
    
    func();
    
  • 函数每次调用时都会创建一个新的词法环境。以存储改函数运行时的变量。

    • 所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
    function makeCounter() {
      let count = 0;
      return function() {
        return count++;
      };
    }
    let counter = makeCounter();  // 新的词法环境
    let counter2 = makeCounter(); // 新的词法环境
    
    • counter.[[Environment]] 有对 {count: 0} 词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]] 引用在函数创建时被设置并永久保存。稍后,当调用 counter() 时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 counter.[[Environment]]:
  • 当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

  • Remark:

    • “词法环境”是一个规范对象(specification object):它仅仅是存在于 编程语言规范 中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
    • 但 JavaScript 引擎同样可以优化它,比如清除未被使用的变量以节省内存和执行其他内部技巧等,但显性行为应该是和上述的无差。

垃圾收集

  • 通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
  • 但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。
  • 请注意,如果多次调用 f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:
    function f() {
    let value = Math.random();
      return function() {
        alert(value); 
      };
    }
    
    // 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
    let arr = [f(), f(), f()];
    
+ 当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value)也会被从内存中删除:
```javascript
function f() {
  let value = 123;
    return function() {
      alert(value);
    }
}
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
  • 实际中的优化
    • 正如我们所看到的,理论上当函数可达时,它外部的所有变量也都将存在。但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
    • 在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
      • 当代码执行暂停时,在控制台中输入 alert(value)
    function f() {
      let value = Math.random();
      function g() {
        debugger; // 在 Console 中:输入 alert(value); No such variable!
      }
      return g;
    }
    let g = f();
    g();
    

闭包

  • 闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。在某些编程语言中,这是不可能的,或者应该以特殊的方式编写函数来实现。但是如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。

  • 也就是说:JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量

  • 在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于 [[Environment]] 属性和词法环境原理的技术细节。

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();  // counter.[[Environment]] 存储了对相应 makeCounter() 调用的词法环境的引用
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?  ----> 0
alert( counter2() ); // ?  ----> 1

counter = null; // ……现在内存被清理了
posted @ 2022-04-26 17:59  lutwelve  阅读(54)  评论(0编辑  收藏  举报