重学JavaScript – 执行机制

执行过程

JavaScript作为解释型脚本语言,存在解释和执行两个环节。JavaScript代码需要在依托JavaScript引擎才能执行。那么JavaScript引擎的执行机制是怎样的呢?比如下面的代码,是如何执行的呢?

// 示例
var message = "Hello JavaScript";

function foo() {
  var name = "zhangsan";
  
  console.log("foo");
}

foo();
复制代码

第1步

代码在执行之前,JavaScript引擎会先在堆内存中创建一个全局对象(GO, Global Object)

  • 该对象所有的作用域都可以访问
  • 该对象包含Array、String、Number、SetTimeout、SetInterval等
  • 其中还有一个window属性指向自身

01_创建全局对象.png

第2步

JavaScript引擎内部有一个执行上下文栈(ECS, Execution Context Stack),是用于执行代码的调用栈。首先会执行全局代码块,全局代码块为了执行,会创建全局执行上下文(GEC, Global Execution Context)

  • 将全局定义的变量加入到GO中,但不会赋值。这个过程称为变量的作用域提升(hoisting)
  • 将全局声明的函数,会在堆内存中创建函数对象,并将其内存地址返回(如下图示例的0x00A)

02_声明变量和函数.png

第3步

JavaScript引擎解析完成后,开始从上到下依次执行代码。但对于变量和函数,有着不同的执行机制。

变量赋值

如果是变量赋值,将值保存到变量中。执行结果如下图

03_变量赋值.png

函数调用

如果是函数调用,也会创建一个活动对象(AO,Activation Object)。根据函数体创建一个函数执行上下文(FEC, Functional Execution Context),并且压入到ECS中。开始解析函数体内的代码,同样存在变量作用域提升。执行结果如下图

04_函数调用-声明变量.png

函数体内的代码解析完成后,开始执行代码,给函数体内定义的变量赋值。执行结果如下图

04_函数调用-执行代码.png

函数执行完毕后,函数执行上下文(FEC)会弹出执行上下文栈(ECS),并且销毁对应的活动对象(AO,Activation Object),执行结果如下图

04_函数调用-执行完毕.png

GEC和FEC

全局执行上下文(GEC)和函数执行上下文(FEC)在创建时,除了保存变量对象(VO,Variable Object)外,还包括作用域链(Scope Chain),以及thisBinding属性。不过二者的值有所区别,如下图所示

05_GEC和FEC.png

从上图时可以看出,函数的作用域(Scope),在创建FEC时,已经确定了。

在最新的ECMA规范中,关于变量对象(VO,Variable Object)命名作了修改,改成了变量环境(VE,Variable Environment)。

  • 早期的规范中,每一个执行上下文会被关联到一个变量对象(VO),在源代码中的变量和函数声明会被作为属性添加到VO中。对于函数来说,参数也会被添加到VO中。
  • 新版的规范中,每一个执行上下文会被关联到一个变量环境(VE)中,在执行代码中的变量和函数声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数与会被添加到VE中。

思考

  1. 对本文开头的示例代码稍加改造,会是什么结果呢?
console.log(message); // undefined - 此时GO只声明了变量,引擎未执行到赋值语句,所以此时变量的值是 undefined。称为由于变量作用域提升

var message = "Hello JavaScript";

console.log(message); // Hello JavaScript - 执行了赋值语句,此时变量的值是指定的值

function foo() {
  console.log(name); // undefined - 此时AO也只声明了变量但未赋值,所以变量的值为undefined

  var name = "zhangsan";

  console.log(name); // zhangsan - 执行了赋值了语句,此时变量的值是指定的值

  console.log("foo"); // foo
}

foo();
复制代码
  1. 如果在函数声明之前调用,会是什么结果?
foo(); // 函数声明之前调用

function foo() {
  console.log(name); // undefined- 此时AO也只声明了变量但未赋值,所以变量的值为undefined

  var name = "zhangsan";

  console.log(name); // zhangsan - 执行了赋值了语句,此时变量的值是指定的值

  console.log("foo"); // foo
}
复制代码

在函数声明之前或之后调用,结果相同。原因是在执行代码时,全局对象(GO)中已经保存了函数对象的内存地址(如示例图中的0x00A)。不管是声明之前或之后调用,都可以通过保存的函数对象创建函数执行上下文(FEC),然后进行函数的解析、执行。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享