深入解析JavaScript预解释机制:变量提升与作用域规则

作者:异步漫步

时间:2026-10-22

浏览:308

本文深入解析JavaScript引擎在代码执行前的预解释(预处理)机制。详细阐述var声明与function声明在内存中的不同处理方式,解析作用域链的查找规则,并重点剖析预解释的五大反直觉表现,包括条件语句中的变量提升、赋值表达式与函数表达式的区别、自执行函数行为以及函数声明优先于变量声明的覆盖规则。通过具体代码示例,帮助开发者理解并避免常见的JavaScript陷阱。

文章目录

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作用域其二-预解析/

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考学习价值。如有疑问,欢迎留言交流,感谢大家对本站的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章