什么是预编译?
引擎会在解释 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
函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序执行