JavaScript 作用域与执行机制深度解析
一、JS 执行机制概述
V8 引擎的两个阶段
JavaScript 的执行分为两个阶段:编译阶段 和执行阶段。
- 编译阶段:创建执行上下文,处理变量和函数声明
- 执行阶段:逐行执行代码,赋值和运算
调用栈与执行上下文
javascript
// 执行上下文示例
function foo() {
console.log('foo');
bar();
}
function bar() {
console.log('bar');
}
foo();
// 调用栈过程:
// 1. 全局执行上下文入栈
// 2. foo() 执行上下文入栈
// 3. bar() 执行上下文入栈
// 4. bar() 执行完成出栈
// 5. foo() 执行完成出栈
// 6. 全局执行上下文出栈
每个执行上下文包含:
- 变量环境 :存放
var声明的变量 - 词法环境 :存放
let、const声明的变量 - 外部环境引用:形成作用域链
二、变量提升(Hoisting)
什么是变量提升
变量提升是指 JavaScript 在编译阶段将变量和函数声明移动到其作用域顶部的行为。
【1.js】变量提升示例
javascript
showName();
console.log(name);
var name = '路明非';
function showName(){
console.log('函数showName 执行了');
}
// 实际执行顺序(编译后):
// function showName(){ console.log('函数showName 执行了'); }
// var name; // 提升但未赋值
// showName(); // 输出:函数showName 执行了
// console.log(name); // 输出:undefined
// name = '路明非';
运行结果:
javascript
函数showName 执行了
undefined
变量提升的设计原因
JavaScript 最初是为了给页面添加简单的动态效果,设计周期极短。为了快速实现,采用变量提升是最简单的方式:
- 不需要复杂的块级作用域支持
- 统一将变量提升到作用域顶部
- 用大写函数(构造函数)+ prototype 模拟面向对象
变量提升带来的问题
问题一:变量容易被覆盖
【3.js】变量覆盖示例
javascript
var name = '路明非';
function showName(){
console.log(name);
if(false){
var name = 'sadas';
}
}
showName();
// 实际执行:
// var name;(全局)
// function showName(){
// var name;(函数内提升)
// console.log(name);
// if(false){ name = 'sadas'; }
// }
// name = '路明非';
// showName(); // 输出:undefined(函数内的 name 覆盖了全局的)
运行结果:
javascript
undefined
原因分析: 函数内部的 var name 被提升到函数顶部,初始值为 undefined,覆盖了对外部 name 的访问。
问题二:变量应该销毁但没有销毁
javascript
// 本应销毁的变量因为提升而没有及时销毁
function test() {
for (var i = 0; i < 3; i++) {
// 循环体
}
console.log(i); // 输出:3(i 应该在循环结束后销毁)
}
test();
三、作用域类型
1. 全局作用域
在任何地方都能访问,生命周期等于页面生命周期。
javascript
var globalVar = "我是全局变量"; // 全局作用域
function myFunction() {
console.log(globalVar); // 可以访问
}
myFunction();
console.log(globalVar); // 可以访问
2. 函数局部作用域
只能在函数内部访问,生命周期等于函数执行周期。
【2.js】局部作用域示例
javascript
var globalVar = "我是全局变量";
function myFunction() {
var localVar = "我是局部变量";
console.log(globalVar); // 输出:我是全局变量
console.log(localVar); // 输出:我是局部变量
}
myFunction();
console.log(globalVar); // 输出:我是全局变量
console.log(localVar); // 报错:localVar is not defined
运行结果:
vbnet
我是全局变量
我是局部变量
我是全局变量
ReferenceError: localVar is not defined
3. 块级作用域(ES6 引入)
ES5 不支持块级作用域,ES6 通过 let 和 const 支持。
【5.js】块级作用域示例
javascript
// 块级作用域支持的语法
if(1){} // if 块
while(1){} // while 块
for(let i=0;i<10;i){} // for 块(使用 let)
function foo(){} // 函数作用域(不是块级)
四、ES6 如何实现块级作用域
一国两制:变量环境 + 词法环境
| 声明方式 | 存放位置 | 特性 |
|---|---|---|
var |
变量环境 | 变量提升、函数作用域 |
let/const |
词法环境 | 暂时性死区、块级作用域 |
词法环境的栈结构
执行到块级作用域时,let/const 声明的变量会被放入词法环境的一个独立区域,形成栈结构。
【7.js】执行上下文分析
javascript
function foo() {
var a = 1; // 变量环境
let b = 2; // 词法环境(函数级)
{
let b = 3; // 词法环境(块级,入栈)
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); // 报错:d is not defined(块级变量已出栈)
}
foo();
运行结果:
1
3
2
4
执行上下文结构:
css
执行上下文
├── 变量环境
│ ├── a: 1
│ └── c: 4
└── 词法环境(栈结构)
├── 函数级: { b: 2 }
├── 块级①: { b: 3 } ← 执行时压栈
└── 块级②: { d: 5 } ← 执行时压栈
暂时性死区(TDZ)
【8.js】暂时性死区示例
javascript
let name = '刘锦苗';
{
console.log(name); // 报错:Cannot access 'name' before initialization
let name = '大厂的苗子';
}
原因: 块级作用域内,let 声明不会被提升,但在声明前访问会形成暂时性死区。
【4.js】var vs let 对比
javascript
// var 版本(3.js)
var name = '路明非';
function showName(){
console.log(name); // undefined(var 提升)
if(false){
var name = 'sadas';
}
}
showName();
// let 版本(4.js)
let name = '路明非';
function showName(){
console.log(name); // 输出:路明非(查找外层)
if(false){
let name = 'sadas';
}
}
showName();
运行结果(4.js):
路明非
五、var 与 let 的核心区别
| 特性 | var |
let/const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 变量提升 | 是(初始 undefined) |
是但未初始化(TDZ) |
| 重复声明 | 允许 | 不允许 |
| 全局声明 | 挂载到 window |
不挂载到 window |
| 暂时性死区 | 无 | 有 |
【6.js】var 在循环中的表现
javascript
function foo(){
console.log(i); // undefined(var 提升)
for(var i=0;i<10;i++){}
console.log(i); // 10(循环结束 i 仍存在)
}
foo();
如果使用 let,则不会有此问题:
javascript
function foo(){
// console.log(i); // 报错:TDZ
for(let i=0;i<10;i++){}
// console.log(i); // 报错:i is not defined
}
六、作用域链与变量查找
查找规则
当访问一个变量时,JavaScript 会按照以下顺序查找:
- 当前作用域的词法环境(栈顶)
- 当前作用域的变量环境
- 外层作用域的词法环境
- 外层作用域的变量环境
- 重复直到全局作用域
- 未找到则报错(严格模式)或返回
undefined
图示:词法环境的栈结构
css
执行上下文
┌─────────────────────────────┐
│ 词法环境(栈结构) │
│ ┌─────────────────────┐ │
│ │ 块级作用域②: {d:5} │ ← 栈顶(当前执行)
│ ├─────────────────────┤ │
│ │ 块级作用域①: {b:3} │ │
│ ├─────────────────────┤ │
│ │ 函数级: {b:2} │ │
│ └─────────────────────┘ │
├─────────────────────────────┤
│ 变量环境 │
│ ┌─────────────────────┐ │
│ │ a: 1, c: 4 │ │
│ └─────────────────────┘ │
└─────────────────────────────┘
七、总结
核心概念
- 变量提升:JS 设计初期的特性,导致代码与直觉不符
- 作用域:变量查找的规则,决定变量的可见性
- 执行上下文:包含变量环境和词法环境
- 块级作用域 :ES6 通过
let/const+ 词法环境栈实现 - 暂时性死区 :
let/const声明前不可访问的区域
最佳实践
javascript
// ✅ 推荐:使用 let/const 避免变量提升问题
let name = '张三';
if (true) {
let name = '李四';
console.log(name); // 李四
}
console.log(name); // 张三
// ❌ 不推荐:依赖变量提升
console.log(x); // undefined(容易造成困惑)
var x = 10;
// ✅ 推荐:变量先声明后使用
let y = 10;
console.log(y); // 10
设计哲学
- ES5:函数作用域 + 变量提升(简单快速,但有缺陷)
- ES6:块级作用域 + 暂时性死区(向下兼容,更合理)
JavaScript 采用"一国两制"策略,在同一套执行上下文中用变量环境和词法环境分别处理 var 和 let/const,既保持了向下兼容,又实现了更合理的块级作用域。