JavaScript的执行过程

171 阅读7分钟

前言

书接上文,浏览器工作原理。JavaScript是如何执行的呢?

一、执行前创建全局对象

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

  • 该对象所有的作用域(scope)都可以访问。
  • 该对象包含Date、Array、String、Number、setTimeout、setInterval等。
  • 其中还有一个window属性指向自己。
  • 还有定义的变量
image.png

二、全局代码执行过程

1.执行上下文(Execution Contexts)

1.1.概念

  1. js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),也叫函数调用栈。它是用于执行代码的调用栈

  2. 执行全局代码块

  • 构建一个Global Execution Context(GEC,全局执行上下文)
  • GEC会被放入到ECS中执行。GEC包含两部分:
    • 代码执行前,在parser转成AST过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值。这个过程也称之为变量的作用域提升(hoisting)函数内的内容不会被解析,在函数被执行的时候才会被解析。
    • 代码执行中,对变量赋值,或者执行其它的函数

1.2.VO对象(Variable Object)

每个执行上下文会关联一个VO(Variable Object,变量对象)变量和函数声明会被添加到这个VO对象中。全局上下文关联的VO就是GO

2.全局代码执行过程

// 示例代码
var message = 'Global Message'
function foo () {
  var message  = 'Foo Message'
}

var num1 = 10
var num2 = 20
var result = num1 + num2
console.log(result);
  1. 执行前 image.png 注:变量和函数都存在作用域提升。但是用function关键字声明的函数优先被提升到最顶端。所以函数先被声明,并按函数的定义顺序依次声明。

  2. 开始执行

从上往下依次执行代码。 image.png

三、函数执行过程

执行到函数时,根据函数体创建一个函数执行上下文(Function Execution Context,简称FEC),并压入到执行上下文栈(ECS)中。

函数执行上下文关联的VO

  • 进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
  • 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
  • 这个AO对象会作为执行上下文的VO存放变量的初始化
  • AO对象属性依次包含:形参arguments定义的变量

image.png

// 示例代码
var message = 'Global Message'
function foo (num) {
  var message  = 'Foo Message'
  var age = 10
  var height = 1.88
  console.log('foo function');
}
// 新增:执行foo函数
foo(123)
var num1 = 10
var num2 = 20
var result = num1 + num2
console.log(result);
  1. 执行前 image.png

  2. 开始执行 image.png

函数执行完毕,会弹出执行上下文栈,关联的AO被销毁,销毁行为取决于垃圾回收器的机制。会继续执行执行上下文栈中处于栈顶上下文,可能是另一个函数上下文也可能是全局上下文

注:如果函数的返回值是一个函数,那么返回的是这个被返回函数的内存地址。

1.函数代码多次执行

当函数再次执行时,会重复上面函数的执行过程:创建函数执行上下文—>入栈—>执行—>出栈—>销毁

2.函数代码相互调用

// 示例代码
var message = 'Global Message'
var obj = {
  name: 'hello'
}
function bar () {
  console.log('bar function')
  var address = 'bar'
}
function foo (num) {
  var message = 'Foo Message'
  bar()
  var age = 10
  var height = 1.88
  console.log('foo function')
}
foo(123)
  1. 全局代码执行前 image.png

  2. 全局代码开始执行 image.png

  3. 全局代码中foo函数开始执行 image.png

  4. foo函数中bar函数开始执行 image.png

  5. foo函数中bar函数执行完毕并出栈 image.png

  6. foo函数继续执行,执行完毕并出栈 image.png

  7. 全局代码继续执行 image.png

全局代码执行完毕,全局执行上下文出栈,而GO不会被销毁

四、作用域作用域链(Scope Chain)

1. 作用域

任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分

执行上下文分为:

  • 全局上下文
  • 函数上下文
  • 块级上下文

2.作用域链

进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)

  • 作用域链是一个对象列表,用于变量标识符的求值,也就是搜索变量和函数
  • 进入到一个执行上下文时,作用域链被创建,并且根据代码类型添加一系列的对象
  • 函数在哪里被定义,作用域就在哪里。
// 示例代码:多层嵌套函数
var message = 'Global Message'
function foo () {
  const name = 'foo'
  function bar () {
    console.log(name)
  }
  return bar
}

var bar = foo()
bar()
  1. 全局代码执行前 image.png 首先解析全局代码,创建一个GO对象GO对象包含一个foo函数message变量bar变量foo函数指向一个foo函数对象foo函数对象包含一个scopes属性scopes属性指向一个作用域链,这个作用域链的第一个元素指向GO对象,即这个函数所在的VO对象

  2. 全局代码开始执行 image.png 开始执行全局函数,message变量赋值,bar变量赋值前执行foo函数

  3. 执行全局代码中的foo函数 image.png 执行foo函数时,创建一个foo函数执行上下文,关联一个AO对象AO对象包含一个bar函数name变量bar函数指向一个bar函数对象bar函数对象包含一个scopes属性scopes属性指向一个作用域链,这个作用域链第一个元素指向bar函数所在的AO对象foo函数对应的AO对象执行foo函数name变量赋值foo函数执行完毕并出栈,并将foo函数返回的bar函数地址赋值给GO中的bar变量

  4. 执行全局代码中的bar函数 image.png 执行foo函数时,创建一个foo函数执行上下文,关联一个AO对象执行bar函数,打印name变量。此时:

  • 先在自己作用域中的AOname,如果找到直接打印
  • 如果没找到,则按着作用域链一层一层的查找,直到找到name变量
  • 如果找到最后一层GO都没有发现name变量,则执行报错提示'name is not defined'

5.bar函数执行完毕出栈 image.png

总结:函数所在的作用域取决于函数在哪里被定义。函数被定义时已经决定了它的作用域链。

五、作用域面试题

1.面试题1

var n = 100
function foo () {
  n = 200
}
foo()
console.log(n) 

// ->200
// 访问全局变量n,并赋值为200

2.面试题2

var n = 100
function foo () {
  console.log(n)
  var n = 200
  console.log(n)
}
foo()

// 第一次打印:-> undefined
// 在foo函数的AO对象有n,在赋值前访问则为undefined
// 第二次打印:-> 200
// 在foo函数的AO对象有n,在赋值后访问则为200

3.面试题3

var n = 100
function foo1 () {
  console.log(n)
}
function foo2 () {
  var n = 200
  console.log(n)
  foo1()
}
foo2()

// ->200 
// 先执行foo2,foo2作用域中有n,赋值后访问则为200
// ->100 
// 后执行foo1,foo1作用域中没有n,则按foo1定义时关联的作用域链访问到GO中的n为100

4.面试题4(有坑)

var a = 100
function foo () {
  console.log(a)
  return
  var a = 100
}
foo()

// ->undefined
/// return是代码执行的时候才起作用,在解析阶段有变量a的定义,在执行阶段先访问a,则为undefined

5.面试题5

function foo () {
  var a = b = 100
}
foo()
// console.log(a)
console.log(b)

// ->报错:a is not defined。
// a只在foo的作用域里
// ->100
// b没有使用var关键字定义,则为全局变量,定义在GO中

下文,JavaScript闭包。

六、附录

  1. 视频:V8引擎执行代码流程