前言
书接上文,浏览器工作原理。JavaScript是如何执行的呢?
一、执行前创建全局对象
JavaScript引擎在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)。
- 该对象
所有的作用域(scope)都可以访问。 - 该对象包含
Date、Array、String、Number、setTimeout、setInterval等。 - 其中还有一个
window属性指向自己。 - 还有
定义的变量。
二、全局代码执行过程
1.执行上下文(Execution Contexts)
1.1.概念
-
js引擎内部有一个执行上下文栈(
Execution Context Stack,简称ECS),也叫函数调用栈。它是用于执行代码的调用栈。 -
执行
全局代码块。
- 构建一个
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);
-
执行前
注:变量和函数都存在作用域提升。但是用function关键字声明的函数优先被提升到最顶端。所以函数先被声明,并按函数的定义顺序依次声明。 -
开始执行
从上往下依次执行代码。
三、函数执行过程
执行到函数时,根据函数体创建一个函数执行上下文(Function Execution Context,简称FEC),并压入到执行上下文栈(ECS)中。
函数执行上下文关联的VO:
- 进入一个
函数执行上下文时,会创建一个AO对象(Activation Object)。 - 这个
AO对象会使用arguments作为初始化,并且初始值是传入的参数。 - 这个
AO对象会作为执行上下文的VO来存放变量的初始化。 AO对象属性依次包含:形参、arguments、定义的变量
// 示例代码
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);
-
执行前
-
开始执行
函数执行完毕,会弹出执行上下文栈,关联的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)
-
全局代码执行前
-
全局代码开始执行
-
全局代码中
foo函数开始执行 -
foo函数中bar函数开始执行 -
foo函数中bar函数执行完毕并出栈 -
foo函数继续执行,执行完毕并出栈 -
全局代码继续执行
全局代码执行完毕,全局执行上下文出栈,而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()
-
全局代码执行前
首先解析
全局代码,创建一个GO对象。GO对象包含一个foo函数、message变量、bar变量,foo函数指向一个foo函数对象。foo函数对象包含一个scopes属性,scopes属性指向一个作用域链,这个作用域链的第一个元素指向GO对象,即这个函数所在的VO对象。 -
全局代码开始执行
开始执行全局函数,
message变量赋值,bar变量赋值前执行foo函数。 -
执行全局代码中的foo函数
执行
foo函数时,创建一个foo函数执行上下文,关联一个AO对象。AO对象包含一个bar函数和name变量,bar函数指向一个bar函数对象。bar函数对象包含一个scopes属性,scopes属性指向一个作用域链,这个作用域链的第一个元素指向bar函数所在的AO对象即foo函数对应的AO对象。执行foo函数对name变量赋值。foo函数执行完毕并出栈,并将foo函数返回的bar函数地址赋值给GO中的bar变量。 -
执行全局代码中的
bar函数执行
foo函数时,创建一个foo函数执行上下文,关联一个AO对象。执行bar函数,打印name变量。此时:
- 先在
自己作用域中的AO找name,如果找到直接打印。 - 如果
没找到,则按着作用域链一层一层的查找,直到找到name变量。 - 如果找到
最后一层GO都没有发现name变量,则执行报错提示'name is not defined'。
5.bar函数执行完毕出栈
总结:函数所在的作用域取决于函数在哪里被定义。函数被定义时已经决定了它的作用域链。
五、作用域面试题
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中
六、附录
- 视频:V8引擎执行代码流程