概念
javaScript引擎就是根据ECMAScript定义的语言标准来动态执行JavaScript字符串
对于静态语言(如:C、C++、Java),处理上述这些事情的叫编译器Compiler。对于JavaScript这样的动态语言则被称为解释器Interpreter。不同的地方在于编译器是将源代码编译为另外一种代码(比如:机器码、或者字节码),而解释器是直接解析并将代码运行结果输出。
Chrome JS引擎V8是用C++编写的,为了提高浏览器执行JavaScript的性能,V8将JavaScript转为了更高效的机器码(JIT编译器:Just-In-Time compiler),而不只是使用解释器。
过程
解析JS的过程分为两个阶段: 语法检查阶段和运行阶段
语法检查包含词法分析和语法分析(注:这两个阶段是编译原理,所有语言都适用。eq:在浏览器工作原理)
运行阶段包含预编译和执行代码
词法分析
JavaScript解释器先把JavaScript代码(字符串)的字符流按照ECMAScript标准转换为记号流
eq:
a = (b - c);
转换为记号流
NAME "a"
EQUALS
OPEN_PARENTHESIS
NAME "b"
MINUS
NAME "c"
CLOSE_PARENTHESIS
SEMICOLON
语法分析
JavaScript语法分析器在经过词法分析后,将记号流按照ECMAScript标准把词法分析所产生的记号生成语法树,又被成为"AST(语法抽象树)"。
分析该js脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译阶段
预编译
预编译包含两步: 创建执行上下文和属性填充
概念:JS的执行环境
- 全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)
- 函数环境(函数调用执行时,进入该函数环境)
- eval环境
概念:函数调用栈
函数调用栈就是使用栈存取的方式进行管理运行环境,特点是先进后出,后进先出。
每进入一个不同的运行环境都会创建一个相应的执行上下文(Execution Context),那么在一段JS程序中一般都会创建多个执行上下文,js引擎会以栈的方式对这些执行上下文进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文(Global Execution Context),栈顶则永远是当前执行上下文。
当浏览器第一次加载你的script的时候,它默认的进了全局执行环境。如果在你的全局代码中你调用了一个函数,那么顺序流就会进入到你调用的函数当中,创建一个新的执行环境并且把这个环境添加到执行栈的顶部
function foo () {
function bar () {
return 'I am bar';
}
return bar();
}
foo();
1.创建执行上下文
JS引擎将语法检查正确后生成的语法树复制到执行上下文中
- 创建变量对象(Variable Object)
- 建立作用域链(Scope Chain)
- 确定this的指向
1.1 创建变量对象
- 创建arguments对象,检查当前上下文中的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行,全局环境没有此过程
- 检查当前上下文的函数声明,按代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则为指向该函数所在堆内存地址的引用,如果存在,则会被新的引用覆盖。
- 检查当前上下文的变量声明,按代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有该变量名属性,则在该变量对象以变量名建立一个属性,属性值为undefined;如果存在,则忽略该变量声明
PS:在全局环境中,window对象就是全局执行上下文的变量对象,所有的变量和函数都是window对象的属性方法。
函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。
1.2 建立作用域链
作用域链由当前执行环境的变量对象(未进入执行阶段前)与父级的一系列活动对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
1.3 确定this指向
在全局环境下,全局执行上下文中变量对象的this属性指向为window;函数环境下的this指向却较为灵活,需根据执行环境和执行方法确定,
2.属性填充: 扫描上下文中声明的函数形参、函数以及变量,并依次填充变量对象的属性
执行代码
- 变量对象赋值
- 变量赋值
- 函数表达式赋值
- 调用函数
- 顺序执行其他代码
当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。
函数的作用域是在函数创建即“预编译”阶段就已经就已经定义了,而在代码执行阶段则是将函数的作用域添加到作用域链上。
var aa="123";
console.log('bb',typeof bb, bb());
function bb(){
console.log('cc', cc);
console.log('aa', aa);
return 'bb';
}
var cc="cc";
console.log('bb',typeof bb, bb());
执行过程中当前函数的AO永远是在最前面的,保存在堆栈上,而每当函数激活的时候,这些AO都会压栈到该堆栈上,查询变量是先从栈顶开始查找,也就是说作用域链的栈顶永远是当前正在执行的代码所在环境的VO/AO(当函数调用结束后,则会从栈顶移除)。
PS:JavaScript解释器通过作用域链将不同执行位置上的变量对象串连成列表,并借助这个列表帮助JavaScript解释器检索变量的值。作用域链相当于一个索引表,并通过编号来存储它们的嵌套关系。当JavaScript解释器检索变量的值,会按着这个索引编号进行快速查找,直到找到全局对象为止,如果没有找到值,则传递一个特殊的 undefined值。
原型链查询
当创建函数时,同时也会创建原型链对象(prototype)函数天生的。原型链对象在作用域链中没有找到变量对时,那么就会通过原型链来查找。
执行上下文的数量限制
执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景
总结
- JavaScript是单线程
- 栈顶的执行上下文处于执行中,其它需要排队
- 全局上下文只有一个处于栈底,页面关闭时出栈
- 函数执行上下文可存在多个,但应避免递归时堆栈溢出
- 函数调用时就会创建新的上下文,即使调用自身,也会创建不同的执行上下文
网友评论