JavaScript 高级核心:垃圾回收、执行上下文与闭包完全指南
本篇深入 JavaScript 运行时机制 :内存如何回收、代码如何执行、闭包如何形成。配有 Mermaid 图示 、可运行 HTML 示例 与 【代码注释】 。示意图可参考同目录
垃圾回收示意图.jpg。
权威参考:
- MDN:内存管理
- MDN:闭包
- V8 博客:垃圾回收(英文,了解现代引擎思路)
目录
- 零、导读与学习价值
- [0.1 案例覆盖清单](#0.1 案例覆盖清单)
- [0.2 核心名词速查](#0.2 核心名词速查)
- [0.3 为什么要学本篇?](#0.3 为什么要学本篇?)
- 一、垃圾回收机制
- [1.1 核心概念](#1.1 核心概念)
- [1.2 内存相关术语](#1.2 内存相关术语)
- [1.3 引用计数算法](#1.3 引用计数算法)
- [1.4 标记清除算法](#1.4 标记清除算法)
- [1.5 两种算法对比](#1.5 两种算法对比)
- 【本章小结】垃圾回收
- [1.6 V8 分代回收进阶](#1.6 V8 分代回收进阶)
- 堆的分代结构
- Scavenger(新生代复制算法)
- [增量标记(Incremental Marking)](#增量标记(Incremental Marking))
- 对开发的实际影响
- [1.7 WeakRef 与 FinalizationRegistry(ES2021)](#1.7 WeakRef 与 FinalizationRegistry(ES2021))
- [1.8 V8 Hidden Class(隐藏类)优化](#1.8 V8 Hidden Class(隐藏类)优化)
- 二、执行上下文
- [2.1 什么是执行上下文?](#2.1 什么是执行上下文?)
- [2.2 全局执行上下文](#2.2 全局执行上下文)
- [2.3 函数执行上下文](#2.3 函数执行上下文)
- [2.4 执行上下文对比](#2.4 执行上下文对比)
- [2.5 变量提升与暂时性死区(TDZ)](#2.5 变量提升与暂时性死区(TDZ))
- 【本章小结】执行上下文
- 三、执行栈
- [3.1 什么是执行栈?](#3.1 什么是执行栈?)
- [3.2 执行栈工作原理](#3.2 执行栈工作原理)
- 【本章小结】执行栈
- 四、作用域与执行上下文
- [4.1 核心区别](#4.1 核心区别)
- [4.2 关系图](#4.2 关系图)
- 【本章小结】作用域与执行上下文
- 五、闭包完全解析
- [5.1 什么是闭包?](#5.1 什么是闭包?)
- [5.2 产生闭包的条件](#5.2 产生闭包的条件)
- [5.3 闭包与作用域](#5.3 闭包与作用域)
- [5.4 闭包与垃圾回收](#5.4 闭包与垃圾回收)
- [5.5 闭包的应用场景](#5.5 闭包的应用场景)
- [5.6 闭包陷阱:React Hooks 与 Vue 响应式](#5.6 闭包陷阱:React Hooks 与 Vue 响应式)
- [React Hooks 中的 Stale Closure(过期闭包)](#React Hooks 中的 Stale Closure(过期闭包))
- [Vue 响应式系统中的闭包原理](#Vue 响应式系统中的闭包原理)
- 【本章小结】闭包
- 六、闭包经典面试题
- [6.1 选项卡闭包问题](#6.1 选项卡闭包问题)
- [6.2 经典面试题解析](#6.2 经典面试题解析)
- [6.3 面试口述模板与常见追问](#6.3 面试口述模板与常见追问)
- 七、最佳实践与性能优化
- [7.1 闭包使用建议](#7.1 闭包使用建议)
- [7.2 性能优化清单](#7.2 性能优化清单)
- 【本章小结】最佳实践与性能优化
- 八、工程场景专题实战
- [8.1 内存泄漏排查实战](#8.1 内存泄漏排查实战)
- [8.2 执行上下文实战:变量提升陷阱排查](#8.2 执行上下文实战:变量提升陷阱排查)
- [8.3 闭包实战:插件系统与中间件](#8.3 闭包实战:插件系统与中间件)
- [8.4 闭包实战:响应式数据绑定(手写 mini-Vue)](#8.4 闭包实战:响应式数据绑定(手写 mini-Vue))
- [8.5 V8 内存优化:Hidden Class 实战](#8.5 V8 内存优化:Hidden Class 实战)
- [8.6 执行上下文实战:call/apply/bind 深度解析](#8.6 执行上下文实战:call/apply/bind 深度解析)
- [8.7 综合实战:任务队列与异步流控制](#8.7 综合实战:任务队列与异步流控制)
- 总结
- 知识点回顾(思维导图)
- 学习建议
- [与 Day15、Day16 的关系](#与 Day15、Day16 的关系)
零、导读与学习价值
0.1 案例覆盖清单
| 模块 | 课堂主题 | 本篇对应 |
|---|---|---|
| 垃圾回收 | 概念、引用计数、标记清除 | 第一章 §1.1~1.5 |
| 执行上下文/栈 | 执行栈、递归与入栈出栈 | 第二、三章 |
| 闭包 | 产生闭包、应用、经典题目 | 第五、六章 |
垃圾回收 GC
执行上下文 EC
执行栈 Call Stack
作用域 vs EC
闭包 Closure
0.2 核心名词速查
| 名词 | 解析 |
|---|---|
| 垃圾 | 无法再被程序访问的对象(无引用路径) |
| 内存泄漏 | 应回收的对象仍被引用,内存持续增长 |
| 引用计数 | 记录对象被引用次数,为 0 则回收;循环引用会泄漏 |
| 标记清除 | 从根对象遍历标记可达对象,清除未标记区域(现代 JS 主流) |
| 执行上下文 | 代码执行时的环境:变量、this、作用域链等 |
| 执行栈 | 存放执行上下文的栈结构,后进先出 |
| 作用域 | 函数声明时确定的变量查找范围(静态) |
| 闭包 | 函数 + 其创建时的词法环境,可访问外层变量 |
0.3 为什么要学本篇?
| 维度 | 价值 |
|---|---|
| 排错 | 理解 var 提升、闭包变量、循环中 i 错乱等经典 Bug |
| 性能 | 避免闭包滥用、定时器/DOM 引用导致的内存泄漏 |
| 面试 | GC 算法、执行上下文、闭包题为高频考点 |
| 框架 | React Hooks、Vue 响应式依赖追踪都建立在作用域与闭包之上 |
| 进阶 | 为阅读 V8、Worker、异步调度打基础 |
一、垃圾回收机制
1.1 核心概念
垃圾(Garbage):程序中不再被使用的数据对象
垃圾回收(Garbage Collection):自动识别并回收垃圾对象,释放内存空间的过程
内存分配
对象使用
对象失去引用
标记为垃圾
垃圾回收器
内存释放
1.2 内存相关术语
| 术语 | 描述 | 原因 |
|---|---|---|
| 内存溢出 | 程序需要的内存超过可用内存 | 内存泄漏积累过多 |
| 内存泄漏 | 应该回收的垃圾未被回收 | 代码设计问题 |
语言分类:
| 类型 | 特点 | 代表语言 |
|---|---|---|
| 手动管理 | 程序员需要手动申请和释放内存 | C, C++ |
| 自动管理 | 垃圾回收器自动管理内存 | Java, Python, JavaScript |
1.3 引用计数算法
工作原理
是
否
对象创建
引用计数 = 0
新增引用
引用计数 +1
删除引用
引用计数 -1
引用计数 = 0?
回收对象
规则:
- 对象创建时,引用计数初始化为 0
- 每新增一个引用,计数 +1
- 每删除一个引用,计数 -1
- 当计数变为 0 时,对象被回收
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>引用计数算法演示</title>
<style>
.demo-container {
max-width: 900px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.memory-box {
padding: 20px;
background: #f5f5f5;
border-radius: 10px;
margin: 20px 0;
}
.object-visual {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 15px;
margin: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
min-width: 100px;
}
.object-visual .name {
font-size: 14px;
margin-bottom: 5px;
}
.object-visual .count {
font-size: 24px;
font-weight: bold;
}
.reference-line {
display: flex;
align-items: center;
margin: 10px 0;
}
.reference-line .variable {
padding: 5px 10px;
background: #48dbfb;
color: white;
border-radius: 5px;
margin-right: 10px;
}
.reference-line .arrow {
flex: 1;
text-align: center;
color: #999;
}
.code-block {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 5px;
font-family: monospace;
font-size: 14px;
overflow-x: auto;
}
.circular-ref-warning {
padding: 15px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 5px;
margin: 20px 0;
}
.circular-ref-warning h4 {
margin: 0 0 10px 0;
color: #856404;
}
.circular-ref-visual {
display: flex;
justify-content: space-around;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
margin: 20px 0;
}
.circular-object {
text-align: center;
padding: 20px;
background: #ff6b6b;
color: white;
border-radius: 10px;
position: relative;
}
.circular-arrow {
font-size: 24px;
color: #ff6b6b;
}
</style>
</head>
<body>
<h1 style="text-align: center;">引用计数算法演示</h1>
<div class="demo-container">
<h2>1. 基本引用计数演示</h2>
<div class="code-block">
// 创建对象,引用计数 = 0
var user01 = {name: '小乐'}; // 引用计数 = 1
var address = user01; // 引用计数 = 2
user01 = 200; // 引用计数 = 1
address = null; // 引用计数 = 0 → 对象被回收
</div>
<div class="memory-box">
<h3>引用计数变化过程:</h3>
<div class="object-visual">
<div class="name">{name: '小乐'}</div>
<div class="count" id="refCount">0</div>
</div>
</div>
<h2>2. 循环引用问题(引用计数的致命缺陷)</h2>
<div class="circular-ref-warning">
<h4>⚠️ 引用计数算法的致命缺陷</h4>
<p>当两个对象互相引用时,即使没有其他变量引用它们,引用计数也不会变为 0,导致内存泄漏。</p>
</div>
<div class="circular-ref-visual">
<div class="circular-object">
<div>user01</div>
<div>{name: '小乐'}</div>
</div>
<div class="circular-arrow">⟷</div>
<div class="circular-object">
<div>user02</div>
<div>{name: '老乐'}</div>
</div>
</div>
<div class="code-block">
// 循环引用示例
var user01 = {name: '小乐'}; // 引用计数 = 1
var user02 = {name: '老乐'}; // 引用计数 = 1
user01.children = user02; // user02 引用计数 = 2
user02.children = user01; // user01 引用计数 = 2
user01 = null; // user01 引用计数 = 1
user02 = null; // user02 引用计数 = 1
// 问题:两个对象互相引用,引用计数永远不会变为 0
// 结果:内存泄漏!
</div>
<div style="margin-top: 20px; padding: 15px; background: #d4edda; border-radius: 5px;">
<h4>✅ 解决方案:使用标记清除算法</h4>
<p>现代浏览器主要使用标记清除算法来解决循环引用问题。</p>
</div>
</div>
<script>
// ===== 核心逻辑(详见下方【代码注释】) =====
(function() {
const refCountDisplay = document.getElementById('refCount');
const steps = [
{ count: 0, delay: 0 },
{ count: 1, delay: 1000 },
{ count: 2, delay: 2000 },
{ count: 1, delay: 3000 },
{ count: 0, delay: 4000 }
];
let currentStep = 0;
function animateRefCount() {
if (currentStep >= steps.length) return;
const step = steps[currentStep];
refCountDisplay.textContent = step.count;
// 根据计数改变颜色
if (step.count === 0) {
refCountDisplay.style.color = '#ff6b6b';
refCountDisplay.textContent += ' (回收)';
} else {
refCountDisplay.style.color = 'white';
}
currentStep++;
setTimeout(animateRefCount, step.delay - (currentStep > 1 ? 1000 : 0));
}
setTimeout(animateRefCount, 500);
})();
</script>
</body>
</html>
【代码注释】
1. 页面内嵌的循环引用示例(user01 / user02)
| 步骤 | 代码 | 引用计数变化 |
|---|---|---|
| ① 创建 | var user01 = {name:'小乐'} |
user01 对象 = 1 |
| ② 互相引用 | user01.children = user02 |
user02 = 2(user02 变量 + user01.children) |
| ③ 互相引用 | user02.children = user01 |
user01 = 2(user01 变量 + user02.children) |
| ④ 断开变量 | user01 = null |
user01 对象仍 = 1(被 user02.children 引用) |
| ⑤ 断开变量 | user02 = null |
user02 对象仍 = 1(被 user01.children 引用) |
→ 结论 :两个对象互相持有引用,计数永远 ≥ 1,GC 无法回收 → 内存泄漏。这正是 IE6/7 时代引用计数 GC 的致命缺陷。
2. 动画脚本 steps 数组
javascript
{ count: 0 }, { count: 1 }, { count: 2 }, { count: 1 }, { count: 0 }
模拟「创建 → 被引用 → 被多次引用 → 减少引用 → 归零回收」的完整生命周期。count === 0 时文字变红并标注「(回收)」,直观展示「计数归零 = 可回收」的条件。
3. 面试要点
- 引用计数:优点 是对象无人引用时立即回收;缺点是无法处理循环引用。
- 现代浏览器(V8)不以引用计数为唯一方案,而是在标记清除基础上做分代优化。
- 追问:「WeakMap 为什么 key 必须是对象?」→ 因为 WeakMap 用弱引用存 key,不增加引用计数,对象无其他强引用时可被 GC。
4. 市面应用
- 理解 Node.js
EventEmitter监听器未off导致对象无法回收(本质仍是引用链未断)。 - 前端排查:Chrome Memory → 搜索
Detached节点,往往是 DOM 被 JS 变量强引用。
引用计数优缺点
| 优点 | 缺点 |
|---|---|
| 及时回收垃圾 | 无法处理循环引用 |
| 实现简单 | 需要维护引用计数,有性能开销 |
| 回收时机精确 | 容易导致内存泄漏 |
1.4 标记清除算法
现代 JavaScript 引擎(如 V8)以标记清除及其改进算法为主;下图便于理解「根对象 → 可达性」思路。

工作原理
是
否
标记阶段开始
从根对象出发
遍历所有可达对象
标记可达对象
清除阶段开始
遍历整个内存
是否被标记?
保留对象,清除标记
回收内存
本轮结束
根对象(Root):全局对象、当前执行栈中的变量等
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>标记清除算法演示</title>
<style>
.mark-sweep-demo {
max-width: 1000px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.heap-visualization {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 15px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
margin: 20px 0;
}
.memory-cell {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
font-weight: bold;
transition: all 0.3s;
}
.memory-cell.root {
background: #667eea;
color: white;
border: 3px solid #48dbfb;
}
.memory-cell.reachable {
background: #28a745;
color: white;
}
.memory-cell.unreachable {
background: #dc3545;
color: white;
opacity: 0.3;
}
.memory-cell span {
font-size: 12px;
margin-top: 5px;
}
.controls {
display: flex;
gap: 15px;
justify-content: center;
margin: 30px 0;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 5px;
background: #667eea;
color: white;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.btn:hover {
background: #764ba2;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.phase-indicator {
text-align: center;
padding: 15px;
background: #e3f2fd;
border-radius: 10px;
margin: 20px 0;
font-size: 18px;
font-weight: bold;
}
.legend {
display: flex;
justify-content: center;
gap: 30px;
margin: 20px 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
}
.legend-box {
width: 30px;
height: 30px;
border-radius: 5px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">标记清除算法演示</h1>
<div class="mark-sweep-demo">
<div class="phase-indicator" id="phaseIndicator">
点击"开始标记清除"按钮启动演示
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-box" style="background: #667eea; border: 3px solid #48dbfb;"></div>
<span>根对象</span>
</div>
<div class="legend-item">
<div class="legend-box" style="background: #28a745;"></div>
<span>可达对象</span>
</div>
<div class="legend-item">
<div class="legend-box" style="background: #dc3545;"></div>
<span>不可达对象(垃圾)</span>
</div>
</div>
<div class="heap-visualization" id="heapVisualization">
<!-- 内存单元将通过 JS 生成 -->
</div>
<div class="controls">
<button class="btn" id="startBtn">开始标记清除</button>
<button class="btn" id="resetBtn" disabled>重置</button>
</div>
</div>
<script>
// ===== 核心逻辑(详见下方【代码注释】) =====
(function() {
const heapVisualization = document.getElementById('heapVisualization');
const phaseIndicator = document.getElementById('phaseIndicator');
const startBtn = document.getElementById('startBtn');
const resetBtn = document.getElementById('resetBtn');
// 模拟堆内存结构
const heapStructure = [
{ id: 'Root', name: '全局对象', isRoot: true },
{ id: 'A', name: '对象A', reachable: false },
{ id: 'B', name: '对象B', reachable: false },
{ id: 'C', name: '对象C', reachable: false },
{ id: 'D', name: '对象D', reachable: false },
{ id: 'E', name: '对象E', reachable: false },
{ id: 'F', name: '对象F', reachable: false },
{ id: 'G', name: '对象G', reachable: false },
{ id: 'H', name: '对象H', reachable: false },
{ id: 'I', name: '对象I', reachable: false }
];
// 引用关系:Root → A → B, C;A → D;E → F(孤立)
const references = {
'Root': ['A'],
'A': ['B', 'C', 'D'],
'B': [],
'C': [],
'D': [],
'E': ['F'],
'F': ['E'], // 循环引用
'G': [],
'H': [],
'I': []
};
function renderHeap() {
heapVisualization.innerHTML = '';
heapStructure.forEach(cell => {
const div = document.createElement('div');
div.className = 'memory-cell';
div.id = `cell-${cell.id}`;
if (cell.isRoot) {
div.classList.add('root');
} else if (cell.reachable) {
div.classList.add('reachable');
} else {
div.classList.add('unreachable');
}
div.innerHTML = `
${cell.id}
<span>${cell.name}</span>
`;
heapVisualization.appendChild(div);
});
}
function markReachable(objectId, visited = new Set()) {
if (visited.has(objectId)) return;
visited.add(objectId);
const cell = heapStructure.find(c => c.id === objectId);
if (cell && !cell.isRoot) {
cell.reachable = true;
}
const refs = references[objectId] || [];
refs.forEach(refId => markReachable(refId, visited));
}
async function runMarkSweep() {
startBtn.disabled = true;
// 重置状态
heapStructure.forEach(cell => {
cell.reachable = false;
});
renderHeap();
// 标记阶段
phaseIndicator.textContent = '📍 标记阶段:从根对象开始标记所有可达对象...';
await sleep(500);
// 从根对象开始标记
markReachable('Root');
renderHeap();
await sleep(1000);
// 清除阶段
phaseIndicator.textContent = '🧹 清除阶段:回收所有未被标记的对象...';
await sleep(1000);
// 显示回收结果
const unreached = heapStructure.filter(c => !c.isRoot && !c.reachable);
phaseIndicator.innerHTML = `
✅ 清除完成!<br>
<span style="color: #28a745;">保留了 ${heapStructure.filter(c => c.isRoot || c.reachable).length} 个对象</span> |
<span style="color: #dc3545;">回收了 ${unreached.length} 个对象</span>
`;
// 标记被回收的对象
unreached.forEach(cell => {
const el = document.getElementById(`cell-${cell.id}`);
if (el) {
el.style.opacity = '0.3';
el.innerHTML = `${cell.id}<span>已回收</span>`;
}
});
resetBtn.disabled = false;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function reset() {
heapStructure.forEach(cell => {
cell.reachable = false;
});
renderHeap();
phaseIndicator.textContent = '点击"开始标记清除"按钮启动演示';
startBtn.disabled = false;
resetBtn.disabled = true;
}
startBtn.addEventListener('click', runMarkSweep);
resetBtn.addEventListener('click', reset);
renderHeap();
})();
</script>
</body>
</html>
【代码注释】
1. 堆内存模拟结构 heapStructure
| 对象 ID | 含义 | 初始状态 |
|---|---|---|
Root |
全局对象(window) | 始终可达,不参与回收 |
A→B,C,D |
从根可达的对象链 | 标记阶段被标记为 reachable |
E↔F |
循环引用但无根路径 | 即使互相引用,从 Root 出发无法到达 → 被回收 |
G,H,I |
孤立对象 | 无引用 → 被回收 |
2. markReachable 递归函数(DFS 深度优先)
javascript
function markReachable(objectId, visited = new Set()) {
if (visited.has(objectId)) return; // 防止重复访问
visited.add(objectId);
cell.reachable = true;
references[objectId].forEach(refId => markReachable(refId, visited));
}
visited集合防止在有环图(如 E↔F)中无限递归。- 从
Root出发,沿references邻接表遍历,只标记从根可达的节点 ------这正是标记清除与引用计数的根本区别:可达性 > 引用次数。
3. 两阶段执行流程
| 阶段 | phaseIndicator 提示 |
DOM 变化 |
|---|---|---|
| 标记 | 「从根对象开始标记...」 | 可达格子变绿(.reachable) |
| 清除 | 「回收所有未被标记的对象...」 | 不可达格子透明度 0.3,文字「已回收」 |
4. 关键结论(面试必背)
- 根对象 :
window、当前调用栈中的变量、DOM 树节点等;引擎从这些根出发做可达性分析。 - 循环引用 E↔F 仍被回收:因为没有从 Root 到 E/F 的路径------这是标记清除解决引用计数缺陷的核心机制。
- V8 在此基础上叠加:分代(新生代/老生代)、增量标记、写屏障,减少 STW 暂停。
5. 调试技巧
Chrome DevTools → Memory → Heap Snapshot → 对比两次快照的「Delta」,红色表示新增、灰色表示已回收。
标记清除优缺点
| 优点 | 缺点 |
|---|---|
| 解决循环引用问题 | 需要深度递归遍历 |
| 不需要维护引用计数 | 标记和清除阶段会暂停程序 |
| 实现相对简单 | 碎片化内存问题 |
1.5 两种算法对比
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>垃圾回收算法对比</title>
<style>
.comparison-table {
max-width: 1000px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #667eea;
color: white;
}
.algorithm-name {
font-weight: bold;
color: #667eea;
}
.pros {
color: #28a745;
}
.cons {
color: #dc3545;
}
</style>
</head>
<body>
<h1 style="text-align: center;">垃圾回收算法对比</h1>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>对比维度</th>
<th>引用计数算法</th>
<th>标记清除算法</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>工作原理</strong></td>
<td>维护每个对象的引用计数,计数为0时回收</td>
<td>从根对象标记可达对象,回收未标记对象</td>
</tr>
<tr>
<td><strong>循环引用</strong></td>
<td class="cons">❌ 无法处理,导致内存泄漏</td>
<td class="pros">✅ 可以正确处理</td>
</tr>
<tr>
<td><strong>回收时机</strong></td>
<td class="pros">✅ 引用计数变为0时立即回收</td>
<td>需要等待垃圾回收器运行</td>
</tr>
<tr>
<td><strong>性能开销</strong></td>
<td>每次引用变化都需要更新计数</td>
<td>标记和清除阶段会暂停程序</td>
</tr>
<tr>
<td><strong>实现复杂度</strong></td>
<td class="pros">✅ 相对简单</td>
<td>需要遍历整个对象图</td>
</tr>
<tr>
<td><strong>现代浏览器</strong></td>
<td>很少单独使用</td>
<td class="pros">✅ 主要采用(V8、SpiderMonkey)</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
【代码注释】
本段为对比表格(非可执行代码),用于面试时快速组织答案:
| 对比维度 | 引用计数 | 标记清除 | 面试怎么说 |
|---|---|---|---|
| 循环引用 | ❌ 无法处理 | ✅ 正确处理 | 「标记清除从根出发,无根路径即垃圾」 |
| 回收时机 | 计数归零立即回收 | 需等 GC 调度 | 「标记清除有 STW 暂停,V8 用增量标记缓解」 |
| 性能开销 | 每次引用变化更新计数 | 需遍历整个对象图 | 「V8 分代后只扫描新生代,降低全堆扫描频率」 |
| 现代浏览器 | 几乎不单独使用 | 主流基础 | 「JS 以标记清除为基础,V8 在此之上做分代+增量」 |
记忆口诀:引用计数看「被引用几次」,标记清除看「从根能不能走到」。
建议结合上文 垃圾回收示意图.jpg 与 §1.3/§1.4 的交互演示对照理解。
【本章小结】垃圾回收
| 算法 | 优点 | 缺点 | 现代 JS |
|---|---|---|---|
| 引用计数 | 实现简单、及时回收 | 循环引用泄漏 | 已不作为唯一方案 |
| 标记清除 | 解决循环引用 | 遍历成本 | 主流基础 |
1.6 V8 分代回收进阶
标记清除是理论基础,V8 在此之上做了大量工程优化。了解这些有助于在面试中回答「V8 怎么做 GC」这类进阶问题。
堆的分代结构
存活两次以上
堆内存 Heap
新生代 New Space
短期对象・容量小
老生代 Old Space
长期对象・容量大
Scavenger 算法
Minor GC・高频・快
Mark-Sweep-Compact
Major GC・低频・全面
晋升老生代
| 新生代 | 老生代 | |
|---|---|---|
| 典型内容 | 局部变量、临时对象 | 全局变量、闭包捕获变量、DOM 缓存 |
| GC 算法 | Scavenger(半空间复制) | Mark-Sweep + Compact 整理碎片 |
| GC 频率 | 高频(毫秒级) | 低频(可能造成长暂停) |
| 晋升条件 | 在新生代存活 ≥ 2 次 | --- |
Scavenger(新生代复制算法)
新生代内存分为两个等大的半空间(From-space / To-space):
- 对象始终在 From-space 分配
- GC 触发时,将 From-space 中所有存活对象复制到 To-space
- From-space 整体清空,两者角色互换
- 复制次数 ≥ 2 的对象直接晋升到老生代
优点:无碎片、速度极快;缺点:只用一半内存。
增量标记(Incremental Marking)
普通标记清除会产生 Stop-the-World(STW) 暂停,页面卡顿。V8 引入增量标记:
主线程空闲
多轮后
JS 执行
标记一小片
标记完成
清除/整理
- 把整个标记阶段切成许多小片,穿插在 JS 执行间隙运行
- 配合写屏障(Write Barrier):JS 修改对象引用时通知 GC,避免漏标
- 进一步优化:并发标记(辅助线程并行执行标记,主线程不暂停)
对开发的实际影响
javascript
// 短期对象 → 新生代,GC 快速回收,无需担心
function processRequest(data) {
const temp = data.map(x => x * 2); // 新生代
return temp.reduce((a, b) => a + b, 0);
}
// 长期对象 → 晋升老生代,需要注意
const globalCache = (function () {
const store = new Map(); // 一直存活 → 老生代
return {
get: key => store.get(key),
set: (key, val) => store.set(key, val),
// ✅ 提供清理接口,避免老生代无限增长
clear: () => store.clear()
};
})();
// ❌ 隐形老生代驻留:闭包意外持有大 DOM / 大数组
function attachHandler() {
const bigData = new Array(100000).fill(0); // 被闭包捕获 → 晋升
document.addEventListener('click', function () {
console.log(bigData.length); // bigData 永远无法回收
});
}
// ✅ 修复:用完立即解绑,或只保存必要数据
【代码注释】
1. processRequest --- 短期对象走新生代
javascript
const temp = data.map(x => x * 2); // 每次调用创建,函数返回后无引用
return temp.reduce(...); // temp 在 Scavenger Minor GC 中快速回收
- 局部变量、临时数组在函数执行完毕后失去引用,留在新生代。
- 新生代 GC(Scavenger)频率高、耗时 < 1 ms,对性能影响极小。
2. globalCache IIFE --- 长期对象晋升老生代
javascript
const store = new Map(); // 被闭包持有,整个页面生命周期存活
- 一旦对象在新生代存活 ≥ 2 次 Scavenger,即晋升到老生代。
- 老生代 GC(Mark-Compact)低频但可能 > 100 ms,造成页面卡顿(STW)。
clear()接口是工程必备:允许主动清空 Map,避免老生代无限膨胀。
3. attachHandler --- 闭包意外持有大对象(反面教材)
| 问题 | 原因 | 修复 |
|---|---|---|
bigData 永不回收 |
事件回调闭包持有 bigData 引用 |
解绑监听器 + bigData = null |
| 进入老生代 | 回调长期存活,连带 bigData 晋升 |
只保存必要字段,不缓存整个数组 |
4. 面试标准回答(30 秒版)
「V8 采用分代回收:短期对象在新生代用 Scavenger 半空间复制,快速回收;长期对象晋升到老生代,用 Mark-Compact 整理碎片。配合增量标记和写屏障,将 STW 暂停切分到 JS 执行间隙,减少卡顿。」
1.7 WeakRef 与 FinalizationRegistry(ES2021)
名词 :WeakRef 允许持有对象的"弱引用"------不阻止 GC 回收该对象。FinalizationRegistry 允许在对象被回收时执行清理回调。
MDN 参考 :WeakRef | FinalizationRegistry
javascript
// ===== 核心逻辑(详见下方【代码注释】) =====
// --- WeakRef 示例:弱引用缓存 ---
let cache = new Map();
function getOrCreate(key) {
const ref = cache.get(key);
// deref() 返回原对象;若已被 GC 则返回 undefined
if (ref && ref.deref()) {
console.log('命中缓存:', key);
return ref.deref();
}
const value = { data: new Array(1000).fill(key) }; // 模拟大对象
cache.set(key, new WeakRef(value)); // 弱引用存入缓存
console.log('创建新对象:', key);
return value;
}
const obj1 = getOrCreate('user_1');
// obj1 持有强引用,GC 不会回收
// 如果 obj1 = null 后,GC 可能在下次回收 value,此时 cache 中 WeakRef.deref() 返回 undefined
// --- FinalizationRegistry 示例:监听回收 ---
const registry = new FinalizationRegistry((heldValue) => {
// ⚠️ 回调在 GC 之后异步执行,不保证时机
console.log(`对象已被回收,持有值: ${heldValue}`);
cache.delete(heldValue); // 清理 Map 中的 WeakRef 条目
});
function createTracked(key) {
const obj = { data: key };
// 注册:obj 被回收时,以 key 作为 heldValue 调用回调
registry.register(obj, key);
return obj;
}
let tracked = createTracked('session_42');
// tracked = null; // 让 GC 有机会回收,触发 FinalizationRegistry 回调
【代码注释】
1. getOrCreate --- WeakRef 可选缓存模式
javascript
const ref = cache.get(key);
if (ref && ref.deref()) { ... } // 缓存命中:对象仍存活
cache.set(key, new WeakRef(value)); // 存入弱引用,不阻止 GC
| 调用时机 | deref() 返回值 |
行为 |
|---|---|---|
强引用 obj1 仍存在 |
原对象 | 命中缓存,直接返回 |
obj1 = null 且 GC 已回收 |
undefined |
视为未命中,重新 getOrCreate 创建 |
- 关键 :
Map本身持有WeakRef对象(强引用),但WeakRef不阻止value被回收;deref()是唯一的"探测"手段。
2. FinalizationRegistry --- 回收后清理副作用
javascript
registry.register(obj, key); // obj 被回收时,以 key 为 heldValue 调用回调
- 回调在 GC 之后 异步执行,时机不确定(可能延迟数秒),禁止在其中做 UI 更新或同步逻辑。
- 典型用途:清理
Map中的过期WeakRef条目、释放关联的非 JS 资源(文件句柄等)。
3. 与 WeakMap / WeakSet 对比
| API | 弱引用对象 | 能否主动读取 | 典型场景 |
|---|---|---|---|
WeakMap |
key 必须是对象 | 通过 key 取值 | DOM 节点 → 元数据映射 |
WeakSet |
成员必须是对象 | 只能判断是否存在 | 标记"已处理"的对象 |
WeakRef |
任意对象 | deref() 手动读取 |
跨模块共享的可选缓存 |
4. 生产建议
- 99% 场景用
WeakMap即可;WeakRef仅用于需要"探测对象是否仍存活"的高级缓存。 - 市面应用 :图片懒加载缓存、Vue 3
effectScope清理、大数据表格行对象的弱引用索引。
1.8 V8 Hidden Class(隐藏类)优化
名词 :V8 为每个对象维护一个隐藏类(Hidden Class / Shape),记录其属性布局。相同属性顺序的对象共享同一隐藏类,JIT 编译器可对其访问做高效内联缓存(Inline Cache)。
javascript
// ===== 核心逻辑(详见下方【代码注释】) =====
// ❌ 反模式:动态添加属性顺序不一致 → 每个对象拥有不同隐藏类 → 无法 JIT 优化
function createPoint_BAD(x, y) {
const p = {};
if (Math.random() > 0.5) {
p.x = x;
p.y = y;
} else {
p.y = y; // 顺序不同!
p.x = x;
}
return p;
}
// ✅ 最佳实践 1:构造函数中按固定顺序初始化所有属性
class Point {
constructor(x, y) {
this.x = x; // 始终 x 先
this.y = y; // 始终 y 后
}
}
// ✅ 最佳实践 2:避免 delete 属性(会导致隐藏类退化为字典模式)
const p = new Point(1, 2);
// delete p.x; ❌ 会使对象进入"慢路径",失去隐藏类优化
// ✅ 最佳实践 3:不要在构造后动态添加新属性
// p.z = 3; ❌ 会创建新的隐藏类(但比 delete 代价小)
【代码注释】
1. createPoint_BAD --- 属性顺序不一致导致隐藏类分裂
javascript
if (Math.random() > 0.5) { p.x = x; p.y = y; }
else { p.y = y; p.x = x; } // 顺序相反!
- V8 为
{x, y}和{y, x}创建两个不同的 Hidden Class。 - JIT 无法对两种布局做统一的内联缓存(Inline Cache),每次属性访问可能触发去优化(Deopt)。
2. class Point --- 正确模式
javascript
constructor(x, y) {
this.x = x; // 始终先 x
this.y = y; // 始终后 y
}
- 所有
new Point()实例共享同一 Hidden Class → 属性访问走固定偏移量,速度接近 C 结构体。
3. 三种退化触发条件
| 操作 | 后果 | 性能影响 |
|---|---|---|
delete p.x |
对象退化为字典模式 | 属性访问从 O(1) 偏移 → O(n) 哈希查找 |
p.z = 3(构造后新增) |
创建新 Hidden Class | 轻微,但破坏 IC 稳定性 |
| 属性顺序不一致 | 多个 Hidden Class 并存 | JIT 无法统一优化 |
4. 验证与排查
Chrome DevTools → Performance → 录制 → 查看 Deoptimization 事件,若热路径频繁 Deopt,检查对象属性是否动态增删。
5. 框架层面的体现
- React Fiber 节点、Vue 3 响应式对象的属性在构造时一次性、按固定顺序初始化,正是为了避免 Hidden Class 分裂。
- 面试追问:「为什么 Vue 不推荐
delete obj.prop?」→ 触发字典模式 + 破坏响应式追踪。
二、执行上下文
2.1 什么是执行上下文?
执行上下文(Execution Context):代码执行时的环境信息,包含了变量、函数、this 等执行所需的所有信息。
全局代码
函数代码
eval代码
JavaScript代码执行
代码类型
创建全局执行上下文
创建函数执行上下文
创建eval执行上下文
执行上下文对象
变量对象 VO
作用域链
this指向
2.2 全局执行上下文
创建时机:打开页面时
生命周期:
javascript
// ===== 核心逻辑(详见下方【代码注释】) =====
// 1. 创建阶段(代码执行前)
window 对象创建
↓
确定全局执行上下文对象为 window
↓
预处理:
- 找 var 声明的变量,添加为 window 属性(undefined)
- 找 function 声明的函数,添加为 window 属性(函数对象)
- this 赋值为 window
↓
// 2. 执行阶段
console.log(name); // undefined(变量提升)
function hello() {} // 函数声明已可用
var name = '张三'; // 赋值
// 3. 销毁阶段
关闭页面时销毁
【代码注释】
本段为伪代码流程图(非可执行 JS),描述全局执行上下文的三阶段生命周期:
| 阶段 | 引擎动作 | 对开发者的影响 |
|---|---|---|
| ① 创建阶段 | 创建 window;扫描 var → 挂到 window(undefined);扫描 function → 挂函数对象;this = window |
解释「变量提升」:console.log(name) 在 var name 前得到 undefined |
| ② 执行阶段 | 逐行执行全局代码,赋值、调用函数 | var name = '张三' 才真正赋值 |
| ③ 销毁阶段 | 关闭页面/标签页时销毁全局 EC | 全局变量随页面卸载而释放 |
关键细节
function声明在创建阶段整体提升 (含函数体),所以可以在声明前调用hello()。var只提升变量名 (值为undefined),赋值留在执行阶段------这就是「提升」与「赋值」分离的本质。- ES6 后全局 EC 还包含词法环境 (
let/const的 TDZ),但var/function仍走 VO(变量对象)路径。
面试一句话:全局 EC 在页面加载时创建一次,直到页面关闭才销毁,整个程序周期只有一个。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全局执行上下文演示</title>
<style>
.execution-demo {
max-width: 900px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.phase-timeline {
position: relative;
padding-left: 100px;
}
.phase-timeline::before {
content: '';
position: absolute;
left: 40px;
top: 0;
bottom: 0;
width: 4px;
background: #e0e0e0;
}
.phase-item {
position: relative;
margin-bottom: 30px;
}
.phase-number {
position: absolute;
left: -70px;
width: 30px;
height: 30px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.phase-content {
background: #f5f5f5;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #667eea;
}
.phase-content h3 {
margin-top: 0;
color: #667eea;
}
.code-output {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 5px;
font-family: monospace;
margin: 10px 0;
}
.window-object {
background: #e3f2fd;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
.property-item {
display: flex;
justify-content: space-between;
padding: 10px;
background: white;
border-radius: 5px;
margin: 5px 0;
}
.property-name {
font-weight: bold;
color: #667eea;
}
.property-value {
color: #28a745;
}
</style>
</head>
<body>
<h1 style="text-align: center;">全局执行上下文详解</h1>
<div class="execution-demo">
<div class="phase-timeline">
<div class="phase-item">
<div class="phase-number">1</div>
<div class="phase-content">
<h3>创建阶段(页面打开时)</h3>
<p>浏览器创建 window 对象,并将其作为全局执行上下文对象</p>
<div class="code-output">
// 伪代码表示
GlobalExecutionContext = {
VariableObject: {
// 预处理阶段添加属性
},
ScopeChain: [],
this: window
}
</div>
</div>
</div>
<div class="phase-item">
<div class="phase-number">2</div>
<div class="phase-content">
<h3>预处理阶段</h3>
<p>在代码执行前,对全局代码进行预处理:</p>
<ul>
<li>找 <code>var</code> 声明的变量,添加为 window 属性(值为 undefined)</li>
<li>找 <code>function</code> 声明的函数,添加为 window 属性(值为函数对象)</li>
<li>给 <code>this</code> 赋值为 window</li>
</ul>
</div>
</div>
<div class="phase-item">
<div class="phase-number">3</div>
<div class="phase-content">
<h3>window 对象属性预览</h3>
<div class="window-object" id="windowPreview">
<!-- 通过 JS 生成 -->
</div>
</div>
</div>
<div class="phase-item">
<div class="phase-number">4</div>
<div class="phase-content">
<h3>代码执行阶段</h3>
<p>顺序执行全局代码,为变量赋值</p>
<div class="code-output" id="executionOutput">
<!-- 执行结果 -->
</div>
</div>
</div>
</div>
</div>
<script>
// ===== 核心逻辑(详见下方【代码注释】) =====
(function() {
// 模拟全局代码
var globalVar = undefined; // 变量声明
var name = '张三'; // 变量声明并赋值
function globalFunction() { // 函数声明
return '我是全局函数';
}
// 显示 window 对象属性
const windowPreview = document.getElementById('windowPreview');
const properties = [
{ name: 'name', value: 'undefined → "张三"', desc: 'var 声明的变量' },
{ name: 'globalFunction', value: 'function', desc: 'function 声明的函数' },
{ name: 'this', value: 'window', desc: 'this 指向 window' },
{ name: 'window', value: 'window', desc: 'window 自己' }
];
properties.forEach(prop => {
const div = document.createElement('div');
div.className = 'property-item';
div.innerHTML = `
<span class="property-name">${prop.name}</span>
<span class="property-value">${prop.value}</span>
`;
windowPreview.appendChild(div);
});
// 执行示例代码
const executionOutput = document.getElementById('executionOutput');
executionOutput.innerHTML = `
console.log(name); // 输出: 张三
console.log(globalFunction()); // 输出: 我是全局函数
console.log(this === window); // 输出: true
`;
console.log('全局变量 name:', name);
console.log('全局函数调用:', globalFunction());
console.log('this 等于 window:', this === window);
})();
</script>
</body>
</html>
【代码注释】
1. 脚本模拟的全局代码
javascript
var globalVar = undefined; // 创建阶段:var 已提升,值为 undefined
var name = '张三'; // 执行阶段:赋值
function globalFunction() { ... } // 创建阶段:整体提升为函数对象
2. windowPreview 属性表
| 属性 | 创建阶段值 | 执行阶段值 | 说明 |
|---|---|---|---|
name |
undefined |
"张三" |
var 提升后赋值 |
globalFunction |
function |
function |
函数声明整体提升 |
this |
window |
window |
全局 this 固定指向 window |
3. 控制台验证(打开页面后执行)
javascript
console.log(this === window); // true --- 全局 this
console.log(typeof globalFunction); // "function" --- 可在声明前调用
4. DevTools 调试技巧
Sources 面板 → 在 <script> 第一行打断点 → 单步执行(F10)→ 观察 Scope 面板中 Local / Global 变量的变化,直观感受「创建阶段已有变量名,执行阶段才赋值」。
2.3 函数执行上下文
创建时机:每次调用函数时
生命周期:
函数被调用
创建执行上下文对象
预处理阶段
形参作为属性
arguments对象
var变量声明
function函数声明
this赋值
代码执行阶段
函数调用结束
执行上下文出栈销毁
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>函数执行上下文演示</title>
<style>
.function-demo {
max-width: 1000px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.function-call-flow {
display: flex;
flex-direction: column;
gap: 15px;
margin: 20px 0;
}
.flow-step {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: #f5f5f5;
border-radius: 10px;
}
.step-number {
width: 40px;
height: 40px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
}
.step-content {
flex: 1;
}
.step-content h4 {
margin: 0 0 10px 0;
color: #333;
}
.context-object {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 5px;
font-family: monospace;
font-size: 14px;
}
.key-point {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">函数执行上下文详解</h1>
<div class="function-demo">
<div class="key-point">
<strong>⚠️ 重要概念:</strong>函数每次调用都会创建一个新的执行上下文对象,调用结束后立即销毁。
</div>
<h3>函数执行上下文创建流程</h3>
<div class="function-call-flow">
<div class="flow-step">
<div class="step-number">1</div>
<div class="step-content">
<h4>函数被调用</h4>
<code>function foo(a, b) { ... }</code>
<br>
<code>foo(1, 2);</code> // 触发创建
</div>
</div>
<div class="flow-step">
<div class="step-number">2</div>
<div class="step-content">
<h4>创建执行上下文对象</h4>
<div class="context-object">
FunctionExecutionContext = {
VariableObject: { ... },
ScopeChain: [ ... ],
this: ...
}
</div>
</div>
</div>
<div class="flow-step">
<div class="step-number">3</div>
<div class="step-content">
<h4>预处理阶段</h4>
<ul style="list-style: none; padding: 0;">
<li>✅ 将形参作为属性,赋值为实参</li>
<li>✅ 创建 arguments 对象</li>
<li>✅ 找 var 变量声明,添加属性(值为 undefined)</li>
<li>✅ 找 function 函数声明,添加属性(值为函数)</li>
<li>✅ 确定 this 指向</li>
</ul>
</div>
</div>
<div class="flow-step">
<div class="step-number">4</div>
<div class="step-content">
<h4>代码执行阶段</h4>
<p>顺序执行函数体内的代码</p>
</div>
</div>
<div class="flow-step">
<div class="step-number">5</div>
<div class="step-content">
<h4>函数调用结束</h4>
<p>执行上下文对象出栈,等待垃圾回收</p>
</div>
</div>
</div>
<div class="key-point">
<strong>💡 记忆技巧:</strong>函数执行上下文 = VO(变量对象) + 作用域链 + this
</div>
</div>
</body>
</html>
【代码注释】
本段为静态 HTML 流程说明 (无 <script> 逻辑),用五步卡片展示函数 EC 生命周期:
| 步骤 | 时机 | 引擎动作 |
|---|---|---|
| 1 | foo(1, 2) 被调用 |
触发创建函数 EC |
| 2 | 创建阶段 | 构建 FunctionExecutionContext = { VO, ScopeChain, this } |
| 3 | 预处理 | 形参 a=1, b=2;创建 arguments;提升局部 var/function;确定 this |
| 4 | 执行阶段 | 顺序执行函数体内代码 |
| 5 | return 或执行完毕 |
EC 出栈销毁,局部变量失去引用(若无闭包则 GC 回收) |
与全局 EC 的核心差异
| 对比项 | 全局 EC | 函数 EC |
|---|---|---|
| 数量 | 整个程序 1 个 | 每次调用 1 个 (foo() 调 3 次 = 3 个 EC) |
| 变量对象 | window |
隐式 AO(Activation Object) |
this |
固定 window |
由调用方式决定(默认绑定/隐式/显式/new) |
| 生命周期 | 页面关闭 | 函数返回即销毁 |
记忆公式 :函数 EC = AO(变量对象)+ 作用域链 + this,每次调用都是全新的一份。
2.4 执行上下文对比
| 特性 | 全局执行上下文 | 函数执行上下文 |
|---|---|---|
| 创建时机 | 页面打开时 | 函数调用时 |
| 销毁时机 | 页面关闭时 | 函数调用结束 |
| 数量 | 整个程序周期只有1个 | 每次调用创建1个 |
| 变量对象 | window 对象 | 隐式创建的 AO 对象 |
| this 指向 | window | 调用函数的对象 |
2.5 变量提升与暂时性死区(TDZ)
执行上下文创建阶段 的预处理规则,直接决定了
var/let/const/ 函数声明的行为差异,是面试高频考点。
提升规则对比
var 变量
function 声明
let / const
创建阶段:扫描声明
声明类型
VO/AO 添加属性
初始值 = undefined
VO/AO 添加属性
初始值 = 函数对象
登记绑定
进入 TDZ 暂时性死区
执行阶段:赋值
执行到声明行 → 退出 TDZ
才可访问
javascript
// ===== var 提升 =====
console.log(a); // undefined(提升了,但值还没赋)
var a = 1;
console.log(a); // 1
// ===== function 声明整体提升 =====
foo(); // ✅ 正常执行:'foo'
function foo() { console.log('foo'); }
// ===== var 函数表达式:只提升变量名 =====
bar(); // ❌ TypeError: bar is not a function
var bar = function () { console.log('bar'); };
// ===== let / const 暂时性死区 =====
console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 2;
// ===== const 不可重新赋值 =====
const PI = 3.14;
PI = 3; // ❌ TypeError: Assignment to constant variable
// ✅ 但对象/数组内部属性可变
const obj = { x: 1 };
obj.x = 2; // ✅ 合法
【代码注释】
逐段执行结果预测(建议先在控制台验证)
| 代码 | 输出 / 行为 | 原因 |
|---|---|---|
console.log(a) |
undefined |
var a 在创建阶段已提升,值为 undefined |
foo() |
'foo' |
function 声明整体提升,创建阶段即可调用 |
bar() |
TypeError: bar is not a function |
var bar 提升为 undefined,此时 bar 不是函数 |
console.log(b) |
ReferenceError |
let b 在 TDZ 中,创建阶段已知存在但禁止访问 |
PI = 3 |
TypeError |
const 不可重新赋值绑定 |
obj.x = 2 |
✅ 合法 | const 只保护绑定,不保护对象内部属性 |
TDZ(暂时性死区)本质
创建阶段:let/const 已登记到词法环境,但标记为「未初始化」
↓
执行到声明行:绑定完成,退出 TDZ
↓
声明行之前访问 → ReferenceError(比 var 的 undefined 更严格)
设计意图 :var 的 undefined 掩盖了「先用后声明」的 Bug;TDZ 强制你在使用前必须完成声明,与 const/let 的块级作用域配合,让代码更安全。
经典面试题:提升优先级
javascript
// 函数声明优先于 var
var fn;
function fn() { return 1; }
console.log(typeof fn); // "function"(函数声明覆盖了 var 的 undefined)
// 多个同名函数声明:后者覆盖前者
function fn() { return 2; }
function fn() { return 3; }
console.log(fn()); // 3
// let/const 块级作用域 vs var 函数作用域
{
var x = 10; // 泄漏到外层函数
let y = 20; // 仅在块内有效
}
console.log(x); // 10
console.log(y); // ❌ ReferenceError: y is not defined
【代码注释】
1. 同名 var 与 function 的提升优先级
javascript
var fn;
function fn() { return 1; }
console.log(typeof fn); // "function"
创建阶段等价于:
javascript
function fn() { return 1; } // function 后声明,覆盖 var 的 undefined
var fn; // var 发现已有 function,不再覆盖
2. 多个同名 function 声明:后者覆盖前者
javascript
function fn() { return 2; }
function fn() { return 3; }
console.log(fn()); // 3
3. var vs let 块级作用域
javascript
{ var x = 10; let y = 20; }
console.log(x); // 10 --- var 泄漏到外层
console.log(y); // ReferenceError --- let 仅在块内有效
4. 工程规范
| 声明方式 | 提升行为 | 作用域 | 推荐场景 |
|---|---|---|---|
const |
TDZ,不提升为可用 | 块级 | 默认首选 |
let |
TDZ | 块级 | 需要重新赋值的变量 |
var |
提升为 undefined |
函数/全局 | 避免使用 |
function |
整体提升 | 函数 | 需要声明提升的函数 |
【本章小结】执行上下文
| 知识点 | 核心要点 |
|---|---|
| 全局 EC | 页面加载时创建,全局对象(window/global)+ this |
| 函数 EC | 每次调用创建,包含变量环境、词法环境、this 绑定 |
| 创建阶段 | 变量提升(var→undefined,函数声明→整体提升) |
| 执行阶段 | 按代码顺序赋值、调用 |
let/const |
块级作用域,存在暂时性死区(TDZ),不提升 |
| 提升优先级 | function 声明 > var(同名时函数覆盖) |
记忆口诀 :EC 分两步------先"创建"扫描变量/函数,再"执行"逐行赋值 。这解释了为什么
var声明在前赋值在后会得到undefined,而不是ReferenceError。
三、执行栈
3.1 什么是执行栈?
执行栈(Execution Stack):也叫调用栈,是一种后进先出(LIFO)的数据结构,用于管理执行上下文对象。
执行栈
栈底: 全局执行上下文
func3 执行上下文
func2 执行上下文
func1 执行上下文
栈顶: 当前执行
3.2 执行栈工作原理
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>执行栈演示</title>
<style>
.stack-demo {
max-width: 800px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.stack-visual {
display: flex;
flex-direction: column-reverse;
align-items: center;
gap: 5px;
padding: 30px;
background: #f5f5f5;
border-radius: 10px;
min-height: 400px;
position: relative;
}
.stack-frame {
width: 300px;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 5px;
text-align: center;
animation: pushIn 0.3s ease-out;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
.stack-frame.global {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
}
.stack-frame .function-name {
font-weight: bold;
font-size: 16px;
}
.stack-frame .locals {
font-size: 12px;
margin-top: 5px;
opacity: 0.9;
}
@keyframes pushIn {
from {
transform: translateX(100px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes popOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(-100px);
opacity: 0;
}
}
.stack-label {
position: absolute;
right: 20px;
top: 20px;
font-size: 14px;
color: #666;
}
.controls {
display: flex;
gap: 15px;
justify-content: center;
margin: 20px 0;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 5px;
background: #667eea;
color: white;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
}
.btn:hover:not(:disabled) {
background: #764ba2;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.code-display {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 5px;
font-family: monospace;
margin: 20px 0;
}
.current-line {
background: rgba(255, 255, 0, 0.3);
}
</style>
</head>
<body>
<h1 style="text-align: center;">执行栈可视化演示</h1>
<div class="stack-demo">
<div class="code-display" id="codeDisplay">
function func3() {
console.log('func3 执行');
}
function func2() {
console.log('func2 执行');
func3();
}
function func1() {
console.log('func1 执行');
func2();
}
func1();
</div>
<div class="stack-visual" id="stackVisual">
<div class="stack-label">栈顶 ↑(活跃执行)</div>
<div class="stack-frame global">
<div class="function-name">全局执行上下文</div>
</div>
</div>
<div class="controls">
<button class="btn" id="stepBtn">单步执行</button>
<button class="btn" id="autoBtn" disabled>自动执行</button>
<button class="btn" id="resetBtn">重置</button>
</div>
</div>
<script>
// ===== 核心逻辑(详见下方【代码注释】) =====
(function() {
const stackVisual = document.getElementById('stackVisual');
const stepBtn = document.getElementById('stepBtn');
const autoBtn = document.getElementById('autoBtn');
const resetBtn = document.getElementById('resetBtn');
// 模拟函数调用序列
const callSequence = [
{ action: 'call', func: 'func1', locals: '{}' },
{ action: 'call', func: 'func2', locals: '{}' },
{ action: 'call', func: 'func3', locals: '{}' },
{ action: 'return', func: 'func3' },
{ action: 'return', func: 'func2' },
{ action: 'return', func: 'func1' }
];
let currentStep = 0;
let stack = ['全局'];
function renderStack() {
stackVisual.innerHTML = '<div class="stack-label">栈顶 ↑(活跃执行)</div>';
stack.forEach((funcName, index) => {
const frame = document.createElement('div');
frame.className = 'stack-frame';
if (funcName === '全局') {
frame.classList.add('global');
}
frame.innerHTML = `
<div class="function-name">${funcName}</div>
${index === stack.length - 1 && funcName !== '全局' ? '<div class="locals">执行中...</div>' : ''}
`;
stackVisual.appendChild(frame);
});
}
function step() {
if (currentStep >= callSequence.length) {
stepBtn.disabled = true;
autoBtn.disabled = true;
return;
}
const step = callSequence[currentStep];
if (step.action === 'call') {
stack.push(step.func);
} else {
if (stack.length > 1) {
stack.pop();
}
}
renderStack();
currentStep++;
if (currentStep >= callSequence.length) {
stepBtn.disabled = true;
autoBtn.disabled = true;
}
}
function autoRun() {
stepBtn.disabled = true;
autoBtn.disabled = true;
const interval = setInterval(() => {
if (currentStep >= callSequence.length) {
clearInterval(interval);
return;
}
step();
}, 1000);
}
function reset() {
currentStep = 0;
stack = ['全局'];
renderStack();
stepBtn.disabled = false;
autoBtn.disabled = false;
}
stepBtn.addEventListener('click', step);
autoBtn.addEventListener('click', autoRun);
resetBtn.addEventListener('click', reset);
renderStack();
})();
</script>
</body>
</html>
【代码注释】
1. callSequence 模拟的调用链
func1() → func2() → func3() → return func3 → return func2 → return func1
| 步骤 | action |
栈变化(栈顶在上) |
|---|---|---|
| 初始 | --- | [全局] |
| call func1 | push | [全局, func1] |
| call func2 | push | [全局, func1, func2] |
| call func3 | push | [全局, func1, func2, func3] ← 当前执行 |
| return func3 | pop | [全局, func1, func2] |
| return func2 | pop | [全局, func1] |
| return func1 | pop | [全局] |
2. renderStack 与 LIFO 原则
- 只有栈顶的 EC 处于「执行中」状态;下层 EC 暂停等待。
stack.pop()在return时触发,对应函数 EC 销毁、局部变量出作用域。
3. 栈溢出(Stack Overflow)
javascript
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 每次递归 push 一个新 EC
}
factorial(100000); // RangeError: Maximum call stack size exceeded
- 浏览器执行栈深度通常 约 1 万~2 万层(因引擎而异)。
- 修复:改尾递归(需引擎优化)或改循环;React 组件嵌套过深也会触发。
4. DevTools 对照
Sources 打断点后,左侧 Call Stack 面板即执行栈的实时可视化------点击任意帧可查看该帧的局部变量,与本文 stack 数组完全对应。
【本章小结】执行栈
| 知识点 | 核心要点 |
|---|---|
| 数据结构 | 后进先出(LIFO)栈 |
| 入栈时机 | 调用函数 / 进入全局 |
| 出栈时机 | 函数返回 / 抛出未捕获异常 |
| 栈溢出 | 递归无终止条件 → Maximum call stack size exceeded |
| DevTools | Call Stack 面板实时查看调用链,点击帧可查看该帧变量 |
调试技巧 :在 Chrome DevTools Sources 打断点后,左侧 Call Stack 面板即是执行栈的可视化------最顶层是当前正在执行的函数。
四、作用域与执行上下文
4.1 核心区别
作用域
静态-词法作用域
函数声明时确定
执行上下文
动态
函数调用时创建
| 对比维度 | 作用域 | 执行上下文 |
|---|---|---|
| 性质 | 静态(词法作用域) | 动态 |
| 确定时机 | 函数声明时 | 函数调用时 |
| 数量 | 一个函数一个作用域 | 每次调用创建一个 |
| 生命周期 | 永久存在 | 调用结束销毁 |
| 包含内容 | 变量、函数、参数 | VO、作用域链、this |
4.2 关系图
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>作用域与执行上下文关系</title>
<style>
.scope-demo {
max-width: 1000px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.diagram-container {
display: flex;
justify-content: space-between;
padding: 30px;
background: #f5f5f5;
border-radius: 10px;
margin: 20px 0;
}
.scope-box, .context-box {
flex: 1;
margin: 0 15px;
}
.scope-box h3, .context-box h3 {
text-align: center;
color: #667eea;
margin-bottom: 20px;
}
.scope-item, .context-item {
padding: 15px;
background: white;
border-radius: 5px;
margin: 10px 0;
border-left: 4px solid #667eea;
}
.context-item {
border-left-color: #28a745;
}
.relationship-arrow {
text-align: center;
font-size: 24px;
color: #667eea;
padding: 20px;
}
.key-difference {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">作用域与执行上下文的关系</h1>
<div class="scope-demo">
<div class="key-difference">
<strong>🎯 核心结论:</strong>执行上下文从属于所在的作用域。全局执行上下文对应全局作用域,函数执行上下文对应函数作用域。
</div>
<div class="diagram-container">
<div class="scope-box">
<h3>📌 作用域(静态)</h3>
<div class="scope-item">
<strong>全局作用域</strong>
<p>函数外部区域</p>
</div>
<div class="scope-item">
<strong>函数作用域</strong>
<p>函数内部区域</p>
</div>
<div class="scope-item">
<strong>块级作用域</strong>
<p>let/const 块区域</p>
</div>
</div>
<div class="relationship-arrow">
← 从属于 →
</div>
<div class="context-box">
<h3>⚙️ 执行上下文(动态)</h3>
<div class="context-item">
<strong>全局执行上下文</strong>
<p>页面创建时生成</p>
</div>
<div class="context-item">
<strong>函数执行上下文</strong>
<p>每次调用创建一个</p>
</div>
<div class="context-item">
<strong>eval执行上下文</strong>
<p>eval函数创建</p>
</div>
</div>
</div>
<div style="margin-top: 30px; padding: 20px; background: #e3f2fd; border-radius: 10px;">
<h4>📝 示例说明:</h4>
<div class="code-block">
function outer() {
var a = 10;
function inner() {
var b = 20;
console.log(a + b); // 可以访问外层作用域的 a
}
return inner;
}
// 作用域关系:outer 作用域包含 inner 作用域
// 执行上下文:每次调用 outer() 创建一个,每次调用 inner() 也创建一个
</div>
</div>
</div>
</body>
</html>
【代码注释】
本段为静态关系说明页 (无 <script>),核心示例:
javascript
function outer() {
var a = 10;
function inner() {
var b = 20;
console.log(a + b); // inner 通过作用域链访问 outer 的 a
}
return inner;
}
作用域 vs 执行上下文 --- 逐点对照
| 维度 | 作用域(Scope) | 执行上下文(EC) |
|---|---|---|
| 何时确定 | 写代码时(词法/静态) | 运行时(调用时动态创建) |
| 数量关系 | outer 1 个作用域 |
outer() 调 N 次 = N 个 EC |
| 存储内容 | 变量/函数的查找规则 | VO + 作用域链 + this 的具体值 |
| 与闭包关系 | 决定 inner 能访问哪些外层变量 | 提供运行时变量的实际绑定值 |
关系一句话 :执行上下文的作用域链指向定义时的词法作用域;EC 是「现场」,作用域是「地图」。
面试标准答法:「作用域在函数定义时就确定了,与调用位置无关;执行上下文在每次函数调用时创建,包含变量对象、作用域链和 this。闭包让 inner 在 outer 的 EC 销毁后,仍能通过作用域链访问 outer 作用域中的变量。」