Web前端的知识之旅哟——预编译

• 我们在书写js代码的时候,会发现两点和c/c++语言不同的地方。第一个是当我们在后面定义了一个函数之后,我们在定义函数之前使用这个函数也是可以的。第二个是我们在后面声明的一个变量,但是在前面调用这个变量的时候并不会报错而是undefiend。

这两点不同在js中被称为函数声明提升变量声明提升,函数声明提升是一种整体提升,它会把函数声明和函数体一起提升到前面。变量声明提升则是一种局部提升,它仅仅将变量的声明提前了,但是并没有将赋值也一起提前。

那么为什么会出现这种提升的现象呢?

这是因为js运行的时候有一个阶段叫做预编译阶段,而我们的声明提升现象都是发生在预编译的时候哟~

预编译

• js运行三部曲

1.语法分析

2.预编译

3.解释执行

语法分析:js引擎在解析js代码之前,会先通篇扫描一下,找出低级的语法错误,比如写错大括号之类的。

编译执行:我们前面提到js是一种解释型语言,编译一行执行一行,当语法分析没有问题,并且已经完成预编译阶段之后,就开始解释执行代码。

这里我们着重介绍预编译。

预编译前奏

在介绍预编译之前,我们有两个重要概念需要掌握。

1.imply global 暗示全局变量

如果任何变量未经声明就赋值使用,此变量就会为全局对象window所有,并且成为window对象的一个属性。


1. window.a = 123;
2. window.a === a // true;
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

或者


1. a = 123
2. window.a===a  // true
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

2.一切声明的全局变量,都是window的属性。


1. var a = 123;
2. console.log(window.a); // 123
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

• 这样看不论全局变量有没有声明,似乎都会成为全局对象上的属性,那么两者之间有什么区别呢?

区别在于:经过声明的全局变量不能通过delete操作来删除,但是未经声明的全局变量可以被删除


1. a = 123;
2. console.log(window.a === a)  // true
3. delete window.a;
4. console.log(window.a); // undefiend
5. var b = 123;
6. delete window.b;
7. console.log(window.b); // 123
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

正是这一种特性,导致es5有一种弊端,我们总会在无形中声明一些全局变量。


1. function test () {
2.       var a = b = 0;
3. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这段代码的原意是:在函数体中声明两个变量a、b,然后初始化a、b都是0。但是我们这么写之后,a经过了声明,但是b却没有声明,这时候b就会成为一个全局变量。

了解这两点之后,我们正式介绍一下预编译的过程。

预编译的过程我总结为以下四步

1.创建AO对象

2.寻找形参和变量声明,将变量和形参作为AO对象的属性名添加到对象中,值为undefined。值得注意的是,函数声明不叫变量。

3.将实参值和形参值相统一。

4.在函数体里面寻找函数声明,将函数名作为属性名,值为这个函数的函数体。

函数在执行的前一刻会产生一个上下文,这个上下文就是Activeaction Object对象,简称AO对象。

AO = {}

这个对象是空的,但是里面有一些我们看不到的却存在的隐式属性,比如this: window属性和arguments: [];属性

这个对象用来存放一些属性和方法,这些属性和方法就按照前面的四步来产生。


1. function test (a, b) {
2.       console.log a 
3.       function a () {}
4.       a = 222;
5.       console.log a
6.       function b () {};
7.       console.log b 
8.       var b = 111;
9.       var a ;
10. }
11. test(1)
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这里我们用这一个样例代码来简单介绍一下预编译的过程。

首先第一步,创建一个AO对象。

var AO = {};

第二步,寻找形参值和变量声明,并且将值赋为undefined。


1. AO = {
2.       a: undefined,
3.       b: undefined
4. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

3.将实参值和形参值相统一。这里因为属性名都已经存在了,所以直接赋值就可以了。


1. AO = {
2.       a: 1,
3.       b: undefined
4. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

4.寻找函数声明,将函数体赋值给属性。


1. AO = {
2.       a: function () {},
3.       b: function () {}
4. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这样在编译执行之前,我们预编译阶段创建的AO对象就是这个样子了,这个时候我们再看看分别打印的值是什么。

第一个console.log a –> function () {}

第二个console.log a –> 222 因为执行了a = 222这一行代码,所以重新赋值了。

第三个console.log b –> function () {}

var b = function () {}这种不叫做函数声明,这个函数是赋值给b变量的,b变量是声明。


1. function fn(a){
2.       a//function
3.       d//function
4.       var a = 123;
5.       a//123
6.       function a () {}
7.       b//undefined
8.       var b = function () {}
9.       b//function
10.       function d (){}
11. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这里的var b = function () {}只是声明了b变量,在第四步寻找函数声明里面并不会把b赋值成function () {},因为后面的函数并不是声明,当代码开始解释执行之后,执行到这一行之后才把b赋值成这个函数。

• 寻找变量声明的时候,不会管里面的代码到底会不会执行,执行是后面的事,这里只负责寻找所有变量。


1. funciton test(b) {
2.       a //undefined 
3.       if(1 > 5){
4.             var a = 123;
5.             function b () {}
6.       }
7.       a//undefined
8.       b//function
9.       var b = 234;
10.       b// 234
11. }
12. test(2);
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

打印第一个a的时候并不会报错而是undefined,当a没有声明的时候才会报错,因此这里a是有声明的,只是没有赋值而已,它根本不看有没有if,if的条件是不是真对寻找变量声明都没有关系。

第二步寻找形参和变量声明时候的AO对象:


1. AO = {
2.       a: undefined,  //这里的a虽然在if里面但是也被声明了
3.       b: undefined
4. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

以上就是我总结的预编译的知识点,希望对大家有帮助哟!~