本文深入解析JavaScript引擎在代码执行前的预解释(预处理)机制。详细阐述var声明与function声明在内存中的不同处理方式,解析作用域链的查找规则,并重点剖析预解释的五大反直觉表现,包括条件语句中的变量提升、赋值表达式与函数表达式的区别、自执行函数行为以及函数声明优先于变量声明的覆盖规则。通过具体代码示例,帮助开发者理解并避免常见的JavaScript陷阱。
JavaScript预解释机制概述
前言
JavaScript属于解释型语言,其代码运行需经历两个核心阶段。
- 编译阶段:即通常所说的JavaScript预解释(或预处理)阶段,此阶段解释器负责将源代码转换为字节码。
- 执行阶段:在编译完成后,解释器利用执行环境将字节码转化为机器码,并按代码顺序自上而下执行。
本文重点探讨预解释机制,相关流程框架如下:

预解释的基本概念与声明定义区别
一、预解释的基本概念
在JavaScript中,涉及var和function关键字的代码需要进行预解释处理。
所谓预解释,是指在JavaScript代码正式执行前,解释器会预先扫描代码,将所有使用var声明的变量和使用function声明的函数在内存中进行声明或定义预留。
简而言之,预解释是浏览器在执行代码前,默认将所有带var和function的代码进行提前声明或定义的过程。
1. 理解声明与定义的区别
声明(declare):如var num;=>所示,告知浏览器在全局作用域中存在一个名为num的变量。若变量仅声明而未赋值,其默认值为undefined。
定义(defined):如num=12;=>所示,为变量赋予具体的值。
2. var与function在预解释中的差异
对于var =>关键字,在预解释阶段仅执行提前声明操作。
对于function =>关键字,在预解释阶段同时完成提前声明和定义操作。
3. 预解释的作用域限制
预解释仅在当前作用域内生效。例如,全局预解释仅针对window对象进行;只有当函数被执行时,才会对该函数内部的作用域进行预解释。
作用域链与变量查找规则
二、作用域链机制
1. 私有变量与全局变量的区分
1)在全局作用域下声明(预解释阶段)的变量被视为全局变量。
2)只有函数执行时才会创建私有作用域;诸如for(){}、if(){}和switch(){}等结构不会创建私有作用域。
3)在“私有作用域中通过var声明的变量”以及“函数的形参”均属于私有变量。在私有作用域中执行代码时,若遇到变量,首先判断其是否为私有变量。若是,则与外部无关;若不是,则沿当前作用域的上级作用域逐级向上查找,直至找到window对象,这一查找过程即为作用域链。
以下示例用于区分私有变量与全局变量:
//=>变量提升:var a;var b;var c;test=AAAFFF111;
var a=10,b=11,c=12;
function test(a){
//=>私有作用域:a=10 var b;
a=1;//=>私有变量a=1
var b=2;//=>私有变量b=2
c=3;//=>全局变量c=3
}
test(10);
console.log(a);//10
console.log(b);//11
console.log(c);//3
判断私有变量的标准是:是否在函数内部通过var声明的变量以及函数的形参。在本题的test函数中,参数a和通过var定义的变量b均为私有变量。
2. 函数传参机制
当函数执行时,首先会创建一个新的私有作用域,随后按以下步骤执行:
1)若存在形参,先为形参赋值。
2)对该私有作用域进行预解释。
3)自上而下执行私有作用域中的代码。
参考以下例题进行理解:
var total=0;
function fn(num1,num2){
console.log(total);//->undefined 外面修改不了私有的
var total=num1 +num2;
console.log(total);//->300
}
fn(100,200);
console.log(total);//->0 私有的也修改不了外面的
3. JavaScript内存分类
栈内存(Stack):用于提供JavaScript代码执行的环境,即作用域(包括全局作用域和私有作用域)。
堆内存(Heap):用于存储引用数据类型的值。对象存储属性名和属性值,函数存储代码字符串。
全局作用域下var与无var的差异
三、全局作用域下带var与不带var的区别
首先观察以下两个示例:
//例题1
console.log(num);//->undefined
var num=12;
//例题2
console.log(num2);//->Uncaught ReferenceError:num2 is not defined
num2=12;//不能预解释
当看到var num=12时,可能误以为这只是一个声明。但实际上,JavaScript将其解析为两条声明语句:var num;和num=12;。第一个定义声明在预解释阶段进行,第二个赋值声明保留在执行阶段。num2=12 相当于为window对象增加了一个名为num2的属性,值为12;而var num=12 首先相当于为全局作用域增加了一个全局变量num,同时也相当于为window对象增加了一个名为num2的属性,值为12。两者的最大区别在于:带var的语句可以进行预解释,因此在赋值前执行不会报错;而不带var的语句无法进行预解释,若在赋值前执行则会报错。
接下来通过具体示例说明:
//例题1
var total=0;
function fn(){
console.log(total);//undefined
var total=100;
}
fn();
console.log(total);//0
//例题2
var total=0;
function fn(){
console.log(total);//0
total=100;
}
fn();
console.log(total);//100
在例题1中,带var的变量在私有作用域内可被预解释,因此第一个console输出的值为undefined。若私有作用域中遇到的变量不是私有的,则向上级作用域查找,若无则继续向上直至window。在例题2中,不带var的变量非私有,因此会向上级查找。
预解释的五大反直觉表现
四、预解释的五大反直觉表现
1. 无论条件是否成立,预解释阶段都会将带var的变量提前声明。
请参考以下例题:
if(!("num" in window)){
var num=12;//这句话会被提到大括号之外的全局作用域:var num;->window.num;
}
console.log(num);//undefined
2. 预解释仅处理“=”左边的部分,右边的值不参与预解释。
请参考以下例题:
fn();//报错
var fn=function (){ //window下的预解释:var fn;
console.log("ok");
};
3. 自执行函数:定义与执行同步完成。
自执行函数中的function在全局作用域下不进行预解释,当代码执行到该位置时,定义与执行同时完成。常见形式包括:
(function(num){})(10);
~function(num){}(10);
+function(num){}(10);
-function(num){}(10);
!function(num){}(10);
4. 函数体中return语句下方的代码虽不再执行,但仍需进行预解释;return后跟随的返回值不参与预解释。
function fn(){
//预解释:var num;
console.log(num);//->undefined
return function(){};
var num=100;
}
5. 函数声明和变量声明均会被提升。但需注意一个细节(常见于存在多个“重复”声明的代码中):函数声明会优先于变量声明被提升。在预解释阶段,若名称已声明,则无需重新声明,但需重新赋值。 首先观察两个简单示例:
//例题1
function a() {}
var a
console.log(typeof a)//'function'
//例题2
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c(2)//Uncaught TypeError: c is not a function
当遇到函数声明和变量声明同时被提升的情况时,函数声明的优先级较高,最终变量声明会被函数声明覆盖,但变量仍可重新赋值。因此,上述示例可等价理解为:
function c(c) {
console.log(c)
var c = 3
}
c = 1
c(2)
接下来分析两道较为复杂的题目:
//例题3
fn();
function fn(){console.log(1);};
fn();
var fn=10;
fn();
function fn(){console.log(2);};
fn();
1. 初始预解释时,函数声明与赋值同时发生,fn即为function fn(){console.log(1);};。遇到var fn=10;时不会重新声明,但遇到function fn(){console.log(2);}时会重新赋值,因此初始fn()的值为2。
2. 再次执行fn();时,值保持不变,仍为2。
3. 将fn重新赋值为10,因此运行fn()时引发报错,后续语句不再执行。
//例题4
alert(a);
a();
var a=3;
function a(){
alert(10)
}
alert(a);
a=6;
a()
1. 函数声明优先于变量声明,预解释时函数声明与赋值同步进行,a即为function a(){alert(10)} 。后续遇到var a=3时,无需重复声明,因此先弹出function a(){alert(10)}。
2. 执行a() ,调用函数后弹出10。
3. 接着执行var a=3; ,因此alert(a)显示为3。
4. 由于a已不再是函数,继续执行至a()时引发报错。
参考文章
- JavaScript中的预解析
- http://heartwalker.cc/2016/04/04/js作用域其二-预解析/
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考学习价值。如有疑问,欢迎留言交流,感谢大家对本站的支持。