本文深入解析JavaScript引擎的预解析机制,详细区分变量提升与函数提升的执行差异。通过经典面试题案例,阐明var声明与函数声明在预解析阶段的行为,以及作用域链对最终结果的影响,帮助开发者彻底理解undefined与报错的根源。
JavaScript预解析是前端面试中的高频考点,许多开发者对其理解尚不透彻。与多数传统编程语言不同,JavaScript(ES5语法)具有独特的执行机制,其中预解析环节尤为关键。本文将通过具体示例,带你彻底厘清这一核心概念。
提出问题
为了直观理解预解析的影响,我们先观察两个最基础且容易出错的代码片段,请思考它们的运行结果是否符合你的预期?
坑一
<script>
// 坑一
console.log(num);
var num = 10;
</script>

上述代码中,console.log(num) 的输出结果为 undefined。这看似反直觉,但别急,我们稍后揭晓原因。
坑二
观察这种函数定义方式,你会发现无论函数调用语句写在函数声明之前还是之后,代码均能正常执行。
// 下面这种函数定义方式,函数的调用在函数前后都可以
fn();
function fn(){
console.log(15);
}
//fn();

那么,JavaScript中另一种常见的函数定义方式——函数表达式,是否也具备同样的特性呢?
// 下面这种函数定义方式,函数的调用只能在函数后面
fun();
var fun = function(){
console.log(15);
}
// fun();

当我们将函数调用语句置于函数表达式之前时,控制台会抛出错误。这背后的原因是什么?让我们通过下面的解答来一探究竟。
问题解答
要理解上述两个“坑”产生的根本原因,必须掌握JavaScript引擎的工作流程,即预解析机制。
JavaScript引擎执行JS代码主要分为两个阶段:预解析阶段和代码执行阶段。
- 在预解析阶段,JS引擎会将当前作用域内所有的 var 声明和 function 声明提升至作用域的最顶端。
- 在代码执行阶段,引擎严格按照代码书写的顺序,从上至下逐行执行逻辑。
预解析过程具体包含两个子过程:变量预解析(即变量提升)和函数预解析(即函数提升)。
- 变量提升仅提升变量声明,不提升赋值操作。这意味着声明会被提到作用域顶部,但赋值仍保留在原位。
- 函数提升则是将整个函数声明语句提升至当前作用域的最前面,使得函数可以在声明之前被调用。
理解了上述原理,你是否对之前的疑惑有了初步解答?如果仍感模糊,请通过下面的代码复现来巩固理解。
// // 坑一
// console.log(num);
// var num = 10;
// 相当于执行了以下代码
var num; // 所有的 var 提升到当前作用域的最前面,不提升赋值操作。
console.log(num); //num此时未赋值,所以是undefined。
num = 10;
// // 坑二
// 下面这种函数定义方式,函数的调用在函数前后都可以
// fn();
// function fn(){
// console.log(15);
// }
// 相当于执行下面代码
// 函数提升到当前作用域的最前面
function fn(){
console.log(15);
}
fun();
// 下面这种函数定义方式,函数的调用只能在函数后面
// fun();
// var fun = function(){
// console.log(15);
// }
// 相当于执行了以下代码
var fun; // 所有的 var 提升到当前作用域的最前面,不提升赋值操作。
fun(); // 此时压根没有fun()这个函数,所以会报错
fun = function(){
console.log(15);
}
现在,预解析的过程应该已经清晰可见。
案例练习
案例一
请思考,以下代码的运行结果是多少?
// 案例一
var num=10;
fun();
function fun(){
console.log(num);
var num = 20;
}
// 相当于执行了以下代码
// var num;
// function fun(){
// var num;
// console.log(num); //根据作用域链的就近原则,此时num未赋值,所以是undefined
// num = 20;
// }
// num = 10;
// fun(); // 调用函数

案例二
请思考,以下代码的运行结果是多少?
// 案例二
var num = 10;
function fn(){
console.log(num);
var num = 20;
console.log(num);
}
fn();
// 相当于执行了以下代码
// var num;
// function fn(){
// var num;
// console.log(num); //根据作用域链就近原则,此时num未赋值,所以是undefined
// num = 20;
// console.log(num); // 根据作用域链就近原则,num是20
// }
// num = 10;
// fn(); // 调用函数

案例三
请思考,以下代码的运行结果是多少?
// 案例三
var a = 18;
f1();
function f1(){
var b = 9;
console.log(a);
console.log(b);
var a = '123';
}
// // 相当于执行了以下代码
// var a;
// function f1() {
// var b;
// var a;
// b = 9;
// console.log(a); //根据作用域链就近原则,此时a未赋值,所以是undefined
// console.log(b); // 9
// a = '123';
// }
// a = 18;
// f1();

案例四(经典的面试题)
请思考,以下代码的运行结果是多少?
// 案例四
f1();
console.log(c);
console.log(b);
console.log(a);
function f1(){
var a = b = c = 9;
// 相当于var a = 9; b = 9 ; c = 9 ; 在javascript中不用var申明直接赋值的变量是全局变量,所以b,c是全局变量
// 区别于集体声明 var a = 9,b = 9,c = 9; 等价于var a = 9,var b = 9,var c = 9;
console.log(a);
console.log(b);
console.log(c);
}
// // 相当于执行了以下代码
// function f1(){
// var a;
// b = 9;
// c = 9;
// a = 9;
// console.log(a); // 9
// console.log(b); // 9
// console.log(c); // 9
// }
// f1(); // 函数调用
// console.log(c); // 9
// console.log(b); // 9
// console.log(a); // a是函数里的局部变量,所以函数外没有申明的话会报错

结语
通过本节内容的学习,相信你对JavaScript中的预解析机制已有了扎实的掌握。掌握这一知识点,将使你在应对相关面试问题时更加从容自信。