我们都明白js在运行中会进行三个步骤,1.整体语法分析(排除基本错误),2.进行预编译,3.解释执行(按照顺序解释一行执行一行)。那么所谓的预编译是什么呢?
先看两个基本例子
console.log(a); var a =123; console.log(a); //undefined //123
test(); function test(){ console.log('a'); } // a
我们可以发现这两个例子均未报错,这就是预编译的作用,他会使得函数声明整体提升(即函数无论写在第几行都会被提升至逻辑的最前面),同时变量声明会提升(即把上题中的var a 提升至最前面)
但光光了解这些并不能解决所有问题,我们看下面的例子
function a(a){ var a =a+1; console.log(a); } var a; a(1); console.log(a);
利用两个提升显然无法解决这个问题,这就要求我们深入的学习预编译,因为它的存在本就是用于解决执行顺序问题,我在之前作用域和作用域链里提到了两个词 Activation Object 和 Global Object (以下简称AO 和GO),我们来分析上述问题。
在执行完js第一步语法分析后,我们进入预编译环节,全局预编译第一步,生成一个GO对象(其实就是window),第二步,找变量声明,并将变量作为GO对象的一个属性名,值为undefined 第三步 找函数声明,并将函数名作为GO对象的属性名,值为函数体 我们会发现,GO对象中a属性的值从undefined转化成了a函数的函数体。也就是说第二个console输出的结果是
ƒ a(a) { a = a + 1; console.log(a); }而局部函数的预编译发生在函数执行前的一刻,第一步我们会创建一个AO对象,第二步找形参和变量声明,同样将它们作为AO对象的属性名,值为undefined 第三步,将实参的值传到AO对象中,作为形参属性的值,第四步在函数体内找函数声明,值赋予函数体。
AO={ AO={ a :undefined; => a:1 } }
a值为1,进行js解释执行后,a值为2,也就是第一个console的结果。
这就是全局的预编译和局部的预编译,熟悉之后我们可以解决一切关于执行顺序的问题。值得注意的是,在局部访问一个变量时,当AO中不存在此属性名,需要往GO中寻找。
function a() { console.log(a); } var a; a(); console.log(a);
// ƒ a() { // console.log(a); // } // ƒ a() { // console.log(a); // }