概述
《你不知道的javascript》
js是一种非常灵活的语言,理解js引擎的执行过程对于我们学习js是非常有必要的。
js是单线程语言:在浏览器中一个页面永远只有一个线程在执行js脚本代码
但是代码解析是非常迅速的,不会发生解析阻塞:原因:js是异步执行的,通过事件循环(event loop)方式实现的
console.log(person)
console.log(personFun)
var person = "saucxs";
console.log(person)
function personFun() {
console.log(person)
var person = "songEagle";
console.log(person)
}
personFun()
console.log(person)
全面分析js引擎的执行过程,分为三个阶段
1、语法分析
2、预编译阶段
3、执行阶段
说明:浏览器先按照js的顺序加载<script>标签分隔的代码块,js代码块加载完毕之后,立刻进入到上面的三个阶段
,然后再按照顺序找下一个代码块,再继续执行三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的,并且都在同一个全局作用域中。
语法分析
js的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:
分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误(syntaxError)
,停止该js代码的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。
类似的语法报错的如下图所示:
预编译阶段
js代码块通过语法分析阶段之后,语法都正确的会进入预编译阶段。
在分析预编译阶段之前,我们先来了解一下js的运行环境
,运行环境主要由三种:
1、全局环境(js代码加载完毕后,进入到预编译也就是进入到全局环境
)
2、函数环境(函数调用的时候,进入到该函数环境
,不同的函数,函数环境不同)
3、eval环境(不建议使用,存在安全、性能问题)
每进入到一个不同的运行环境都会创建 一个相应的执行上下文
(execution context),那么在一段js程序中一般都会创建多个执行上下文,js引擎会以栈的数据结构对这些执行进行处理,形成函数调用栈
(call stack),栈底永远是全局执行上下文
(global execution context),栈顶则永远时当前的执行上下文。
(函数调用栈
就是使用栈存取的方式进行管理运行环境,特点是先进后出)
function bar() {
var B_context = "bar saucxs";
function foo() {
var f_context = "foo saucxs";
}
foo()
}
bar()
上面代码块通过语法分析后,进入预编译阶段,如图所示
1、首先进入到全局环境,创建全局执行上下文(global Execution Context ),推入到stack中;
2、调用bar函数,进入bar函数运行环境,创建bar函数执行上下文(bar Execution Context),推入stack栈中;
3、在bar函数内部调用foo函数,则再进入到foo函数运行环境中,创建foo函数执行上下文(foo Execution Context),如上图,由于foo函数内部没有再调用其他函数,那么则开始出栈;
5、foo函数执行完毕之后,栈顶foo函数执行上下文(foo Execution Context)首先出栈;
6、bar函数执行完毕,bar函数执行上下文(bar Execution Context)出栈;
7、全局上下文(global Execution Cntext)在浏览器或者该标签关闭的时候出栈。
说明:不同的运行环境执行都会进入到代码预编译和执行两个阶段,语法分析则在代码块加载完毕时统一检查语法。
创建执行上下文
执行上下文可以理解成当前的执行环境
,与该运行环境相对应。创建执行上下文的过程中,主要是做了下面三件事,如图所示:
1、创建变量对象(variable object)
2、创建作用域链(scope chain)
3、确定this的指向
创建变量对象
1、创建arguments对象,检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行的,全局环境没有此过程。
2、检查当前上下文的函数声明
,也就是使用function关键字声明的函数,按照代码顺序查找,将找到的函数提前声明。如果当前上下文
的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则指向该函数所在堆内存地址引用
,如果存在,则会被新的引用覆盖掉。(函数可覆盖
)
3、检查当前上下文的变量声明
,按照代码顺序查找,将找到的变量提前声明。如果当前上下文
的变量对象没有变量名属性,则在该变量对象以变量名建立一个属性,属性值为undefined;如果存在,则忽略该变量声明。(变量只能忽略
)
说明:
-
在全局环境中,window对象就是全局执行上下文的变量对象,所有的变量和函数都是window对象的属性方法。
-
所以函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明(先提升函数声明,那么如果变量声明有重复,就会发生覆盖)
-
创建变量对象发生在预编译阶段,还没有进入到执行阶段,该变量对象都不能访问的,因为此时的变量对象中的变量属性尚未赋值,值仍为undefined。
只有全局上下文的变量对象允许通过VO的属性名称来间接访问,在其他上下文中是不能直接访问VO对象的 -
只有进行执行阶段,变量中的变量属性才进行赋值后,
变量对象(Variable Object)转为活动对象(Active Object)
后,才能进行访问,这个过程就是VO->AO过程。
对于函数上下文来讲,活动对象与变量对象其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于执行上下文栈栈顶的函数执行上下文中的变量对象,才会变成活动对象。
- 变量对象会有以下四种特性:
全局上下文的变量对象初始化是全局对象
函数上下文的变量对象初始化只包括Arguments对象
在进入执行上下文的时候会给变量对象添加形参,函数声明,变量声明等初始的属性值
在代码执行阶段,会再次修改变量对象的属性值。
var a = 10;
function b () {
console.log('全局的b函数')
};
function bar(a, b) {
console.log('1', a, b)
var a = 1
function b() {
console.log('bar下的b函数')
}
console.log('2', a, b)
}
bar(2, 3)
console.log('3', a, b)
从执行上下文的创建阶段来分析变量对象:
// 创建阶段:
// 第一步,遇到了全局代码,进入全局上下文,此时的执行上下文栈是这样
ECStack = [
globalContext: {
VO: {
// 根据1.2,会优先处理全局下的b函数声明,值为该函数所在内存地址的引用
b: <reference to function>,
// 紧接着,按顺序再处理bar函数声明,此时根据1.1,因为是在全局上下文中,并不会分析bar函数的参数
bar: <refernce to function>,
// 根据1.3,再处理变量,并赋值为undefined
a: undefined
}
}
];
// 第二步,发现bar函数被调用,就又创建了一个函数上下文,此时的执行上下文栈是这样
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: undefined
}
},
<bar>functionContext: {
VO: {
// 根据1.1,优先分析函数的形参
arguments: {
0: 2,
1: 3,
length: 2,
callee: bar
},
a: 2,
// b: 3,
// 根据1.2, 再分析bar函数中的函数声明b,并且赋值为b函数所在内存地址的引用, 它发现VO中已经有b:3了,就会覆盖掉它。
//因此上面一行中的b:3实际上不存在了。
b: <refernce to function b() {}>
// 根据1.3,接着分析bar函数中的变量声明a,并且赋值为undefined, 但是发现VO中已经有a:2了,因此下面一行中的a:undefined也是会不存在的。
// a: undefined
}
}
]
执行上下文的代码执行阶段
// 执行阶段:
// 第三步:首先,执行了bar(2, 3)函数,紧接着,在bar函数里执行了console.log('1', a, b)。全局上下文中依然还是VO,但是函数上下文中VO就变成了AO。并且代码执行到这,就已经修改了全局上下文中的变量a.
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: 10,
}
},
<bar>functionContext: {
AO: {
arguments: {
0: 2,
1: 3,
length: 2,
callee: bar
},
a: 2,
b: <refernce to function b() {}>
}
}
]
// 因此会输出结果: '1', 2, function b() {console.log('bar下的b函数')};
// 第四步:执行console.log('2', a, b)的时候, 发现里面的变量a被重新赋值为1了(创建EC的时候只是声明提前,var a=1还有赋值呢)
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: 10,
}
},
<bar>functionContext: {
AO: {
arguments: {
0: 2,
1: 3,
length: 2,
callee: bar
},
a: 1,
b: <refernce to function b() {}>
}
}
]
// 因此会输出结果: '2', 1, function b() {console.log('bar下的b函数')};
// 第五步,执行到console.log('3', a, b)的时候,ECStack发现bar函数已经执行完了,就把bar从ECStack给弹出去了。此时的执行上下文栈是这样的。
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: 10,
}
}
]
// 因此会输出结果: '3', 10, function b() {console.log('全局的b函数')}
注:< execution reference>表示的是execution函数在堆内存地址的引用
创建作用域链
作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成,保证了当前执行环境对符合访问权限的变量和函数有序访问。
理解清楚作用域链可以帮助我们理解js很多问题包括闭包问题等,下面我们结合一个例子来理解一下作用域链。
var num = 30;
function test() {
var a = 10;
function innerTest() {
var b = 20;
return a + b
}
innerTest()
}
test()
在上面例子中,当执行到调用innerTest函数,进入到innerTest函数环境。全局执行上下文和test函数执行上下文已进入到执行阶段,innerTest函数执行上下文在预编译阶段创建变量对象,所以他们的活动对象和变量对象分别是AO(global),AO(test)和VO(innerTest),而innerTest的作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成,如下:
innerTestEC = {
//变量对象
VO: {b: undefined},
//作用域链
scopeChain: [VO(innerTest), AO(test), AO(global)],
//this指向
this: window
}
我们这里可以直接使用数组表示作用域链,作用域链的活动对象或者变量对象可以直接理解成作用域。
1、作用域链的第一项永远是当前作用域(当前上下文的变量对象或者活动对象);
2、最后一项永远是全局作用域(全局上下文的活动对象);
3、作用域链保证了变量和函数的有序访问
,查找方式是沿着作用域链从左至右查找变量或者函数,找到则会停止找,找不到则一直查找全局作用域,再找不到就会输出错误。
闭包
function foo() {
var num = 20;
function bar() {
var result = num + 20;
return result
}
bar()
}
foo()
以浏览器的闭包为准来分析闭包
如图所示,谷歌浏览器理解的闭包是foo,那么按照浏览器的标准是如何定义的闭包,自己总结为三点:
1、在函数内部定义新函数
2、新函数访问外层函数的局部变量,即访问外层函数环境的活动对象属性
3、新函数执行,创建新函数的执行上下文,外层函数即为闭包
确定this指向
1、在全局环境下,全局执行的上下文中变量对象的this属性指向为window;
2、在函数环境下的this指向比较灵活,需要根据执行环境和执行方法确定
最后
以上就是动人火车为你收集整理的(一)js引擎执行的过程的理解--语法分析和预编译阶段的全部内容,希望文章能够帮你解决(一)js引擎执行的过程的理解--语法分析和预编译阶段所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复