我的变量去哪了?JS 作用域入门指南

在 JavaScript 的学习过程中,作用域变量提升 是两个绕不开的核心概念。它们不仅影响着代码的执行逻辑,也常常成为初学者"踩坑"的重灾区。本文将结合几段典型代码,从实际运行结果出发,梳理 JS 中作用域的演变过程,重点解释 var 的缺陷let/const 的改进 ,以及现代 JS 引擎如何通过执行上下文统一处理这两类变量声明。


一、变量提升:JS 的"历史包袱"

先看这段代码(1.js):

javascript 复制代码
showName() // ✅ 正常执行
console.log(myname) // undefined

var myname = '路明非'

function showName(){
  console.log('函数showName 执行了')
}

这里体现了两个关键现象:

  • 函数声明提升showName 不仅声明被提升,函数体也被提升,因此可以在定义前调用。
  • 变量提升(仅声明)var myname 的声明被提升到顶部,但赋值仍在原位置执行,所以首次 console.log 输出 undefined

这就是经典的 hoisting(变量提升) 机制。它源于 JS 引擎的两阶段执行模型:编译阶段 收集声明,执行阶段进行赋值和调用。

⚠️ 变量提升虽解决了早期 JS 的作用域问题,但也带来了不符合直觉的行为,被视为语言设计上的缺陷。


二、作用域链:全局 vs 局部

2.js 中:

javascript 复制代码
var globalVar = '我是全局变量'

function myFunction(){
  var localVar = '我是局部变量'
  console.log(globalVar) // ✅ 打印全局变量
  console.log(localVar)  // ✅ 打印局部变量
}

myFunction()
console.log(globalVar) // ✅
console.log(localVar)  // ❌ ReferenceError

这展示了 作用域链 的查找规则:

  • 函数内部优先查找局部作用域
  • 若未找到,则沿作用域链向上查找至全局作用域
  • 全局无法访问函数内部的局部变量

这是 JS 作用域的基本规则,也是封装和避免命名冲突的基础。


三、var 的致命伤:不支持块级作用域

来看 3.js

javascript 复制代码
var name = '刘锦苗'

function showName(){
  console.log(name) // undefined
  if(false){
    var name = '大厂的苗子'
  }
  console.log(name) // undefined
}

尽管 if(false) 块永远不会执行,但 var name 仍被提升到函数作用域顶部 ,导致函数内 name 被初始化为 undefined。这是因为 var 不支持块级作用域,其声明会被提升到最近的函数或全局作用域。

对比 4.js 使用 let

javascript 复制代码
var name = '刘锦苗'

function showName() {
  console.log(name) // '刘锦苗'
  if (false) {
    let name = '大厂的苗子' // ❌ 不会影响外层
  }
}

由于 let 具有块级作用域if 内的 name 仅在该块中有效,不会污染函数作用域,因此外层仍能正确访问全局变量。


四、let/const 如何解决提升问题?

8.js 展示了一个关键特性:

ini 复制代码
let name = '刘锦苗'

{
  console.log(name) // ❌ ReferenceError: Cannot access 'name' before initialization
  let name = '大厂的苗子'
}

这里报错并非因为变量未声明,而是进入了 暂时性死区(Temporal Dead Zone, TDZ)
let/const 虽然也会"提升",但不会像 var 那样初始化为 undefined,而是在声明前处于不可访问状态。

这正是 ES6 对变量提升缺陷的修正:提升存在,但禁止提前访问


五、执行上下文视角:变量环境 vs 词法环境

现代 JS 引擎(如 V8)通过 执行上下文(Execution Context) 统一管理变量:

  • 变量环境 :存放 var 声明的变量。
  • 词法环境 :存放 let/const 声明的变量,并支持块级作用域栈结构

7.js 为例:

javascript 复制代码
function foo(){
  var a = 1
  let b = 2
  {
    let b = 3  // 新的 b,与外层无关
    var c = 4
    let d = 5
    console.log(a) // 1(从变量环境中找到)
    console.log(b) // 3(当前块级作用域栈顶)
  }
  console.log(b) // 2(块级作用域出栈,恢复外层 b)
  console.log(c) // 4(var 提升到函数作用域)
  console.log(d) // ❌ ReferenceError(d 已随块级作用域销毁)
}

这里的关键在于:

  • let 在块级作用域中创建独立的绑定,块执行完后自动出栈销毁;
  • var 无视块级作用域,始终属于函数或全局作用域;
  • 引擎通过词法环境的栈结构实现了对块级作用域的支持。

六、为什么早期 JS 要这样设计?

JavaScript 最初是"KPI 项目" ,设计周期极短,目标只是给网页加点动态效果。为了快速实现,设计者选择了最简单的方案:

  • 不引入复杂的块级作用域;
  • 用"变量提升"统一处理作用域问题;
  • 用函数模拟"类",规避面向对象的复杂性。

这种设计在当时够用,但随着 JS 应用复杂度飙升,var 的缺陷日益凸显------变量覆盖、生命周期混乱、难以调试。

ES6 引入 let/const 和块级作用域,正是对这一历史问题的修复。


结语:拥抱 let/const,理解执行上下文

如今,我们应优先使用 letconst ,避免 var 带来的陷阱。同时,理解 JS 引擎如何通过 变量环境 + 词法环境 的双轨机制,兼容新旧语法,是深入掌握作用域的关键。

JavaScript 的演进告诉我们:好的语言设计,既要向前兼容,也要勇于修正过去的错误

通过这几段小代码,我们不仅看到了变量提升的"坑",更见证了 JS 如何在保持灵活性的同时,逐步走向严谨与规范。希望这篇文章能帮你理清思路,在掘金社区分享你的成长!

相关推荐
小皮虾43 分钟前
告别服务器!小程序纯前端“图片转 PDF”工具,隐私安全又高效
前端·javascript·微信小程序
倚栏听风雨1 小时前
TypeScript 中,Promise
前端
AAA简单玩转程序设计1 小时前
JW进阶小技巧:告别小白,优雅拿捏基础操作
javascript
影i1 小时前
Vue 3 踩坑实录:如何优雅地把“上古”第三方插件关进 Iframe 小黑屋
前端
小明记账簿_微信小程序1 小时前
vue项目中使用echarts做词云图
前端
浪浪山_大橙子1 小时前
Trae SOLO 生成 TensorFlow.js 手势抓取物品太牛了 程序员可以退下了
前端·javascript
出征1 小时前
Pnpm的进化进程
前端
屿小夏1 小时前
openGauss020-openGauss 向量数据库深度解析:从存储到AI的全栈优化
前端
Y***98512 小时前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端