个人认为,不需要过分去强调JavaScript解析引擎到底是什么,了解它究竟做了什么事情就可以了。对于编译器或者解释器究竟是如何看懂代码的,翻出大学编译课的教材就可以了。
这里还要强调的就是,JavaScript引擎本身也是程序,由代码编写而成。比如V8就是用C/C++
写的。
三、JavaScript 解析引擎到底是干什么的
JavaScript解析引擎就是根据ECMAScript
定义的语言标准来动态执行JavaScript
字符串。虽然之前说现在很多浏览器不全是按照标准来的,解释机制也不尽相同,但动态解析JS的过程还是分成两个阶段:语法检查阶段和运行阶段。
语法检查包括词法分析和语法分析,运行阶段又包括预解析和运行阶段(像V8引擎会将JavaScript
字符串编译成二进制代码,此过程应该归到语法检查过程中)。
3.1 JavaScript解析过程
在JavaScript
解析过程中,如遇错误就直接跳出当前代码块,直接执行下一个 script
代码段。所以在同一个 script
内的代码段有错误的话就不会执行下去,但是不会影响下一个 script
内的代码段。
3.2 第一阶段:语法检查
语法检查也是JavaScript
解析器的工作之一,包括 词法分析
和 语法分析
,过程大致如下:
3.2.1 词法分析
词法分析:JavaScript解释器先把JavaScript
代码(字符串)的字符流按照ECMAScript
标准转换为记号流。
例如:把字符流:
<span style="font-size:18px;">a = (b - c);</span>
转换为记号流:
NAME "a"
EQUALS
OPEN_PARENTHESIS
NAME "b"
MINUS
NAME "c"
CLOSE_PARENTHESIS
SEMICOLON
3.2.2 语法分析
语法分析:JavaScript语法分析器在经过词法分析后,将记号流按照ECMAScript
标准把词法分析所产生的记号生成语法树。
通俗地说就是把从程序中收集的信息存储到数据结构中,每取一个词法记号,就送入语法分析器进行分析。
语法分析不做的事:去掉注释,自动生成文档,提供错误位置(可以通过记录行号来提供)。
当语法检查正确无误之后,就可以进入运行阶段了。
3.3 第二阶段:运行阶段
3.3.1 预解析
第一步:创建执行上下文。JavaScript引擎将语法检查正确后生成的语法树复制到当前执行上下文中。
第二步:属性填充。JavaScript引擎会对语法树当中的变量声明、函数声明以及函数的形参进行属性填充。
“预解析”从语法检查阶段复制过来的信息如下:
- 内部变量表
varDecls
:varDecls
保存的用var
进行显式声明的局部变量。- 内嵌函数表
funDecls
:在“预解析”阶段,发现有函数定义的时候,除了记录函数的声明外,还会创建一个原型链对象(prototype)。
3.4 执行上下文(execution context)
(一)预解析阶段创建的执行上下文包括:变量对象、作用域链、this。
- 变量对象(
Variable Object
):由var declaration
、function declaration
(变量声明、函数声明)、arguments
(参数)构成。变量对象是以单例形式存在。- 作用域链(
Scope Chain
):variable object
+all parent scopes
(变量对象以及所有父级作用域)构成。this
值:(this Value
):content object
。this
值在进入上下文阶段就确定了。一旦进入执行代码阶段,this
值就不会变了。
(二)“预解析”阶段创建执行上下文之后,还会对变量对象/活动对象(VO/AO
)的一些属性填充数值。
注:函数申明提升优先级高于变量声明提升。
- 函数的形参:执行上下文的变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为
undefined
。- 函数声明:执行上下文的变量对象的一个属性,属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则会替换它的值。
- 变量声明:执行上下文的变量对象的一个属性,其属性名即为变量名,其值为
undefined
;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的函数声明的属性。
变量对象/活动对象(VO/AO
)填充的顺序也是按照以上顺序:函数的形参->函数声明->变量声明;
在变量对象/活动对象(VO/AO
)中权重高低也按照函数的形参->函数声明->变量声明
顺序来。
如下代码:
var a=1;
function b(a) {
alert(a);
}
var b;
alert(b); // function b(a) { alert(a); }
b(); //undefined
以上代码在进入执行上下文时,按照函数的形参->函数声明->变量声明
顺序来填充,并且优先权永远都是函数的形参>函数声明>变量声明
,所以只要alert(a)
中的a是函数中的形参,就永远不会被函数和变量声明覆盖。就算没有赋值也是默认填充的undefined
值。
第二部分:执行代码
经过“预解析”创建执行上下文之后,就进入执行代码阶段,VO/AO
就会重新赋予真实的值,“预解析”阶段赋予的undefined
值会被覆盖。
此阶段才是程序真正进入执行阶段,Javascript引擎会一行一行的读取并运行代码。此时那些变量都会重新赋值。
假如变量是定义在函数内的,而函数从头到尾都没被激活(调用)的话,则变量值永远都是undefined
值。
进入了执行代码阶段,在“预解析”阶段所创建的任何东西可能都会改变,不仅仅是VO/AO,this和作用域链也会因为某些语句而改变,后面会讲到。
了解完Javascript
的解析过程,最后我们再来了解下firebug
控制台对Javascript
的报错提示。
其实firebug
的控制台也算是JavaScript解释器,而且他们会提示我们哪行出现了错误或者错误发生在哪个时期,语法检查阶段错误,还是运行期错误。
如下:
alert(var);// SyntaxError: syntax error 语法分析阶段错误 :语法错误
var=1; // SyntaxError: missing variable name 语法分析阶段错误 :var是保留字符,导致变量名丢失
a=b=v // ReferenceError: v is not defined 运行期错误: v 是未定义的JavaScript错误信息)
有如此详细的错误提示,是不是就很快就知道代码中到底是哪里错了呢!
四、作用域链(Scope Chain)
作用域链是处理标识符时进行变量查询的变量对象列表,每个执行上下文都有自己的变量对象:对于全局上下文而言,其变量对象就是全局对象本身;对于函数而言,其变量对象就是活动对象。
4.1 作用域链以及执行上下文的关系
在Javascript
中只有函数能规定作用域,全局执行上下文中的 Scope
是全局上下文中的属性,也是最外层的作用域链。
函数的属性Scope
是在“预解析”的时候就已经存在的了,它包含了所有上层变量对象,并一直保存在函数中。就算函数永远都没被激活(调用),Scope
也都还是存在函数对象上。
创建执行上下文的 Scope
属性和进入执行上下文的过程如下:
Scope = AO + [[Scope]] //预解析时的 Scope 属性
Scope = [AO].concat([[Scope]]); //执行阶段,将AO添加到作用域链的最前端
4.2 执行上下文定义的 Scope 属性变化过程
执行上下文中的AO
是函数的活动对象,而Scope
则是该函数属性作用域。当前函数的AO
永远是在最前面的,保存在堆栈上,而每当函数激活的时候,这些AO
都会压栈到该堆栈上,查询变量是先从栈顶开始查找,也就是说作用域链的栈顶永远是当前正在执行的代码所在环境的VO/AO
(当函数调用结束后,则会从栈顶移除)。
通俗点讲就是:JavaScript解释器通过作用域链将不同执行位置上的变量对象串连成列表,并借助这个列表帮助JavaScript解释器检索变量的值。作用域链相当于一个索引表,并通过编号来存储它们的嵌套关系。当JavaScript解释器检索变量的值,会按着这个索引编号进行快速查找,直到找到全局对象为止,如果没有找到值,则传递一个特殊的undefined
值。
是不是又想到了一条JavaScript
高效准则:为什么说在该函数内定义的变量,能减少函数嵌套,能提高JavaScript
的效率?因为函数定义的变量,此变量永远在栈顶,这样子查询变量的时间变短了。
4.3 作用域的特性
保证有序的访问所有变量和函数;
作用域链感觉就是一个VO
链表,当访问一个变量时,先在链表的第一个VO
上查找,如果没有找到则继续在第二个VO
上查找,直到搜索结束,也就是搜索到全局执行环境的VO
中。这也就形成了作用域链的概念。
var color="blue";
function changecolor(){
var anothercolor="red";
function swapcolors(){
var tempcolor=anothercolor;
anothercolor=color;
color=tempcolor; // Todo something
}
swapcolors();
}
changecolor();//这里不能访问tempcolor和anothercolor;但是可以访问color;
alert("Color is now "+color);
五、原型链查询
在介绍“预解析”阶段时,我们有提到当创建函数时,同时也会创建原型链对象。原型链对象在作用域链中没有找到变量时,那么就会通过原型链来查找。
function Foo() {
function bar() {
alert(x);
}
bar();
}
Object.prototype.x = 10;
Foo(); // 10
上例中在作用域链中遍历查询,到了全局对象了,该对象继承自Object.prototype,因此,最终变量“x”的值就变成了10。不过,在原型链上定义变量对象有些浏览器不支持,譬如IE6,而且这样增加了变量对象的查询时间。所以变量声明尽量在调用函数AO里,即在用到该变量的函数内声明变量对象。
作用域是在“预解析”时就已经决定的,所以作用域被叫做静态作用域,而在执行阶段的则被叫做动态链,因为在执行阶段会改变作用域链中填充的值。
代码执行阶段对“预解析”的改变
创建了函数就有一个闭包,而变量是在函数的执行上下文保存起来的静态作用域链上查询的,而当前函数内创建的的变量会在函数结束后就被销毁。而闭包就能在函数结束之后还能让这些变量一直保存在作用域链上。
六、自由变量
自由变量是指在函数中使用的,但既不是函数参数也不是函数局部变量的变量。
七、闭包
理论角度:所有函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量。
应用角度:当在代码中引用了自由变量,即使创建它的上下文已经销毁,此变量还能访问。
ECMAScript
标准中,同一个上下文创建的闭包(理论上的闭包)是共用一个作用域的,也就是说在闭包中对其中变量修改会影响到其他闭包对其变量的读取。
所谓创建额外的闭包就是创建函数,不管是匿名函数、函数表达式、函数声明(除了构造函数),只要能创建作用域链就行,与函数类型无关,然而创建额外的函数不是唯一的方法。
遍历最外层代码:
最后:
总结来说,面试成功=基础知识+项目经验+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。
面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
遍历最外层代码:
最后:
总结来说,面试成功=基础知识+项目经验+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。
面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】