Day17_JavaScript高级核心垃圾回收执行上下文闭包完全指南(上)

JavaScript 高级核心:垃圾回收、执行上下文与闭包完全指南

本篇深入 JavaScript 运行时机制 :内存如何回收、代码如何执行、闭包如何形成。配有 Mermaid 图示可运行 HTML 示例【代码注释】 。示意图可参考同目录 垃圾回收示意图.jpg

权威参考


目录


零、导读与学习价值

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?
回收对象

规则

  1. 对象创建时,引用计数初始化为 0
  2. 每新增一个引用,计数 +1
  3. 每删除一个引用,计数 -1
  4. 当计数变为 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) 始终可达,不参与回收
AB,C,D 从根可达的对象链 标记阶段被标记为 reachable
EF 循环引用但无根路径 即使互相引用,从 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):

  1. 对象始终在 From-space 分配
  2. GC 触发时,将 From-space 中所有存活对象复制到 To-space
  3. From-space 整体清空,两者角色互换
  4. 复制次数 ≥ 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 更严格)

设计意图varundefined 掩盖了「先用后声明」的 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. 同名 varfunction 的提升优先级

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 绑定
创建阶段 变量提升(varundefined,函数声明→整体提升)
执行阶段 按代码顺序赋值、调用
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 作用域中的变量。」

相关推荐
卷帘依旧17 小时前
Transpiler和Polyfill分别是什么作用
javascript
山上三树17 小时前
协程详细介绍
开发语言
赵钰老师17 小时前
R语言与作物模型(以DSSAT模型为例)融合应用
开发语言·数据分析·r语言
是星辰吖~17 小时前
C++_string类_调用及模拟实现
开发语言·c++
梦想的旅途217 小时前
实现企微外部群主动发送接口:从 0 到 1 实现主动给客户发送的业务实战
java·开发语言·企业微信
csdn_aspnet17 小时前
C++ 算法 LeetCode 编号 70 - 爬楼梯
开发语言·c++·算法·leetcode
神仙别闹17 小时前
基于C语言来实现图形界面画板的功能
c语言·开发语言·单片机
xyq202417 小时前
AJAX 简介
开发语言
清风一徐17 小时前
Python文件处理
开发语言·python
I Promise3418 小时前
C++ 单例模式超详细讲解
开发语言·c++·单例模式