🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥
前言:为什么 JavaScript 如此“奇怪”?
如果你是从其他编程语言(比如 Python、Java 或 C++)转到 JavaScript 的,你可能会对它的某些行为感到困惑:
console.log(name); // 输出:undefined,而不是报错!
var name = "Alice";
{
let age = 25;
console.log(age); // 输出:25
}
console.log(age); // 报错:age is not defined
- 为什么
var
变量可以在声明前访问? - 为什么
let
和const
又不行? - 为什么
{}
块能影响变量的作用域?
这些现象背后,是 JavaScript 独特的 编译与执行机制,以及 作用域管理方式。在传统语言中,变量通常需要先声明再使用,而 JavaScript 的 var
却允许“先使用后声明”,这源于它的 变量提升(Hoisting) 特性。而 let
和 const
的出现,则修复了 var
的缺陷,引入了更严格的 块级作用域(Block Scope) 和 暂时性死区(TDZ)。
本章将深入剖析:
✅ JavaScript 代码的执行流程(编译 vs. 执行阶段)
✅ 变量提升的本质(var
与函数声明的特殊行为)
✅ let
和 const
如何避免变量提升问题
✅ 作用域链与闭包的底层机制
无论你是 JavaScript 新手,还是想彻底理解它的运行原理,这篇文章都会让你豁然开朗!
🚀 现在,让我们开始探索 JavaScript 的独特世界!
First:JavaScript代码是如何跑起来的
想要了解这些现象的本质,我们第一个需要了解的是,JavaScript的代码是怎么跑的,如何运行的,究竟是什么神奇的妙妙♂工具能让它的规则如此灵动~
JavaScript代码运行的基本过程:
解析阶段
JavaScript代码在运行时的第一阶段,在这一阶段中浏览器的引擎会进行解析(Phrasing),在这一阶段会进行词法分析和语法分析。
词法分析:会将代码字符串拆分成有意义的“单词”或“符号”,称为 Token
例如:let x = 5 + 3;
会被拆分成:[let, x, =, 5, +, 3, ;]
语法分析:根据 JavaScript 的语法规则,将这些 Token 组织成一个树状结构,称为 抽象语法树 (Abstract Syntax Tree - AST)
- AST 代表了代码的结构和逻辑关系。
- 例如:
let x = 5 + 3;
的 AST 会表示:声明一个变量x
,它的值是一个加法表达式(操作数是 5 和 3)
此阶段的重点:
- 检查代码是否有语法错误 (Syntax Errors)。如果代码写得不合语法规则(比如缺少括号、错误的关键字),解析阶段就会失败并报错。
- 只关心代码的结构和形式,不关心变量具体代表什么值、函数具体做什么操作。
- 输出:AST (抽象语法树) 。这是代码的“结构化蓝图”。
编译阶段
编译阶段会 静态分析 作用域关系(但不创建运行时词法环境):
-
确定变量和函数的作用域归属:
- 识别全局作用域、函数作用域、块级作用域(
let/const
)。 - 标记变量声明(
var
、let
、const
)和函数声明(function
)的作用域。
- 识别全局作用域、函数作用域、块级作用域(
-
处理变量提升(Hoisting) :
-
var
和function
声明会被记录到作用域顶部(但未赋值)。 -
let/const
也会被记录,但不会提前绑定(形成暂时性死区)。
-
-
建立作用域链的静态结构:
- 确定嵌套作用域的引用关系(闭包的基础)。
什么是作用域?
看到上面编译阶段的小伙伴们可能会有疑问:什么叫作用域啊?作用域链是什么东西嘞?
作用域,作用域,就是变量能发挥作用的区域,变量能在某个区域耀武扬威,但是到了别的地方,就得喝肾宝咯~
比如以下例子:
var a = 1;
function add(a1,a2){
var c = 2;
return a1+a2+c;
}
{
let f = 1;
const g = 2;
var h = 3;
}
这个例子的作用域就是这么组成的:
对应的变量只会在相应的作用域中发挥作用,在其他的作用域中不可发挥作用,图中,c,a1,a2仅仅能在add函数中被使用,f,g仅能在块级作用域中被使用,而a,h处于全局作用域,可以在任何地方被使用。
这个时候就有人要说了:“主播主播,h不是在块级作用域中被定义的吗?怎么跑到了全局作用域了呢?”
答案其实就是提升(hoistings)。
提升(Hoisting)
提升是JavaScript中一个比较重要的特性,它决定了我们利用定义的变量和普通函数后的,它们的特殊行为:即无论在哪里定义变量(var)和函数,都会在编译阶段提升到当前顶层
通俗一点讲,就是一个人在刷抖音,他的抖音里有各种各样的内容,他会总览一下,把所有看到的美女都先收藏一下,就算不知道她们的ID,也要收藏一下,毕竟感觉来了谁会管她ID是啥呢0v0.
而在代码编译阶段,所有变量和函数就是美女,让编译器离不开眼,把她们全部拉到顶层了,不管它们是什么值,不管他们有没有值,全部拉到顶部!
就像下面这个例子:
console.log(a); // 输出 undefined
var a = 3;
var c = 114514;
add(1,3);
function add(a,b){
console.log(a,b); // 输出 1,3
console.log(c); // 输出 undefined
var c = 4;
}
这一段代码在编译后就相当于以下内容:
var a; // 全局作用域内变量提升至顶部
var c;
function add(a,b){ // 函数提升到顶部
var c; // 函数作用域内变量提升
console.log(a,b); // 输出 1,3
console.log(c); // 输出 undefined
c = 4;
}
console.log(a);
a = 3;
c = 114514;
add(1,3);
在编译器的法眼下,所有var变量和函数都被标记出来了,提升到了对应的顶部,这也就是为什么这一串看似反人类的代码可以执行而不报错的原因,就在于提升(Hoisting)
!!!!提醒:
能做到提升的变量只有 var ,但是如果var存在于函数中,那么只能提升到函数作用域的顶部,而不会溢出到全局作用域。
let和const均不可以提升,这是由于其设计造成的,JavaScript仅仅用了一周的时间就被设计了出来,var是最早表示变量的标识,后来在ES6中为了消除这种反人类的设计才引入了let和const,用了let和const的变量拥有和其他语言一样的特性,不再有提升。
这个时候有的童鞋可能想问了:如果我在add函数中没有定义var c,那么c会输出什么呢?
这个就和作用域链有关系了
作用域链
作用域链(Scope Chain) 是 JavaScript 在执行过程中寻找变量的机制,它决定了 当前作用域 可以访问哪些变量,以及它们的查找顺序。
当访问一个变量时,JavaScript 引擎会 沿着作用域链逐级向上查找,直到找到该变量或到达全局作用域(未找到则报 ReferenceError
)。
例如上方的例子中,要访问c变量时,就会先访问函数作用域中的c,如果没有,就会沿着作用域链一步步向外寻找。就像小孩子自己被困在家里,睡醒了找不到妈妈一样,先从房间找,又跑到客厅,最后跑出房子找是一样的....(大家一定要照顾好孩子233333~)
执行阶段
编译阶段的几个问题解答完了,我们来解释一下执行阶段。
目的: 真正逐行运行代码,产生程序的实际效果(计算、修改数据、操作 DOM、网络请求等)。
-
过程:
-
JavaScript 引擎从上到下、逐行执行编译阶段准备好的代码(字节码或机器码),创建执行上下文。
-
赋值操作: 给变量赋予实际的值(覆盖掉编译阶段
var
变量的undefined
初始值)。 -
函数调用:
- 遇到函数调用时,会为该函数创建一个新的函数执行上下文,同样经历编译(创建) 和 执行 阶段。
- 执行函数体内部的代码。
- 函数执行完毕后,其执行上下文通常会被销毁(闭包除外)。
-
表达式求值: 计算表达式的结果(如
5 + 3
,x > y
)。 -
逻辑控制: 执行条件判断 (
if/else
)、循环 (for
,while
) 等逻辑流程。
-
其中,创建执行上下文是最重要的部分。
执行上下文
简单来说,执行上下文就是记录了各种信息的载体,方便对应规则来运行代码。
执行上下文一般有三种:
- 全局执行上下文(Global Execution Context) → 代码首次运行时创建。
- 函数执行上下文(Function Execution Context) → 每次调用函数时创建。
-
eval
执行上下文(较少用,不推荐)。
执行上下文都包含了啥呢?
V8引擎就是根据这些信息来对应着一定的规则,来运行了代码。
Second:let?const?var?有啥区别?
JavaScript 有三种变量声明方式:const
、let
和 var
,它们在 作用域、提升(Hoisting)、重复声明、TDZ(暂时性死区) 等方面有显著区别。
首先来说说Var,这个是JavaScript最初设计用来表示变量的标识,它具有提升的特性,这一点与其他编程语言不同,比较反人类,所以呢,后来在ES6中就设计了let和const,用来替代var,去除这一反人类的特性。那为什么后来没有废除var呢,我猜可能是用var的代码已经太多了,如果删除掉var,那么开发者们就要掀桌子了哈哈哈哈哈哈哈哈。
const
、let
、var
核心区别
特性 | var |
let |
const |
---|---|---|---|
作用域 | 函数作用域(function-scoped ) |
块级作用域(block-scoped ) |
块级作用域(block-scoped ) |
声明提升 | ✅(初始化为 undefined ) |
✅(未初始化,访问报错) | ✅(未初始化,访问报错) |
重复声明 | ✅(可以多次声明) | ❌(同一作用域禁止重复声明) | ❌(同一作用域禁止重复声明) |
重新赋值 | ✅ | ✅ | ❌(常量,不能重新赋值) |
TDZ(暂时性死区) | ❌(无 TDZ) | ✅(进入块级作用域到声明前不可访问) | ✅(同 let ) |
TDZ(Temporal Dead Zone,暂时性死区)
简单来讲呢,就是当你定义一个let
或者const
变量的时候,不能在没有声明的情况下提前访问它,这一点和其他编程语言一致.
TDZ 是 let
和 const
特有的行为:
-
在变量声明之前,如果访问它,会抛出
ReferenceError
(而不是得到undefined
)。 - TDZ 的范围:变量所在的作用域顶部 → 变量声明的位置。
示例 1:let
的 TDZ
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10; // 声明前访问会报错(TDZ 区域)
执行过程:
- 进入作用域 →
a
被提升(let a
) - 但
a
未被初始化(处于 TDZ) - 执行
console.log(a)
→ 报错 - 执行
a = 10
→ TDZ 结束,可以访问a
示例 2:const
的 TDZ
console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 20; // 声明前访问会报错(TDZ 区域)
const
和 let
的 TDZ 行为一致。
示例 3:var
没有 TDZ
console.log(c); // undefined(不会报错)
var c = 30; // var 提升并初始化为 undefined
var
不会进入 TDZ,因为它在编译阶段已经被初始化为 undefined
。
全局作用域下的绑定
-
var
声明的全局变量 → 会 绑定到window
(浏览器)或global
(Node.js) -
let
和const
声明的全局变量 → 不会绑定到window
/global
示例
// 浏览器环境
var globalVar = "var 变量";
let globalLet = "let 变量";
const globalConst = "const 变量";
console.log(window.globalVar); // "var 变量"(绑定到 window)
console.log(window.globalLet); // undefined(不绑定)
console.log(window.globalConst); // undefined(不绑定)
为什么 let
和 const
不绑定到 window
?
-
历史遗留问题:
var
是 ES5 的写法,会污染全局对象(window
)。 -
块级作用域优化:
let
和const
是 ES6 引入的,设计初衷是避免全局污染。
使用场景总结
声明方式 | 适用场景 |
---|---|
var |
旧代码兼容 / 不需要块级作用域的场景(但现代 JS 基本不用)。 |
let |
需要重新赋值的变量(如循环计数器 for (let i = 0; ...) )。 |
const |
常量(如 API 密钥、数学常量 PI )、对象/数组引用(可修改属性但不能重新赋值)。 |
总结
关键点 | var |
let |
const |
---|---|---|---|
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
提升(Hoisting) | ✅ (undefined ) |
✅ (TDZ) | ✅ (TDZ) |
重复声明 | ✅ | ❌ | ❌ |
重新赋值 | ✅ | ✅ | ❌ |
TDZ(暂时性死区) | ❌ | ✅ | ✅ |
全局绑定 window |
✅ | ❌ | ❌ |
最佳实践
-
默认使用
const
(避免意外修改)。 -
需要重新赋值时用
let
(如循环变量)。 -
避免使用
var
(除非维护旧代码)。
总结
好了,这一篇文章就到这里吧,我们在这里讲解了JS代码运行的原理,详细介绍了作用域和执行上下文等各种概念,还是希望大家能够有一些收获,能够更好的了解JavaScript,如果你觉得我写的好,那么请给我一个免费的赞吧嘻嘻嘻》》》