JavaScript 作用域与执行机制深度解析

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 声明的变量
  • 词法环境 :存放 letconst 声明的变量
  • 外部环境引用:形成作用域链

二、变量提升(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 通过 letconst 支持。

【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 会按照以下顺序查找:

  1. 当前作用域的词法环境(栈顶)
  2. 当前作用域的变量环境
  3. 外层作用域的词法环境
  4. 外层作用域的变量环境
  5. 重复直到全局作用域
  6. 未找到则报错(严格模式)或返回 undefined

图示:词法环境的栈结构

css 复制代码
执行上下文
┌─────────────────────────────┐
│ 词法环境(栈结构)           │
│ ┌─────────────────────┐     │
│ │ 块级作用域②: {d:5}  │ ← 栈顶(当前执行)
│ ├─────────────────────┤     │
│ │ 块级作用域①: {b:3}  │     │
│ ├─────────────────────┤     │
│ │ 函数级: {b:2}       │     │
│ └─────────────────────┘     │
├─────────────────────────────┤
│ 变量环境                    │
│ ┌─────────────────────┐     │
│ │ a: 1, c: 4          │     │
│ └─────────────────────┘     │
└─────────────────────────────┘

七、总结

核心概念

  1. 变量提升:JS 设计初期的特性,导致代码与直觉不符
  2. 作用域:变量查找的规则,决定变量的可见性
  3. 执行上下文:包含变量环境和词法环境
  4. 块级作用域 :ES6 通过 let/const + 词法环境栈实现
  5. 暂时性死区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 采用"一国两制"策略,在同一套执行上下文中用变量环境和词法环境分别处理 varlet/const,既保持了向下兼容,又实现了更合理的块级作用域。

相关推荐
暗不需求1 小时前
深入理解 React 受控组件与非受控组件:从源码到面试
前端·react.js·面试
Yue1681 小时前
天津理工大学前端组大一末期考核随记(2)
前端·javascript
冰凌时空1 小时前
Swift 类型系统入门:从 Int、String 到自定义类型
前端·ios·ai编程
hexu_blog1 小时前
前端vue后端java+springboot如何实现pdf,word,excel之间的相互转换
java·前端·vue.js·spring boot·文档转换
w_t_y_y2 小时前
vue父子组件通信(二)祖先调用inject
前端·javascript·vue.js
哆哆啦002 小时前
URL 重写规则和静态资源解析逻辑
前端·浏览器·url
wkj0012 小时前
JavaScript模块化技术进程详解
开发语言·javascript·ecmascript
IT_陈寒2 小时前
Java的Stream.peek()千万别乱用,血泪教训
前端·人工智能·后端
w_t_y_y2 小时前
VUE组件配置项(二)data和props
前端·javascript·vue.js