深入基础:js的上下文是什么

1,049 阅读7分钟

  很多时候我们在各种资料中看到js的上下文,那么上下文到底是什么?在《js高级程序设计》中说上下文其实是一个概念,变量或者函数的上下文决定他们可以访问哪些数据,以及他们的行为。每个上下文都有一个关联的变量对象,这这个上下文定义的所有变量和函数都存在于这个对象上。虽然无法通过代码直接访问这个对象,但是后台处理数据的时候会用到它。我在github上面找到了更加容易理解的解释,搬运到这里,原文见参考链接

执行上下文

  js的执行上下文的类型包括:全局上下文,函数上下文和eval/with代码中的上下文 (eval和with会导致作用域副作用和无法优化,所以基本上我们不用)。

  全局上下文指的是最外层的上下文,根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。上下文在其所有代码都执行完毕会被销毁。

  每个函数调用都有自己的上下文,当代码执行到函数时,函数的上下文会被推到一个上下文栈中,在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返回给之前的执行上下文,程序的执行流就是通过这个上下文栈来控制的。

当我们执行到一个函数的时候就会进行准备工作,这里的准备工作就是执行上下文(execution context)。在实际运行中,js引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。我们可以使用伪代码来模拟函数的执行过程。

ECStack = []//首先将执行上下文栈定义为数组;
/**
* 在js引擎开始解释代码的时候,最先遇到的是全局代码,所以在初始化的时候往里面注入一个全局上下文变量(globalContext),当整个程序结束之后,栈里面才会被清空,所以在程序执行过程中,栈的底部始终保存globalContext
*/
ECStack = [globalContext];
/** 待执行代码
* function fun3() {console.log('fun3')}
* function fun2() {fun3();}
* function fun1() {fun2();}
* fun1();
*/
ECStack.push(<fun1>functionContext) // 在执行fun1之前先push它的上下文
ECStack.push(<fun2>functionContext) // push fun2的上下文
ECStack.push(<fun3>functionContext) // push fun3的上下文
ECStack.pop() //fun3的上下文执行完毕,删除
ECStack.pop() //fun2的上下文执行完毕,删除
ECStack.pop() //fun1的上下文执行完毕,删除
/**
*执行上下文栈接着运行,去执行下面的函数,但是全局上下文globalContext一直保存着
*/
  • 需要注意的是js采用的是词法作用域,函数的作用域在函数定义的时候就已经决定。 作用域指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问区域。,如下:
    var value = 1;
    function foo() {
        console.log(value);
    }
    function bar() {
        var value = 2;
        foo();
    }

    bar();//1

  开始的时候我们还提到了每个上下文都有一个管理的变量对象。但实际上对于每个执行上下文来说,都有三个重要的属性:

  • 变量对象(variable object, VO)
  • 作用域链 (scope chain)
  • this

变量对象

这里我们先介绍变量对象。变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。

全局上下文中的变量对象实际上就是全局对象。在浏览器中的全局对象就是window对象,上面定义了很多方法和函数。全局对象是object构造函数实例化的一个对象。

函数上下文中的变量对象,我们使用活动对象来表示(activation object),我的理解是变量对象是规范定义,引擎实现的一个对象,但是实际在js环境中我们访问不到,只有当我们进入到函数执行上下文中的时候,才会被激活成为活动对象。活动对象在进入函数上下文的时候被创建的,通过函数的arguments属性被初始化,arguments的属性值是Arguments对象。

进入上下文的时候代码分为两个阶段进行处理:进入执行上下文和代码执行。 以下面这个函数为例:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}
foo(1);
  • 进入执行上下文 (需要注意,形参/函数声明/变量声明的创建是有优先级的) 变量对象包括:(这里实际上是变量提升的规则)
    • 函数的所有形参
      • 创建一个名称-对应值的变量对象的属性,没有实参则属性值为undefined
    • 函数声明
      • 创建一个名称-function object的变量对象的属性,如果存在相同名称,则替换掉原来的属性
    • 变量声明
      • 创建一个名称-undefined的变量对象的属性,如果名称存在,则不影响之前的属性 此时的活动对象为:
    AO = {  
        arguments:{
              0:1,
              length:1
        },
        a:1,
        c:reference to function c(){},
        d:undefinded,//函数表达式不会被函数声明提升
        b:undefinded
    }
    
  • 代码执行 在代码执行阶段,按顺序执行代码,依次改变变量对象中的值。当上面的函数执行之后,活动对象为:
     AO = {  
        arguments:{
              0:1,
              length:1
        },
        a:1,
        c:reference to function c(){},
        d:refrence to function Expressions "d",
        b:3
    }
    

作用域链

  当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
由于js采用词法作用域的关系,当函数定义的时候函数的作用域就已经确定了。这是因为函数有个内部属性[[scope]],当函数创建的时候就会保存所有的父变量对象到里面,我们可以把[[scope]]理解为所有父变量对象的层级链([[scope]]并不代表完整的作用域链)。 比如:

function foo(){
	function bar(){
    }
}

在创建时候的[[scope]]为:

foo.[[scope]] = [globalContext.VO]
bar.[[scope]] = [fooContext.AO,globalContext.VO]

每当新的函数被激活的时候将新的函数的变量对象添加到作用域链的最顶端,这个时候执行上下文的作用域链为:scope=[AO].concat([[scope]])

执行过程总结

所以我们整理一下执行上下文中创建变量对象和执行作用域链的过程。 示例函数:

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

过程步骤为:

  1. 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack= [globalContext]
  1. 全局上下文初始化
globalContext={
    VO:[global],
    Scope:[globalContext.VO],
    this:globalContext.VO 
}
  1. 初始化的同时,checkscope函数被创建,在[[scope]]上面保存作用域链,同时将其推入执行上下文堆栈中
ECStack = [
    checkscopeContext,
    globalContext
]
  1. checkscope函数进行上下文初始化,
    • 复制[[scope]]属性创建作用域链
    • 使用arguments创建活动对象
    • 初始化活动对象,加入形参,函数声明,变量声明
    • 将活动对象压入 checkscope 作用域链顶端。
    • 同时f函数被创建,保存作用域链到f函数的内部属性[[scope]]
checkscopeContext = {
  AO:{
      arguments:{
      	length:0
      },
      scope:undefinded,
      f: reference to function f(){}
  }
  Scope:[AO,[[scope]]],
  this:undefinded
}
  1. 执行checkscope函数,变量对象的属性发生变化
checkscopeContext = {
  AO:{
      arguments:{
      	length:0
      },
      scope:"local scope",
      f: reference to function f(){}
  }
  Scope:[AO,[[scope]]],
  this:undefinded
}
  1. checkscope函数执行完成,checkscope函数执行上下文从执行上下文堆栈中弹出,同时f函数执行上下文初始化。
ECStack = [    fContext,    globalContext]
fContext = {
  AO:{
      arguments:{
      	length:0
      },
  }
  Scope:[AO,checkscopeContext.AO,[[scope]]],
  this:undefinded
}
  1. 由于scope中维护了checkscopeContext.AO,所以在f函数执行的时候依然可以找到scope的对应值。f函数执行完成,f函数执行上下文从执行上下文堆栈中弹出
ECStack = [
    globalContext
]

参考文档: