JavaScript之执行上下文、作用域和作用域链

43 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

执行上下文

:一种数据结构,遵循先进后出,后进先出的规则

执行上下文栈call stack,所有执行上下文组成的内存空间

执行上下文:分为 全局执行上下文函数执行上下文

  • 全局执行上下文

JS代码执行之前,必须先创建全局执行上下文,也就是说,全局执行上下文必须最先加入执行栈,位于栈底

  • 函数执行上下文

一个函数运行之前创建的一块内存空间,空间中包含有该函数执行所需要的数据,为该函数执行提供支持

执行栈顶的执行上下文就是当前正在执行的代码环境

执行上下文 .png

执行上下文中的内容

  • 创建变量对象(VO: variable object)(重点)

    • 确定所有形参值以及特殊变量arguments
    • 确定函数中通过var声明的变量,将它们的值设置为undefined,如果VO中已有该名称,则直接忽略
    • 确定函数中通过字面量声明的函数,将它们的值设置为指向函数对象,如果VO中已存在该名称,则覆盖

当一个上下文中的代码执行的时候,如果上下文中不存在某个属性,则会从之前的上下文寻找。

  • 确定 this 指向:略,详见 this指向

  • 确定作用域(详见下文)

全局执行上下文中 VO 也叫做 GO(global object)

VO可以看作是拥有下列三个属性的一个对象:

  • variableObject{},变量对象,包含 arguments 对象,形参、函数和局部变量等
  • scopeChain: {},作用域链,包含内部上下文所有变量对象的列表
  • this:{},上下文中 this 指向的对象
const foo = function(i){
    var a = "Hello";
    var b = function privateB(){};
    var c;
    c = "World";
    function c(){}
}
foo(10);

代码执行到 foo(10) 时,先创建变量对象如下:

fooExecutionContext = {
    variavleObject : {
       arguments: { 0: 10, length: 1 }, // 确定 arguments
       i: 10, // 确定形参
       a: undefined, // 确定变量,赋值为undefined
       b: undefined, // 确定变量,赋值为undefined
       // 确定变量阶段:c 作为变量,赋值为 undefined,
       // 确定字面量声明的函数:覆盖 VO 中已经存在的变量c,指向 ƒ c(){}
       c: ƒ c(){}, 
    },
    scopeChain : {},
    this : {}
}

代码执行阶段,为变量赋值

fooExecutionContext = {
    variavleObject : {
       arguments: { 0: 10, length: 1 }, 
       i: 10,
       a: "Hello",
       b: ƒ privateB(){},
       c: "World", // 执行到 c = "Wolrd",给 c 重新赋值 
    },
    scopeChain : {},
    this : {}
}

小结

创建变量对象的阶段,字面量声明的函数优先级最高,当前 VO 中存在同名变量时,函数覆盖原变量的值,而 var 声明的变量则会直接忽略当前变量的初始化操作(赋值为 undefined

试题

var foo = 1;

function bar(a) {
    console.log(a); // ?1
    var a1 = a;
    var a = foo;
    function a() {
        console.log(a); // ?2
    }
    a1();
}

bar(3);
// 全局上下文 创建阶段
globalExecutionContext: {
    variableObject: {
        foo: undefined // 确定变量
    }
}

// 执行阶段
globalExecutionContext: {
    variableObject: {
        foo: 1 // 确定变量
    }
}

bar(3)

// bar函数上下文 创建阶段
barExecutionContext: {
    variableObject: {
        arguments: { 0: 3, length: 1 }, // 确定 arguments
        // 先确定形参 a: 3
        // 再确定var 声明的 变量 a,由于当前 VO 中已经存在变量 a, 所以不会再赋值为undefined(被忽略)
        // 最后确定字面量声明的函数a,当前VO存在变量a,将其覆盖为 ƒ a(){...}
        a:  ƒ a(){...}, 
        a1: undefined,
    }
}

// bar函数上下文 执行阶段
barExecutionContext: {
    variableObject: {
        arguments: { 0: 3, length: 1 },
        a:  1, // 赋值为foo,当前 VO 中不存在,会从之前的上下文中寻找
        a1: ƒ a(){...}, // a1 被赋值为创建阶段的 a(先执行的 a1 = a,再对 a 进行赋值)
    }
}

通过上面的示例分析可以知道: 问号1 输出的 变量a 的值为 ƒ a(){...}; 问号2 输出的 变量a 的值为 1

作用域及作用域链

  1. VO 中包含一个额外的属性,该属性指向创建该 VO 的函数本身
  2. 每个函数在创建时,会有一个隐藏属性[[scope]],它指向创建该函数时VO
  3. 当访问一个变量时,会先查找自身 VO 中是否存在,如果不存在,则依次查找[[scope]]属性

代码示例:

var g = 0;

function A() {
    var a = 1;
    function B() {
        var b = 2;
        console.log(b, a, g); // 2 1 0
    }
    B();
}
A();

图示

作用域链.png

通过上图可以知道:在函数B的执行上下文中访问b,a,g三个变量时,分别会:

  • 访问 b ,查找当前 VO ,存在 变量b ,直接使用
  • 访问 a ,查找当前 VO ,不存在 变量a ,查找 函数B[[scope]]函数AVO,存在 变量a ,直接使用
  • 访问 g ,查找当前 VO,不存在 变量a,查找 函数B[[scope]]函数AVO,不存在 变量g,继续查找 函数A[[scope]]全局上下文GO,存在 变量g,直接使用