程志辉博客 - 一个伪文艺青年
贵在坚持,难在坚持,成在坚持!
程志辉

前言:JS执行三步走

众所周知JavaScript是解释性语言,主要特点为:解释一行执行一行

JS执行三步走:语法分析,预编译,解释执行

语法分析:在代码执行之前对代码进行通篇检查,以排除一些低级错误

预编译:代码执行的前一刻 ==> 在内存中开辟一些空间,存放一些变量与函数

解释执行:执行代码

JavaScript预编译

在讲解JS预编译之前得先了解两个东西,什么函数声明?什么是变量声明?

//JS两种常见的函数声明方式
function a(){}
var a=function(){}

//JS常见的变量声明方式
var a=10;

1.未经声明的对象直接复制,直接归window所有

2.全局上的任何变量,即使声明了也归window所有

3.window就是全局

function a(){
    var a=10; //声明了,但是在函数体内,是局部变量
    b=10;    //没有声明,归window所有
}
var c=10;  //全局上的变量,声明了归window所有

这样解释的话应该就好懂多了~

①函数声明整体提升 ②变量声明提升 ③预编译

先来讲讲变量声明提升

// 变量声明可以看成两部分:声明操作(var a)+赋值操作(a=10)
// 声明操作会在编译阶段进行,声明操作会被提升到执行环境的顶部,值是undefined(未初始化)
// 赋值操作会留在原地等待执行操作
var a = 2;

function foo() {
  console.log(a); // undefined
  var a = 10;
  console.log(a); // 10
}
foo();
// 上面的代码相当于
var a = 2;

function foo() {
  var a; 
  console.log(a); // undefined
  a = 10;
  console.log(a); // 10
}
foo()

预编译发生在函数执行的前一刻做了这些事情:

1.创建AO对象 (Activation Object) 作用域/执行上下文

2.找形参和变量声明,将变量和形参名作为AO属性名,值为undefined

3.将实参值和形参统一

4.在函数体里面找函数声明,值赋予函数体

这一块我迷迷糊糊的理解了,但是记录下来应该会理解的更加透彻

先来一个简单点的

function fn(z){
    console.log(z);
    console.log(a);
    var a=123;
    console.log(a);
    function a(){}
    console.log(a);
    console.log(b);
    var b=function b(){}
    console.log(b);
    function d(){}
}
fn(1);

我们来分析一下,第一步创建AO对象。找形参和变量声明,将变量和形参名作为AO属性名,值为undefined。上面这块代码中fn(1) ==> 1是实参 function(a){...} ==> a是形参

AO{
    z:undefined,
    a:undefined,
    b:undefined,
    d:undefined
}

紧接着实参值和形参统一,在函数体里面找函数声明值赋予函数体,所以AO发生变化

a先是被提升AO值为undefiend,最后又因为函数声明提升值赋予函数体,解释的好像有点绕,多看几遍就差不多了!

AO{
    z:1,
    a:function a(){...},
    b:undefined,
    d:function d(){...}
}

这里有一个注意点,大家会好奇,b为什么会是undefined,大家再看看前面讲的,变量声明提升值为undefined,b只是值赋予一个函数体,但是在预编译环节,它不是一个函数声明,所以值还是undefined!!!

这样预编译就完成了,然后我们来看看代码执行会发生什么?

function fn(z){
    console.log(z); //1
    console.log(a); //AO==>a:function a(){...}
    //拿AO里面的值
    var a=123;
    //a被赋值 所以AO里面的a也会变为123
    console.log(a); //123
    function a(){}
    console.log(a); //123
    //AO里面的值为123
    console.log(b); //undefined
    //AO里面的b为undefiend
    var b=function b(){}
    //前面把b赋值了 所以b的AO也会发生变化
    console.log(b);//function b(){...}
    function d(){}
}
fn(1);

看了这么多,大家差不多应该能够理解。预编译是在代码执行的前一刻执行。代码执行实质是拿预编译的内容。上面的第一个console.log(a);因为没有任何赋值,所以会拿预编译的结果,a是一个函数体。后面一行a被赋值123。所以第二次打印a的结果的时候显示的就是123了。

看完了AO对象,其实GO对象就更好理解了。GO对象同理,指向的是window对象。其实window对象也就是GO对象的意思了,只是少了形参和实参统一的步骤罢了

1.生成了一个GO对象 Global Object

2.找形参和变量声明,将变量和形参名作为AO属性名,值为undefined

3.在函数体里面找函数声明,值赋予函数体

还是来一道题,一起来分析一下!

function test(){
    var a=b=123;
    console.log(window.a); //undefined
    console.log(window.b); //123
}
test();

先生成GO后生成AO。上面这个仔细分析一下a是一个声明变量。而b并没有声明就赋值归GO所有。所以window上找a自然找不到返回undefined,而b返回123。

在来一个小知识点,下面是一个函数,有形参,实参。

function gouge(love){
    console.log(love); // I love GouGe
    arguments[0]=123;
    console.log(love); // 123
}
gouge("I love GouGe");

当实参传入"I love GouGe",这个时候打印输出形参会返回实参的值。因为前面也说过预编译阶段会将实参值和形参统一。紧接着第二次调用前,我们通过rguments[0]将形参改为了123,所以这个时候我们调用他会返回123。

最后的最后再来一道恶心的题,弄懂这道题预编译应该就真的懂了!

a=100;
function demo(e){
    function e(){}
    arguments[0]=2;
    console.log(e);    //2
    if(a){
        var b=123;
        function c(){
            //猪都能做出来
            //if里面定义一个function函数语法不通过
            //以前的浏览器可以
        }
    }
    var c;
    a=10;
    var a;
    console.log(b);    //undefined
    f=123;
    console.log(c);    //undefined
    console.log(a);    //10
}
var a;
demo(1);
console.log(a);    //100
console.log(f);    //123

我还是把GO和AO写出来吧~

GO{
    a:100,
    demo:function(e){...},
    f:123
    
}
AO{
    e:2,
    b:undefined,
    c:undefined,
    a:10
}

切记!按照规则一步步来!a实际上是变量声明,尽管a的赋值在前面,在预编译阶段变量声明还是会提到前面来。还有if语句,在执行if之前a是没有进行赋值的,所以if语句不会往里面走。

大家有不懂的地方欢迎提出一起交流学习,当然文章难免也会出现一些错误,欢迎大家评论区指出~

2019年04月26日
Icefox Theme . 鄂ICP备16001608号-1