javascript预编译

272 阅读7分钟

在JavaScript中存在一种预编译的机制,这也是Java等一些语言中没有的特性,也就正是因为这个预编译的机制,导致了js中变量提升的一些问题,下面这两句话能解决开发当中一部份问题,但不能解决所有问题,还有一些问题是你必须通过学习预编译才能解决的。

  1. 函数声明整体提升
  2. 变量声明提升(*注意是变量声明 )

tip:JS函数的调用永远都是在函数声明下面调用,即使你的调用是写在函数声明之前的,它隐式也是在函数声明下调用的。

预编译什么时候发生

预编译分为全局预编译和局部预编译,全局预编译发生在页面加载完成时执行,而局部预编译发生在函数执行的前一刻。

tip:预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 。只有在解释执行阶段才会进行变量初始化 。

js运行三步曲

  1. 语法分析
  2. 预编译
  3. 解释执行

预编译前奏

imply global暗示全局变量,任何变量,如果变量未经声明就赋值,这些变量就为全局对象所有。一切声明的全局变量和未经声明的变量,全归window所有。

例如:

var a = 123;
window.a = 123;

下面这个函数里面只有一个连等的操作,赋值操作都是自右向左的,而b是未经声明的变量,所以它是归window的,我们可以直接使用window.b去使用它。

function test(){
	// 这里的b是未经声明的变量,所以是归window所有的。
	var a = b = 110;
}

预编译步骤

首先JavaScript的执行过程会先扫描一下整体语法语句,如果存在逻辑错误或者语法错误,那么直接报错,程序停止执行,没有错误的话,开始从上到下解释一行执行一行。

局部预编译的4个步骤:

  1. 创建AO对象(Activation Object)执行期上下文。
  2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
  3. 将实参值和形参统一。
  4. 在函数体里面找函数声明,值赋予函数体。

全局预编译的3个步骤:

  1. 创建GO对象(Global Object)全局对象。
  2. 找变量声明,将变量名作为GO属性名,值为undefined
  3. 查找函数声明,作为GO属性,值赋予函数体

由于全局中没有参数的的概念,所以省去了实参形参相统一这一步。

tip: GO对象是全局预编译,所以它优先于AO对象所创建和执行

巩固基础练习

关于AO对象的例子

// 函数
function fn(a){
        console.log(a);
	// 变量声明+变量赋值(只提升变量声明,不提升变量赋值)
	var a = 123;
	console.log(a);
	// 函数声明
	function a(){};
	console.log(a);
	// 函数表达式
	var b = function(){};
	console.log(b);
	// 函数
	 function d(){};
}
//调用函数
fn(1);

1.预编译第1步: 创建AO对象

AO{ 

}

2.预编译第2步: 找形参和变量声明,将形参名和变量名作为AO对象的属性名

AO{
     a : undefined,
     b : undefined
}

3.预编译第3步: 将实参值和形参统一

AO{
     a : 1,
     b : undefined
}

4.预编译第4步: 在函数体里面找函数声明,值赋予函数体。

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

最后输出结果:

// 函数
function fn(a){
        console.log(a);	 	//根据AO对象中的数据第一个打印的是:fn()
	// 变量声明+变量赋值(只提升变量声明,不提升变量赋值)
	var a = 123;		// 执行到这时,由于变量赋值是不提升的,所以函数被123覆盖了
	console.log(a);		// 123
	// 函数声明
	function a(){};		// 这里被提升上去了,可以忽略
	console.log(a);		// 123
	// 函数表达式
	var b = function(){};
	console.log(b);		// 根据AO对象中的数据:fn()
	// 函数
	 function d(){};
}
//调用函数
fn(1);

函数执行完毕,销毁AO对象。

关于GO对象的例子

global = 100;
function test(){
	console.log(global);	
	var global = 200;
	console.log(global);
	var global = 300;
}
test();
var global;

1.全局预编译第1步: 创建GO对象

GO{

}

2.全局预编译第2步: 找变量声明,将变量名作为GO属性名,值为undefined

GO{
    globalundefined
  
}

3.全局预编译第3步: 查找函数声明,作为GO属性,值赋予函数体

GO{
globalundefined
  test:function (){}
}

4.局部预编译第1步: 创建AO对象

AO{
      
}

5.局部预编译第2步: 找形参和变量声明,将形参名和变量名作为AO对象的属性名

AO{
     global: undefined
}

6.局部预编译第3步: 将实参值和形参统一(此函数没有形参)

AO{
     global: undefined
}

7.局部预编译第4步: 在函数体里面找函数声明,值赋予函数体。(此函数内没有函数声明)

AO{
     global: undefined
}

最的结果:

global = 100;
function test(){
	console.log(global);		// 根据AO对象中的数据:undefined
	var global = 200;		// 执行到这时,200覆盖了undefined
	console.log(global);		// 200
	var global = 300;
}
test();
var global;

tip:关于GO对象和AO对象,它们俩是一个种链式关系,就拿上面的这个例子来说吧,如果在函数体的内部没有定义global变量,这也意味着AO对象中将有这个global这个属性。那如果没有会怎么办?它会去GO对象中寻找,说白了也就是一种就近原则。

例三:

<script>
var a = 1;
console.log(a);

function test(a) {
  console.log(a);
  var a = 123;
  console.log(a);
  function a() {}
  console.log(a);
  var b = function() {}
  console.log(b);
  function d() {}
}

var c = function (){
console.log("I at C function");
}
console.log(c);

test(2);
</script>

结果如下:

Snipaste_2021-07-27_20-15-22.png

分析过程如下:

  1. 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
  2. 第一个脚本文件加载;
  3. 脚本加载完毕后,分析语法是否合法;
  4. 开始预编译 查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,作为GO属性,值赋予函数体;

预编译

//抽象描述
    GO/window = {
        a: undefined,
        c: undefinedtest: function(a) {
            console.log(a);
            var a = 123;
            console.log(a);
            function a() {}
            console.log(a);
            var b = function() {}
            console.log(b);
            function d() {}
        }
    }

解释执行代码(直到执行调用函数test(2)语句)

//抽象描述
    GO/window = {
        a: 1,
        c: function (){
            console.log("I at C function");
        }
        test: function(a) {
            console.log(a);
            var a = 123;
            console.log(a);
            function a() {}
            console.log(a);
            var b = function() {}
            console.log(b);
            function d() {}
        }
    }

执行函数test()之前,发生预编译

  1. 创建AO活动对象(Active Object);
  2. 查找形参和变量声明,值赋予undefined;
  3. 实参值赋给形参;
  4. 查找函数声明,值赋予函数体;

预编译之前面1、2两小步如下:

//抽象描述
    AO = {
        a:undefined,
        b:undefined,
    }

预编译之第3步如下:

//抽象描述
        AO = {
            a:2,
            b:undefined,
        }

预编译之第4步如下:

//抽象描述
    AO = {
        a:function a() {},
        b:undefined
        d:function d() {}
    }

执行test()函数时如下过程变化:

//抽象描述
    AO = {
        a:function a() {},
        b:undefined
        d:function d() {}
    }
    --->
    AO = {
        a:123,
        b:undefined
        d:function d() {}
    }
    --->
    AO = {
        a:123,
        b:function() {}
        d:function d() {}
    }

例四:

 <script>
        console.log(a)
        var a = 100
        console.log(a)
        function a() {
            console.log(111)
        }
        console.log(a)
/*       
         1.js引擎整合所有script标签,生产window对象
         2.查找变量的声明,把a作为window对象的属性名,属性的值为undefined
         3.查找函数的声明,把函数名a作为window对象的属性名,属性值为function,undefined被替换
         4.全局预编译结束,结束后,代码从上到下依次执行。
        如果存在同名的变量和函数,函数的优先级高,因为查找函数声明在第二步,会覆盖掉变量的声明
  */
    </script>

结果:

Snipaste_2021-07-27_20-26-21.png

注意:

预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 ; 只有在解释执行阶段才会进行变量初始化 ;

预编译(函数执行前)

  1. 创建AO对象(Active Object)
  2. 查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为undefined
  3. 实参形参相统一,实参值赋给形参
  4. 查找函数声明,函数名作为AO对象的属性,值为函数引用

预编译(脚本代码块script执行前)

  1. 查找全局变量声明(包括隐式全局变量声明,省略var声明),变量名作全局对象的属性,值为undefined
  2. 查找函数声明,函数名作为全局对象的属性,值为函数引用

预编译小结

  • 函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)
  • 变量 声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)
  • 函数预编译发生在函数执行前一刻。