初学者视角下的JavaScript作用域理解

初学者视角下的JavaScript作用域理解

前言

在学习JavaScript的过程中,作用域是一个必须理解的核心概念。本文从初学者的视角,谈谈我对JS引擎和作用域的理解。


一、JavaScript引擎------V8

V8是JavaScript的解释器,负责执行JS代码。每一段代码在执行前都要经历编译过程,分为三个阶段:

markdown 复制代码
① 分词(词法分析)
    ↓
② 解析(语法分析)
    ↓
③ 执行(代码生成+运行)

1.1 分词

将代码字符串分解成词法单元

javascript 复制代码
var a = 1;

分解为:['var', 'a', '=', '1', ';']

1.2 解析

将词法单元流转换成抽象语法树(AST),代表程序的语法结构。

1.3 执行

将AST转换为可执行的机器指令并运行。

核心原则:先声明,再访问。 在执行阶段,V8会先处理所有的声明,然后再执行代码。这解释了为什么会出现"变量提升"等现象。


二、作用域

承接V8的执行过程,V8在执行代码时需要知道变量在哪里可以访问,这就涉及作用域

2.1 作用域是什么?

在JS文件中有一个叫作用域的机制,它决定了变量和函数的可访问范围。

2.2 作用域分为什么?

  • 全局作用域:在JS文件中,所有变量和函数都是全局作用域的
  • 局部作用域:在函数中,所有变量和函数都是局部作用域的
  • 块级作用域letconst{} 配合形成的(ES6新增)

2.3 作用域有什么用?

作用域链查找规则:

在V8的执行过程中,会先在当前作用域中查找,如果找不到,就去外层作用域中查找,直到找到全局作用域,还是找不到就会报错。

javascript 复制代码
var a = 1;

function foo() {
  var a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

分析:

  • foo() 内部声明了 var a = 2,所以在函数作用域内找到了 a,输出 2
  • 全局的 console.log(a) 在全局作用域找到了 var a = 1,输出 1
  • 这说明:内部作用域可以访问外部作用域的变量,但外部作用域不能访问内部作用域的变量

三、从var到let和const

3.1 var

var 是JavaScript最早的变量声明方式。

var 的问题:

  • 没有块级作用域,变量会泄露到外部
  • 存在声明提升
  • 可以重复声明

var 是为了兼容旧的代码,现在不建议使用。

3.2 let

let 是为了规范后续代码,避免变量的重复定义。

  • let + {} 会形成块级作用域
  • let 不会带来声明提升

3.3 const

const 声明的是常量,不能重新赋值。

3.4 暂时性死区

javascript 复制代码
var a = 100;

if (true) {
  // # 暂时性死区
  console.log(a); // ❌ 报错:Cannot access 'a' before initialization
  // # 暂时性死区结束
  let a = 10;
}

为什么报错?

  1. 全局有 var a = 100
  2. if块内用 let 声明了 a = 10let + {} 形成了块级作用域
  3. 在这个块级作用域内,let 创建了一个新的 a
  4. let a = 10 之前,这个新的 a 处于"暂时性死区",访问会报错
  5. 即使全局有 a = 100,也不会向外查找,因为在当前作用域已经存在 a 的声明

四、var、let、const在各作用域的所有情况

以下基于同一份示例代码,将 var 逐一替换为 letconst,分析V8在不同情况下的行为。

基础示例

javascript 复制代码
var a = 1;

function foo() {
  var a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

情况1:全局用var,函数用var(原始示例)

javascript 复制代码
var a = 1;

function foo() {
  var a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

V8执行过程:

  1. 分词、解析阶段完成
  2. 执行阶段:先处理声明
    • 全局作用域:声明 a(var提升)
    • foo函数作用域:声明 a(var提升)
  3. 执行赋值
    • 全局 a = 1
    • 调用 foo()
  4. foo() 内部执行
    • 函数作用域的 a = 2
    • console.log(a) → 先在当前函数作用域查找 → 找到 a = 2 → 输出 2
  5. foo() 执行完毕,回到全局
  6. console.log(a) → 在全局作用域查找 → 找到 a = 1 → 输出 1

结论: var在全局和函数中各声明了一个 a,互不影响。


情况2:全局用let,函数用let

javascript 复制代码
let a = 1;

function foo() {
  let a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

V8执行过程:

  1. 执行阶段:先处理声明
    • 全局作用域:let a(不提升,但创建了标识符绑定)
    • foo函数作用域:let a(同上)
  2. 执行赋值
    • 全局 a = 1
    • 调用 foo()
  3. foo() 内部执行
    • 函数作用域的 a = 2
    • console.log(a) → 先在当前函数作用域查找 → 找到 a = 2 → 输出 2
  4. console.log(a) → 在全局作用域查找 → 找到 a = 1 → 输出 1

结论: 与情况1结果完全一致。let和var在函数作用域中的查找规则相同,区别在于块级作用域和声明提升。


情况3:全局用const,函数用const

javascript 复制代码
const a = 1;

function foo() {
  const a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

V8执行过程: 与情况2一致,const和let的作用域规则相同。

结论: const与let的作用域行为一致,区别仅在于const声明后不能重新赋值。


情况4:全局用var,函数用let

javascript 复制代码
var a = 1;

function foo() {
  let a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

V8执行过程:

  1. 执行阶段:先处理声明
    • 全局作用域:var a(提升)
    • foo函数作用域:let a(创建标识符绑定)
  2. 执行赋值和查找过程同前

结论: 全局var和函数let互不干扰,结果与情况1一致。


情况5:全局用let,函数用var

javascript 复制代码
let a = 1;

function foo() {
  var a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

V8执行过程:

  1. 执行阶段:先处理声明
    • 全局作用域:let a(创建标识符绑定)
    • foo函数作用域:var a(提升)
  2. 执行赋值和查找过程同前

结论: 全局let和函数var互不干扰,结果与情况1一致。


情况6:全局用var,函数用const

javascript 复制代码
var a = 1;

function foo() {
  const a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

结论: 与前述情况一致,const在函数作用域中的行为与let相同。


情况7:全局用const,函数用var

javascript 复制代码
const a = 1;

function foo() {
  var a = 2;
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

结论: 与前述情况一致。


情况8:var + 块级作用域(if内用var)

javascript 复制代码
var a = 1;

function foo() {
  if (true) {
    var a = 2;
  }
  console.log(a); // 输出:2
}

foo();
console.log(a); // 输出:1

V8执行过程:

  1. 执行阶段:先处理声明
    • 全局作用域:var a(提升)
    • foo函数作用域:var a(提升到函数顶部,不是if块顶部)
  2. foo() 内部执行
    • var a 提升到函数顶部,if内的 a = 2 赋值覆盖了函数作用域的 a
    • console.log(a) → 在函数作用域查找 → 找到 a = 2 → 输出 2

结论: var没有块级作用域,if内的 var a = 2 属于整个函数作用域。


情况9:let + 块级作用域(if内用let)

javascript 复制代码
var a = 1;

function foo() {
  if (true) {
    let a = 2;
    console.log(a); // 输出:2
  }
  console.log(a); // 输出:1
}

foo();
console.log(a); // 输出:1

V8执行过程:

  1. 执行阶段:先处理声明
    • 全局作用域:var a(提升)
    • foo函数作用域:没有声明 a
    • if块级作用域:let a(创建标识符绑定)
  2. foo() 内部执行
    • 进入if块,块级作用域内 a = 2
    • console.log(a) → 在if块级作用域查找 → 找到 a = 2 → 输出 2
    • 离开if块,块级作用域销毁
    • console.log(a) → 在函数作用域查找 → 没找到 → 向外到全局作用域查找 → 找到 a = 1 → 输出 1

结论: let + {} 形成块级作用域,if内的 let a = 2 不会影响函数作用域和全局作用域。


情况10:暂时性死区(全局var + 块内let)

javascript 复制代码
var a = 100;

if (true) {
  console.log(a); // ❌ 报错:Cannot access 'a' before initialization
  let a = 10;
}

V8执行过程:

  1. 执行阶段:先处理声明
    • 全局作用域:var a(提升)
    • if块级作用域:let a(创建标识符绑定,进入TDZ)
  2. 进入if块
    • console.log(a) → 先在if块级作用域查找 → 发现 let a 的声明存在,但还在TDZ中 → 报错
    • 注意:即使全局有 var a = 100,也不会向外查找,因为当前作用域已经存在 a 的声明

结论: 一旦当前作用域存在 let/const 声明,V8就不会向外层查找该变量。在声明语句执行之前,变量处于暂时性死区。


情况11:var和let在同一作用域重复声明

javascript 复制代码
var a = 1;
let a = 2; // ❌ 报错:Identifier 'a' has already been declared
javascript 复制代码
let a = 1;
var a = 2; // ❌ 报错:Identifier 'a' has already been declared
javascript 复制代码
let a = 1;
let a = 2; // ❌ 报错:Identifier 'a' has already been declared

结论: 在同一作用域中,var和let不能重复声明同一个变量。let就是为了规范后续代码,避免变量的重复定义。


情况12:const声明后重新赋值

javascript 复制代码
const a = 1;

function foo() {
  a = 2; // ❌ 报错:Assignment to constant variable
}

foo();
javascript 复制代码
var a = 1;

function foo() {
  const a = 2;
  a = 3; // ❌ 报错:Assignment to constant variable
}

foo();

结论: const声明的是常量,不能重新赋值。


情况汇总表

情况 全局声明 函数/块内声明 结果
1 var var(函数) 各自独立,互不影响
2 let let(函数) 同上
3 const const(函数) 同上
4 var let(函数) 同上
5 let var(函数) 同上
6 var const(函数) 同上
7 const var(函数) 同上
8 var var(if块内) var无块级作用域,赋值覆盖函数作用域
9 var let(if块内) let形成块级作用域,互不影响
10 var let(if块内,TDZ) 块内let声明前访问报错
11 var+let 同一作用域 报错:不可重复声明
12 - const重新赋值 报错:常量不可重新赋值

总结

  1. V8引擎:分词 → 解析 → 执行。先声明,再访问。
  2. 作用域:分全局作用域、局部作用域、块级作用域。V8从当前作用域向外层查找,直到全局作用域。
  3. var → let → const:let规范了变量定义,const声明常量。let + {} 形成块级作用域,let不会带来声明提升。
  4. 各情况分析:var/let/const在全局和函数作用域中各声明同名变量时互不影响;var在块内没有块级作用域,let有;暂时性死区是因为当前作用域存在let声明时不会向外查找。
相关推荐
廖松洋(Alina)1 小时前
07答案比对与反馈UI-鸿蒙PC端Electron开发
javascript·ui·华为·electron·开源·harmonyos·鸿蒙
nexustech2 小时前
JavaScript日期处理工具date-fns,累计36.5k Star
开发语言·javascript·其他·ecmascript
Lan.W3 小时前
vue3-element-admin里新增mock接口一直没有生成,不生效
前端·javascript·vue.js·mock
仙古.梦回~3 小时前
vue-skills
前端·javascript·vue.js
gCode Teacher 格码致知3 小时前
Javascript提高:canvas画布的网格背景-由Deepseek产生
javascript·css·css3
清灵xmf4 小时前
JS 原生深拷贝的终极方案——structuredClone
前端·javascript·vue.js·json.stringify·structuredclone
前端 贾公子4 小时前
响应式系统基础:依赖追踪的基础 —— 发布订阅模式(前端应用最广的设计模式)上
javascript·vue.js
gCode Teacher 格码致知4 小时前
Javascript提高:使用canvas绘制一个绚丽的按钮-由Deepseek产生
javascript·css·css3
小四的小六4 小时前
WebView安全防护实战:从XSS到中间人攻击,我的踩坑与防御总结
javascript·webview