一步一步的理解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的预编译过程。说了半天,学习这个预编译到底有什么用呢?在开发的时候我们也不可能这么命名变量与函数名的呀。其实在这里学习预编译主要是为了下面的作用域来做铺垫,理解了作用域之后再谈我们开发中常见的闭包。这样才能更深入的去理解闭包。

猜你喜欢

转载自www.cnblogs.com/daimaxiaocai/p/11890727.html