一步一步的理解javascript的预编译
首先,我们要知道javascript是单线程、解释性语言。所谓解释性语言,就是翻译一句执行一句。而不是通篇编译成一个文件再去执行。
其实这么说还没有这么直观,读一句执行一句那是到最后的事了。到JS执行前还有两大步骤。
那就是1.语法分析(或语意分析)→2.预编译→3.解释执行(真正的读一句执行一句)
第一步:语法分析(即扫描一下看一看有没有低级的语法错误,比如多个大括号啊,写个中文标点等等,只通篇检查语法,但不执行。这就是语法分析的过程。)
第二步:预编译过程(发生在函数执行时,也可说成执行的前一刻,下面重点讲解)
第三步:解释执行(解释一句执行一句)
好了,了解了js执行的三大步骤接着说一下js预编译。说预编译之前先看几段代码
function test() { console.log(123456); } test();
上边这段代码毫无疑问可以执行,正常输出123456
接下来换一种写法,先写执行语句,再写函数体,如下:
test(); function test() { console.log(123456); }
这样依然可以正常执行。打印出123456
再看下边这段代码:
var num = 123; console.log(num);
这个也毫无疑问可以执行,输出123。
但是,如果直接这样写
console.log(num);
这样属于一个变量未经声明就被访问,会报错。
再换一下写法:
console.log(num); var num = 123;
其实这也是一种变量未经声明就访问,但是这样写不但不会不报错,还可以打印出结果,打印结果为undefined。
这是为什么呢? 这时候,有些经验的人会让你记住两句话:
1.函数声明整体提升(意思是函数的声明无论写到那个位置,在执行的时候都会把函数声明的语句提到最前执行)。
2.变量的声明提升(意思是变量的声明无论写到什么位置,在执行的时候都会提到最前执行,这里注意是变量的声明,没有赋值什么事)。
这两句话虽然可以解决大部分问题,但是下面的实例它就解决不了了,要真正解释这两种现象就不得不说预编译了。学会了预编译以后上边那两句话永远不需要去记,轻松解决各种问题。
下边来看一个实例:
function test(a) { console.log(a); console.log(b); console.log(c); var a = 123; console.log(a); function a() {}; console.log(a); var b = function () {}; console.log(b); function c() {}; console.log(c); } test(1);
这段代码就是上边那两句话解决不了的。先思考一下这段代码的运行结果会是什么呢?
想要明白这个运行结果,首先我们得明白一个事,这里边既有函数,又有变量声明,还有形参,而且大家的名字还都一样,像打仗一样,都在抢的用。到底谁能抢过谁呢?
我们已经知道函数的预编译在函数执行的前一刻了,也就是说在函数运行之前函数的预编译就帮助我们调和了这个“打仗”的矛盾。
函数的预编译分为4大步骤。
第一步:生成一个Activation Object(执行期上下文)对象,简称AO对象。在访问函数中的变量的时候会直接从我们函数对应的的AO中获取。
AO{ }
这就是一个AO对象。
第二步:找形参和变量声明,将形参和变量名作为AO对象的属性名,值为undefined。
注意:var a=123;这条语句需要拆分成两部分,一部分为var a;(变量的声明) 一部分为a=123;(变量的赋值)。在这里我们找的是变量声明。所以a=123并没有在预编译 过程中发现。
所以对于上边的函数:
AO{ a:undefined, b:undefined }
第三步:将实参值和形参统一
此时
AO{ a:1, b:undefined }
第四步:在函数体里找函数声明,值赋予函数体
注意:这里找的是函数声明,而b=function () {};属于函数表达式,不是这里需要的。
所以此时
AO{ a:function a(){}, b:undefined, c :function c(){} }
以上AO就是函数的预编译全部完成之后的AO。
接下来该到了真正的读一句执行一句的时候了。
1.读console.log(a);语句,从AO中找到a的值: function a(){},所以输出结果就为 function a(){} 。
2.读console.log(b);语句,从AO中找到b的值: undefined,所以输出结果就为 undefined。
3.读console.log(c);语句,从AO中找到c的值: function c(){},所以输出结果就为 function c(){} 。
4.读var a = 123;语句,var a = 123分为var a;和a=123;两部分,第一部分变量的声明看过,现在只看a=123;此时:
AO{ a:123, b:undefined, c :function c(){} }
5.读console.log(a);语句,从AO中找到a的值: 123,所以输出结果就为 123 。
6.function a(){};语句在预编译时已经看过,现在不管,直接下一句console.log(a); 从AO中找到a的值: 123,所以输出结果就为 123 。
7.var b = function() {};语句同样也是分为var b;和 b = function() {};两部分,第一部分变量的声明看过,现在只看 b = function() {};此时:
AO{ a:123, b: function () {}, c :function c(){} }
8.读console.log(b);语句,从AO中找到b的值: function() {},所以输出结果就为 function() {}。
9.function c(){};也在预编译中看过了,在这里不看,直接下一句console.log(c);从AO中找到c的值: function c() {},所以输出结果就为 function c() {}。
运行结果如下图所示:
以上四部曲说的是Javascript函数的预编译,预编译不仅发生在函数体,在全局也会发生预编译。全局的预编译相对于函数的就简单一些了。接着我们看一下全局的预编译。
全局的预编译只有三个步骤,因为在全局不会涉及到参数。
继续来看一个发生在全局的预编译的实例
console.log(a); console.log(b); var a = 123; var b = function (){}; console.log(a); function a() {}; console.log(a); console.log(b);
思考一下这段代码的运行结果。同样也是有变量声明,函数名,只不过发生在全局不会有参数的出现,其实步骤与函数的预编译一致,只是去掉有关参数的部分即可。
第一步:生成一个Global Object(执行期上下文)对象,简称GO对象。因为是全局生成的不再叫AO,但是道理和AO一样,可以理解为换一种叫法而已。
GO{ }
这就是一个GO对象
第二步:在全局中找变量声明,将变量名作为GO对象的属性名,值为undefined。
同样需要注意:var a=123;这条语句需要拆分成两部分,一部分为var a;(变量的声明) 一部分为a=123;(变量的赋值)。在这里我们找的是变量声明。所以a=123并没有在预编译过程中发现。
此时GO是这样的:
GO{ a:undefined, b:undefined }
由于没有参数,实参形参统一的步骤直接省略
第三步:在全局中找函数声明,值赋予函数体
同样需要注意:这里找的是函数声明,而b=function () {};属于函数表达式,不是这里需要的。
所以此时:
GO{ a: function a() {}, b: undefined }
接下来该到了真正的读一句执行一句的时候了。
1.读console.log(a);语句,在GO中找到a的值:function a(){},所以输出结果就为function a(){}。
2.读console.log(b);语句,在GO中找到b的值:undefined,所以输出结果就为undefined。
3.读var a = 123;语句,var a = 123分为var a;和a=123;两部分,第一部分变量的声明看过,现在只看a=123;此时:
GO{ a:123, b:undefined }
4.读var b = function() {};语句同样也是分为var b;和 b = function() {};两部分,第一部分变量的声明看过,现在只看 b = function() {};此时:
GO{ a:123, b:function() {} }
5.读console.log(a);语句,在GO中找到a的值:123,所以输出结果就为123。
6.function a() {};语句在预编译时已经看过,现在不管,直接下一句console.log(a); 从GO中找到a的值: 123,所以输出结果就为 123 。
7.读console.log(b);语句,在GO中找到b的值:function (){},所以输出结果就为function (){}。
运行结果如下图所示:
这里有一个特别的:未经声明的变量就直接赋值,该变量归GO所有。什么意思呢?我们看下边的实例
function f() { var a = b = 6; c = 8; } f(); console.log(a);
这段代码会报错,因为变量a在函数中声明,他归该函数的AO所有,当函数执行完AO被销毁,所以在全局找不到a。
但是这样:
function f() { var a = b = 6; c = 8; } f(); console.log(b); console.log(c);
运行结果:
在全局访问b和c不但没报错,而且还正确的打印出了运行结果。正如我们刚刚所说的未经声明的变量就直接赋值,该变量归GO所有。所以在全局可以访问到也是顺其自然的事情了。
好了,以上就是javascript的预编译过程。说了半天,学习这个预编译到底有什么用呢?在开发的时候我们也不可能这么命名变量与函数名的呀。其实在这里学习预编译主要是为了下面的作用域来做铺垫,理解了作用域之后再谈我们开发中常见的闭包。这样才能更深入的去理解闭包。