初学者视角下的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文件中,所有变量和函数都是全局作用域的
- 局部作用域:在函数中,所有变量和函数都是局部作用域的
- 块级作用域 :
let和const与{}配合形成的(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;
}
为什么报错?
- 全局有
var a = 100 - if块内用
let声明了a = 10,let + {}形成了块级作用域 - 在这个块级作用域内,
let创建了一个新的a - 在
let a = 10之前,这个新的a处于"暂时性死区",访问会报错 - 即使全局有
a = 100,也不会向外查找,因为在当前作用域已经存在a的声明
四、var、let、const在各作用域的所有情况
以下基于同一份示例代码,将 var 逐一替换为 let 和 const,分析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执行过程:
- 分词、解析阶段完成
- 执行阶段:先处理声明
- 全局作用域:声明
a(var提升) - foo函数作用域:声明
a(var提升)
- 全局作用域:声明
- 执行赋值
- 全局
a = 1 - 调用
foo()
- 全局
foo()内部执行- 函数作用域的
a = 2 console.log(a)→ 先在当前函数作用域查找 → 找到a = 2→ 输出2
- 函数作用域的
foo()执行完毕,回到全局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执行过程:
- 执行阶段:先处理声明
- 全局作用域:
let a(不提升,但创建了标识符绑定) - foo函数作用域:
let a(同上)
- 全局作用域:
- 执行赋值
- 全局
a = 1 - 调用
foo()
- 全局
foo()内部执行- 函数作用域的
a = 2 console.log(a)→ 先在当前函数作用域查找 → 找到a = 2→ 输出2
- 函数作用域的
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执行过程:
- 执行阶段:先处理声明
- 全局作用域:
var a(提升) - foo函数作用域:
let a(创建标识符绑定)
- 全局作用域:
- 执行赋值和查找过程同前
结论: 全局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执行过程:
- 执行阶段:先处理声明
- 全局作用域:
let a(创建标识符绑定) - foo函数作用域:
var a(提升)
- 全局作用域:
- 执行赋值和查找过程同前
结论: 全局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执行过程:
- 执行阶段:先处理声明
- 全局作用域:
var a(提升) - foo函数作用域:
var a(提升到函数顶部,不是if块顶部)
- 全局作用域:
foo()内部执行var a提升到函数顶部,if内的a = 2赋值覆盖了函数作用域的aconsole.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执行过程:
- 执行阶段:先处理声明
- 全局作用域:
var a(提升) - foo函数作用域:没有声明
a - if块级作用域:
let a(创建标识符绑定)
- 全局作用域:
foo()内部执行- 进入if块,块级作用域内
a = 2 console.log(a)→ 在if块级作用域查找 → 找到a = 2→ 输出2- 离开if块,块级作用域销毁
console.log(a)→ 在函数作用域查找 → 没找到 → 向外到全局作用域查找 → 找到a = 1→ 输出1
- 进入if块,块级作用域内
结论: 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执行过程:
- 执行阶段:先处理声明
- 全局作用域:
var a(提升) - if块级作用域:
let a(创建标识符绑定,进入TDZ)
- 全局作用域:
- 进入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重新赋值 | 报错:常量不可重新赋值 |
总结
- V8引擎:分词 → 解析 → 执行。先声明,再访问。
- 作用域:分全局作用域、局部作用域、块级作用域。V8从当前作用域向外层查找,直到全局作用域。
- var → let → const:let规范了变量定义,const声明常量。let + {} 形成块级作用域,let不会带来声明提升。
- 各情况分析:var/let/const在全局和函数作用域中各声明同名变量时互不影响;var在块内没有块级作用域,let有;暂时性死区是因为当前作用域存在let声明时不会向外查找。