前言
在语言学习中有时了解底层原理比记忆一些知识点特性要有效的多,了解底层原理也可以更方便理解这门语言,而且不论代码任何变化,都可以依据底层原理来一句一句解释。
什么是预编译
预编译就是就是js执行的第二个步骤,js执行总共分为三个步骤:
1.语法分析: V8引擎在解析js代码之前,会先通篇进行扫描,若有语法、逻辑错误,报错并停止执行。
2.预编译: 语法和语句全部会被转换成对象,GO(Global Object),AO(Active Object)把代码按照 一定的规则,放到GO和AO中。GO及AO在代码执行完后会被销毁。
3.解释执行: 编译一行执行一行,当语法分析没有问题,并且已经完成预编译阶段之后,就开始解释执行代码。
在讲解预编译过程前,先讲解下声明提升
使用var关键字声明的变量存在声明提升。在代码执行前阶段,变量声明会被移动到作用域的顶部(详细可见我另外一篇有关作用域的文章),但赋值操作保留在原来的位置。
例如
console.log(a); // 输出:undefined
var a = 1;
按照正常代码从从上往下运行理解,应该会报错才对,没有找到变量a。结果输出是undefined,a没有定义,这说明a已经被声明了。所以代码效果看起来和下面代码一样,变量声明提升了,赋值声明留在原地。
var a ;
console.log(a); // 输出:undefined
a = 1;
并且,函数也存在声明提升,并且会整体提升
foo();
function foo()
{
console.log("nihao"); // 打印nihao
}
按正常逻辑思维来,函数调用在函数声明前,代码怎么看都运行不起来,函数调用找不到函数,怎么就能打印结果呢。 这说明,在该作用域内函数的声明提升到了调用前。代码效果起来和下面代码一样。
function foo()
{
console.log("nihao"); // 打印nihao
}
foo();
函数中未定义的变量,默认将其定义在全局中。
function foo() {
var a = 1
b = 2
console.log(a) // 1
console.log(b) // 2
}
foo()
console.log(a) // 报错:a 是 foo() 中的变量,全局中无法找到(作用域的一个问题,可以看我其他文章)
console.log(b) // 2 b = 2 扩散到全局了
那下面的代码到底输出什么?既有函数又有变量,而且还同名,那变量的声明提升和函数提升到底谁的优先级高呢?想必是很疑惑吧,没关系,跟着我一起来通过预编译一行一行理解下。
function fn(a)
{
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function () {}
console.log(b);
function d() {}
}
fn(1);
预编译过程
因为执行环境不同,所以分为全局环境预编译和函数环境预编译:
1.函数环境预编译
- 创建函数的AO对象 (Action Object)
- 找形参和变量声明,将形参和变量声明作为AO的属性名,赋予undefined
- 将形参和实参统一
- 在函数体内找到函数声明,将函数名作为AO对象的属性名,值赋予函数体
2.全局环境编译
- 创建GO对象 (Global Object)
- 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
- 在全局找到函数声明,将函数名作为GO对象的属性名,值赋予函数体
function fn(a)
{
console.log(a); //
var a = 123;
console.log(a); //
function a() {}
console.log(a); //
var b = function () {}
console.log(b); //
function d() {}
}
fn(1);
因为是函数环境,首先先创建OA对象
AO{
}
再找形参和变量声明,将形参和变量声明作为AO的属性名,赋予undefined
AO{
a:undefined,
b:undefined,
}
再将形参和实参统一,就是找到实参的值,赋值给形参
AO{
a:1,
b:undefined
}
最后在函数体内找到函数声明,将函数名作为AO对象的属性名,值赋予函数体。函数体预编译就这样完成了
AO{
a:function a() {},//函数声明
b:undefined, //注意了 var b = function () {}这个不是函数声明,是函数表达式,所以b的值还是undefined
d:function d() {}//函数声明
}
同一变量在同一作用域中被多次定义时一般会被覆盖修改。
此时AO对象已经创建完,那么就开始执行函数,开始打印。
function fn(a)
{
console.log(a);// 输出function a() {}
var a = 123;//预编译时已经变量提升了,但是赋值还没有操作。所以此时AO对象中a的值改变为123
// AO{
// a:123,
// b:undefined,
// d:function d() {}
//}
console.log(a); // 输出:123
function a() {}
console.log(a); //输出123
var b = function () {} // 改变AO对象中b的值
// AO{
// a:123,
// b:function () {},
// d:function d() {}
//}
console.log(b); //6.输出function () {}
function d() {}
}
结果如图,确实与推算一致
我们再来看看下面这个代码,
global = 100
function fn() {
console.log(global); // undefined
global = 200
console.log(global); // 200
var global = 300
}
fn()
var global;
预编译后
// GO = {
// global: undefined,
// fn= function fn() {},
// }
global = 100
function fn() {
console.log(global); // undefined
global = 200
console.log(global); // 200
var global = 300
}
fn()
// AO = {
// global:undefined ,
// }
var global;
大家可以自己试着预编译实践看看,看看和我预编译的是否一致,并且验证打印结果是否一致