深入理解JavaScript:变量提升、执行上下文与作用域链原理
本文深入解析JavaScript引擎的执行机制,从变量提升和编译阶段入手,详细阐述执行上下文中变量环境与词法环境的区别。通过调用栈分析栈溢出原理,并探讨块级作用域、作用域链及闭包的实现方式,最后解析this在不同执行上下文中的指向规则。
变量提升机制:JavaScript执行顺序解析
变量提升定义:JavaScript代码执行期间,JavaScript引擎将变量与函数声明移至代码起始处的行为(提升后变量以undefined为默认值)
callName();
function callName() {
console.log('callName Done!');
}
console.log(personName);
var personName = 'james';
//变量提升后 类似以下代码
function callName() {
console.log('callName Done!');
};
var personName = undefined;
callName();//callName已声明 所以正常输出calName Done!
console.log(personName);//undefined
personName = 'james';
//代码所作改变:
1.将声明的变量和函数移到了代码顶部
2.去除变量的var 声明
JavaScript执行流程解析:部分观点认为变量提升即声明移至代码最前,此说法有误。变量与函数在源码中的位置固定不变,所谓提升实为编译阶段由JavaScript引擎处理并载入内存的结果(即js代码执行前需经引擎编译,随后进入执行阶段)流程示意如下

编译阶段实现变量提升的具体机制如下,仍以示例代码进行分析
第一部分:涉及变量提升的代码片段
function callName() {
console.log('callName Done!')
}
var personName = undefined;
第二部分:代码执行阶段代码
callName();
console.log(personName);
personName = 'james'
执行流程示意图如下

编译完成后,生成执行上下文与可执行代码两部分内容
执行上下文:JavaScript代码运行时的环境(如调用函数时进入该函数的执行上下文,确定函数执行期间的this、变量、对象等)。执行上下文包含变量环境(Variable Environment)与词法环境(Lexical Environment),其中变量环境保存变量提升内容,例如上述myName及callName
既然变量环境存储这些提升后的变量,其对象如何生成?仍以上述代码为例说明
callName();
function callName() {
console.log('callName Done!');
}
console.log(personName);
var personName = 'james';
- 第一、三行非变量声明,JavaScript引擎不进行特殊处理
- 第二行检测到function定义的函数,将函数定义存储于堆中,并在变量环境中创建
callName属性,将该属性指向堆中函数的位置 - 第四行检测到
var定义,在变量环境中创建personName属性,并使用undefined进行初始化
经上述步骤,变量环境对象生成完毕。此时已具备执行上下文与可执行代码,接下来进入代码执行阶段
代码执行阶段详解
众所周知,js代码按顺序逐行从上往下执行,以下仍使用前述示例进行分析
- 执行至
callName()时,JavaScript引擎在变量环境中查找该函数。因变量环境中存在该函数引用,引擎开始执行该函数并输出"callName Done!" - 接着执行至
console.log(personName); 引擎在变量环境中找到personName变量,但此时其值为undefined,于是输出undefined - 继续执行至
var personName = 'james'这一行,在变量环境中找到personName,并将其值修改为james
以上即为一段代码的编译与执行全流程。相信阅读至此,您对JavaScript引擎如何执行代码已有了更深刻的理解
Q:若代码中出现同名变量或函数如何处理?
A:首先处于编译阶段,若遇到同名变量或函数,在变量环境中后出现的同名变量或函数将覆盖前者,故最终仅保留一个定义
function func() {
console.log('我是第一个定义的')
}
func();
function func() {
console.log('我是将你覆盖掉的')
}
func();
//输出两次"我是将你覆盖掉的"
调用栈:栈溢出的原理分析
您在日常开发中是否遇到过此类报错

根据报错信息可知出现了栈溢出问题,那么什么是栈溢出?为何会发生栈溢出?
Q1:什么是栈?
A1:一种后进先出(LIFO)的数据结构
Q2:什么是调用栈?
A2:代码中通常包含大量函数,且存在函数调用其他函数的情况。调用栈是用于管理调用关系的一种数据结构
当在函数中调用另一个函数(如调用自身的递归),若处理不当,极易产生栈溢出,例如下面这段代码
function stackOverflow(n) {
if(n == 1) return 1;
return stackOverflow(n - 2);
}
stackOverflow(10000);//栈溢出
既然明确了调用栈与栈溢出的概念,那么在代码执行过程中调用栈如何工作?我们通过以下示例进行说明
var personName = 'james';
function findName(name, address) {
return name + address;
}
function findOneDetail (name, adress) {
var tel = '110';
detail = findName(name, address);
return personName + detail + tel
};
findOneDetail('james', 'Lakers')
可以看到,我们在findOneDetail中调用了findName函数,那么调用栈的变化过程如下
第一步:创建全局上下文,并将其压入栈底

接下来开始执行personName = 'james'的操作,将变量环境中的personName设置为james
第二步:执行findOneDetail函数,此时JavaScript会为其创建一个执行上下文,最后将该函数的执行上下文压入栈中

接下来执行完tel = ‘110'后,将变量环境中的tel设置为110
第三步:当执行detail = findName()时,会为findName创建执行上下文并压入栈中

接下来执行完findName函数后,将其执行上下文弹出调用栈,随后再弹出findOneDetail的执行上下文以及全局执行上下文,至此整个JavaScript的执行流程结束
因此,调用栈是JavaScript引擎追踪函数执行的机制。当一次有多个函数被调用时,通过调用栈即可追踪到哪个函数正在被执行以及各函数之间的调用关系
如何有效利用调用栈
1.使用浏览器查看调用栈信息
点击Source面板并打上断点刷新页面后,即可在Call Stack中查到调用栈信息(也可通过代码中输入console.track()进行查看)

2.注意防范栈溢出
编写递归代码时,极易发生栈溢出,可通过尾调用优化来避免栈溢出问题
块级作用域:var、let以及const的使用差异
作用域概念解析
作用域指程序中定义变量的区域,该位置决定了变量的生命周期。通俗而言,作用域即为变量与函数的可访问范围,它控制着变量和函数的可见性及生命周期
众所周知,使用var会产生变量提升,而变量提升会引发诸多问题,如变量覆盖、本应销毁的变量依旧存在等。ES6引入了let和const两种声明方式,使js具备块级作用域。那么let和const如何实现块级作用域?其实关键在于理解执行上下文
我们知道,JavaScript引擎在编译阶段,会将使用var定义的变量以及function定义的函数声明在对应执行上下文中的变量环境内创建相应属性。但我们发现执行上下文中还有一个词法环境对象未被充分利用,其实,词法环境对象便是关键所在。我们仍通过举例来说明
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
- 第一步:执行并创建上下文

- 函数内部通过var声明的变量,在编译阶段全都被存放至变量环境中
- 通过let声明的变量,在编译阶段会被存放至词法环境(Lexical Environment)中
- 在函数的作用域内部,通过let声明的变量并未被存放至词法环境中
- 接下来,第二步继续执行代码。当执行到代码块内部时,变量环境中a的值已被设置为1,词法环境中b的值已被设置为2
此时函数的执行上下文如下图所示:

可以看到,当进入函数的作用域块时,作用域块中通过let声明的变量,会被放入词法环境中的一个独立区域。该区域并不影响作用域块外部的变量(例如声明了b = undefined 但不影响外部的b = 2)
实际上,在词法作用域内部,维护着一个小型的栈结构。栈底为函数最外层的变量,进入一个作用域块后,便会将过海作用域内部耳朵变量压到栈顶。当作用域执行完毕后,就会弹出(通过let和const声明的变量)

当作用域块执行完毕后,其内部定义的变量便会从词法作用域的栈顶弹出

小结
块级作用域是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现。通过这两者的结合,JavaScript引擎也就同时支持了变量提升和块级作用域了。
作用域链和闭包机制
在开始作用域链和闭包的学习之前,我们先来看下这部分代码
function callName() {
console.log(personName);
}
function findName() {
var personName = 'james';
callName();
}
var personName = 'curry';
findName();//curry
//你是否以为输出james 猜想callName不是在findName中调用的吗 那findName中已经定义了personName = 'james' 那为什么是输出外面的curry呢 这其实是和作用域链有关的
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,称之为outer
当代码使用一个变量时,会先从当前执行上下文中寻找该变量。如果找不到,就会向outer指向的执行上下文查找

可以看到callName和findName的outer都是指向全局上下文的。所以当在callName中找不到personName的时候,会去全局找,而不是调用callName的findName中找,所以输出的是curry而不是james
作用域链是由词法作用域决定的
词法作用域指作用域由代码中函数声明的位置来决定,因此词法作用域是静态的作用域。通过它即可预测代码在执行过程中如何查找表示符
因此词法作用域是在代码阶段就决定好的,和函数怎么调用的没有关系
块级作用域中的变量查找规则
我们来看下下面这个例子
function bar() {
var myName = " 极客世界 "
let test1 = 100
if (1) {
let myName = "Chrome 浏览器 "
console.log(test)
}
}
function foo() {
var myName = " 极客邦 "
let test = 2
{
let test = 3
bar()
}
}
var myName = " 极客时间 "
let myAge = 10
let test = 1
foo()
我们知道,如果是let或者const定义的,就会储存在词法环境中,所以寻找也是从该执行上下文的词法环境找。如果找不到,就去变量环境,还是找不到则去outer指向的执行上下文寻找,如下图

闭包机制详解
JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包
举个例子
function foo() {
var myName = " 极客时间 "
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName(" 极客邦 ")
bar.getName()
console.log(bar.getName())
首先我们看看当执行到 foo 函数内部的return innerBar这行代码时调用栈的情况,你可以参考下图:

从上面的代码可以看出,innerBar是一个对象,包含了 getName和setName的两个方法。这两个方法都是内部定义的,且都引用了函数内部的变量
根据词法作用域的规则 getName和setName总是可以访问到外部函数foo中的变量。所以当foo执行结束时,getName和setName依然可以以后使用变量myName和test,如下图所示

可以看出,虽然foo从栈顶弹出,但是变量依然存在内存中。这个时候,除了setName和getName,其他任何地方都不能访问到这两个变量,所以形成了闭包
那如何使用这些闭包呢?可以通过bar来使用。当调用了bar.seyName时,如下图

可以使用chrome的Clourse查看闭包情况
闭包怎么回收
通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭。但如果这个闭包以后不再使用的话,就会造成内存泄漏
如果引用闭包的函数是各局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收的时候,判断闭包这块内容不再被使用了,就会回收
所以在使用闭包的时候,请记住一个原则:如果该闭包一直使用,可以作为全局变量而存在;如果使用频率不高且占内存,考虑改成局部变量
小练
var per = {
name: 'curry';
callName: function() {
console.log(name);
}
}
function askName(){
let name = 'davic';
return per.callName
}
let name = 'james';
let _callName = askName()
_callName();
per.callName();
//打印两次james
//只需要确定好调用栈就好 调用了askName()后 返回的是per.callName 后续就和askName没关系了(出栈) 所以结果就是调用了两次per.callName 根据词法作用域规则 结果都是james 也不会形成闭包
this:从执行上下文分析this
相信大家都有被this折磨的时候,而this确实也是比较难理解和令人头疼的问题。接下来我将从执行上下文的角度来分析JavaScript中的this,这里先抛出结论:this是和执行上下文绑定的,每个执行上下文都有一个this
接下来,我将带大家一起理清全局执行上下文的this和函数执行上下文的this、
全局执行上下文的this
全局执行上下文的this和作用域链的最底端一样,都是指向window对象
函数执行上下文的this
我们通过一个例子来看一下
function func() {
console.log(this)//window对象
}
func();
默认情况下调用一个函数,其执行上下文的this也是指向window对象
那如何改变执行上下文的this值呢?可以通过apply call 和bind实现,这里讲下如何使用call来改变
1.通过call
let per = {
name: 'james',
address: 'Lakers'
}
function callName() {
this.name = 'curry'
}
callName.call(per);
console.log(per)//name: 'curry', address: 'Lakers'
可以看到这里this的指向已经改变了
2.通过对象调用
var person = {
name: 'james';
callName: function() {
console.log(this.name)
}
}
person.callName();//james
使用对象来调用其内部方法,该方法的this指向对象本身的
person.callName() === person.callName.call(person)
这个时候我们如果讲对象赋给另一个全局变量 this又会怎样变化呢
var person = {
name: 'james';
callName: function() {
this.name = 'curry';
console.log(this.name);
}
}
var per1 = person;//this又指向window
- 在全局环境中调用一个函数,函数内部的
this指向全局变量window - 通过一个对象调用内部的方法,该方法的
this指向对象本身
3.通过构造函数设置
当使用new关键字构建好了一个新的对象,构造函数的this其实就是对象本身
this的缺陷以及应对方案
1.嵌套函数的this不会从外层函数中继承
var person = {
name: 'james',
callName: function() {
console.log(this);//指向person
function innerFunc() {
console.log(this)//指向window
}
innerFunc()
}
}
person.callName();
//如何解决
1.使用一个变量保存
let _this = this //保存指向person的this
2.使用箭头函数
() => {
console.log(this)//箭头函数不会创建其自身的执行上下文 所以箭头函数中的this指向外部函数
}
2.普通函数中的this指向全局对象window
在默认情况下调用一个函数,其指向上下文的this默认就是指向全局对象window
总结
相信看到这里,大家对于作用域、作用域链、执行上下文和this都有了更深的理解。笔者后期还会更新更多关于浏览器的原理和实践,感兴趣的小伙伴可以点波关注一起学习。文中错误之处请在评论区指出!
以上就是JS作用域作用链及this使用原理详解的详细内容,更多关于JS作用域作用链this的资料请关注本站其它相关文章!
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
