Js中的预编译
什么是预编译?
引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
1.预编译什么时候发生
预编译分为全局预编译和局部预编译,全局预编译发生在页面加载完成时执行,而局部预编译发生在函数执行的前一刻。
预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 。只有在解释执行阶段才会进行变量初始化 。
目的:定义作用域中的初始化词法环境、减少运行时报错
2.预编译前奏
一切声明的全局变量和未经声明的变量,全归window所有。
下面这个函数里面只有一个连等的操作,赋值操作都是自右向左的,而b是未经声明的变量,所以它是归window的,我们可以直接使用window.b去使用它。
function test(){
// 这里的b是未经声明的变量,所以是归window所有的。
var a = b = 110;
console.log(a,b);
}
test();
console.log(a,b);//报错
3.预编译步骤
首先JavaScript的执行过程会先扫描一下整体语法语句,如果存在逻辑错误或者语法错误,那么直接报错,程序停止执行,没有错误的话,开始从上到下解释一行执行一行。
1.全局编译的步骤
- 生成GO对象 GO{}(global object) 这个GO就是window
- 将全局的变量声明(的名)储存一GO对象中,value为undefinde
- 将全局的函数声明的函数名作为go对象中的key,函数整体内容为value储存到go对象中
2.局部编译的步骤
- 执行前的一瞬间,会生成一个AO(action object)对象
- 到函数体作用域里找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
- 将实参和形参统一
- 分析函数声明,函数名作为AO对象的属性名,值为函数体,如果遇到同名的,直接覆盖
关于GO对象的例子:
全局预编译:在逐行执行;语法检测之前
var a;
function fun(){
}
function abc(){
}
function a(){
}
console.log(a);
var a = 100;
console.log(a);
1. 会生成一个对象(GO),这个对象封装的就是作用域,称为GO(global object)。当全部挂载完成之后,然后代码在去逐行执行
GO{
}
2. 分析变量声明(var)——变量作为GO对象的属性名,值为undefined
GO{
a:undefined
}
3. 分析函数声明(function)——函数名作为GO对象的属性名,值为函数体(如果遇到同名,直接覆盖)
GO={
a:undefined;function a(){},
fun:function fun(){},
abc:function abc(){}
}
4. 当走到某一行的时候;a产生了一次赋值;此时GO对象变成了:
GO={
a:100,
fun:function fun(){}
abc:function abc(){};
}
5. 逐行执行(看着GO对象里面的执行)
输出结果:ƒ a() {} 100
关于AO对象的例子:
概念:函数在每次运行时会重新创建函数内所有定义的变量,函数其实也是一种变量,因为它是变量名指向一个函数方法,所有的变量创建后加入到自身AO对象中,然后将在scope属性中加入自身AO对象。
什么是AO:
是函数执行前的一瞬间,生成一个AO对象(在函数执行前的一瞬间会生成自己的AO,如果函数执行2次,生成了两次AO,这两次的AO是没有任何关联)
function fn(a) {
console.log(a);//1(第一个 console.log())
var a = 123;
console.log(a);//2
function a() {}
console.log(a);//3
var b = function() {}//函数表达式
console.log(b);//4
function d() {}
var d = a;
console.log(d);//5
}
fn(1);
//输出结果:
[Function: a]
123
123
[Function: b]
123
1. 创建AO对象
AO{
}
2. 到函数体作用域里找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
AO{
a:undefined;
b:undefined;
d:undefined;
}
3. 将实参和形参统一
AO{
a:undefined; 1;
b:undefined;
d:undefined;
}
- 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
AO{
a:undefined; 1; function (){ }
b:undefined;
d:undefined; function() { }
}
逐行执行(看着AO对象里面的执行)
第一个console.log:此时函数的执行环境内的变量为
AO{
a:undefined; 1; function (){ }
b:undefined;
d:undefined; function() { }
}
第二、三、四、五个console.log:此时函数的执行环境内的变量为
AO{
a:undefined; 1; function (){ }; 123;
b:undefined;
d:undefined; function() { }
}
解释:当函数调用时,创建函数执行上下文,然后根据实参填充arguments对象,即:形参var a = arguments[0],然后根据函数内的函数声明将函数名a进行提升,此时会发现函数名和形参名发生了冲突,由于形参与arguments中的数据共享状态,所以接下来后提升的函数名a内存放的函数引用地址会将前面定义的形参名a指向的arguments[0]直接覆盖掉
函数执行完毕,销毁AO对象。
在预编译这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端,然后在进行接下来的处理,下面通过示例看下变量提升和函数提升
变量的提升:
示例1:
function hoistVariable() {
if (!foo) {
var foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
预编译之后
function hoistVariable() {
var foo;
if (!foo) {
foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
引擎将变量声明提升到了函数顶部,初始值为undefined,自然,if语句块就会被执行,foo变量赋值为5
示例2:
var foo = 3;
function hoistVariable() {
var foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
foo || 5这个表达式的结果是5而不是3,虽然外层作用域有个foo变量,但函数内是不会去引用的
预编译之后:
var foo = 3;
function hoistVariable() {
var foo;
foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
函数的提升:
示例1:
function hoistFunction() {
foo(); // 2
var foo = function() {
console.log(1);
};
foo(); // 1
function foo() {
console.log(2);
}
foo(); // 1
}
hoistFunction();
第一次调用时实际执行了下面定义的函数声明,然后第二次调用时,由于前面的函数表达式与之前的函数声明同名,故将其覆盖,以后的调用也将会打印同样的结果
预编译之后
function hoistFunction() {
var foo;
foo = function foo() {
console.log(2);
}
foo(); // 2
foo = function() {
console.log(1);
};
foo(); // 1
foo(); // 1
}
hoistFunction();
示例2:
var foo = 3;
function hoistFunction() {
console.log(foo); // function foo() {}
foo = 5;
console.log(foo); // 5
function foo() {}
}
hoistFunction();
console.log(foo); // 3
可以看到,函数声明被提升至作用域最顶端,然后被赋值为5,而外层的变量并没有被覆盖
预编译之后:
var foo = 3;
function hoistFunction() {
var foo;
foo = function foo() {};
console.log(foo); // function foo() {}
foo = 5;
console.log(foo); // 5
}
hoistFunction();
console.log(foo); // 3
函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序执行

浙公网安备 33010602011771号