前置知识: HTML5、CSS

JavaScript 学习总结:核心知识体系回顾

9 min中级

回顾 JavaScript 模块的核心知识体系:类型与作用域、闭包与原型、Promise 与事件循环、DOM 与模块化、内存管理,并用虚拟歌手音乐平台的示例串联全部要点。

JavaScript 模块共 64 篇文档,从”JavaScript 是什么”讲到 Service Worker 与 PWA。这篇总结把全部内容收拢为一张知识地图,并用”虚拟歌手音乐平台”这一贯穿领域重写核心示例:歌姬对象与原型链、播放计数器的闭包、异步拉取歌单、事件委托的播放列表、ES Modules 的代码分割——每个示例都使用 const、箭头函数、async/await 等现代惯用写法。读完本文,你应该能对着一段 JS 代码口算出它的执行顺序。

前置知识

学习目标

  1. 说清原始类型与引用类型的复制、传参与相等比较差异,避免共享引用带来的副作用。
  2. 用作用域链与词法环境解释闭包的成因,并能识别闭包导致的内存滞留。
  3. 讲述原型链如何支撑属性查找与继承,说明 class 只是构造函数加原型的语法糖。
  4. 口算”同步代码、微任务、宏任务”的执行顺序,并用 Promise.all 正确组织并发。
  5. 用事件委托与模块化组织一个中等规模的前端页面,理解 Tree-shaking 的前提。

知识地图

flowchart TD
    ROOT["JavaScript 学习总结<br/>007-javascript 模块 64 篇文档"]

    subgraph G1["类型与作用域"]
        D1["001-007:是什么 / 运行环境 / 程序结构<br/>变量与数据类型 / 类型与运算符 / 控制流 / 对象数组"]
        D2["008 / 010 / 014:函数作用域与闭包<br/>this 关键字深度解析 / 自定义错误类型"]
        D3["015-017:高阶函数 / 递归与尾调用优化<br/>柯里化与函数组合"]
    end

    subgraph G2["原型与闭包"]
        D4["018 / 019:原型继承 / 原型链与 class 本质"]
        D5["020 / 021 / 023:深浅拷贝 / 对象静态方法<br/>Map-Set-WeakMap-WeakSet"]
        D6["024 / 033 / 034:ArrayBuffer 与 TypedArray<br/>Proxy 与 Reflect / 代理实践"]
        D7["035 / 036 / 037:闭包内存泄漏 / 内存泄漏排查<br/>内存管理与垃圾回收"]
    end

    subgraph G3["异步与事件循环"]
        D8["025 / 026 / 027:异步编程 / Promise 构造深度解析<br/>Promise 静态方法"]
        D9["028 / 032:异步并发控制 / JS 中的协程"]
        D10["029 / 030:事件循环 / 事件循环详解"]
        D11["063:Web Workers 多线程"]
    end

    subgraph G4["DOM 与模块"]
        D12["041 / 042 / 043:DOM 操作与事件 / BOM / Web API"]
        D13["044 / 045:Fetch 与 AbortController / Web Streams"]
        D14["046 / 047 / 048:Web 存储 / IndexedDB / 全局错误捕获"]
        D15["038 / 039 / 040:模块化 / 动态导入与代码分割<br/>打包与 Tree-shaking"]
        D16["064:Service Worker 与 PWA"]
    end

    subgraph G5["工程化与新特性"]
        D17["011-013 / 061:正则表达式 / 命名捕获组<br/>Unicode 属性转义 / 断言"]
        D18["049-054:防抖节流 / 调试与性能 / Web Vitals<br/>Node 性能 / 包管理命令 / Console API"]
        D19["055 / 056 / 062:项目实践 / Todo 应用 / 语言理论"]
        D20["031 / 022 / 057-060:迭代器助手 / ES6 新特性 / ES2024<br/>最新特性 / Temporal API / Records 与 Tuples"]
    end

    ROOT --> G1
    ROOT --> G2
    ROOT --> G3
    ROOT --> G4
    ROOT --> G5

核心概念回顾

1. 变量声明与类型系统

现代 JavaScript 只用 const 与 let:默认 const,确需重新赋值才用 let,var 因函数作用域与变量提升的历史包袱应彻底退役。类型层面要牢记”七种原始类型按值复制,对象按引用共享”:把对象赋给另一个变量时复制的是引用,两个变量指向同一份内存,一处修改处处可见。深浅拷贝、Object.freeze、structuredClone 都是为这一语义服务的补救工具。

// 1. const 优先:声明后不再重新绑定的一律用 const
const platformName = '虚拟歌手音乐平台';

// 2. let 只用于确需重新赋值的量
let totalPlays = 0;

// 3. 原始类型按值复制,对象按引用共享
const singer = { name: '初音未来', themeColor: '#39C5BB' };
const sameSinger = singer;           // 复制的是引用,不是对象本身
sameSinger.themeColor = '#31C6C0';   // 两处看到的是同一次修改

// 4. 块级作用域:let 的 i 每轮循环都是一个新绑定
for (let i = 0; i < 3; i++) {
  totalPlays += i + 1;
}
console.log(`${platformName} 共 ${totalPlays} 次试听`);

2. 函数、作用域与 this

函数是 JS 的一等公民:可赋值、可传参、可返回。作用域由词法结构决定——函数在定义时就记住了它能访问哪些变量。this 则不同,它由”调用方式”决定:方法调用指向调用者,普通调用在严格模式下是 undefined,箭头函数没有自己的 this,沿用定义处的词法 this。模块 010 篇的结论可以浓缩为一句:箭头函数适合回调,普通函数适合对象方法。

// 1. 普通函数作为对象方法:this 指向调用者
const concert = {
  title: 'MAGICAL MIRAI',
  open() {
    return `${this.title} 现已开演`;   // this === concert
  },
};

// 2. 箭头函数没有自己的 this,沿用外层词法作用域
const fanclub = {
  name: '39 团',
  members: ['小林', '佐藤'],
  greetAll() {
    // 3. 方法内用箭头函数:this 仍是 fanclub,而非回调调用者
    this.members.forEach((m) => console.log(`${m} 欢迎来到 ${this.name}`));
  },
};

console.log(concert.open());
fanclub.greetAll();

3. 闭包:函数记住它的出生地

闭包是函数与其定义时词法环境的组合:内层函数引用外层变量时,即使外层函数已返回,这些变量依然存活。闭包实现了私有状态(外部无法直接访问)、柯里化、防抖节流等大量模式;代价是被引用的变量无法被回收,滥用会造成内存滞留。模块 035 篇给出的排查手段是:用 DevTools 的内存面板对闭包持有的对象做堆快照比对。

// 1. 工厂函数返回的对象方法记住了创建时的词法环境
function createPlayCounter(songTitle) {
  let plays = 0;                    // 2. 私有变量:外部无法直接读写
  return {
    play() {
      plays += 1;                   // 3. 闭包让 plays 跨调用存活
      return `${songTitle} 第 ${plays} 次播放`;
    },
    getPlays: () => plays,          // 4. 只读出口,不暴露可变状态
  };
}

const melt = createPlayCounter('Melt');
console.log(melt.play());           // Melt 第 1 次播放
console.log(melt.play());           // Melt 第 2 次播放
console.log(`累计:${melt.getPlays()} 次`);

4. 原型与 class 的本质

JavaScript 没有”类模板复制实例”的机制,只有对象到对象的原型链接:读取属性时沿 [[Prototype]] 链逐级查找,直到 null。ES6 的 class 只是构造函数加原型的语法糖——方法仍然挂在 prototype 上,实例之间共享同一份方法。理解这一点后,instanceof(检查构造函数的 prototype 是否在原型链上)、继承(链接两个原型)都不再是黑盒。

// 1. class 本质是构造函数加原型:方法共享于 prototype
class VSinger {
  constructor(name, themeColor) {
    this.name = name;
    this.themeColor = themeColor;
  }
  introduce() {                     // 2. 定义在原型上,所有实例共享
    return `我是 ${this.name},应援色 ${this.themeColor}`;
  }
}

// 3. extends 建立原型链,super 沿链向上查找
class FanClubStar extends VSinger {
  introduce() {
    return `${super.introduce()},粉丝团已集结`;
  }
}

const miku = new FanClubStar('初音未来', '#39C5BB');
console.log(miku.introduce());
// 4. 验证原型本质:实例的原型就是类的 prototype
console.log(Object.getPrototypeOf(miku) === FanClubStar.prototype); // true

5. Promise 与 async/await

Promise 是”未来值”的一等表示:对象一旦落定为 fulfilled 或 rejected,状态不可再变,then 注册的回调进入微任务队列。async/await 是 Promise 的语法糖:await 暂停当前 async 函数但不阻塞主线程,之后的代码相当于注册在 Promise 上的回调。组织并发的关键是 Promise.all(全部成功才成功)与 allSettled(容忍部分失败),而不是在循环里逐个 await 造成串行等待。

// 1. 用 Promise 包装异步任务:resolve 走成功路径,reject 走失败路径
function fetchSong(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      id > 0 ? resolve({ id, title: `song-${id}` })
             : reject(new Error('歌曲编号非法'));
    }, 100);
  });
}

// 2. await 让异步代码获得同步的书写顺序,但执行仍是异步的
async function loadSetlist() {
  try {
    // 3. Promise.all 并行请求,总耗时约等于最慢的一个请求
    const songs = await Promise.all([fetchSong(1), fetchSong(2)]);
    return songs.map((s) => s.title);
  } catch (err) {
    // 4. 链上任意一步失败都会被 catch 捕获
    console.error('歌单加载失败:', err.message);
    return [];
  }
}

loadSetlist().then(console.log);   // ['song-1', 'song-2']

6. 事件循环:单线程的调度艺术

JavaScript 主线程单线程运行,异步能力来自宿主提供的事件循环:同步代码执行完毕后,先清空全部微任务(Promise 回调、queueMicrotask、MutationObserver),再取一个宏任务(setTimeout、I/O 回调)执行,如此循环;浏览器还会在合适时机穿插渲染。推论有三:微任务总在下一个宏任务之前执行;setTimeout(fn, 0) 不是零延迟;长任务会阻塞渲染,需要切片或交给 Web Worker。

// 1. 宏任务:setTimeout 的回调进入宏任务队列
setTimeout(() => console.log('3. 宏任务:演唱会开场'), 0);

// 2. 微任务:Promise 回调进入微任务队列,同步代码结束后立即清空
Promise.resolve().then(() => console.log('2. 微任务:检票进场'));

// 3. 同步代码最先执行
console.log('1. 同步代码:观众到齐');

// 4. 输出顺序恒为 1 -> 2 -> 3,这就是事件循环的调度规则

7. DOM 操作与事件委托

DOM 是浏览器把 HTML 解析成的节点树,操作 DOM 是前端性能开销的大头。原则是”批量读、批量写”:避免在同一次流程里交替读写布局属性(强制同步回流)。事件委托把子元素的监听收敛到父节点:利用事件冒泡,在父节点上判断真实来源(event.target),动态增删的子节点无需重新绑定监听。

// 1. 查询播放列表容器
const list = document.querySelector('#setlist');

// 2. 事件委托:只在父节点绑定一次监听
list.addEventListener('click', (event) => {
  // 3. closest 沿祖先链找到列表项,未命中则忽略
  const item = event.target.closest('li');
  if (!item) return;
  item.classList.toggle('playing');              // 4. 切换样式类
  console.log(`切换播放:${item.dataset.title}`);
});

// 5. 后插入的子节点同样被委托监听覆盖,无需重新绑定
const li = document.createElement('li');
li.dataset.title = 'Tell Your World';
li.textContent = 'Tell Your World';
list.append(li);

8. 模块化:从全局污染到 Tree-shaking

ES Modules 是语言原生模块:每个模块有独立作用域,顶层 export/import 显式声明依赖,依赖图在加载时静态解析,因此打包器可以做 Tree-shaking(删除未引用的导出)与代码分割(按路由或交互懒加载)。动态 import() 返回 Promise,是实现”首屏只加载必需代码”的关键手段;模块顶层还可以直接 await(Top-level await)。

// player.js —— 1. 命名导出:一个模块可以有多个导出
export function play(song) {
  return `正在播放:${song.title}`;
}

// index.js —— 2. 静态导入在加载期解析依赖图,可被 Tree-shaking 分析
import { play } from './player.js';

// 3. 动态 import 返回 Promise,按需加载舞台模块并自动代码分割
async function openStage() {
  const { default: stage } = await import('./stage.js');
  return stage;
}

console.log(play({ title: 'Melt' }));   // 正在播放:Melt
void openStage;

9. 内存管理与垃圾回收

JS 引擎用可达性做垃圾回收:从根(全局对象、当前调用栈)出发能引用到的对象不可回收,引用不到的交给 GC。这解释了两类经典泄漏:被全局变量或闭包长期持有的 DOM 引用,以及忘记清除的定时器与事件监听。防御手段是”用完即断”:removeEventListener、clearTimeout、把不再需要的引用置 null,必要时用 WeakMap/WeakRef 持有弱引用。

易混淆概念对比

null vs undefined

维度undefinednull
语义系统级”还没有值”代码级”刻意置空”
出现场景未赋值变量、缺失参数、未 return 的函数手动清空引用、API 约定的空值
类型typeof 得到 "undefined"typeof 得到 "object"(历史包袱)
参与算术1 + undefined 得到 NaN1 + null 得到 1(当 0 用)
相等比较null == undefined 为 truenull === undefined 为 false
典型判断if (x === undefined) 检查未初始化if (x === null) 检查显式空
建议用法不主动写 undefined 赋值主动表示”无对象”时使用

var vs let vs const

维度varletconst
作用域函数作用域块级作用域块级作用域
变量提升提升并初始化为 undefined提升但不初始化(暂时性死区)同 let
重复声明允许报错报错
重新赋值允许允许禁止(对象属性仍可改)
循环闭包行为每轮共享一个绑定(经典陷阱)每轮新建绑定每轮新建绑定
现代建议不再使用需要重新赋值时使用默认选择

常见误区与排查

误区 1:var 循环里的闭包共享同一个变量

// 错误:var 是函数作用域,三个回调共享同一个 i,输出 3 3 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(`第 ${i} 首歌`), 0);
}
// 修正:let 让每轮循环都创建一个新绑定,输出 0 1 2
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(`第 ${i} 首歌`), 0);
}

误区 2:回调里丢失 this

// 错误:普通函数回调的 this 不再指向 fanclub
const fanclub = {
  name: '39 团',
  greet() {
    this.members.forEach(function (m) {
      console.log(`${this.name} 欢迎 ${m}`);   // this 是 undefined
    });
  },
  members: [],
};
// 修正:箭头函数沿用定义处的词法 this
const fanclub = {
  name: '39 团',
  members: ['小林'],
  greet() {
    this.members.forEach((m) => console.log(`${this.name} 欢迎 ${m}`));
  },
};
fanclub.greet();

误区 3:在循环里逐个 await 造成串行

// 错误:每个请求都等上一个完成,总耗时是所有请求之和
async function loadAll(ids) {
  const songs = [];
  for (const id of ids) {
    songs.push(await fetchSong(id));   // 串行等待
  }
  return songs;
}
// 修正:先用 map 生成 Promise,再用 Promise.all 一次性并发
async function loadAll(ids) {
  const tasks = ids.map((id) => fetchSong(id));   // 同时发出
  return Promise.all(tasks);                      // 总耗时约等于最慢的一个
}

误区 4:浅拷贝后修改嵌套对象

// 错误:展开运算符只拷贝第一层,嵌套对象仍是共享引用
const singer = { name: '初音未来', profile: { debut: 2007 } };
const copy = { ...singer };
copy.profile.debut = 9999;       // 原对象也被改掉了
console.log(singer.profile.debut); // 9999
// 修正:需要独立副本时使用深拷贝
const singer = { name: '初音未来', profile: { debut: 2007 } };
const copy = structuredClone(singer);   // 原生深拷贝
copy.profile.debut = 9999;
console.log(singer.profile.debut);      // 2007,原对象不受影响

误区 5:用 == 做隐式类型比较

// 错误:== 触发隐式转换,规则晦涩且易错
console.log(0 == '');      // true
console.log('1' == 1);     // true
// 修正:一律使用全等,需要转换时显式写出来
console.log(0 === '');     // false
console.log(Number('1') === 1);   // true,转换意图明确

误区 6:忘记清理定时器与监听器

// 错误:组件销毁后定时器仍持有引用,闭包与 DOM 都无法回收
function startTicker() {
  setInterval(() => updateProgressBar(), 1000);   // 返回值被丢弃
}
// 修正:保存句柄,在适当时机清理
let tickerId = null;
function startTicker() {
  tickerId = setInterval(updateProgressBar, 1000);
}
function stopTicker() {
  clearInterval(tickerId);   // 离开页面前调用,切断引用链
  tickerId = null;
}
function updateProgressBar() { /* 更新播放进度条 */ }

自检清单

  • 能解释原始类型与引用类型在赋值、传参、相等比较上的行为差异
  • 能用词法环境说清闭包的成因,并用堆快照定位闭包导致的内存滞留
  • 能说明 class 与构造函数加原型的等价关系,并手写 Object.create 实现继承
  • 能口算同步代码、微任务、宏任务混排的输出顺序
  • 能正确使用 Promise.all/allSettled/race 组织并发请求
  • 能用事件委托把动态列表的监听收敛到一个父节点
  • 能区分 ESM 的静态导入与动态 import(),说出 Tree-shaking 的生效前提
  • 能列出三种常见内存泄漏(全局引用、遗忘的定时器、闭包持有 DOM)及排查方法
  • 能说出 const 对象属性仍可修改的原因,并用 Object.freeze 或 structuredClone 应对
  • 能用 defer 与 <script type="module"> 控制脚本的加载与执行时机

2026-09 增补:新特性与新增专篇

本模块在 2026-09 完成一次知识点补齐,回看时优先补读四篇:

  1. 590-ES2023To2026NewFeatures:按年份串讲 ES2023-ES2025 全部定稿特性与 ES2026 展望;
  2. 620-ExplicitResourceManagement:using/await using 确定性资源释放(Stage 4,ES2026 核心);
  3. 610-TemporalJavaScriptAPI:Temporal 已升格为 ES2026 标准,状态与旧文不同;
  4. 630-ImmutableDataStructures:Records & Tuples 提案已撤回,深不可变的现代替代方案;
  5. 650-IntlApi:内置国际化(ECMA-402);560-PackageJsonEngineeringDeepDive:package.json 工程契约与 lint/format 工具链。

后续学习路径

  1. 精读 事件循环详解 与 Promise 构造函数深度解析,把执行模型焊死在脑子里。
  2. 进入 内存管理与垃圾回收 与 内存泄漏排查,掌握 DevTools 内存面板的完整用法。
  3. 学习 Fetch API 与 Web Streams 与 IndexedDB,给音乐平台补上离线缓存能力。
  4. 用 Web Workers 多线程 把歌词解析搬出主线程,再以 Service Worker 与 PWA 实现整站离线可用。
  5. 以 JavaScript 项目实战:Todo 应用 与 调试与性能优化 收官,完整走一遍开发、性能度量与上线流程;随后即可进入 TypeScript 模块。