深入理解JavaScript在浏览器中的执行机制:从变量提升到编译执行

作者:页面旅途

时间:2026-10-30

浏览:398

本文深入解析JavaScript在浏览器中的执行机制,重点阐述变量提升、编译阶段与执行阶段的交互过程。通过分析var、let、const的区别及暂时性死区概念,揭示JavaScript引擎如何先编译生成执行上下文,再逐行执行代码的底层原理,帮助开发者理解函数覆盖、作用域及闭包等核心特性。

文章目录

讨论JavaScript时,执行上下文是一个无法回避的核心概念。只有透彻理解执行上下文,才能更准确地把握JavaScript的运行逻辑,例如变量提升、栈溢出、作用域以及闭包等关键机制。

本文并非专门讲解JavaScript语言基础,因此不会对语言本身做过多的理论铺陈,而是聚焦于JavaScript的执行顺序,深入剖析其运行原理。

首先,我们观察以下代码示例:

showName()
console.log(myname)
var myname = '掘金'
function showName() {
  console.log('function showName run')
}

作为前端开发者,大家通常认为上述代码是同步执行的,因此预期输出结果应该是:

  • 当执行第一行时,由于 showName 方法尚未定义,按照直觉应该会报错。
  • 同理,执行第二行时,因为变量 name 也未定义,预期同样会报错。

然而,实际的执行结果却与我们的预期大相径庭:

变量提升

第1行输出了“function showName run”,第2行输出了“undefined”。这种结果与之前设想的顺序执行逻辑存在显著差异。

上述结果表明,函数或变量可以在定义之前被使用。那么,如果代码中完全未定义这些变量,执行会正常进行吗?可以参考下面这段去除了第三行 myName 定义的代码:

showName()
console.log(myname)
function showName() {
  console.log('function showName run')
}

变量提升

使用了未定义的变量——执行报错

从上述两段代码的执行结果对比中,我们可以归纳出以下三个结论。

  • 在执行过程中,若使用了未声明的变量,JavaScript引擎会抛出错误。
  • 在变量定义之前使用它,不会导致错误,但该变量的值将表现为undefined,而非定义时的实际值。
  • 在函数定义之前使用它,不会导致错误,且函数能够正确执行。

第一个结论易于理解:由于变量未定义,JavaScript在执行代码时无法找到该变量,因此会抛出错误。

然而,第二个和第三个结论往往令人费解。要解答这两个疑问,我们需要引入接下来要介绍的核心概念——变量提升。

变量提升

在正式探讨变量提升之前,我们需要先厘清JavaScript中的两个基本概念:声明和赋值。

var myName = "掘金"

这段代码可以拆解为以下部分来看:

var myName        //声明部分
myName = "掘金"   //赋值部分

变量提升

上述内容说明了变量的声明与赋值过程,接下来我们讨论函数的声明与赋值:

function foo(){
  console.log('foo')
}
var bar = function(){
  console.log('bar')
}

第一个函数foo是一个完整的函数声明,不涉及赋值操作;第二个函数则是先声明变量bar,再将 function(){console.log('bar')} 赋值给bar。为了直观理解,你可以参考下图:

变量提升

通过上述分析,我们了解了变量和函数的声明与赋值机制,接下来我们将深入探讨什么是变量提升。

所谓的变量提升,是指JavaScript代码在执行过程中,JavaScript引擎将变量和函数的声明部分提升到代码开头的“行为”。变量提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined 。

具体变化如下代码所示:

/*
* 变量提升部分
*/
// 把变量 myname提升到开头,
// 同时给myname赋值为undefined
var myname = undefined
// 把函数showName提升到开头
function showName() {
    console.log('function showName run');
}
/*
* 可执行代码部分
*/
showName()
console.log(myname)
// 去掉var声明部分,保留赋值语句
myname = '掘金'

可以通过下图更直观地感受这一过程:

变量提升

从图中可以看出,对原始代码主要进行了两处调整:

  • 第一处是将声明部分都提升到了代码开头,如变量 myName 和函数 showName,并给变量设置默认值 undefined;
  • 第二处是移除原本声明的变量和函数,如 var myName = '掘金' 的语句移除了 var 声明,整个移除 showName 的函数声明。

通过这两步操作,即可实现变量提升的效果。你可以执行这段模拟变量提升的代码,其输出结果与第一段代码应该完全一致。

通过这段模拟的变量提升代码,我们明白了为何可以在定义之前使用变量或函数——因为函数和变量在执行前都已被提升至代码开头。

let和const会变量提升吗?

我们执行以下代码进行尝试:

console.log(myName)
const myName = '掘金'

变量提升

上图的报错为:ReferenceError: Cannot access 'variable' before initialization。这与未定义的报错不同,未定义报错如下

console.log(myName1)
const myName = '掘金'

变量提升

由此可见,用 var 定义的变量可以在声明之前被访问而不报错,但是用 let 或 const 定义的变量却不行。

由 let 或 const 定义的变量在提升时不会默认初始化,因此在声明之前访问会报错:ReferenceError: Cannot access 'variable' before initialization。

然而由 var 定义的变量在提升时会被初始化为默认值 undefined,因此在声明之前访问会得到 undefined。这里面涉及到了一个概念 暂时性死区,let/const 定义的变量被提升却无法正常访问,是因为存在暂时性死区(Temporal Dead Zone)。

JavaScript 代码的执行顺序

通过上述对变量提升的介绍,你可能会误以为变量和函数在物理层面被移动到了代码最前面,正如图中所展示的那样。但实际上,变量和函数声明在代码中的位置并未改变,而是在编译阶段被JavaScript引擎放入到内存中。没错,一段JavaScript代码在被执行前需要经过JavaScript引擎的编译,编译完成后才会被执行,如下图所示:

变量提升

1.编译阶段

编译和变量提升有什么关系呢?我们可以从下面两部分代码来看:

第一部分:变量提升部分的代码。

var myName = undefined
function showName() {
    console.log('function showName run');
}

第二部分:执行部分的代码。

showName()
console.log(myName)
myName = '掘金'

我们可以将JavaScript的执行流程细化,如下图所示:

变量提升

从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

执行上下文是JavaScript执行一段代码时的运行环境,例如调用一个函数,就会进入该函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。

可以用下面的伪代码简单理解环境变量对象:

VariableEnvironment:
     myname -> undefined, 
     showName ->function : {console.log(myname)

接下来我们看一下生成环境变量对象的过程:

showName()
console.log(myName)
var myName = '掘金'
function showName() {
    console.log('function showName run');
}
  • 第1行和第2行,由于这两行代码不是声明操作,因此JavaScript引擎不会做任何处理;
  • 第3行,由于这行是经过var声明的,因此JavaScript引擎将在环境对象中创建一个名为 myName 的属性,并使用 undefined 对其进行初始化;
  • 第4行,JavaScript引擎发现了一个通过 function 定义的函数,因此它将函数定义存储到堆(HEAP)中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置。

这样就生成了变量环境对象。接下来JavaScript引擎会把声明以外的代码编译为字节码,可以类比如下的伪代码:

showName()
console.log(myname)
myname = '掘金'

好了,现在有了执行上下文和可执行代码,那么接下来就到了执行阶段了。

2. 执行阶段

JavaScript引擎开始执行“可执行代码”,按照顺序一行一行地执行。下面我们就来一行一行分析下这个执行过程:

  • 当执行到 showName 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出“函数 showName 被执行”结果。
  • 接下来打印 myName 信息,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在 myName 变量,并且其值为 undefined,所以这时候就输出 undefined。
  • 接下来执行第3行,把“掘金”赋给 myName 变量,赋值后变量环境中的 myname 属性值改变为“掘金”,变量环境如下所示:
VariableEnvironment:
     myname -> "掘金", 
     showName ->function : {console.log(myName)

代码中出现相同的变量或者函数怎么办?

现在你已经知道了,在执行一段JavaScript代码之前,会编译代码,并将代码中的函数和变量保存到执行上下文的变量环境中,那么如果代码中出现了重名的函数或者变量,JavaScript引擎会如何处理?

我们先看下面这样一段代码:

function showName() {
    console.log('沸点');
}
showName();
function showName() {
    console.log('掘金');
}
showName(); 

分析这两次调用打印出来的值是什么吗?

完整执行流程:

  • 首先是编译阶段。遇到了第一个 showName 函数,会将该函数体存放到变量环境中。接下来是第二个 showName 函数,继续存放至变量环境中,但是变量环境中已经存在一个 showName 函数了,此时,第二个 showName 函数会将第一个 showName 函数覆盖掉。这样变量环境中就只存在第二个 showName 函数了。
  • 接下来是执行阶段。先执行第一个 showName 函数,但由于是从变量环境中查找 showName 函数,而变量环境中只保存了第二个 showNam e函数,所以最终调用的是第二个函数,打印的内容是“掘金”。第二次执行showName函数也是走同样的流程,所以输出的结果也是“掘金”。

综上所述,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。

总结

好了,今天就到这里,下面我来简单总结下今天的主要内容:

  • JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

以上就是今天所讲的主要内容,当然,学习这些内容并不是让你掌握一些 JavaScript 小技巧,其主要目的是让你清楚JavaScript的执行机制:先编译,再执行。

以上就是JavaScript在浏览器中的执行机制的详细内容,更多关于JavaScript执行机制的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章