Skip to content

Javascript 的执行上下文 #4

@XDfield

Description

@XDfield

执行上下文

Javascript 中代码的运行环境有三种:

  1. 全局执行的代码
  2. 函数级别的代码
  3. eval 中执行的代码

Javascript 的解释器在执行代码时,首先进入全局上下文,每次调用一个函数时将会创建一个新的执行上下文,该执行上下文会被添加到执行栈的顶部,浏览器总是运行位于执行栈顶部的当前上下文。一旦该函数的代码执行完成,当前的执行上下文将从栈顶被移除,并将控制权归还到之前的执行上下文。

在不同的执行上下文中,如果存在相同的变量名,将从顶部至底部优先级逐渐降低;当调用的变量不在当前上下文中时,也会向下查找。这个叫做作用域链

执行上下文的创建过程

在调用一个函数时,一个新的执行上下文被创建,该创建过程分为两个阶段:

  1. 建立阶段(即调用一个函数时,但未真正执行内部具体代码)
  2. 执行阶段

建立阶段,会在上下文中建立变量,函数,arguments 对象,参数;然后建立作用域链;最后确定 this 的值。我们可以将执行上下文看作一个对象:

executionContextObj = {
    variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
    scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
    this: {}
}

在建立 variableObject 对象时,具体顺序为:

  1. 建立 arguments 对象,检查当前上下文中的参数,建立该对象下的属性以及属性值。
  2. 检查当前上下文中的函数声明。每找到一个函数声明,就在 variableObject 下面用函数名建立一个属性,属性值就是该函数的引用。若该函数名已存在于 variableObject 中,那更新该引用。
  3. 检查当前上下文中的变量声明。每找到一个变量声明,就在 variableObject 下面用变量名建立一个属性,属性值为 undefined。若该变量名已存在于 variableObjecet 中,则直接跳过
  4. 初始化作用域链
  5. 确定上下文中的 this 的值。

这里需要注意的是,在建立阶段,函数声明会优先确定,且晚声明的函数会覆盖早声明的函数引用。之后才进行变量声明(值为 undefined),且若有同名函数已被声明,该变量声明会被跳过。

执行阶段,会一行行的运行代码,给 variableObject 中的变量属性赋值。

举个栗子

function foo(i) {
    var a = 'hello';
    var b = function B() {};
    function c() {};
}
foo(22);

建立阶段:

fooExecutionContextObject = {
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: "pointer to function c()",
        a: undefined,
        b: undefined
    },
    scopeChain: {},
    this: {}
}

执行阶段:

fooExecutionContextObject = {
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: "pointer to function c()",
        a: 'hello',
        b: "pointer to function B()"
    },
    scopeChain: {},
    this: {}
}

局部变量作用域的提升

来看这样一个例子:

(function() {
    console.log(typeof foo);  // function pointer
    console.log(typeof bar);  // undefined
    
    var foo = 'hello';
    var bar = function() {
        return 'world';
    };
    function foo() {
        return 'hello'
    }
}());

该匿名函数被直接执行,此时便有一个执行上下文被创建。可以看到 typeof 输出 foo 为一个函数的引用,而 barundefined

了解了上下文创建阶段,我们就能理解为什么 foo 可以在定义前被调用了,因为在创建阶段 foo 便被加入到 variableObject 中且值为 foo 函数的引用。而 bar 在创建阶段是作为变量的,其值默认就是 undefined

再一个例子:

var foo = 1;
(function foo() {
    foo = 10;
    console.log(foo);
}())
// ƒ foo() { foo = 10 ; console.log(foo) }

这个例子中,foo 为非匿名函数,此时会创建一个辅助对象,并将该函数名作为属性,但这个名为 foo 的属性是只读的,所以后来的赋值并不会改变其属性值。最后也就打印了调用时的函数内容。

参考资料

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions