2. 彻底搞懂javascript-运行上下文(Execution Context)

3,144 阅读8分钟

上一篇我们提到的变量会登记在一个叫Lexical Environments(词法环境)上面。我们说了Lexical Environments(词法环境)的组成部分,我们也知道GlobalEnvironment是一个Lexical Environments(词法环境),它的outer为null,它的EnvironmentRecord和global相关,复习一下:

//Lexical Environment
function LexicalEnvironment() {
    this.EnvironmentRecord = undefined;
    this.outer = undefined; //outer Environment Reference
}

function EnvironmentRecord(obj) {

    if(isObject(obj)) {
        this.bindings = object;
        this.type = 'Object';
    }
    this.bindings = new Map();
    this.type = 'Declarative';
}


EnvironmentRecord.prototype.register = function(name) {
    if (this.type === 'Declarative')
        this.bindings.set(name,undefined)
    this.bindings[name] = undefined;
}

EnvironmentRecord.prototype.initialize = function(name,value) {
      if (this.type === 'Declarative')
        this.bindings.set(name,value);
    this.bindings[name] = value;
}

EnvironmentRecord.prototype.getValue = function(name) {
    if (this.type === 'Declarative')
        return this.bindings.get(name);
    return this.bindings[name];
}

//全局环境GlobalEnvironment

function creatGlobalEnvironment(globalobject) {
	var globalEnvironment = new LexicalEnvironment();
	globalEnvironment.outer = null
	globalEnvironment.EnvironmentRecord = new EnvironmentRecord(globalobject)
	return globalEnvironment;
}

GlobalEnvironment = creatGlobalEnvironment(globalobject)//可以看作是浏览器环境下的window

我们上一篇最后提出了一个问题:

词法环境(Lexical Environments),是用来登记变量和相关函数名字的,也知道这个名字是登记在 词法环境的 >EnvironmentRecord上的。那时候登记,怎么登记?是直接找老师(LexicalEnvironments)登记,还是设置一个 办公厅,办公厅设置登记窗口提供登记服务?

答案是JS引擎我们学校一样,设置了一个办公厅,老师(Lexical Environments)坐在办公厅了面,手里拿着登记簿(EnvironmentRecord上的),等别人来办理注册。

JS的这个办公厅叫运行上下文(Execution Contexts),而且还有两个办事窗口,这个两个窗口还分别有个名字:LexicalEnvironment,VariableEnvironment。看起来有点像:

运行上下文(Execution Contexts)

运行上下文(Execution Contexts)有三个组成部分

  • LexicalEnvironment:是一个词法环境(Lexical Environment),用来解析引用(两个工作窗口之一)
  • VariableEnvironment::也是一个词法环境(Lexical Environment),用来登记var和function声明,(两个工作窗口之一),一般和LexicalEnvironment指向同一个词法环境
  • ThisBinding:哈哈,这个就是你代码里面this的值,this后面专门讲,还会和它打交道

用伪代码表示看起来像这样的:

function ExecutionContext() {
    this.LexicalEnvironment = undefined;
    this.VariableEnvironment =  undefined;
    this.ThisBinding = undefined;
}

这边需要说明一下,运行上下文(Execution Contexts)中的LexicalEnvironment是名称,它的值是一个Lexical Environment(词法环境),不要搞混。 小伙伴们可能会疑惑,LexicalEnvironment 和VariableEnvironment 一般会初始化为同一个词法环境,那要两个干吗呢?LexicalEnvironment使用过程有时候会被替换,而VariableEnvironment不会,后面会提到使用场景。

这个“办公厅”(Execution Contexts)就是javascript代码的"办公环境"的组成部分。和学校一样,并不是所有班级都设置一个办公室,javascript也不是运行任意的代码是都要创建一个Execution Contexts。在javascript中有三种情况下,会创建Execution Contexts。

可运行代码(Executable Code)

ECMAScript 5 规范,定义了三类可运行代码(Executable Code) ,运行这些代码时候会创建运行上下文(Execution Contexts):

  • global code:就是js整个“程序”,就是源代码文件中所有不在function体中的代码。
  • function code:就是函数体中的代码,除了内嵌函数体中的代码以外
  • eval code : 就是传给内置eval函数的代码字符串

运行栈

那么问题又来了,除了global code,每次运行function code,eval code ,如果我们写的函数多,那运行上下文也就多了,如何管理创建的那么多执行上下文呢?

其实呀,运行上下文(Execution Contexts)创建出来会被放在一个叫运行栈结构里,也叫调用栈。每当代码执行进入global code、function code、eval code,就会创建一个运行上下文(Execution Contexts),并把它放到调用栈的顶部(PUSH,入栈),调用栈顶部的运行上下文(Execution Contexts)称为运行时运行上下(runing Execution Contexts),或叫做当前运行上下文(current Execution Contexts),当运行时运行上下(runing Execution Contexts)对应代码运行完毕后,它就会被从调用栈顶拿掉(POP,出栈)。

之前,大家可能都知道js的调用栈,但是调用栈存的是什么东西可能不清楚,其实就是运行上下文(Execution Contexts)。

到这里,我们就可以构造一个JS运行的基本环境,js代码运行基于运行栈,它变量的存取都是从运行栈上的Execution Contexts来登记、获取的。我们用一个伪代码来模拟一个JS的运行环境:


Runtime = {
    executionContextStack: [];
};

//获取当前的运行上下文,也就是运行栈,栈顶的运行上下文
Runtime.getRunningExecutionContext = function() {
    return this.executionContextStack[this.executionContextStack.length - 1];
}

//把运行栈站定的运行上下文弹出
Runtime.pop = function() {
    this.executionContextStack.pop();
}

//把一个运行上下文放到运行栈的栈顶
Runtime.push = function(newContext) {
    this.executionContextStack.push(newContext);
}

//在当前运行上下文登记一个变量信息
Runtime.register = function(name) {
     var env = this.getRunningExecutionContext().VariableEnvironment;
     env.EnvironmentRecord.register(name);
}

//在当前运行上下文初始化一个变量信息
Runtime.initialize = function(name,value) {
     var env = this.getRunningExecutionContext().VariableEnvironment;
     env.EnvironmentRecord.initialize(name,value);
}
//在当前运行上下文上,解析一个标识符
Runtime.getIdentifierVaule = function (name) {

    var env = this.getRunningExecutionContext().LexicalEnvironment;

    while(env){
        var envRec = env.EnvironmentRecord;
        var exists = envRec.isExist(name);
        if(exists) return envRec.getValue(name);
        env = env.outer;
    }
}

全局运行上下文(global execution context)

以一段代码为例,来说明当JS引擎开始执行你的代码时,会干哪些事情。

    console.log(a);
    var a = 4;
    print();
    function print(){
        console.log(a)
    }

第一步 初始化运行环境

当JS引擎开始要进行global code代码运行之前,会先创建一个全局运行上下文(global execution context),并放入运行栈中:


//创建一个空的运行上下文
var globalExecutionContext = new ExecutionContext();

//创建全局词法环境
GlobalEnvironment = creatGlobalEnvironment(globalobject)//可以看作是浏览器环境下的window

//设置运行上下文
globalExecutionContext.LexicalEnvironment = GlobalEnvironment;
globalExecutionContext.VariableEnvironment = GlobalEnvironment;
globalExecutionContext.ThisBinding = globalobject;

Runtime.push(globalExecutionContext);

//这时的Runtime是这样的:
Runtime = {
    executionContextStack: [globalExecutionContext];
};

此时的当前运行上下文为globalExecutionContext。这个时候看起来像这样:

第二步 提升(Hoisting)

基本运行环境初始化完,然后开始解析代码,找出var声明和函数声明,并登记到globalExecutionContext.VariableEnvironment:

  • 找到var a ,登记并初始化为undefined:

        执行Runtime.register('a') ;等于下面的操作:
            //获取当前运行上下文
           var  runningExecutionContext = Runtime.getRunningExecutionContext();//runningExecutionContext是globalExecutionContext
           //获取当前运行上下文的词法环境,//variableEnvironment是globalEnviroment
           var variableEnvironment = runningExecutionContext.VariableEnvironment 
           //在词法环境上登记'a'
           variableEnvironment.EnvironmentRecord.register('a');
    

  • 找到函数声明,创建函数对象fo,并登记到globalExecutionContext.VariableEnvironment: function print()...:

        fo = FunctionCreate(...)//函数创建下篇讲
        Runtime.initialize('print',fo) ;
        等于:
            variableEnvironment.EnvironmentRecord.initialize('print',fo);
    

    有没发现,现在代码还没执行,但是环境中已经有a和print的记录了而且函数print记录的值是一个实实在在的函数对象不是undefined,这就是javascript的变量提升时,变量的值是undefined,而函数却不是undefined,而是可运行的。这时整个环境看起来是这样的:

第三步 执行代码

  • 开始代码执行

    • 执行console.log(a):发现有对a的引用,就是要a进行解析.:

          //其实就是variableEnvironment.EnvironmentRecord.getValue('a') ;
          var aValue = Runtime.getIdentifierVaule('a'):
          //这时,aValue为undefined
          打印出undefined
      
    • 执行var a = 4:

      Runtime.initialize('a',4);//variableEnvironment.EnvironmentRecord.initialize('a',4);
      

  • 执行print(),发现print引用,就是要print进行解析:

    var fun = Runtime.getIdentifierVaule('print') //variableEnvironment.EnvironmentRecord.getValue('print') ;
    //得到一个函数对象,运行该函数
    

    函数运行的细节留在下一篇说明。

  • 执行完毕,退出当前运行上下文,把globalExecutionContext从调用栈上移除:

        Runtime.pop();
        //这时的Runtime,为空
    Runtime = {
        executionContextStack: [];
    };
    

到目前为止,我们还没详细涉及函数,只知道函数是词法环境上登记的时候是马上初始化为具体函数对象的,但没谈及函数是如何被创建以及如何运行function里的代码。

下一篇我们将开始讨论这些内容。

运行模型

总结上述,过程我们构建一个JS的运行模型,进入可执行代码,都会走这个运行模型:

可运行代码(Executable Code)

ECMAScript 5 规范,定义了三类可运行代码(Executable Code) ,运行这些代码时候会创建运行上下文(Execution Contexts):

  • global code:就是js整个“程序”,就是源代码文件中所有不在function体中的代码。
  • function code:就是函数体中的代码,除了内嵌函数体中的代码以外
  • eval code : 就是传给内置eval函数的代码字符串
运行代码 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句;

总结

运行代码模型

可运行代码的运行 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句;

运行环境模型:


Runtime = {
    executionContextStack: [];
};

//获取当前的运行上下文,也就是运行栈,栈顶的运行上下文
Runtime.getRunningExecutionContext = function() {
    return this.executionContextStack[this.executionContextStack.length - 1];
}

//把运行栈站定的运行上下文弹出
Runtime.pop = function() {
    this.executionContextStack.pop();
}

//把一个运行上下文放到运行栈的栈顶
Runtime.push = function(newContext) {
    this.executionContextStack.push(newContext);
}

//在当前运行上下文登记一个变量信息
Runtime.register = function(name) {
     var env = this.getRunningExecutionContext().VariableEnvironment;
     env.EnvironmentRecord.register(name);
}

//在当前运行上下文初始化一个变量信息
Runtime.initialize = function(name,value) {
     var env = this.getRunningExecutionContext().VariableEnvironment;
     env.EnvironmentRecord.initialize(name,value);
}
//在当前运行上下文上,解析一个标识符
Runtime.getIdentifierVaule = function (name) {

    var env = this.getRunningExecutionContext().LexicalEnvironment;

    while(env){
        var envRec = env.EnvironmentRecord;
        var exists = envRec.isExist(name);
        if(exists) return envRec.getValue(name);
        env = env.outer;
    }
}

function ExecutionContext() {
    this.LexicalEnvironment = undefined;
    this.VariableEnvironment =  undefined;
    this.ThisBinding = undefined;
}



function LexicalEnvironment() {
    this.EnvironmentRecord = undefined;
    this.outerEnvironmentReference = undefined;
}

function EnvironmentRecord(obj) {

    if(isObject(obj)) {
        this.bindings = object;
        this.type = 'Object';
    }
    this.bindings = new Map();
    this.type = 'Declarative';
}


EnvironmentRecord.prototype.register = function(name) {
    if (this.type === 'Declarative')
        this.bindings.set(name,undefined)
    this.bindings[name] = undefined;
}

EnvironmentRecord.prototype.initialize = function(name,value) {
      if (this.type === 'Declarative')
        this.bindings.set(name,value);
    this.bindings[name] = value;
}

EnvironmentRecord.prototype.getValue = function(name) {
    if (this.type === 'Declarative')
        return this.bindings.get(name);
    return this.bindings[name];
}

function creatGlobalEnvironment(globalobject) {
	var globalEnvironment = new LexicalEnvironment();
	globalEnvironment.outer = null
	globalEnvironment.EnvironmentRecord = new EnvironmentRecord(globalobject)
	return globalEnvironment;
}

GlobalEnvironment = creatGlobalEnvironment(globalobject)//可以看作是浏览器环境下的window