CLAYYA

乌贼馋辣鱼的博客

执行上下文

2024-11-27

执行上下文

执行上下文 3 个重要的属性:

  • 变量对象(Variable Object,VO)
  • 作用域链(Scope Chain)
  • this

变量对象

变量对象(VO)是与执行上下文相关的 数据作用域,存储了上下文中定义的 变量函数声明,分为全局上下文的变量对象(GO)和函数上下文的变量对象(活动对象,AO)

GO

全局对象是由 Object 构造函数实例化的一个对象。预定义一大堆函数和属性。作为全局变量的宿主。

全局上下文的变量对象就是全局对象,web 浏览器是 window、self 或者 frames ,node 中是 global, Web Workers 中是 self,不同环境下的统一标准的全局变量是 globalThis。

AO

活动对象在进入函数上下文时被创建,通过函数的 arguments 属性初始化。即,调用函数时,会为其创建一个 Arguments 对象,并自动初始化局部变量 arguments,指代该 Arguments 对象。所有作为参数传入的值都会成为 Arguments 对象的数组元素。活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,才叫 activation object。

执行上下文

js的执行是一段一段的而不是一行一行的。当执行一段代码的时候,会进行一个 “准备工作”,比如变量提升(var 声明的变量)或函数提升(函数声明,非函数表达式),这个“准备工作”就是构建执行上下文。而执行上下文由执行上下文栈(ECS) 管理。

变量提升(Hoisting)是指变量声明被提升到它们所在作用域的顶部的过程。

关于提升:函数创建有函数声明和函数表达式之分,函数表达式是不会被提升的函数声明存在变量提升同一作用域下,函数提升比变量提升得更靠前。

var foo = function () {
    console.log('foo1');
}
foo();  // foo1
var foo = function () {
    console.log('foo2');
}
foo(); // foo2

// 原因:存在 var foo​ 变量提升,函数表达式不会提升,实质代码如下:

var foo;
foo = function () {
    console.log('foo1');
}
foo();  // foo1
foo = function () {
    console.log('foo2');
}
foo(); // foo2
function foo() {
    console.log('foo1');
}
foo();  // foo2
function foo() {
    console.log('foo2');
}
foo(); // foo2

// 原因:存在函数提升,函数声明可以提升,实质代码如下:

var foo;
foo = function () {
    console.log('foo1');
}
foo = function () {
    console.log('foo2');
}
foo();  // foo2
foo(); // foo2

JavaScript的可执行代码

  • 全局代码(表达式,赋值语句等)
  • 函数代码
  • eval代码

js引擎在解释代码的时候会优先遇到全局代码,所以会先向ECS压入全局执行上下文,并且只有当整个应用程序结束的时候,ECS才会被清空,所以程序结束之前,ECS底部永远有一个 globalContext。

当执行一个函数的时候,会创建一个执行上下文,并压入ESC,函数执行完毕时才弹出。

eval 是 JavaScript 中的一个内置函数,它接受一个字符串作为参数,并将其作为 JavaScript 代码执行。这个字符串可以包含任意的 JavaScript 代码,包括变量声明、表达式、语句等。eval 函数会计算这个字符串中的表达式,并返回表达式的值。eval代码在执行时会创建一个新的执行上下文,并可能影响调用上下文(在 eval 中声明的变量不会在外部作用域中创建,它们只在 eval 调用的上下文中存在)

执行过程

执行上下文生命周期:

  1. 进入执行上下文(分析)
  2. 代码执行(执行)
  3. 回收阶段(GC)

1进入执行上下文

进入执行上下文时,还没有执行代码。初始化顺序如下,变量对象包括:

  1. 函数的所有形参 由名称和对应值组成的一个变量对象的属性被创建;没有实参,属性值设为 undefined

  2. 函数声明 如果变量对象已经存在相同名称的属性,则完全替换这个属性

  3. 变量声明 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;
//打印函数,而不是undefined

所以进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。 即,函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖。

2代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

举例:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}

foo(1);

在进入执行上下文后,这时候的 AO 是:
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

当代码执行完后,这时候的 AO 是:
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。它们其实都是同一个对象,只是处于执行上下文的不同生命周期。

变量对象的创建

变量对象的创建,依次经历了以下几个过程:

  • 建立 arguments 对象。检查当前上下文中的参数,建立该对象下的属性与属性值(全局环境下没有这步)。
  • 检查当前上下文的函数声明,也就是使用 function 关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  • 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值 undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为 undefined,则会直接跳过,原属性值不会被修改

作用域链

在变量对象中,当查找变量的时候,对象中会先从当前执行上下文中的变量对象中查找变量(当前作用域),如果没有找到,就会从父级(词法层面的父级:书写位置)执行上下文中的变量对象中查找变量(父级作用域),一直到全局上下文的变量对象中,也就是全局对象中(全局作用域)。这样由多个执行上下文的变量对象构成的链叫做 作用域链

作用域链是在执行上下文(execution context)被创建时构建的,JavaScript 引擎会从当前作用域开始查找该变量,如果当前作用域中没有找到,就会沿着作用域链向上查找,直到全局作用域。如果在全局作用域中仍然没有找到该变量,就会返回 undefined​。

函数创建时的作用域

函数的作用域在函数定义的时候就决定了。 因为函数有一个内部属性 [[scope]]​,当函数创建的时候,就会保存所有父变量对象到其中, [[scope]]​ 就是所有父变量对象的层级链,但是注意:[[scope]]​并不代表完整的作用域链!

函数激活后的作用域

函数激活,进入函数上下文,创建 VO、AO 对象后,就会将 AO 添加到作用域链的前端。这时候执行上下文的作用域链,我们命名为 Scope:Scope = [AO].concat([[Scope]])​,至此,作用域链创建完毕。

总结

函数执行上下文中作用域链和变量对象的创建过程:

function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
  1. 创建函数,保存作用域链到内部属性[[scope]]
  2. 执行函数,创建函数执行上下文,函数ECS被压入栈内
  3. 函数并不立即执行,开始做准备工作,第一步:复制函数[[scope]]​属性创建作用域链
  4. 第二部:用arguments创建活动对象,然后初始化活动对象,加入形参,函数声明,变量声明
  5. 第三步:将活动变量压入函数作用域链顶端
  6. 准备工作做完,开始执行函数,修改AO的属性值
  7. 查找到scope2的值,然后返回,函数执行完毕,上下文从ECS中弹出

预解析

js 的函数有闭包性质。如果外层的函数执行结束它的作用域也会被销毁,那栈中的变量同时也被销毁。这就有了预解析。 顺序就是预解析 -> 解析 -> 编译执行​。 预解析也只查看函数的语法与是否引用外部变量。

this

几种调用场景:

  • 作为对象调用时,指向该对象 obj.b();​ // 指向 obj
  • 作为函数调用, var b = obj.b; b();​ // 指向全局 window
  • 作为构造函数调用 var b = new Fun(); ​// this 指向当前实例对象
  • 作为 call 与 apply 调用 obj.b.apply(object, []); ​// this 指向当前的 object

this指向

  • 全局作用域里的 this 是 window,严格模式下是 undefined,window.fn() 把 window. 省略了;
  • 函数的 this,看执行主体前有没有点.​,有点,前面是谁,函数里的 this 就是谁,没有点,函数的 this 就是 window,严格模式下是 undefined
    自执行函数里的 this 是 window,严格模式下是 undefined
  • 回调函数里的 this 一般情况下是 window
  • 箭头函数没有 this,在箭头函数中使用的 this,是上级作用域或作用域链中找到的 this,直到找到全局的 window。
  • 构造函数里的 this 是当前实例
  • 实例原型上的公有方法里的 this 一般是当前实例
  • 给元素绑定事件行为事件里的 this 就是当前被绑定的元素本身