🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥

前言:为什么 JavaScript 如此"奇怪"?

如果你是从其他编程语言(比如 Python、Java 或 C++)转到 JavaScript 的,你可能会对它的某些行为感到困惑:

javascript 复制代码
console.log(name); // 输出:undefined,而不是报错!
var name = "Alice";

{
  let age = 25;
  console.log(age); // 输出:25
}
console.log(age); // 报错:age is not defined
  • 为什么 var 变量可以在声明前访问?
  • 为什么 letconst 又不行?
  • 为什么 {} 块能影响变量的作用域?

这些现象背后,是 JavaScript 独特的 编译与执行机制 ,以及 作用域管理方式 。在传统语言中,变量通常需要先声明再使用,而 JavaScript 的 var 却允许"先使用后声明",这源于它的 变量提升(Hoisting) 特性。而 letconst 的出现,则修复了 var 的缺陷,引入了更严格的 块级作用域(Block Scope)暂时性死区(TDZ)

本章将深入剖析:

JavaScript 代码的执行流程(编译 vs. 执行阶段)

变量提升的本质var 与函数声明的特殊行为)

letconst 如何避免变量提升问题

作用域链与闭包的底层机制

无论你是 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)。
    • 标记变量声明(varletconst)和函数声明(function)的作用域。
  • 处理变量提升(Hoisting)

    • varfunction 声明会被记录到作用域顶部(但未赋值)。
    • let/const 也会被记录,但不会提前绑定(形成暂时性死区)。
  • 建立作用域链的静态结构

    • 确定嵌套作用域的引用关系(闭包的基础)。

什么是作用域?

看到上面编译阶段的小伙伴们可能会有疑问:什么叫作用域啊?作用域链是什么东西嘞?

作用域,作用域,就是变量能发挥作用的区域,变量能在某个区域耀武扬威 ,但是到了别的地方,就得喝肾宝咯~

比如以下例子:

js 复制代码
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.

而在代码编译阶段,所有变量和函数就是美女,让编译器离不开眼,把她们全部拉到顶层了,不管它们是什么值,不管他们有没有值,全部拉到顶部!

就像下面这个例子:

js 复制代码
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;
}

这一段代码在编译后就相当于以下内容:

js 复制代码
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) 等逻辑流程。

其中,创建执行上下文是最重要的部分。

执行上下文

简单来说,执行上下文就是记录了各种信息的载体,方便对应规则来运行代码。

执行上下文一般有三种:

  1. 全局执行上下文(Global Execution Context) → 代码首次运行时创建。
  2. 函数执行上下文(Function Execution Context) → 每次调用函数时创建。
  3. eval 执行上下文(较少用,不推荐)。

执行上下文都包含了啥呢?

V8引擎就是根据这些信息来对应着一定的规则,来运行了代码。

Second:let?const?var?有啥区别?

JavaScript 有三种变量声明方式:constletvar,它们在 作用域、提升(Hoisting)、重复声明、TDZ(暂时性死区) 等方面有显著区别。

首先来说说Var,这个是JavaScript最初设计用来表示变量的标识,它具有提升的特性,这一点与其他编程语言不同,比较反人类,所以呢,后来在ES6中就设计了let和const,用来替代var,去除这一反人类的特性。那为什么后来没有废除var呢,我猜可能是用var的代码已经太多了,如果删除掉var,那么开发者们就要掀桌子了哈哈哈哈哈哈哈哈。


constletvar 核心区别

特性 var let const
作用域 函数作用域(function-scoped 块级作用域(block-scoped 块级作用域(block-scoped
声明提升 ✅(初始化为 undefined ✅(未初始化,访问报错) ✅(未初始化,访问报错)
重复声明 ✅(可以多次声明) ❌(同一作用域禁止重复声明) ❌(同一作用域禁止重复声明)
重新赋值 ❌(常量,不能重新赋值)
TDZ(暂时性死区) ❌(无 TDZ) ✅(进入块级作用域到声明前不可访问) ✅(同 let

TDZ(Temporal Dead Zone,暂时性死区)

简单来讲呢,就是当你定义一个let或者const变量的时候,不能在没有声明的情况下提前访问它,这一点和其他编程语言一致.

TDZ 是 letconst 特有的行为

  • 在变量声明之前,如果访问它,会抛出 ReferenceError (而不是得到 undefined)。
  • TDZ 的范围:变量所在的作用域顶部 → 变量声明的位置。

示例 1:let 的 TDZ

javascript 复制代码
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;     // 声明前访问会报错(TDZ 区域)

执行过程

  1. 进入作用域 → a 被提升(let a
  2. a 未被初始化(处于 TDZ
  3. 执行 console.log(a)报错
  4. 执行 a = 10 → TDZ 结束,可以访问 a

示例 2:const 的 TDZ

javascript 复制代码
console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 20;   // 声明前访问会报错(TDZ 区域)

constlet 的 TDZ 行为一致。

示例 3:var 没有 TDZ

javascript 复制代码
console.log(c); // undefined(不会报错)
var c = 30;     // var 提升并初始化为 undefined

var 不会进入 TDZ,因为它在编译阶段已经被初始化为 undefined


全局作用域下的绑定

  • var 声明的全局变量 → 会 绑定到 window(浏览器)或 global(Node.js)
  • letconst 声明的全局变量 → 不会绑定到 window/global

示例

javascript 复制代码
// 浏览器环境
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(不绑定)

为什么 letconst 不绑定到 window

  • 历史遗留问题var 是 ES5 的写法,会污染全局对象(window)。
  • 块级作用域优化letconst 是 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,如果你觉得我写的好,那么请给我一个免费的赞吧嘻嘻嘻》》》

相关推荐
小小小小宇3 小时前
虚拟列表兼容老DOM操作
前端
悦悦子a啊3 小时前
Python之--基本知识
开发语言·前端·python
安全系统学习4 小时前
系统安全之大模型案例分析
前端·安全·web安全·网络安全·xss
涛哥码咖4 小时前
chrome安装AXURE插件后无效
前端·chrome·axure
OEC小胖胖4 小时前
告别 undefined is not a function:TypeScript 前端开发优势与实践指南
前端·javascript·typescript·web
行云&流水5 小时前
Vue3 Lifecycle Hooks
前端·javascript·vue.js
Sally璐璐5 小时前
零基础学HTML和CSS:网页设计入门
前端·css
老虎06275 小时前
JavaWeb(苍穹外卖)--学习笔记04(前端:HTML,CSS,JavaScript)
前端·javascript·css·笔记·学习·html
三水气象台5 小时前
用户中心Vue3网页开发(1.0版)
javascript·css·vue.js·typescript·前端框架·html·anti-design-vue
灿灿121385 小时前
CSS 文字浮雕效果:巧用 text-shadow 实现 3D 立体文字
前端·css