学习笔记——JavaScript执行过程

2022-01-20  本文已影响0人  SleepWalkerLj

第一步,解析代码

当V8引擎开始解析JavaScript代码时,其内部会在堆内存中创建一个全局对象(GlobalObject也称为go),里面会包含String,Array,Number,setTimeOut等等一些全局对象和全局函数。这就是为什么能直接在代码中使用String、Math、Date这些类,或者使用setTimeout、setInterval这些函数的原因了。

var num1 = 10;
var num2 = 20;
var result = num1 - num2;

// 伪代码示意
var globalObject = {
  String: "类",
  Date: "类",
  setTimeount: "函数",
  // 指向自己的Window属性
  window: globalObject,
  // 解析时会将这些属性定义到go中,但是此时代码还未执行,所以值都是undefined
  num1: undefined,
  num2: undefined,
  result: undefined,
};

第二步,运行代码

1. Execution Context Stack

v8引擎为了执行代码, 内部存在一个执行上下文栈(Execution Context Stack, 简称ECStack)(函数调用栈),是一个执行代码的调用栈。ECStack是一个栈结构,js代码执行函数时,就会把这个函数压入栈中,执行完便会将这个函数弹出栈。

函数调用栈.png
2. Global Execution Context

函数执行

执行函数时,在函数代码的前面就可以执行函数。这是因为在解析函数时,js引擎会为函数开辟一块内存空间来存储它,这块空间会存放函数父级作用域和函数的执行体,下面的bar函数的执行体就是var b = 10; console.log("bar");,父级作用域是globalObject也就是window。

bar()
function bar(a) {
  var b = 10
  console.log("bar");
}

// 伪代码示意
var globalObject = {
  String: "类",
  Date: "类",
  setTimeount: "函数",
  window: globalObject,
  // 这里保存的是bar函数的地址
  bar: 0x100
};
  1. 解析时先生成了一个Go对象,发现里面有个bar函数,就会在内存开辟一块空间用于存放bar函数的父级作用域和执行体,然后把这个地址复制给bar属性。
  2. 解析完后开始执行代码,执行bar时,通过AO找到Go里的bar,发现这个bar是个内存地址,就会根据这个地址找到为bar函数创建的空间。然后继续执行(),括号是执行函数的意思,此时js引擎会创建一个函数执行上下文(Function Execution Context,简称FEC)用来存放bar函数空间里的内容,然后将FEC放入ECStack中执行。
  3. FEC中也有一个VO,指向的是AO对象(自动创建的),用于定义函数内部的参数,变量等等(未赋值),开始执行函数体时才会进行赋值等操作。执行完后这个FEC就会弹出栈并销毁,AO失去指向也会被销毁。如果后面的代码又执行了bar函数,就会重新创建FEC和AO。
函数执行.png

作用域链

var name = 'lj'
bar()
function bar(a) {
  var b = 10
  console.log(name);
}
作用域链.png

练练手

var message = "Hello Global"

function foo() {
  console.log(message)
}

function bar() {
  var message = "Hello Bar"
  foo()
}

bar()

这是一个典型的作用域链问题,最终打印的结果是“Hello Global”,因为在foo函数定义时,他的父级作用域是全局作用域,也就是GO,foo函数的作用域并不受调用位置的影响,而是和定义的位置有关系的。

var a=10
function fun(){
  console.log(a)
  return
  var a = 20
}
fun()

这里会打印“undefined”,因为定义时,fun函数的VO是有a:undefined的,return只是不执行赋值的操作。

理解

无论是执行全局变量,还是函数,都会进入ECStack中执行,全局变量被包裹到全局执行上下文(GEC)里进入ECStack中执行,而函数会被包裹到函数执行上下文(FEC)里进入ECStack中执行。这两个上下文都会在内部创建一个VO对象,分别用于指向GEC的GO和FEC的AO,这里的AO是执行函数时创建的对象,用于存放函数内的参数、变量等属性。执行代码时,用到的变量会按照作用域链进行查找,先从本身的AO或GO中寻找,如果没有则会去父级作用域中查找,就是父级的AO或者GO。

文章的内容都是基于早期的ECMA的版本规范,就是es5之前的规范。es5之后的版本,将VO改成了VE(Variable Environment 变量环境),在执行代码中变量和函数的声明会作为环境记录添加到变量环境VE中,但形式不严格要求成对象,可以是map、list或者对象等等,基本的逻辑是差不多的。

上一篇下一篇

猜你喜欢

热点阅读