概念
JavaScript代码在执行前,引擎会先进行预编译,也称为变量提升。在这个阶段,它会将使用var声明的变量和函数声明提前到其所在的作用域的顶部进行定义(变量赋值留在原地),这就解释了为什么我们可以在声明之前使用变量(值为undefined)或调用函数。
全局作用域下的预编译
当一段JavaScript代码运行时,首先会进入全局作用域。全局下的预编译步骤如下:
1. 创建全局对象(Global Object, GO)
在浏览器中,这就是window对象。
2. 寻找变量声明:
扫描全局代码,找到所有使用var声明的变量,变将这些变量作为属性添加到GO中,其值初始化为undefined
「 注:let和const声明的变量也会被提升,但不会被初始化为undefined,且存在“暂时性死区”,在声明前访问会报错。经典的预编译讨论通常是聚焦于var和function 」
3. 寻找函数声明
扫描全局代码,找到所有函数声明(非函数表达式),并将函数名作为属性添加到GO中,值初始化为指向该函数在内存中的地址(通俗讲,就是函数本身)。
4. 开始执行代码
预编译完成后,代码开始逐行执行。此时,所有的变量和函数都已经存在于GO中。
全局预编译示例
console.log(a); // 输出: undefined
var a = 10;
console.log(a); // 输出: 10
function foo(b) {
console.log(b);
}
foo(100); // 输出: 100
预编译过程(理论上的步骤):
- 创建 GO = { }。
- 找到 var a,在 GO 中创建属性:GO { a: undefined }。
- 找到函数声明 function foo(...),在 GO 中创建属性:GO { a: undefined, foo: function }。
- 开始执行代码。
- 执行 console.log(a),此时 GO 中的 a 是 undefined,所以输出 undefined。
- 执行 a = 10,将 GO 中的 a 赋值为 10。
- 执行 console.log(a),此时 GO 中的 a 是 10,所以输出 10。
- 执行 foo(100),调用函数。此时会为函数 foo 创建一个新的函数作用域,并开始函数自身的预编译过程。
函数作用域下的预编译
每当一个函数被调用时,在函数体代码执行之前,也会发生一个类似的预编译过程。步骤如下:
- 创建活动对象(Activation Object, AO):
也可以理解为函数的局部作用域对象。 - 寻找形参和变量声明:
- 将函数的形参作为属性添加到 AO 中,其值初始化为 undefined。
- 扫描函数体内所有 使用 var 声明的变量,将这些变量名作为属性添加到 AO 中,其值同样初始化为 undefined。
- 注意:如果形参和变量同名,则不会重新创建,直接覆盖值(详见示例)。
- 将实参赋值给形参: 将调用函数时传入的实参赋值给 AO 中对应的形参属性。
- 寻找函数声明: 扫描函数体内所有函数声明,并将函数名作为属性添加到 AO 中,其值初始化为指向该函数。如果函数名和变量或形参同名,则会覆盖掉之前的值。
- 开始执行函数体代码: 函数内部的预编译完成后,开始逐行执行函数体内的代码。
函数预编译示例
function fn(a) {
console.log(a); // 第一步:输出 function a() {}
var a = 123;
console.log(a); // 第二步:输出 123
function a() {}
console.log(a); // 第三步:输出 123
var b = function() {}; // 这是函数表达式,不是函数声明,不会被提升
console.log(b); // 第四步:输出 function() {}
}
fn(1);
让我们一步步分析函数 fn 被调用时的预编译和执行过程:
1. 创建 AO:
AO = { }
2. 找形参和变量声明:
- 形参:a -> AO { a: undefined }
- var 变量:var a(与形参同名,忽略),var b -> AO { a: undefined, b: undefined }
3. 将实参赋值给形参:
实参是 1 -> AO { a: 1, b: undefined }
4. 找函数声明:
找到 function a() {},将 AO 中的 a 覆盖 -> AO { a: , b: undefined }
预编译完成,此时的 AO 内容为:
AO {
a: function a() {},
b: undefined
}
5. 开始执行函数体代码:
- console.log(a); // 从 AO 中查找 a,结果是函数,输出 function a() {}
- a = 123; // 赋值操作,将 AO 中的 a 修改为 123
- console.log(a); // AO 中的 a 现在是 123,输出 123
- function a() {} // 这行在预编译阶段已经处理过了,执行阶段直接跳过
- console.log(a); // AO 中的 a 仍然是 123,输出 123
- b = function() {}; // 赋值操作,将 AO 中的 b 修改为这个匿名函数
- console.log(b); // AO 中的 b 现在是函数,输出 function() {}
下面我们来做点题巩固一下吧
1. 题目一:
function test(a, b){
console.log(a); //1
console.log(b); //function b(){}
console.log(c); //undefined
c = 0;
console.log(c); //0
var c;
a = 3;
b = 2;
console.log(a); //3
console.log(b); //2
function b(){}
console.log(b); //2
}
test(1)
预编译和执行过程:
- 创建AO
AO = {}
- 找形参和变量声明
- 形参:a,b
AO{
a:undefined,
b:undefined
}
- var变量:var c
AO{
a:undefined,
b:undefined,
c:undefined
}
- 将实参赋值给形参
实参是1
AO{
a:1,
b:undefined,
c:undefined
}
- 找函数声明
找到function b(){},将函数体复制给AO的b属性,预编译完成,此时的AO为:
AO{
a:1,
b:function b(){},
c:undefined
}
- 开始执行函数体代码
- console.log(a);//AO中的a是1,输出1
- console.log(b);//AO中的b是函数,输出function b(){}
- console.log(c);//AO中的c是undefined,输出undefined
- c = 0;//赋值操作将AO中c修改为0
AO{
a:1,
b:function b(){},
c:0
}
- console.log(c);//AO中的c是0,输出0
- var c;这行在预编译阶段已经处理过,执行阶段直接跳过
- a = 3;//赋值操作将AO中a修改为3
AO{
a:3,
b:function b(){},
c:0
}
- b = 2;//赋值操作将AO中b修改为2
AO{
a:3,
b:2,
c:0
}
- console.log(a);//AO中的a是3,输出3
- console.log(b);//AO中的b是2,输出2
- function b(){}这行在预编译阶段已经处理过,执行阶段直接跳过
- console.log(b);//AO中的b是2,输出2
执行代码最终输出的结果是:
1
function b(){}
undefined
0
3
2
2
2. 题目二:
function test(a, b){
console.log(a); //function a(){}
console.log(b); //undefined
var b = 234;
console.log(b); //234
a = 123;
console.log(a); //123
function a(){}
var a;
b = function (){}
console.log(a); //123
console.log(b); //function (){}
}
test(1)
预编译和执行过程:
- 创建AO
AO = {}
- 找形参和变量声明
- 形参:a,b
AO{
a:undefined,
b:undefined
}
- var变量:var b,var a,此时AO中已经存在属性b,a就不用再创建了
AO{
a:undefined,
b:undefined
}
- 将实参赋值给形参
实参是1
AO{
a:1,
b:undefined
}
- 找函数声明
找到function a(){},将函数体复制给AO的a属性,预编译完成,此时的AO为:
AO{
a:function a(){},
b:undefined
}
- 开始执行函数体代码
- console.log(a);//AO中的a是函数,输出function a(){}
- console.log(b);//AO中的b是undefined,输出undefined
- var b = 234;//赋值操作将AO中b修改为234
AO{
a:function a(){},
b:234
}
- console.log(b);//AO中的b是234,输出234
- a = 123;//赋值操作将AO中c修改为0
AO{
a:123,
b:234
}
- console.log(a);//AO中的c是123,输出123
- function a(){}这行在预编译阶段已经处理过,执行阶段直接跳过
- var a;这行在预编译阶段已经处理过,执行阶段直接跳过
- b = function b(){};//赋值操作将AO中函数赋值给属性b
AO{
a:123,
b:function (){}
}
- console.log(a);//AO中的a是123,输出123
- console.log(b);//AO中的b是function (){},输出function (){}
执行代码最终输出的结果是:
function a(){}
undefined
234
123
123
function (){}
3. 题目三:
console.log(test); // function test(test){}
function test(test){
console.log(test); //test(){}
var test = 234;
console.log(test); // 234
function test(){}
}
test(1)
var test = 123;
预编译和执行过程:
- 创建GO
GO = {}
- 查找变量声明
GO = {
test: undefined
}
- 查找函数声明
GO = {
test: function test(test){
console.log(test);
var test = 234;
console.log(test);
function test(){}
}
}
- 开始执行代码
- console.log(test); GO中的test是函数,输出function test(){}
- test函数在预编译是已经处理过,直接跳过
- 执行函数test(1),此时为函数test创建一个新的函数作用域,并开始了函数自身的预编译。
- 创建AO
AO = {}
- 找形参和变量声明
- 形参:test
AO{
test:undefined
}
- var变量:var test,此时AO中已经存在属性test就不用再创建了
- 将实参赋值给形参
实参是1
AO{
test:1
}
- 找函数声明
找到function test(){},将函数体复制给AO的test属性,预编译完成,此时的AO为:
AO{
test:function test(){}
}
- 开始执行函数体代码
- console.log(test);//AO中的a是函数,输出function test(){}
- var test = 234;//赋值操作将AO中test修改为234
AO{
test:234
}
- console.log(test);//AO中的test是234,输出234
至此函数作用域的预编译执行完,继续执行全局作用于的代码 var test = 123;
GO = {
test:123
}
执行代码最终输出的结果是:
function test(test){
console.log(test);
var test = 234;
console.log(test);
function test(){}
}
test(){}
234
经过上面练习题,是否已经掌握了解答过程呢,下面的几题就不写详细的解题过程了,直接给出结果,自己去测验一下吧。
4. 题目四:
function test(){
console.log(a);
console.log(b);
if(a){
var b = 100;
}
c = 234;
console.log(c);
}
var a;
test()
a = 10;
console.log(a);
console.log(c);
执行代码最终输出的结果是:
undefined
undefined
234
10
234
5. 题目五:
function bar(){
return foo;
foo = 10;
function foo(){}
var foo = 11;
}
console.log(bar())
执行代码最终输出的结果是:
function foo(){}
6. 题目六:
console.log(bar());
function bar(){
foo = 10;
function bar(){}
var foo = 11;
return foo
}
执行代码最终输出的结果是:
11