JS执行上下文和作用域链

205 阅读7分钟

什么是执行上下文(Execution Context)和作用域链(Scope Chain)?

执行上下文(Execution Context)

1:执行上下文是JS代码执行时所需要的环境。它包含了变量、函数声明、this 的值等信息。可以把它想象成一个盒子,这个盒子里面装着当前代码执行所需要的所有东西。

2:JS中执行执行上下文分为三种

全局执行上下文:不在函数内部的代码处于全局环境,一个程序只有一个全局执行上下文,全局环境会生成全局对象,在浏览器中是window,此时this指向该全局对象;在 Node.js 环境中全局对象是global

函数执行上下文:每当一个函数被调用时, 都会为这个函数创建一个新的函数执行上下文,并被推入执行栈中(入栈),当函数执行完成出栈(出栈),当前函数执行上下文会销毁(闭包除外)

Eval函数执行上下文(不推荐):eval函数参数指定的代码会创建eval执行上下文压栈,不使用,有安全和性能问题。

3:首先在了解执行上下文的时候,我们需要先了解执行栈。

执行栈(Execution Stack)

  • 栈:栈(Stack)是一种数据结构,它遵循后进先出的原则。
  • 执行栈:也称调用栈,用于跟踪JS代码的执行顺序。用来管理执行上下文。当JS代码开始执行时,它记录了函数的调用和返回顺序,JS引擎始终执行的是栈顶的上下文。
  • 函数调用时,JS引擎就会为该函数创建一个新的函数执行上下文并push(入栈)到当前执行栈的栈顶当栈顶函数运行结束,其对应的函数执行上下文就会从执行栈中pop(出栈)出去 image.png

执行上下文的创建

执行上下文在创建时,做了下面两件事:创建词法环境(LexicalEnvironment),创建变量环境(VariableEnvironment)
网上对 this 绑定时间说法不一,查看 ECMAScript 原文可知: image.png 大概意思就是执行上下文是通过词法环境来确定this的绑定。

词法环境

词法环境是 ECMAScript 的一种规范类型。它依据代码的词法结构,像 “字典” 一样记录标识符(变量名、函数名等)与具体变量或函数的关联,帮助JS引擎在执行代码时找到标识符对应的实际内容。

词法环境内部组成

  • 环境记录(EnvironmentRecord):用于存储变量和函数声明。在不同的词法环境中,环境记录的内容有所不同。
  • 外部引用(outer):提供了访问父词法环境的能力。当在一个词法环境中无法找到某个标识符时,就会通过外部引用去父词法环境中查找。

词法环境类型区分:词法环境对应执行上下文,分为全局环境(GlobalEnvironment)和函数环境(FunctionEnvironment)。

let c = 3; 
function gn() { 
    let d = 4; 
    function inner() { 
        let e = 5; 
    } 
    inner(); 
} 
gn();
//全局词法环境
GlobalEnvironment: {
    //词法环境
    LexicalEnvironment: {
        //环境记录
        EnvironmentRecord: {
            type: 'object',
            c: 3,
            gn: <function>
        },
        outer: <null>,
        this: <globalObject>
    }
}

//gn函数词法环境
FunctionEnvironmentForGn: {
    LexicalEnvironment: {
        EnvironmentRecord: {
            //表示这是一个声明性的环境记录
            //通常用于函数环境中存储局部变量和参数等。
            type: 'declarative',
            arguments: {length: 0},
            d: <uninitialized>,
        },
        //表示函数gn的词法环境的外部引用是全局词法环境
        outer: <GlobalEnvironment>,
        this: <globalObject>
    }
}

//inner函数词法环境
FunctionEnvironmentForInner: {
    LexicalEnvironment: {
        EnvironmentRecord: {
            type: 'declarative',
            arguments: {length: 0},
            e: <uninitialized>,
        },
        //表示内部函数inner的词法环境的外部引用是函数gn的词法环境
        outer: <FunctionEnvironmentForGn>,
        this: <globalObject>
    }
}

变量环境

变量环境本质是词法环境,只存var声明的变量,将它们初始化时赋值为undefined(实现变量提升)。

var c = 3;
let d = 4;

//全局词法环境
GlobalEnvironment: {
    // 变量环境
    VariableEnvironment: { 
        EnvironmentRecord: {
            type: 'object',
            c: <undefined>,
        },
        //全局词法环境,没有外部引用
        outer: <null>, 
        this: <globalObject>
    },
    // 词法环境
    LexicalEnvironment: { 
        EnvironmentRecord: {
            type: 'object',
            d: <uninitialized>,
        },
        outer: <null>,
        this: <globalObject>		
    }
}

作用域

在JS中,作用域和执行上下文是两个不同但又紧密相关的概念

JavaScript 属于解释型语言,JavaScript 的执行分为:解释执行 两个阶段

解释阶段

1、词法分析:把源代码拆成符号,像 “var a = 1;”,能识别出 “var” 是关键字、“a” 是变量名、“=” 是运算符、“1” 是字面量。

2、语法分析:基于词法分析,把符号组成抽象语法树,检查语法结构,如函数括号是否匹配、语句有无分号。

3、作用域规则确定:确定变量和函数的作用域。函数定义的时候,它的作用域就确定了,它能访问哪些变量和函数也确定了,作用域在编写代码时就已经确定好,执行的时候不变。

执行阶段

1、创建执行上下文:函数被调用的时候,就会给这个函数创建一个执行上下文。这里面有函数执行需要的各种信息,像变量环境、词法环境、this值这些。

2、执行函数代码:按代码从上到下的顺序执行函数里的语句。在这个过程中,会根据之前定好的作用域规则来找变量和函数,然后做计算、赋值之类的操作。

3、垃圾回收:函数执行完了,那些没用的内存空间就会被回收,把资源释放出来。

作用域与执行上下文的区别

确定时间不同

1、作用域在解释阶段确定,而执行上下文在执行阶段创建。

2、这意味着函数的作用域在函数定义的时候就已经固定了,无论在哪里调用这个函数,它的作用域都不会改变。例如:

function outer() {
    var outerVar = 'I am outer';
    function inner() {
        console.log(outerVar);
    }
    return inner;
}
var newFunction = outer();
//仍然可以访问outerVar,因为inner的作用域在定义时就包含了outer的作用域
newFunction();

this 的值在执行上下文中的确定

一般函数this的值是在函数执行前,创建函数执行上下文时被赋值的。this的值取决于函数的调用方式,跟调用该方法的对象有关。谁调用函数,this就指向谁。

箭头函数:箭头函数中的this始终指向该函数定义时所在作用域指向的对象。由于作用域不变,所以箭头函数的this指向在定义时就固定了,不会改变。

var obj = {
    method: function() {
        console.log(this);
    }
};
obj.method(); //this指向 obj
var anotherFunction = obj.method;
anotherFunction(); //this指向全局对象(在浏览器中是window)


var obj = {
    normalFunction: function() {
        console.log(this);
    },
    arrowFunction: () => {
        console.log(this);
    }
};
obj.normalFunction(); //this指向obj
obj.arrowFunction(); //this指向obj定义时所在的作用域指向的对象,通常是全局对象(在浏览器中是window)

作用域链(Scope Chain)

1:执行上下文中包含一个额外的属性,该属性指向创建该执行上下文的函数本身
2:每个函数在创建的时候,会有一个隐藏属性scope,该函数指向创建他的执行上下文

代码中全局作用域有变量 a 和函数 func,func的函数作用域有变量 b。fn 能打印全局变量 a,a 对于 fn 的函数作用域是自由变量,这是借助作用域链实现的从父级作用域取值。

var a = 1;
function func() {
  var b = 2;
  console.log(a, b); // 1, 2
}
func();

再看一个例子,下面的代码最终打印的结果是 1,因为对于 func1 来说,他的父级作用域是全局作用域,而不是 func2

var a = 1;
function func1() {
  console.log(a);
}
function func2() {
  var a = 2;
  func1();
}
func2();