1.原理
解析 + 执行
1.解析
词法分析 -> 语法分析 -> 生成AST -> 作用域确定 -> 生成机器代码
- 词法分析:
- 语法/解析分析:生成AST
- 确定作用域关系:通过词法作用域规则,确定变量的作用域关系。
- 代码生成:生成浏览器js引擎能解析的底层代码。
2.执行
在js引擎中执行代码分为:分析阶段和执行阶段
- 分析阶段
- 执行阶段
分析阶段
- 创建分析对象: 用于处理执行时,访问变量和方法时候,根据一定规则进行作用域的访问。
- 预编译:提高运行时的效率,会把把代码进行预编译,如变量提升。
创建分析对象
VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。ScopeChain作用域链:VO(当前作用域) + ParentScope(父级作用域)this的指向: 可能是window,可能是调用的对象。
//伪代码
{
VO: {
变量:{},
函数地址:{};
},
scopeChain: [VO(当前作用域), ParentScope(父级作用域)]
this: window | object(调用的对象引用)
}
代码预编译
console.log(num1)
var num1 = 20
会被预编译成这样
var num1;
console.log(num1)
num1 = 20
执行阶段
- 创建执行上下文:根据分析的对象创建执行上下文
- 上下文入栈执行
- 变量赋值
- 方法调用
2.执行上下文
定义
执行上下文(Execution context)
遇到函数执行的时候,就会创建一个执行上下文。执行上下文是当前 js 代码被解析和执行时所在环境的抽象概念。
分类
- 全局执行上下文(GEC):这是默认的上下文,任何不在函数内部的代码都在全局上下文中。由浏览器主动调用根函数,包含全局的window对象和this指向这个window ,一个程序中只会有一个全局执行上下文。
- 函数执行上下文(FEC):在函数执行的时候,都会动态创建的执行上下文,跟函数作用域类似。
- Eval 函数执行上下文: 执行在
eval函数内部的代码也会有它属于自己的执行上下文。
执行上下文栈
执行上下文栈 (Execution context stack),调用栈,执行栈。 用来维护执行代码时候创建的上下文,是栈的数据结构,后进先出。
执行流程
- 在执行script代码时,由浏览器主动调用根函数G,创建默认全局执行上下文G,并然后押入栈顶。
- 根函数里,调用方法a,创建的函数执行上下文a,并把a押入栈顶,在G的上面。
- a函数里,调用方法b,创建的函数执行上下文b,并把b押入栈顶,在a的上面。
- 方法a调用完毕,同时执行上下文a也出栈。
- 方法b调用完毕,同时执行上下文b也出栈。
- 根函数G也执行完毕,全局上下文G也出栈。
//根路径默认会创建一个全局的上下执行栈
function a (){
b() //函数调用时,创建b的上下文
}
function b (){
}
a() //函数调用时,创建a的上下文
3.对象的概念 VO GO AO
- VO(Variable Object) : 保存当前指向上下文的 变量和函数引用地址
- GO(Global Object) :全局执行上下文的VO对象 指向 GO对象
- AO(ACtive Object) :函数执行上下文的VO对象 指向 AO对象
4.上下文的概念 EC ECS GEC FEC
- EC(Execution Context) : 执行上下文
- ECS(Execution Context Stack) :执行上下文栈
- GEC(Global Execution Context) :全局执行上下文
- FEC(Function Execution Context) :函数执行上下文
5.实例1
var name = 'xxxx'
console.log(name)
console.log(num1)
var num1 = 20
var num2 = 100
var sum = num1 + num2
console.log(sum)
1.分析阶段:
- 创建全局执行上下文,
- 同时创建全局GO对象,并且把全局定义的变量和方法地址都放到GO中。
- 把当前的
console.log(num1)的num1进行变量提升 最终全局上下文为:
//伪代码
//全局对象
GO: {
//自带的方法和属性
setTimeout: f(),
window:GlobalObject,
alert:f(),
...
//全局定义合并进来的
name: undefined,
num1: undefined,
num2: undefined,
sum: undefined,
},
//全局上下文的分析对象
{
VO: GO,//由于是全局上下文 VO直接指向 GO
scopeChain: VO //VO就是GO 就是最顶级了
this: window
}
分析后的预编译代码如下
var name = 'xxxx'
console.log(name)
var num1
console.log(num1)
var num1 = 20
var num2 = 100
var sum = num1 + num2
console.log(sum)
2.执行代码
- 把全局上下文押入执行上下文栈中,并开始执行
var name = 'xxxx'在VO里面找GO,发现GO有变量name,对name进行赋值console.log(name)在VO里面找GO,发现GO有变量name,打印name的值为xxxvar num1在分析阶段进行变量提升,这是预编译后的生成代码,在调用前做了声明,但未赋值。console.log(num1), 在VO里面找GO,发现GO有变量num1,打印name的值为undefinedvar num1 = 20在VO里面找GO,发现GO有变量num1,对num1进行赋值var num2 = 100在VO里面找GO,发现GO有变量num2,对num2进行赋值var sum = num1 + num2,在VO里面找GO,发现GO有变量num1,num2,sum,对num1和num2进行运算,并赋值给sumconsole.log(sum), 在VO里面找GO,发现GO有变量sum,打印sum的值为120- 全局上下文出栈
4.实例2
var name = 'xxxx'
function foo() {
var num = 20
console.log(name)
}
foo()
1.分析阶段:
- 创建全局执行上下文的分析对象,
- 同时创建全局GO对象,并且把全局定义的变量name和foo地址都放到GO中。
//伪代码
//全局对象
GO: {
//自带的方法和属性
setTimeout: f(),
window:GlobalObject,
alert:f(),
...
//全局定义合并进来的
name: undefined,
foo: fn引用地址,
},
//全局上下文的分析对象
{
VO: GO,
scopeChain: VO
this: window
}
- foo方法分析
- 创建函数执行上下文的分析对象,
- 同时创建AO对象,并且把函数内定义的name放到AO中。
//伪代码
//AO对象
AO: {
num: undefined,
arguments: //是一个数组,这里保存的也是地址
},
//函数上下文的分析对象
{
VO: AO,
scopeChain: [VO,parentScope] // 这里的 parentScope等于 GO
this: window
}
2.执行代码
- 根据全局分析对象,创建全局上下文。
- 把全局上下文押入执行上下文栈中,并开始执行
var name = 'xxxx',在VO中找到GO对象,发现有name属性- 对变量name进行赋值
- 发现是foo()方法调用
- 开始对foo方法进行执行。
- 根据foo的分析对象,创建foo函数上下文。
- 开始调用foo方法
var name = 'xxxx'
function foo() {
var num = 20
console.log(name)
}
foo()
var num = 20先在VO中找到AO对象,并且有num,进行赋值20。console.log(name)先在VO里面找,即AO对象,没有找到。- 则在作用域scopeChain找到父作用GO,GO里面包含name属性,打印name
- foo函数执行完毕,函数上下文出栈。
- 全部代码执行完毕,全局上下文也出栈。