javascript执行上下文以及作用域链

855 阅读4分钟

本文会彻底探讨关于js执行的相关内容。希望通过本文可以帮助你解决相关问题。

概念

首先来了解关于本文要涉及的概念名词,建议理解并熟记。

  • 环境栈: js会维护一个环境栈,在环境栈的顶部为当前执行函数的执行环境。
  • 执行上下文/执行环境(execution context):当执行流进入函数,当前执行环境。主要由3部分构成:1.变量对象; 2.作用域链;3.this
  • 变量对象(variableObject):每一个执行上下文都会分配一个变量对象(variable object),变量对象的属性由 变量(variable) 和 函数声明(function declaration) 构成。
  • 活动变量(activeObject AO)激活了的变量对象。
  • 作用域链(scopechain):每个执行环境的作用域链由当前环境的变量对象及父级环境的作用域链构成。
  • js是一门基于静态作用域的语言,函数的作用域在创建的时候就已经确定。

执行顺序

js的执行顺序是顺序执行,如:

function test1(){
    console.log('test1');
}
test1() // test1

function test2(){
    console.log('test2');
}
test2() // test2

当然,也有例外:

function test(){
    console.log('test1');
}
test() // test2

function test(){
    console.log('test2');
}

由于函数提升问题,这里执行的是第二个test函数。

环境栈

由上面概念知道了js会在执行代码时会创建一个环境栈:ECStack = []。栈的通用属性为LIFO(后进先出),环境栈也一样。当执行流遇到了执行函数就会将函数的执行上下文放入环境栈中,也就是说,环境栈的最底部永远为全局上下文,最顶部为当前执行函数的执行上下文。

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

上面js的环境栈为:

// 先push全局上下文
ECStack = [globalContext]
// 执行fun1,push fun1Context
ECStack = [fun1Context, globalContext]
// 执行fun2,push fun2Context
ECStack = [fun2Context, fun1Context, globalContext]
// 执行fun3,push fun3Context
ECStack = [fun3Context, fun2Context, fun1Context, globalContext]
// fun3执行完成,pop fun3Context
ECStack = [fun2Context, fun1Context, globalContext]
// fun2执行完成,pop fun2Context
ECStack = [fun1Context, globalContext]
// fun1执行完成,pop fun1Context
ECStack = [globalContext]

变量对象

变量对象包括:

  • 函数的所有形参
    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  • 函数声明
    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 变量声明
    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
function test(b){
    var a = 1
    function c(){}
    var d = function(){}
    console.log(a,b,c,d);
    
}

test(2)

以上例子的VO为:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 2,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

思考题:

console.log(foo);  //  [Function: foo]
var foo = 1;

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

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

作用域链

当查找变量时,会先从当前变量对象中寻找,如果寻找不到则往上去父变量对象中寻找对象,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

var scope = "global scope";
function checkscope(){
  var scope2 = 'local scope';
  return scope2;
}
checkscope();

上述例子执行过程:

1.创建checkscope函数,保存作用域链到 内部属性[[scope]]。

checkscope.[[scope]] = [
  globalContext.VO
]

2.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

checkscopeContext = {
  Scope: checkscope.[[scope]],
}

3.用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}]]]
}

4.将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

5.开始执行函数,随着函数的执行,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

基于上述分析,请思考下面例子的函数执行上下文:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
 checkscopeContext = {
     AO: {
        arguments: {
            length: 0
        },
        f: function,
        scope: 'local scope'
     },
     scope: [AO, globalScope]
 }
 
 fContext = {
     AO: {
        arguments: {
            length: 0
        },
     },
     scope: [AO, checkscopeContext.AO, globalScope]
 }

在f函数中,由于f.AO里没有scope,往上checkscopeContext.AO里寻找变量。故,return 'local scope'

结论

记住变量对象与执行上下文已经作用域链之间的关系,外加记住作用域链生成时机,这类题目就能游刃有余。

参考

github.com/mqyqingfeng… github.com/kuitos/kuit…