深入理解JavaScript中的预解析:变量提升与执行上下文
本文深入解析JavaScript引擎在执行代码前的预解析机制。详细阐述变量提升与函数声明在内存中的不同处理,剖析全局与局部作用域下的预解析差异,并探讨自执行函数、条件语句及return语句对预解析的影响,帮助开发者规避常见Bug。
引言:JavaScript预解析概述
前言
JavaScript作为解释型语言,其代码执行过程是否仅仅是运行时自上而下逐行解析?
实际现象与相关文档(如《JavaScript权威指南》)表明,JavaScript存在“预解析”机制。掌握这一特性至关重要,否则在实际开发中可能遭遇难以排查的问题甚至程序错误。本文旨在逐步解析这一现象,作为学习总结,若见解有误,欢迎指正。
在ES6标准引入前,使用var声明变量时会发生变量预解析(函数同样存在预解析)。许多初学者常因此困惑,尽管ES6引入了let和const,但鉴于ES6尚未完全普及且大量遗留代码仍遵循ES5甚至ES3标准,理解预解析依然具有现实意义。
变量与函数在内存中的表现
一、变量和函数在内存中的展示
JavaScript的数据类型与其他语言类似,分为基本数据类型和引用数据类型。基本数据类型包括:undefined、null、boolean、String、Number;引用数据类型主要指对象(涵盖{}、[]、/^$/、Date、Function等)。
var num = 24;
var obj = {name:'iceman' , age:24};
function func() {
console.log('hello world');
}
当浏览器加载HTML页面时,首先会创建一个供全局JavaScript代码执行的环境,即全局作用域。
基本数据类型按值进行操作,而引用数据类型按内存地址进行操作。
基于上述原则,相关代码在内存中的模型如下:
内存模型.png
基本类型数据直接存储在栈内存中,而对象存储在堆内存中,变量仅持有指向该对象的地址。例如,obj持有一个地址oxff44,函数func持有一个地址oxff66。
在现有代码基础上继续执行:
console.log(func);
console.log(func());
第一行输出的是整个函数的定义部分(即函数本身):
第一行代码输出结果.png
前文已说明,func存储的是一个地址,该地址指向堆内存中的一块区域,其中保存了函数的定义。
第二行输出的是func函数的返回结果:
第二行代码输出结果.png
由于func函数未显式返回任何值,因此输出undefined。
注意:函数的返回值取决于return语句后跟随的内容;若没有return语句,默认返回值为undefined。
预解析机制详解
二、预解析
理解上述内存模型后,即可更好地掌握预解析机制。所谓预解析,是指在当前作用域中,JavaScript代码执行之前,浏览器会默认将所有使用var声明的变量和function声明的函数进行提前声明或定义。
2.1. 声明和定义
var num = 24;
这行简单的代码实际上包含两个步骤:声明和定义。
- 声明:
var num;告知浏览器在全局作用域中存在一个名为num的变量。若变量仅声明而未赋值,其默认值为undefined。 - 定义:
num = 12;即为变量赋值。
2.2. var声明的变量和function声明的函数在预解析的区别
var声明的变量在预解析阶段仅进行提前声明,而function声明的函数在预解析时既会提前声明,也会同时定义。换言之,var声明与function声明的关键区别在于声明时是否同步完成了定义。
2.3. 预解析只发生在当前的作用域下
程序启动时,仅对window下的变量和函数进行预解析;只有当函数被执行时,才会对其内部的变量和函数进行预解析。
console.log(num);
var num = 24;
console.log(num);
func(100 , 200);
function func(num1 , num2) {
var total = num1 + num2;
console.log(total);
}

输出结果.png
第一次输出num时,由于预解析仅进行了声明而未定义,因此输出undefined;第二次输出num时,已完成定义,故输出24。
由于函数的声明和定义是同步进行的,因此func()虽然在func函数定义之前被调用,但仍能正常调用并输出300。
内存模型.png
作用域链与执行上下文
三、 作用域链
首先理解以下三个概念:
- 函数内部的作用域称为私有作用域,window所在的作用域称为全局作用域;
- 在全局作用域下声明的变量是全局变量;
- 在“私有作用域中声明的变量”和“函数的形参”均属于私有变量;
在私有作用域中执行代码时,若遇到一个变量,首先判断其是否为私有变量。若是,则与外部无关;若不是,则向当前作用域的上级作用域查找,若上级仍无,则继续向上查找,直至window,此过程即为作用域链。
当函数执行时,首先形成一个新的私有作用域,随后按以下步骤执行:
- 若有形参,先给形参赋值;
- 进行私有作用域中的预解析;
- 私有作用域中的代码从上到下执行
函数形成一个新的私有作用域,保护内部私有变量不受外界干扰(外部无法修改内部变量,内部也无法修改外部变量),这便是闭包的概念。
console.log(total);
var total = 0;
function func(num1, num2) {
console.log(total);
var total = num1 + num2;
console.log(total);
}
func(100 , 200);
console.log(total);
执行上述代码时,第一次输出total会输出undefined(因预解析),当执行func(100,200)时,将执行函数体内容。此时func函数形成一个新的私有作用域,按前述步骤:
- 先给形参num1、num2赋值,分别为100、200;
- func中的代码进行预解析;
- 执行func中的代码
因在func函数内进行了预解析,total变量会被预解析。在函数内第一次输出total时,输出undefined;随后为total赋值,第二次输出total时输出300。由于函数体内有var声明的变量total,此处输出的total并非全局作用域中的total。
最后一次输出total时,输出0,这里输出的是全局作用域中的total。
console.log(total);
var total = 0;
function func(num1, num2) {
console.log(total);
total = num1 + num2;
console.log(total);
}
func(100 , 200);
console.log(total);
将代码稍作变形后,func函数体内的total未使用var声明,因此total不是私有变量,会去全局作用域中寻找total。这意味着此处出现的所有total均指向全局作用域下的变量。
全局作用域下var声明的差异
四、 全局作用域下带var和不带var的区别
在全局作用域中,带var声明的变量可进行预解析,因此在赋值前执行不会报错;而不带var声明的变量无法进行预解析,在赋值前执行会报错。
console.log(num1);
var num1 = 12;
console.log(num2);
num2 = 12;

输出结果.png
num2 = 12; 相当于给window增加了一个名为num2的属性,属性值为12;
var num1 = 12; 相当于给全局作用域增加了一个全局变量num1,同时也相当于给window增加了一个名为num的属性,属性值为12;
问题:在私有作用域中出现一个变量,若非私有,则向上一级作用域查找,若无则继续向上查找,直至window。若window中也不存在呢?
获取值:console.log(total); --> 报错 Uncaught ReferenceError: total is not defined
设置值:total= 100; --> 相当于给window增加了一个名为total的属性,属性值为100
function fn() {
// console.log(total); // Uncaught ReferenceError: total is not defined
total = 100;
}
fn();
console.log(total);
注意:JS中,若在不进行特殊处理的情况下,上述代码报错,后续代码将不再执行。
预解析的特殊行为与边界情况
五、 预解析中的一些变态机制
5.1 不管条件是否成立,都要把带var的进行提前的声明
if (!('num' in window)) {
var num = 12;
}
console.log(num); // undefined
JavaScript进行预解析时,会忽略所有if条件,因为在ES6之前并无块级作用域概念。本例中会先将num预解析,并将该变量添加到window中作为其属性。因此 'num' in window 返回true,取反后为false,代码执行不会进入if块,num未被赋值,最终console.log(num)输出为undefined。
5.2 只预解析“=”左边的,右边的是指,不参与预解析
fn(); // -> undefined(); // Uncaught TypeError: fn is not a function
var fn = function () {
console.log('ok');
}
fn(); -> 'ok'
function fn() {
console.log('ok');
}
fn(); -> 'ok'
建议:声明变量时尽量使用var fn = ...的方式。
5.3 自执行函数:定义和执行一起完成
(function (num) {
console.log(num);
})(100);
自执行函数定义中的function在全局作用域下不进行预解析,当代码执行到该位置时,定义和执行同步完成。
补充:其他定义自执行函数的方式
~ function (num) {}(100)
+ function (num) {}(100)
- function (num) {}(100)
! function (num) {}(100)
5.4 return下的代码依然会进行预解析
function fn() {
console.log(num); // -> undefined
return function () {
};
var num = 100;
}
fn();
函数体中return语句后的代码虽不再执行,但仍需进行预解析;return语句中的代码作为返回值,不参与预解析。
5.5 名字已经声明过了,不需要重新的声明,但是需要重新的赋值
var fn = 13;
function fn() {
console.log('ok');
}
fn(); // Uncaught TypeError: fn is not a function
经典题目
fn(); // -> 2
function fn() {console.log(1);}
fn(); // -> 2
var fn = 10; // -> fn = 10
fn(); // -> 10() Uncaught TypeError: fn is not a function
function fn() {console.log(2);}
fn();
总结
以上就是关于JavaScript中预解析的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
