执行上下文,作用域,闭包

238 阅读3分钟

执行上下文?

执行 JavaScript 代码的环境的抽象概念

  • 全局执行上下文 — 创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。

  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。

执行栈

后进先出数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎执行脚本时,会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎先执行栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

闭包

闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。

被引用的变量直到闭包被销毁时才会被销毁。

闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易。

作用域

作用域决定这个变量的生命周期及其可见性。 当我们创建了一个函数或者 {} 块,就会生成一个新的作用域。需要注意的是,通过 var 创建的变量只有函数作用域,而通过 letconst 创建的变量既有函数作用域,也有块作用域。

词法作用域

词法作用域是指内部函数在定义的时候就决定了其外部作用域。

如下代码:

(function autorun(){
    let x = 1;
    function log(){
      console.log(x);
    };
    
    function run(fn){
      let x = 100;
      fn();
    }
    
    run(log);//1
})();

log() 函数是一个闭包,它在这里访问的是 autorun() 函数中的 x 变量,而不是 run 函数中的变量。

闭包的外部作用域是在其定义的时候已决定,而不是执行的时候。

autorun() 的函数作用域即是 log() 函数的词法作用域。

闭包与循环

闭包只存储外部变量的引用,而不会拷贝这些外部变量的值。 查看如下示例

function initEvents(){
  for(var i=1; i<=3; i++){
    $("#btn" + i).click(function showNumber(){
      alert(i);//4
    });
  }
}
initEvents();

在这个示例中,我们创建了3个闭包,皆引用了同一个变量 i,且这三个闭包都是事件处理函数。由于变量 i 随着循环自增,因此最终输出的都是同样的值。

修复这个问题最简单的方法是在 for 语句块中使用 let 变量声明,这将在每次循环中为 for 语句块创建一个新的局部变量。如下:

function initEvents(){
  for(let i=1; i<=3; i++){
    $("#btn" + i).click(function showNumber(){
      alert(i);//1 2 3
    });
  }
}
initEvents();

但是,如果变量声明在 for 语句块之外的话,即使用了 let 变量声明,所有的闭包还是会引用同一个变量,最终输出的还是同一个值。

闭包 vs 纯函数

闭包就是那些引用了外部作用域中变量的函数。

为了更好的理解,我们将内部函数拆成闭包和纯函数两个方面:

  • 闭包是那些引用了外部作用域中变量的函数。
  • 纯函数是那些没有引用外部作用域中变量的函数,它们通常返回一个值并且没有副作用。

作用域链

每一个作用域都有对其父作用域的引用。当我们使用一个变量的时候,Javascript引擎 会通过变量名在当前作用域查找,若没有查找到,会一直沿着作用域链一直向上查找,直到 global 全局作用域。