Description
JavaScript三部曲
- 语法分析
- 预编译
- 解释执行
语法分析是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树
”(Abstract Syntax Tree,AST)。并检查你的代码有没有什么低级的语法错误,如果有,引擎会停止执行并抛出异常。
解释执行顾名思义便是执行代码了;
预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数 ;
在学习预编译
前先学习下执行上下文
和作用域
一、执行上下文
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的 ,执行上下文是代码运行时的上下文环境。
什么是执行上下文栈?
js引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文,处于活动状态的执行上下文环境只有一个。
执行上下文的类型
执行上下文总共有三种类型:
全局执行上下文
: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:- 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。
- 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
GO global object 全局上下文
1、找变量
2、找函数声明
3、执行
函数执行上下文
: 每次调用函数时,都会为该函数创建一个新的执行上下文。在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫AO
,而只有活动对象上的各种属性才能被访问。
AO activation object 活跃对象 函数上下文
1、找形参和变量声明
2、实参值赋给形参
3、找函数声明,赋值
4、执行
Eval 函数执行上下文
: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,所以在这里不再讨论。
执行上下文的生命周期
执行上下文的生命周期包括三个阶段:创建阶段
→ 执行阶段
→ 回收阶段
,本文重点介绍创建阶段。
1. 创建阶段
当函数被调用,但未执行任何其内部代码之前,会做以下三件事:
- 创建变量对象(Variable Object,VO):首先初始化函数的参数
arguments
,提升函数声明和变量声明
。函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。 - 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。
- 确定
this
指向。
在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先暂时赋值为 undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。
另外,一个函数在执行之前,也会创建一个函数执行上下文环境,跟全局上下文差不多,不过函数执行上下文中会多出 this arguments 和函数的参数
。
2. 执行阶段
执行变量赋值、代码执行
3. 回收阶段
执行上下文出栈等待虚拟机回收执行上下文
二、作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
经典的一道面试题
var a = 1
function out(){
var a = 2
inner()
}
function inner(){
console.log(a)
}
out() // 1
三、预编译
1、预编译在什么时候发生
预编译分为全局预编译和函数预编译:全局预编译发生在页面加载完成时执行,而函数预编译发生在函数执行的前一刻。
2、全局预编译的步骤
- 创建GO(Global Object,全局执行期上下文,在浏览器中为window)对象;
- 寻找var变量声明,并赋值为undefined;
- 寻找function函数声明,并赋值为函数体;
- 执行代码。
3、函数预编译的步骤
- 创建AO对象,函数执行上下文。
- 寻找函数的形参和变量声明,将变量和形参名作为AO对象的属性名,值设定为undefined.
- 将形参和实参相统一,即更改形参后的undefined为具体的形参值。
- 寻找函数中的函数声明,将函数名作为AO属性名,值为函数体。
4、示例分析
先来区分理解一下这2个概念: 变量声明var …
函数声明function(){}
,函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
<script>
var a = 1;
console.log(a);
function test(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
var c = function (){
console.log("I at C function");
}
console.log(c);
test(2);
</script>
分析过程如下:
- 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
- 第一个脚本文件加载;
- 脚本加载完毕后,分析语法是否合法;
- 开始预编译 查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,作为GO属性,值赋予函数体;
预编译
//抽象描述
GO/window = {
a: undefined,
c: undefined,
test: function(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
}
解释执行代码(直到执行调用函数test(2)语句)
//抽象描述
GO/window = {
a: 1,
c: function (){
console.log("I at C function");
}
test: function(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
}
执行函数test()之前,发生预编译
- 创建AO活动对象(Active Object);
- 查找形参和变量声明,值赋予undefined;
- 实参值赋给形参;
- 查找函数声明,值赋予函数体;
预编译之前面1、2两小步如下:
//抽象描述
AO = {
a:undefined,
b:undefined,
}
预编译之第3步如下:
//抽象描述
AO = {
a:2,
b:undefined,
}
预编译之第4步如下:
//抽象描述
AO = {
a:function a() {},
b:undefined
d:function d() {}
}
执行test()函数时如下过程变化:
//抽象描述
AO = {
a:function a() {},
b:undefined
d:function d() {}
}
--->
AO = {
a:123,
b:undefined
d:function d() {}
}
--->
AO = {
a:123,
b:function() {}
d:function d() {}
}
执行结果:
注意:
预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 ; 只有在解释执行阶段才会进行变量初始化 ;
经典面试题
// 求输出是什么
function Foo() {
getName = function() {console.log(1)};
return this;
}
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
var getName = function() {console.log(4)};
function getName() {console.log(5)};
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
1、 首先预编译阶段,变量声明与函数声明提升至其对应作用域的最顶端。
因此上面的代码编译后如下(函数声明的优先级先于变量声明):
GO = {
Foo: function() {
getName = function(){ console.log(1);};
return this;
},
getName: function() { console.log(1)} // getName又被改写了,之前这里是打印4
}
预编译完成后进入执行阶段
2、 Foo.getName()
;直接调用Foo上getName方法,输出2
3、 getName()
;输出4,getName被重新赋值了
4、 Foo().getName()
;执行Foo(),window的getName被重新赋值,返回this;浏览器环境中,非严格模式,this 指向 window,this.getName();输出为1.
如果是严格模式
,this
指向undefined
,此处会抛出错误。
如果是node环境
中,this
指向global
,node的全局变量并不挂在global上,因为global.getName
对应的是undefined
,不是一个function,会抛出错误。
5、 getName()
;已经抛错的自然走不动这一步了;继续浏览器非严格模式;window.getName被重新赋过值,此时再调用,输出的是1
6、 new Foo.getName()
;考察运算符优先级的知识,new 无参数列表,对应的优先级是18;成员访问操作符 .
, 对应的优先级是 19。因此相当于是new (Foo.getName)()
;new操作符会执行构造函数中的方法,因此此处输出为 2.
7、 new Foo().getName()
;new 带参数列表,对应的优先级是19,和成员访问操作符.
优先级相同。同级运算符,按照从左到右的顺序依次计算。new Foo()
先初始化 Foo 的实例化对象,实例上没有getName方法,因此需要原型上去找,即找到了Foo.prototype.getName
,输出3
8、 new new Foo().getName()
; new 带参数列表,优先级19,因此相当于是new (new Foo()).getName()
;先初始化 Foo 的实例化对象,然后将其原型上的 getName 函数作为构造函数再次 new ,输出3
因此最终结果如下:
Foo.getName(); //2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3