天天看点

25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链

25.1 闭包

闭包是指有权访问另一个函数作用域中的变量的函数——js高程

简单来说闭包就是函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

25.2 作用域

作用域即函数或变量的可见区域。通俗点说,作用域是根据名称查找变量的一套规则

在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。

1. 全局作用域

定义在最外层的变量或者函数,可以在任何地方访问到它们。

2. 函数作用域

用函数形式以function(){……}类似的代码包起来的(省略号……)区域
var a = "coffe";//在全局作用域
function func(){
  var b="coffe";//在函数作用域内
  console.log(a);
}

console.log(a);//>> coffe
console.log(b);//>> Uncaught ReferenceError: b is not defined
func();//>> coffe
           

a定义在全局作用域内,任何地方都可见,所以函数func内能访问到a;

而b定义在函数func内,可见区域就是函数代码块,后面的打印命令console.log(b)在函数func之外执行的,访问不到函数func内的b,因此输出Uncaught ReferenceError: b is not defined。

3. ES6带来了块级作用域

目的: 解决var带来的变量提升问题
  • ES6规定,在某个花括号对{ }的内部用let关键字生声明的变量和函数拥有块级作用域,这些变量和函数它们只能被花括号对{ }的内部的语句使用,外部不可访问。在你写下代码的时候,变量和函数的块级作用域就已经确定下来。块级作用域和函数作用域也可以统称为局部作用域。

25.3 作用域嵌套

当一个块或函数嵌套在另一个块或函数时,就发生了作用域的嵌套
//全局作用域
function func() {//作用域A
  var a = "coffe";

  function func1() {//作用域B。定义一个函数,把不想公开的内容隐藏起来
    var a = "1891";//这里的a把外层的a的值覆盖了
    var b = "b";
    //这里可以放有很多其他要对外隐藏的内容:变量或者函数
    //……
    //…
    console.log(a);
  }

  console.log(a);//>> coffe
  console.log(b);//>> Uncaught ReferenceError: b is not defined
  func1();//>> 1891
}
           
  • 上面示例了一个嵌套函数,等于有外层函数

    func

    的作用域

    A

    内嵌了函数

    func1

    的作用域

    B

  • func1

    里面的打印命令

    console.log(a)

    访问变量

    a

    时,

    JS

    引擎会先从离自己最近的作用域

    A

    查找变量

    a

    ,找到就不再继续查找,找不到就去上层作用域(此例中上层作用域是全局作用域)继续查找,此例中

    a

    已经找到且值为"

    coffe

    ",所以打印输出

    coffe

  • 依此类推,执行

    func1

    (),会执行

    func1

    函数内部的

    console.log(a)

    ,随即会在作用域

    B

    查找里面

    a

    ,而作用域里面存在一个a的声明和赋值语句var a = “1891”,所以最先找到a的值是1891,找到便不再继续查找,最终func1()输出1891而不是coffe。
    25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链
    如图所示,有3个作用域,从最外层往内分别是全局作用域、

    func

    IIFE

    (立即执行函数)。

25.4 执行上下文(Execution Context)

定义:执行上下文就是当前

JavaScript

代码被解析和执行时所在的环境,也叫作执行环境。

JavaScript

中运行任何的代码都是在执行上下文中运行,在该执行上下文的创建阶段,变量对象(

Variable Object

)、作用域链(

Scope Chain

)、

this

指向会分别被确定。

执行上下文类型:

  • 全局执行上下文:这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是

    window

    对象;2. 将

    this

    指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论
  • 还可以使用

    eval

    with

    关键字创建块作用域(不推荐)

执行上下文的生命周期

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

a. 创建阶段

当函数被调用,但未执行任何其内部代码之前,会做以下三件事:
  • 创建变量对象:首先初始化函数的参数

    arguments

    ,提升函数声明和变量声明(变量的声明提前有赖于

    var

    关键字)。
  • 创建作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,

    JavaScript

    始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
  • 确定

    this

    指向(如何确定this指向)

b. 执行阶段

  • 创建完成之后,就会开始执行代码,在这个阶段,会完成变量赋值、函数引用、以及执行其他代码。

c. 回收阶段

  • 函数调用完毕后,函数出栈,对应的执行上下文也出栈,等待垃圾回收器回收执行上下文。
    25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链
执行上下文栈
var a = "coffe"; //1.进入全局执行上下文
function out() {
  var b = "18";
  function inner() {
    var c = "91";
    console.log(a + b + c);
  }
  inner(); //3.进入inner函数的执行上下文
}
out(); //2.进入out函数的执行上下文
           
  • 在代码开始执行时,首先会产生一个全局执行上下文,调用函数时,会产生函数执行上下文,函数调用完成后,它的执行上下文以及其中的数据都会被销毁,重新回到全局执行环境,网页关闭后全局执行环境也会销毁。
  • 其实这是一个入栈出栈的过程,全局上下文永远在栈底,而当前正在函数执行上下文在栈顶。以上代码的执行会经历以下过程:
    1. 当代码开始执行时就创建全局执行上下文,全局执行上下文入栈。
    2. 全局执行上下文入栈后,其中的代码开始执行,进行赋值、函数调用等操作,执行到

      out()

      时,激活函数

      out

      创建自己的执行上下文,

      out

      函数执行上下文入栈。
    3. out

      函数执行上下文入栈后,其中的代码开始执行,进行赋值、函数调用等操作,执行到

      inner()

      时,激活函数

      inner

      创建自己的执行上下文,

      inner

      函数执行上下文入栈。
    4. inner

      函数上下文入栈后,其中的代码开始执行,进行赋值、函数调用、打印等操作,由于里面没有可以生成其他执行上下文的需要,所有代码执行完毕后,

      inner

      函数上下文出栈。
    5. inner

      函数执行上下文出栈,又回到了

      out

      函数执行上下文环境,接着执行

      out

      函数中后面剩下的代码,由于后面没有可以生成其他执行上下文的需要,所有代码执行完毕后,

      out

      函数执行上下文出栈。
    6. out

      函数执行上下文出栈后,又回到了全局执行上下文环境,直到浏览器窗口关闭,全局执行上下文出栈。
25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链

我们可以发现:

  • 全局执行上下文在代码开始执行时就创建,有且只有一个,永远在执行上下文栈的栈底,浏览器窗口关闭时它才出栈。
  • 函数被调用的时候创建函数的执行上下文环境,并且入栈。
  • 只有栈顶的执行上下文才是处于活动状态的,也即只有栈顶的变量对象才会变成活动对象。

25.5 变量对象(Variable Object,VO)

变量对象(

VO

)是一个类似于容器的对象,与作用域链、执行上下文息息相关。
变量对象的创建过程的三条规则:
  • 建立

    arguments

    对象。检查当前执行上下文中的参数,建立该对象下的属性与属性值。
  • 检查当前执行上下文的函数声明,也就是使用

    function

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

    undefined

    。如果该变量名的属性已经存在,为了防止同名的函数被修改为

    undefined

    ,则会直接跳过,原属性值不会被修改。
25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链
可以用以下伪代码来表示变量对象:
VO={
    Arguments:{},//实参
    Param_Variable:具体值,//形参
    Function:<function reference>,//函数的引用
    Variable:undefined//其他变量
}
           
  • 当执行上下文进入执行阶段后,变量对象会变为活动对象(

    Active Object,AO

    )。此时原先声明的变量会被赋值。变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段。
我们可以通过以下伪代码来表示活动对象:
AO={
    Arguments:{},//实参
    Param_Variable:具体值,  //形参
    Function:<function reference>,//函数的引用
    Variable:具体值//注意,这里已经赋值了喔
}
           
  • 未进入执行上下文的执行阶段之前,变量对象中的属性都不能访问。但是进入执行阶段之后,变量对象转变为了活动对象(被激活了),里面的属性可以被访问了,然后开始进行执行阶段的操作。

全局执行上下文的变量对象

  • 全局执行上下文的变量对象是window对象,而这个特殊,在

    this

    指向上也同样适用,

    this

    也是指向

    window

  • 除此之外,全局执行上下文的生命周期,与程序的生命周期一致,只要程序运行不结束(比如关掉浏览器窗口),全局执行上下文就会一直存在。其他所有的执行上下文,都能直接访问全局执行上下文里的内容。
再看一段代码,留意注释
function func() {
    console.log('function func');
}
var func = "coffe";
console.log(func); //>> coffe
// 以上代码中,按三条规则,变量声明的 func 遇到函数声明的 func 应该会跳过,
// 可是为什么最后 func 的输出结果仍然是被覆盖了显示"coffe"呢?
// 那是因为三条规则仅仅适用于变量对象的创建阶段,也即执行上下文的创建阶段。
// 而 func="coffe" 是在执行上下文的执行阶段中运行的,输出结果自然会是"coffe"。
           

这种现象很容易让人费解,其实也是因为var声明的变量允许重名导致的,若使用关键字let来声明变量,就可以避免这种令人费解的情况发生。

作用域链(Scope Chain)

  • 当代码在一个执行环境中执行时,会创建变量对象的一个作用域链
  • 多个作用域对应的变量对象串联起来组成的链表就是作用域链,这个链表是以引用的形式保持对变量对象的访问。
  • 作用:作用域链保证了当前执行上下文对符合访问权限的变量和函数的有序访问。
25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链
  • 作用域链的最顶端一定是当前作用域(

    local scope

    )对应的变量对象,最底端一定是全局作用域对应的变量对象(全局VO)。
作用域链可以形象地比如为一个蒸笼。
  • 最底下的一屉,相当于是全局作用域,它里面的蒸汽(变量和函数的可见性)可以渗透到整个蒸笼,底层之上的其他屉相当于局部作用域,这些上面屉的蒸汽只能影响更上面的屉。
作用域链可以理解为下面这种伪代码格式:
{
  Scope: [
    { //当前作用域对应的VO
      实参,
      形参,
      变量,
      函数  
    },
    { //第二个作用域对应的VO
      实参,
      形参,
      变量,
      函数
    },
    ...
    { //全局作用域对应的VO
      变量,
      函数
    }
  ]
}
           

变量/函数的查找机制

  • 查找变量/函数时JS引擎是从里离它最近作用域开始的查找的,也即从离它最近的变量对象(VO)开始查找。
  • 如果在当前的变量对象里面找不到目标变量/函数,就在上一级作用域的变量对象里面查找。若这时找到了目标变量/函数,则停止查找;若找不到,一直回溯到全局作用域的变量对象里查找,若仍找不到目标变量/函数,停止查找。

参考文章

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

继续阅读