执行上下文、作用域链、闭包

193 阅读5分钟

执行上下文

执行上下文:指当前执行环境中的变量、函数声明,参数(arguments),作用域链,this 等信息。

执行上下文分为:

  • 全局执行上下文:最基础的执行上下文,为运行代码主体而创建的执行上下文,也就是说它是为那些存在于 JavaScript 函数之外的任何代码而创建的。它做了两件事:1. 创建一个全局对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文:每个函数都有自己的运行环境,当函数被调用时,才会为函数创建一个新的执行上下文,进入该函数的运行环境。当该环境中的代码被全部执行完毕后,该环境会被销毁。不同的函数运行环境不一样,即使是同一个函数,在被多次调用时也会创建多个不同的函数环境。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。
  • Eval 函数执行上下文:运行在 eval 函数中的代码也获得了自己的执行上下文,在 Javascript 中不常用 eval 函数,不再讨论。

执行上下文的生命周期

执行上下文的生命周期包括 3 个阶段:创建阶段 -> 执行阶段 -> 回收阶段

1. 创建阶段

  • 创建变量对象
  • 创建作用域链
  • 确定 this 的执行

2. 执行阶段

  • 变量对象赋值
    • 变量赋值
    • 函数表达式赋值
  • 调用代码
  • 顺序执行代码

3. 回收阶段

  • 执行上下文出栈等待虚拟机回收执行上下文

创建变量对象

变量对象(VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:变量、函数声明、函数的形参。在浏览器环境中,全局环境的变量对象是 window 对象,因此所有的全局变量和函数都是作为 window 对象的属性和方法创建的。变量对象的创建主要是进行标识符值类型的申明和初始化。

  • 生成 arguments 对象。检查当前上下文中的参数,建立该对象下的属性与属性值
  • 检查当前上下文的函数声明,在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  • 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为 undefined

变量提升

  • 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部。
  • var 存在变量提升,可以在声明之前使用。在全局作用域下声明,会被挂载在 window 上。
  • letconst 存在暂时性死区,不能在声明之前使用。const 声明的变量不能再次赋值

创建作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

确定 this 的指向

  • 在全局环境中,this 指向全局对象
  • 在函数内部,this 的值取决于函数被调用的方式
    • 函数作为对象的方法被调用,this 指向调用这个方法的对象
    • 函数用作构造函数时(使用 new 关键字),它的 this 被绑定到正在构造的新对象
  • 在箭头函数中,this 指向它被创建时的环境
  • 使用apply、call、bind 等方式调用:可切换函数执行的上下文环境,即 this 绑定的对象

JavaScript 的作用域

通俗的讲,作用域就是一个区域,包含了其中变量,常量,函数等等定义信息和赋值信息,以及这个区域内代码书写的结构信息。可以简单的理解为变量的可访问性。分为 3 种类型:

  • 全局作用域:在最外层函数定义的变量即拥有全局作用域,对于任意函数来说,都可以访问到
var a = 1;
var fn = function(){
    console.log(a);//1
}
fn();
  • 函数作用域:局部作用域的变量即是在特定代码块中才能过访问,对于外部是不能够访问的。
var a = 1;
var fn = function(){
    var b = 2;
    console.log(a);//1
}
fn();
console.log(b); // b is not defined
  • 块级作用域:let、const 可形成块级作用域,块级作用域可以形成暂时性死区
var fn = function(){
    for(let i = 0; i < 10; i++){
       console.log(i);//1-9
    }
    console.log(i);// undefined
}
fn()

将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何访问需要的变量或者函数的。

作用域链的作用就是保证当前环境对其有权访问的变量和方法进行有序的访问 ——JavaScript 高级程序设计

JavaScript 闭包

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。—— JavaScript 高级程序设计

可以认为 JS 中的所有函数都是闭包。存在闭包就一定存在作用域链。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1
for (var i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
}

闭包的好处与坏处

好处:

  1. 缓存。将变量隐藏起来不被 GC 回收。
  2. 实现柯里化,利用闭包特性完成柯里化。

坏处:

  1. 内存消耗,闭包产生的变量无法被销毁。
  2. 性能问题,由于闭包内部变量优先级高于外部变量,所以需要多查找作用域链的一个层次,一定程度影响查找速度。