Article

JavaScript 忍者秘籍 笔记

更新于:2026-07-11

语言特性

1. JavaScript 简介

1.1 核心语言特性

名称说明代码示例
变量声明使用 varletconst 声明变量。var 存在函数作用域和变量提升问题,letconst 是 ES6 引入的块级作用域变量,推荐使用。let name = "Alice";
const age = 25;
var oldStyle = "deprecated";
数据类型JavaScript 有 7 种原始数据类型:stringnumberbooleannullundefinedsymbol (ES6)、bigint (ES2020),以及引用类型 objecttypeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof { } // "object"
typeof Symbol() // "symbol"
函数函数是一等公民,可被赋值给变量、作为参数传递或作为返回值。支持函数声明、函数表达式和箭头函数。// 函数声明
function greet(name) {
return "Hello, " + name;
}

// 箭头函数
const add = (a, b) => a + b;
对象对象是键值对的集合,用于存储复杂数据结构。键是字符串(或 Symbol),值可以是任意类型。const person = {
name: "Bob",
age: 30,
greet() {
return "Hi, I'm " + this.name;
}
};
console.log(person.greet());
数组有序的值集合,提供丰富的内置方法(如 mapfilterreduce)进行数据操作。const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8]
条件语句根据条件执行不同代码块,包括 if...elseswitch 等。const score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else {
console.log("C");
}
循环用于重复执行代码,常见有 forwhilefor...offor...in// for 循环
for (let i = 0; i < 3; i++) {
console.log(i);
}

// for...of (遍历可迭代对象)
for (const num of [1, 2, 3]) {
console.log(num);
}
模板字符串ES6 特性,使用反引号 ` 包裹,支持嵌入变量和换行,语法更清晰。const name = "Charlie";
const message = \Hello,<br>${name}!<br>Welcome!`;<br>console.log(message);`
解构赋值ES6 特性,从数组或对象中提取值并赋给变量,语法简洁。const [a, b] = [1, 2];
const { name, age } = { name: "Diana", age: 28 };
console.log(a, name); // 1 "Diana"
默认参数ES6 特性,函数参数可设置默认值,当未传参或参数为 undefined 时使用。function greet(name = "Guest") {
console.log("Hello, " + name);
}
greet(); // "Hello, Guest"
greet("Eve"); // "Hello, Eve"
箭头函数ES6 引入的简写函数语法,不绑定自己的 thisargumentssupernew.targetconst square = x => x * x;
const add = (x, y) => x + y;
const log = () => console.log("Hi");
Promise用于处理异步操作的对象,表示一个可能现在或将来完成的操作及其结果值。const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Done!"), 1000);
});

promise.then(result => {
console.log(result); // "Done!"
});
async/await基于 Promise 的语法糖,使异步代码看起来像同步代码,提高可读性。async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error("Error:", error);
}
}
类 (Class)ES6 引入的面向对象语法糖,基于原型继承,提供更清晰的类定义方式。class Person {
constructor(name) {
this.name = name;
}

greet() {
console.log("Hello, " + this.name);
}
}

const p = new Person("Frank");
p.greet();
模块化 (Modules)ES6 模块系统,使用 importexport 实现代码的模块化组织和复用。// math.js
export const PI = 3.14;
export function add(a, b) { return a + b; }

// main.js
import { PI, add } from './math.js';
console.log(add(PI, 1));

Node.js 默认将 .js 文件视为 CommonJS 模块(使用 require),而不是 ES 模块(使用 import)。如需启用 ES 模块:
1. 配置 package.json 文件:{ "type": "module" }
2. 【可选】将 js 脚本保存为 .mjs 后缀

node main.js

1.2 引擎核心要素

核心要素说明示例/相关技术
词法分析器 (Lexer / Tokenizer)将源代码字符串分解为有意义的”词法单元”(tokens),如关键字、标识符、运算符等。这是解析的第一步。let x = 42; → 分解为 tokens: [keyword:let, identifier:x, operator:=, number:42, punctuation:;]
语法分析器 (Parser)将词法单元(tokens)构造成抽象语法树(AST),表示代码的结构和语法关系。x + 1 被解析为 AST:BinaryExpression (+) / Identifier(x) / Literal(1)
抽象语法树 (AST)代码的树状结构表示,是后续编译和优化的基础。所有转换和分析都基于 AST 进行。Babel、ESLint 等工具都操作 AST。
解释器 (Interpreter)逐行读取 AST 并执行代码。现代引擎通常使用解释器快速启动,再配合编译器优化。V8 引擎的 Ignition 是一个字节码解释器。
编译器 (Compiler)将 JavaScript 编译为更底层的代码(如字节码或机器码),以提高执行速度。现代引擎采用即时编译(JIT)。V8 的 TurboFan 是优化编译器,将热点代码编译为高效机器码。
即时编译 (JIT - Just-In-Time Compilation)在运行时动态将频繁执行的代码(热点代码)编译为机器码,大幅提升性能。V8、SpiderMonkey、JavaScriptCore 都使用多层 JIT 策略。
执行上下文 (Execution Context)JavaScript 执行的环境,包含变量对象、作用域链和 this 值。每个函数调用都会创建新的执行上下文。全局执行上下文、函数执行上下文、eval 执行上下文。
调用栈 (Call Stack)管理函数调用顺序的栈结构。每当函数被调用,其执行上下文被压入栈;函数执行完毕后弹出。递归过深会导致 RangeError: Maximum call stack size exceeded
内存管理 (Memory Management)自动管理内存分配与回收,主要通过垃圾回收机制(GC)实现。使用标记-清除(Mark-and-Sweep)或引用计数等算法回收不再使用的对象内存。
垃圾回收器 (Garbage Collector, GC)自动识别并释放不再使用的内存,防止内存泄漏。是引擎自动内存管理的核心组件。V8 使用分代垃圾回收(新生代 Scavenge,老生代 Mark-Compact / Incremental)。
事件循环 (Event Loop)虽然严格来说是运行时环境(如浏览器、Node.js)的一部分,但与引擎协同工作,负责处理异步操作的回调执行。协调 setTimeoutPromise.then 等异步任务的执行顺序。
内联缓存 (Inline Caching)优化属性访问速度的技术。缓存对象属性的查找结果,避免重复查找原型链。第一次访问 obj.prop 时查找并缓存位置,后续访问直接使用缓存位置。
隐藏类 (Hidden Classes)V8 使用的技术,为相似对象动态创建”隐藏类”,使属性访问接近 C++ 等静态语言的速度。对象属性顺序影响性能:{a:1, b:2}{b:2, a:1} 不共享隐藏类。

总结:现代引擎通过 解释器 + JIT 编译器 + 优化技术(如内联缓存、隐藏类)+ 自动内存管理 的组合,实现了接近原生代码的执行性能。

2. 运行时的页面构建过程

2.1 Web 页面的生命周期

注意:以下代码示例可以在浏览器的开发者工具界面,源代码/来源 → 代码段 → 新建 → 编辑保存新代码段 → 右键 → 运行,在控制台的左下角中,点选用户消息,即可在与页面的交互过程中,看到监控事件的相关输出。

阶段说明JavaScript 事件代码示例
创建 (Creation)浏览器开始创建 windowdocument 对象,但页面内容尚未加载。无直接事件,但全局脚本在此阶段开始解析。// 此处代码在页面构建时执行
console.log("Script parsed");
加载 (Loading)HTML 文档被加载和解析,DOM 树构建完成。外部资源(如图片、样式表、脚本)开始加载。DOMContentLoadedload// DOM 构建完成,不等待资源
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM ready!');
});

// 所有资源(图片等)加载完成
window.addEventListener('load', () => {
console.log('Page fully loaded');
});
交互 (Interactive)用户可以与页面交互(如点击、输入),但某些资源可能仍在加载。document.readyState 变为 "interactive"readystatechangedocument.addEventListener('readystatechange', () => {
if (document.readyState === 'interactive') {
console.log('Document is interactive');
}
});
完全加载 (Complete)页面所有资源(包括图片、iframe 等)都已加载完毕。document.readyState 变为 "complete"readystatechangedocument.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
console.log('Document is complete');
}
});
活动 (Active)页面在前台运行,用户可正常交互。这是页面的主要运行阶段。无特定事件,但可监听各种用户事件(clickscroll 等)。window.addEventListener('click', () => {
console.log('User clicked');
});
可见性变更 (Visibility Change)页面在可见与不可见状态间切换(如切换标签页、最小化窗口)。visibilitychangedocument.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Page is hidden');
} else {
console.log('Page is visible');
}
});
页面切换/卸载 (Page Navigation / Unload)用户离开当前页面(如关闭标签、跳转到新页面)。此阶段适合执行清理操作,但不能进行异步操作。beforeunloadunloadpagehide// 用户尝试离开页面前(可询问是否离开)
window.addEventListener('beforeunload', (e) => {
e.returnValue = 'Are you sure?';
return 'Are you sure?';
});

// 页面即将卸载(仅同步操作)
window.addEventListener('unload', () => {
console.log('Goodbye!');
});
页面恢复 (Page Restore)用户通过前进/后退按钮返回已缓存的页面(如 bfcache 中的页面)。pageshow// 页面显示时(包括从缓存恢复)
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('Restored from cache');
}
});

2.2 页面构建顺序

阶段说明JavaScript 的角色与行为关键事件 / API
1. 导航开始 (Navigation Start)用户输入 URL、点击链接或提交表单,浏览器开始加载新页面。无(旧页面可能触发 beforeunload / pagehide)。window.onbeforeunloadwindow.onpagehide
2. 网络请求 (Network Request)浏览器向服务器发起请求,获取 HTML 文档。无(尚未下载任何 JS)。fetch(后续资源)
3. HTML 解析与 DOM 构建 (HTML Parsing & DOM Construction)浏览器逐行解析 HTML,构建 DOM 树(Document Object Model)。- 阻塞解析:遇到 <script> 标签(无 async/defer)时,暂停 HTML 解析,立即下载并执行 JS。
- 非阻塞解析:<script async><script defer> 不阻塞解析,异步下载。
document.createElement()document.getElementById()(在 DOM 节点后执行)
4. CSS 解析与 CSSOM 构建 (CSS Parsing & CSSOM Construction)浏览器解析 CSS(内联、外链、<style>),构建 CSSOM 树(CSS Object Model)。JS 无法执行依赖样式的操作(如读取 offsetHeight),直到 CSSOM 构建完成。CSS 是渲染阻塞资源。window.getComputedStyle()
5. 渲染树构建 (Render Tree Construction)浏览器将 DOM 和 CSSOM 合并为渲染树(Render Tree),包含所有可见元素及其样式。JS 仍不能安全读取布局,因为尚未进行布局计算。
6. 布局 (Layout / Reflow)计算渲染树中每个节点的几何信息(位置、大小)。若 JS 尝试访问布局属性(如 offsetTopclientWidth),会强制触发同步布局(性能警告:Forced reflow)。element.offsetWidthelement.getBoundingClientRect()
7. 绘制 (Paint)将渲染树的每个节点转换为屏幕上的像素。JS 无法直接干预绘制过程。
8. 合成 (Compositing)将绘制好的图层合并显示到屏幕上。使用 transform/opacity 的动画可在此阶段高效运行(不触发 Layout/Paint)。requestAnimationFrame()
9. 执行内联/同步脚本 (Execution of Inline/Sync Scripts)在 HTML 解析过程中,遇到的 <script> 立即执行。- 可以操作当前已解析的 DOM。
- 可能修改 DOM/CSSOM,导致重新解析或重排。
document.write()(已过时)、console.log()
10. 执行异步/延迟脚本 (Async/Defer Scripts Execution)- async 脚本:下载完成后立即执行,不保证顺序。
- defer 脚本:下载完成后延迟到 DOM 解析结束、DOMContentLoaded 之前执行,保证顺序。
- async:执行时机不确定,可能在 DOMContentLoaded 之前或之后。
- defer:确保在 DOM 构建完成后执行,适合初始化代码。
DOMContentLoadeddefer 脚本在此事件前执行)
11. DOM 构建完成 (DOM Ready)HTML 解析完成,DOM 树构建完毕。外部资源(图片等)可能仍在加载。JS 可以安全操作 DOM,绑定事件等。document.addEventListener('DOMContentLoaded', handler)
12. 资源加载完成 (Resources Loaded)所有资源(图片、视频、async 脚本等)加载完成。JS 可以执行依赖完整资源的操作(如轮播图、画布渲染)。window.addEventListener('load', handler)
13. 页面完全交互 (Fully Interactive)页面所有内容就绪,用户可流畅交互。document.readyState === 'complete'应用主逻辑、数据获取、复杂交互绑定。document.readyStatewindow.onload
14. 空闲处理 (Idle Processing)浏览器空闲时,可执行非关键任务。使用 requestIdleCallback() 执行低优先级任务(如日志上报、预加载)。requestIdleCallback(callback)

2.3 事件处理

鼠标事件

事件名称触发时机代码示例
click鼠标单击(按下并释放)时触发const btn = document.getElementById('btn');
btn.addEventListener('click', () => { console.log('按钮被点击'); });
dblclick鼠标双击时触发const elem = document.getElementById('double');
elem.addEventListener('dblclick', () => { alert('双击触发!'); });
mousedown鼠标按钮在元素上按下时触发(早于 clickelem.addEventListener('mousedown', (e) => { console.log(\按下按钮: ${e.button}`); });`
mouseup鼠标按钮在元素上释放时触发elem.addEventListener('mouseup', () => { console.log('鼠标释放'); });
mousemove鼠标在元素上移动时持续触发elem.addEventListener('mousemove', (e) => { document.getElementById('coords').textContent = \X:${e.clientX}, Y:${e.clientY}`; });`
mouseover鼠标指针移入元素或其子元素时触发(会冒泡)elem.addEventListener('mouseover', () => { elem.style.opacity = '0.7'; });
mouseout鼠标指针移出元素或其子元素时触发(会冒泡)elem.addEventListener('mouseout', () => { elem.style.opacity = '1'; });
mouseenter鼠标指针进入元素本身时触发(不冒泡)elem.addEventListener('mouseenter', () => { elem.style.transform = 'scale(1.1)'; });
mouseleave鼠标指针离开元素本身时触发(不冒泡)elem.addEventListener('mouseleave', () => { elem.style.transform = 'scale(1)'; });
contextmenu右键点击时触发,默认弹出上下文菜单elem.addEventListener('contextmenu', (e) => { e.preventDefault(); console.log('右键菜单被阻止'); });
wheel鼠标滚轮或触摸板滚动时触发window.addEventListener('wheel', (e) => { if (e.deltaY < 0) console.log('向上滚动'); else console.log('向下滚动'); });

键盘事件

事件名称触发时机代码示例
keydown键盘任意键被按下时触发(包括功能键)document.addEventListener('keydown', (e) => { if (e.key === 'Escape') modal.close(); });
keyup键盘键被释放时触发document.addEventListener('keyup', (e) => { console.log(\释放键: ${e.key}`); });`
keypress已废弃,字符键被按下并生成字符时触发(建议用 keydown// 不推荐使用
beforeinputinput 事件前触发,可取消输入const input = document.getElementById('input');
input.addEventListener('beforeinput', (e) => { if (e.data && e.data.toLowerCase() === 'x') e.preventDefault(); });
input<input><textarea>contenteditable 元素内容实时改变时触发input.addEventListener('input', (e) => { console.log('输入内容:', e.target.value); });

表单事件

事件名称触发时机代码示例
focus元素获得焦点(如点击输入框)input.addEventListener('focus', () => { input.classList.add('focused'); });
blur元素失去焦点input.addEventListener('blur', () => { input.classList.remove('focused'); });
change表单元素值改变且元素失去焦点时触发(如 <select> 选择)const select = document.getElementById('mySelect');
select.addEventListener('change', (e) => { console.log('选择变更:', e.target.value); });
submit<form> 表单提交时触发(可阻止)const form = document.getElementById('form');
form.addEventListener('submit', (e) => { e.preventDefault(); validateAndSubmit(); });
reset表单重置按钮被点击时触发form.addEventListener('reset', () => { console.log('表单已重置'); });
select文本被选中时触发(如 <input> 中选中文本)input.addEventListener('select', () => { const selection = window.getSelection().toString(); console.log('选中文本:', selection); });

窗口/文档事件

事件名称触发时机代码示例
load页面及所有资源(图片、CSS、JS)完全加载完成后触发window.addEventListener('load', () => { console.log('页面完全加载完成'); });
DOMContentLoadedHTML 文档解析完成,DOM 构建完毕后触发(不等资源)document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app'); app.innerHTML = '<p>应用已就绪</p>'; });
beforeunload用户即将离开页面时触发(可弹出确认框)window.addEventListener('beforeunload', (e) => { e.preventDefault(); e.returnValue = ''; // 显示确认对话框 });
unload页面或资源卸载时触发(不能阻止)window.addEventListener('unload', () => { console.log('页面正在卸载'); });
resize浏览器窗口大小改变时触发window.addEventListener('resize', () => { const width = window.innerWidth; document.body.style.fontSize = width > 800 ? '16px' : '14px'; });
scroll页面或元素滚动时触发window.addEventListener('scroll', () => { const scrolled = window.pageYOffset; if (scrolled > 100) header.classList.add('scrolled'); });
hashchangeURL 的 hash(# 后面部分)改变时触发window.addEventListener('hashchange', (e) => { console.log('Hash 变更:', location.hash); });
popstate浏览器历史记录改变时触发(如前进/后退)window.addEventListener('popstate', (e) => { console.log('历史状态变更:', e.state); });

剪贴板事件

事件名称触发时机代码示例
copy执行复制操作时触发document.addEventListener('copy', (e) => { e.clipboardData.setData('text/plain', '自定义复制文本'); e.preventDefault(); });
cut执行剪切操作时触发document.addEventListener('cut', (e) => { console.log('内容被剪切'); });
paste执行粘贴操作时触发document.addEventListener('paste', (e) => { const text = e.clipboardData.getData('text'); console.log('粘贴内容:', text); });

拖拽事件

事件名称触发时机代码示例
dragstart拖拽操作开始时触发const dragElem = document.getElementById('drag');
dragElem.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', '拖拽数据'); });
drag拖拽过程中持续触发dragElem.addEventListener('drag', () => { dragElem.style.opacity = '0.5'; });
dragend拖拽操作结束(释放或取消)时触发dragElem.addEventListener('dragend', () => { dragElem.style.opacity = '1'; });
dragenter被拖拽元素进入目标区域时触发const dropZone = document.getElementById('drop');
dropZone.addEventListener('dragenter', (e) => { dropZone.style.background = 'lightblue'; });
dragover被拖拽元素在目标区域上移动时触发(必须阻止默认行为才能触发 dropdropZone.addEventListener('dragover', (e) => { e.preventDefault(); // 关键!允许放置 });
dragleave被拖拽元素离开目标区域时触发dropZone.addEventListener('dragleave', () => { dropZone.style.background = ''; });
drop被拖拽元素在目标区域释放时触发dropZone.addEventListener('drop', (e) => { e.preventDefault(); const data = e.dataTransfer.getData('text'); dropZone.textContent = data; });

媒体事件(音频/视频)

事件名称触发时机代码示例
play媒体开始播放时触发const video = document.getElementById('myVideo');
video.addEventListener('play', () => { console.log('视频开始播放'); });
pause媒体暂停时触发video.addEventListener('pause', () => { console.log('视频已暂停'); });
ended媒体播放结束时触发video.addEventListener('ended', () => { alert('视频播放完毕'); });
timeupdate播放位置改变时触发(用于更新进度条)video.addEventListener('timeupdate', () => { progressBar.value = (video.currentTime / video.duration) * 100; });
volumechange音量或静音状态改变时触发video.addEventListener('volumechange', () => { console.log('音量:', video.volume); });
loadedmetadata媒体元数据(如时长、尺寸)加载完成后触发video.addEventListener('loadedmetadata', () => { console.log('视频时长:', video.duration); });

其他重要事件

事件名称触发时机代码示例
error资源加载失败时触发(如图片、脚本)const img = document.getElementById('myImg');
img.addEventListener('error', () => { img.src = 'fallback.jpg'; });
online浏览器从离线状态变为在线时触发window.addEventListener('online', () => { console.log('网络已连接'); });
offline浏览器从在线状态变为离线时触发window.addEventListener('offline', () => { console.log('网络已断开'); });
storage同源的另一个标签页修改了 localStorage 时触发window.addEventListener('storage', (e) => { console.log(\${e.key} 从 ${e.oldValue} 变为 ${e.newValue}`); });`
message接收到跨窗口消息时触发(postMessagewindow.addEventListener('message', (e) => { if (e.origin !== 'https://trusted.com') return; console.log('收到消息:', e.data); });

关键注意事项

  • 元素选择:所有示例都使用 document.getElementById() 或类似方法先获取元素,避免 undefined 错误。
  • 事件绑定时机:如果脚本在 DOM 之前加载,应使用 DOMContentLoaded 事件来确保元素存在。
  • 阻止默认行为:使用 e.preventDefault() 来阻止链接跳转、表单提交、右键菜单等。
  • 事件冒泡:使用 e.stopPropagation() 阻止事件向上冒泡。
  • 性能mousemovescrollresize 等高频事件建议节流(throttle)或防抖(debounce)。

函数特性

3. 函数和参数

3.1 函数定义方式

定义方式语法示例特点使用场景注意事项
函数声明 (Function Declaration)function greet(name) {
return \Hello, ${name}!`;<br>}`
- 存在函数提升(hoisting)
- 必须有函数名
- 在作用域内可提前调用
通用函数定义,需要提前调用的场景// 可以在声明前调用
console.log(greet("Alice"));
// 正常运行
function greet(name) { return \Hello, ${name}!`; }`
函数表达式 (Function Expression)const greet = function(name) {
return \Hello, ${name}!`;<br>};`
- 不会提升(变量提升,但函数体未赋值)
- 可以是匿名或命名函数
- 更灵活,常用于回调
避免全局污染、作为参数传递、模块化开发// 不能在赋值前调用
console.log(greet("Bob"));
// ❌ 报错: Cannot access 'greet'
const greet = function(name) { return \Hello, ${name}!`; };`
箭头函数 (Arrow Function)const greet = (name) => { return \Hello, ${name}!`; };<br>// 简写(单参数、单表达式)<br>const greet = name => `Hello, ${name}!`;`- 无 this 绑定(继承外层 this
- 不能用作构造函数(无 new
- 无 arguments 对象
- 语法简洁
回调函数、事件处理、数组方法(mapfilter 等)const obj = {
name: "Alice",
// undefined,this 指向外层
greet: () => { console.log(this.name); }
};
构造函数方式 (Using Function Constructor)const greet = new Function('name', 'return "Hello, " + name + "!";');- 动态创建函数
- 参数和函数体为字符串
- 性能较差,有安全风险(类似 eval
极少使用,仅用于动态代码生成- 避免在生产环境使用
- 容易引发 XSS 漏洞
- 不推荐
对象中的方法 (Method in Object)const user = {
name: "Bob",
greet: function() {
return \Hi, I’m ${this.name}`;<br> }<br>};<br>// ES6 简写<br>const user = {<br> name: “Bob”,<br> greet() { return `Hi, I’m ${this.name}`; }<br>};`
- 作为对象属性存在
- 可以使用 this 指向对象
对象封装行为、类方法const user = {
name: "Bob",
greet() { return \Hi, I’m ${this.name}`; }<br>};<br>console.log(user.greet()); // “Hi, I’m Bob”`
类中的方法 (Class Method)class Person {
constructor(name) {
this.name = name;
}
greet() {
return \Hello, I’m ${this.name}`;<br> }<br> static info() {<br> return “This is a Person class”;<br> }<br>}`
- 使用 class 语法
- 支持实例方法和静态方法
- 语法清晰,面向对象
面向对象编程、组件化开发- 类声明不会提升
- 类内部默认使用严格模式
立即执行函数表达式 (IIFE)(function() { console.log("IIFE 执行了"); })();
// 或箭头函数
(() => { console.log("箭头 IIFE"); })();
- 定义后立即执行
- 创建独立作用域,避免变量污染
模块初始化、私有变量封装、避免全局命名冲突const result = (function(a, b) { return a + b; })(3, 4);
console.log(result); // 7
生成器函数 (Generator Function)function* idGenerator() {
let id = 1;
while (true) { yield id++; }
}
const gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
- 使用 function*yield
- 可暂停和恢复执行
- 返回迭代器对象
处理异步流程、惰性求值、无限序列- 用于 redux-saga 等库
- 与 for...ofyield* 配合使用

总结对比

方式是否提升this 行为能否用 new适用性
函数声明✅ 是正常绑定✅ 可以通用
函数表达式❌ 否(变量提升)正常绑定✅ 可以通用、回调
箭头函数❌ 否继承外层 this❌ 不可以回调、简洁表达式
构造函数方式❌ 否正常绑定✅ 可以不推荐
对象方法❌ 否指向对象❌ 通常不对象行为
类方法❌ 否实例方法指向实例✅ 类可 new面向对象
IIFE❌ 否正常或继承取决于内部函数初始化、私有作用域
生成器函数✅ 是(函数声明式)正常绑定❌ 不可以异步、迭代

3.2 参数

参数类型

参数类型语法示例说明使用场景注意事项
默认参数 (Default Parameters)function greet(name = "Guest", age = 18) {
return \Hello, ${name} (${age}岁)`;<br>}<br>console.log(greet());<br>// “Hello, Guest (18岁)“<br>console.log(greet(“Alice”));<br>// “Hello, Alice (18岁)“`
当参数未传或为 undefined 时,使用默认值提供可选参数,增强函数健壮性- 默认值可以是表达式、函数调用等
- 后面的参数可引用前面的参数:function fn(x, y = x * 2)
- 不适用于 nullfalse(它们是有效值)
剩余参数 (Rest Parameters)function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(4, 5)); // 9

// 与其他参数结合
function log(first, ...rest) {
console.log("第一个:", first);
console.log("其余:", rest); }
log("a", "b", "c");
// 第一个: a, 其余: ["b", "c"]
将多个参数收集为一个数组
语法:...parameterName
接收任意数量的参数,替代 arguments- 必须是最后一个参数
- 只能有一个
- 是真正的 Array 实例,可直接使用 mapfilter 等方法
解构参数 (Destructuring Parameters)// 对象解构
function displayUser({ name, age, city = "未知" }) {
console.log(\${name}, ${age}岁, ${city}`);<br>}<br>const user = { name: “Alice”, age: 30 };<br>displayUser(user); // Alice, 30岁, 未知<br><br>// 数组解构<br>function printFirstTwo([first, second]) {<br> console.log(first, second); }<br>printFirstTwo([“a”, “b”, “c”]); // “a b”<br><br>// 结合默认值<br>function config({ url, method = “GET”, timeout = 5000 } = {}) {<br> console.log(method, url, timeout); }<br>config({ url: “/api” }); // GET /api 5000<br>config(); // 不传参也不会报错`
直接从传入的对象或数组中提取参数简化配置对象、API 参数处理- 可结合默认值使用(如 = {} 防止 undefined 错误)
- 代码更清晰,无需在函数体内解构
arguments 对象(类数组对象)function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(oldSum(1, 2, 3)); // 6

// 转数组
const args = Array.from(arguments);
// 或 [...arguments]
函数内部自动存在的类数组对象,包含所有实参在没有剩余参数的旧代码中处理不定参数- 箭头函数中没有 arguments
- 是类数组,不是真数组(无 mapfilter
- 建议用 ...rest 替代
混合使用示例function createPost(author, tags = [], ...content) {
return { author, tags, body: content.join(" ") };
}
const post = createPost(
"张三", ["js", "web"],
"今天学习了", "剩余参数和默认参数"
);
console.log(post);
// { author: "张三", tags: ["js","web"], body: "今天学习了 剩余参数和默认参数" }
结合默认参数、剩余参数、普通参数构建灵活的 API 或配置函数- 普通参数 → 默认参数 → 剩余参数(顺序不能乱)
- 剩余参数必须在最后

参数传递机制

机制说明示例
值传递基本类型(stringnumberbooleannullundefinedsymbolbigint)按值传递function change(x) { x = 10; }
let a = 5;
change(a);
console.log(a); // 5(不变)
引用传递(实际是按共享传递)对象(包括数组、函数)传递的是引用的副本function changeObj(obj) { obj.name = "New"; }
const user = { name: "Old" };
changeObj(user);
console.log(user.name); // "New"(被修改)

注意:JavaScript 中没有真正的”引用传递”,而是”按共享传递”(call by sharing)。你不能改变传入的引用本身(如 obj = {} 不会影响外部),但可以修改引用所指向对象的属性。

参数最佳实践

  • ✅ 优先使用 ...rest 替代 arguments:语法更清晰,是真数组。
  • ✅ 为可选参数设置默认值:提高函数可用性和可读性。
  • ✅ 使用解构参数处理配置对象:避免参数顺序依赖,代码更直观。
  • ✅ 避免过多参数:超过 3 个参数时建议使用配置对象 {}
  • ✅ 注意参数顺序:普通参数 → 默认参数 → 剩余参数。

4. 函数调用

4.1 隐含参数

4.1.1 arguments 参数

特性说明代码示例注意事项
定义函数内部自动创建的类数组对象,包含所有传入的实参function logArgs() { console.log(arguments); }
logArgs("a", "b", "c");
// {"0":"a","1":"b","2":"c","length":3}
- 仅在普通函数中存在
- 箭头函数中没有 arguments
类数组结构具有 length 属性和数字索引,但不是真正的数组function example() {
console.log(arguments[0]); // 第一个参数
console.log(arguments.length); // 参数个数
}
example("x", "y"); // "x", 2
- 无法直接使用数组方法如 pushmapfilter
- 可通过 [...arguments]Array.from(arguments) 转为真数组
动态性实时反映函数调用时传入的参数,即使函数定义没有显式参数function dynamic() { return arguments.length; }
console.log(dynamic()); // 0
console.log(dynamic(1, 2, 3)); // 3
- 可用于处理不定数量参数
- 适合兼容旧代码
可修改性在非严格模式下,arguments 与命名参数双向绑定// 非严格模式
function bind(a) {
a = 10;
console.log(arguments[0]); // 10
arguments[0] = 20;
console.log(a); // 20
}
bind(5);
- 严格模式下无绑定关系
- 易造成混淆,不推荐依赖此特性
严格模式下的行为"use strict" 中,arguments 与命名参数解耦function strict(a) {
"use strict";
a = 10;
console.log(arguments[0]); // 仍为原始值,如 5
}
strict(5);
- 更安全、可预测
- 推荐在严格模式下开发
转换为真数组可通过以下方式转为数组,以便使用数组方法function toArray() {
// 方法1: 扩展运算符
const arr1 = [...arguments];
// 方法2: Array.from
const arr2 = Array.from(arguments);
// 方法3: call
const arr3 = Array.prototype.slice.call(arguments);
return arr1;
}
- 推荐使用 [...arguments](ES6+)
- slice.call 是旧式写法
与剩余参数 ...rest 的对比...rest 是 ES6 引入的替代方案,功能更强大// 使用 rest(推荐)
function sum(...nums) { return nums.reduce((a,b)=>a+b, 0); }
// 使用 arguments(旧方式)
function sumOld() { return [...arguments].reduce((a,b)=>a+b, 0); }
- ...rest 是真数组
- ...rest 可命名,更清晰
- ...rest 可与默认参数、解构结合
- 建议用 ...rest 替代 arguments
性能影响在某些 JavaScript 引擎中,使用 arguments 可能导致函数无法优化// 可能阻止 V8 引擎优化
function bad() { return arguments[0]; }
// 更优写法
function good(first, ...rest) { return first; }
- 尤其在热点函数中应避免 arguments
- 使用 ...rest 更利于引擎优化
没有数组方法不能直接调用 mapfilterforEachfunction badMap() {
// ❌ 报错:arguments.map is not a function
arguments.map(x => x * 2);
}
必须先转换为数组才能使用数组方法
arguments vs ...rest 总结对比
特性arguments...rest
类型类数组对象真正的 Array
是否存在普通函数中自动存在需显式定义 ...paramName
箭头函数支持❌ 不支持✅ 支持
数组方法不能直接使用可直接使用 mapfilter
性能可能影响优化更优,利于引擎优化
可读性较差,需额外转换清晰,语义明确
推荐程度❌ 不推荐新代码使用✅ 推荐使用
arguments 最佳实践
  • ✅ 优先使用 ...rest 参数替代 arguments
  • ✅ 在需要处理不定参数时,使用 function(...args)
  • ❌ 避免在新项目中使用 arguments,除非维护旧代码。
  • ✅ 使用严格模式("use strict")以避免 arguments 的隐式绑定问题。

4.1.2 this 参数

特性说明代码示例注意事项
定义this 是函数执行时的上下文对象,其值取决于函数的调用方式,而非定义位置function logThis() { console.log(this); }
logThis(); // 浏览器中通常为 window(非严格模式)
- this 是运行时绑定,不是词法作用域
- 箭头函数除外
默认绑定(独立函数调用)在非严格模式下,this 指向全局对象(浏览器为 window
严格模式下为 undefined
function standalone() { "use strict"; console.log(this); }
standalone(); // undefined(严格模式)

function nonStrict() { console.log(this); }
nonStrict(); // window(非严格模式)
- 严格模式更安全,避免意外绑定全局对象
隐式绑定(对象方法调用)当函数作为对象的属性被调用时,this 指向该对象const obj = {
name: "Alice",
greet() { console.log(\Hello, ${this.name}`); }<br>};<br>obj.greet(); // “Hello, Alice”`
- 只看直接调用者:
const fn = obj.greet; fn();
// this 为 window 或 undefined(丢失绑定)
显式绑定(callapplybind使用 call()apply()bind() 强制指定 thisfunction introduce(age) { console.log(\${this.name}, ${age}岁`); }<br>const person = { name: “Bob” };<br>introduce.call(person, 25); // “Bob, 25岁”<br>introduce.apply(person, [25]); // 同上<br>const boundFn = introduce.bind(person);<br>boundFn(30); // “Bob, 30岁”`- call:参数列表
- apply:参数数组
- bind:返回新函数,不立即执行
new 绑定(构造函数调用)使用 new 调用函数时,this 指向新创建的实例对象function Person(name) { this.name = name; }
const p = new Person("Charlie");
console.log(p.name); // "Charlie"
- new 会创建空对象,绑定 this,并返回该对象
- 若构造函数返回对象,则 this 失效
箭头函数中的 this箭头函数没有自己的 this,继承外层函数或全局作用域的 thisconst obj = {
name: "Diana",
regular() { setTimeout(function() { console.log(this.name); }, 100); },
// undefined(this 是 window)
arrow() { setTimeout(() => { console.log(this.name); }, 100); }
// "Diana"(继承外层 this)
};
obj.arrow();
- 箭头函数不能用作构造函数(无 new
- 不能使用 call/apply/bind 改变其 this
事件处理中的 this在 DOM 事件监听器中,this 通常指向绑定事件的元素const btn = document.getElementById('btn');
btn.addEventListener('click', function() {
console.log(this === btn); // true
});
// 箭头函数则不同
btn.addEventListener('click', () => {
console.log(this); // window(继承外层)
});
- 使用普通函数可访问触发事件的元素
- 箭头函数需用 e.target 获取元素
丢失 this 绑定(常见陷阱)将方法赋值给变量或作为回调时,this 可能丢失const user = { name: "Eve", greet() { console.log(\Hi, ${this.name}`); } };<br>const fn = user.greet;<br>fn(); // “Hi, undefined”(this 不再是 user)`解决方案:
1. 使用 bind()const fn = user.greet.bind(user);
2. 使用箭头函数包装:() => user.greet()
3. 保存引用:const self = this;
严格模式影响严格模式下,独立函数调用的 thisundefined,而非 window"use strict";
function test() { console.log(this); }
test(); // undefined
- 更安全,避免污染全局对象
this 不能被赋值改变this 是关键字,不能通过 this = obj 修改function bad() { this = {}; // ❌ 语法错误 }this 的值由调用方式决定,无法在函数体内赋值修改
this 绑定优先级(从高到低)
顺序绑定方式说明
1new 绑定new Foo()
2显式绑定callapplybind
3隐式绑定obj.method()
4默认绑定func()

new 会覆盖 bind 的绑定。

this 最佳实践
建议说明
✅ 使用 bind() 防止 this 丢失尤其在回调、事件处理、setTimeout
✅ 箭头函数用于保持外层 this适合在 mapfilterPromise.then 中使用
✅ 构造函数命名首字母大写Person,提醒使用 new
✅ 严格模式开发避免意外绑定全局对象
❌ 避免在对象方法中使用箭头函数会丢失 this 对象引用

4.2 调用方式

调用方式语法示例this 的绑定规则使用场景注意事项
1. 作为普通函数调用 (Function Invocation)function greet() { console.log(\Hello, ${this.name}`); }<br>greet(); // 直接调用`- 非严格模式:this 指向全局对象(浏览器中为 window
- 严格模式("use strict"):thisundefined
通用函数执行"use strict";
function test() { console.log(this); }
test();
// undefined(非严格模式下为 window)
⚠️ 避免在函数内依赖 this,除非明确控制上下文
2. 作为对象方法调用 (Method Invocation)const user = {
name: "Alice",
greet() { console.log(\Hi, ${this.name}`); }<br>};<br>user.greet(); // 通过对象调用`
this 指向调用该方法的对象(即 . 左边的对象)对象封装行为、实例方法const obj = {
name: "Bob",
method: function() { console.log(this.name); }
};
const fn = obj.method;
fn(); // ❌ this 为 window/undefined(丢失绑定)
⚠️ 方法赋值后调用会丢失 this,需用 bind 修复
3. 使用 new 调用 (Constructor Invocation)function Person(name) { this.name = name; }
const p = new Person("Charlie");
this 指向新创建的实例对象
过程:
1. 创建空对象
2. this 指向该对象
3. 执行构造函数
4. 返回对象(除非构造函数返回对象)
创建对象实例、模拟类- 构造函数首字母建议大写
- 若构造函数 return 一个对象,则 this 失效,返回该对象
- 箭头函数不能用作构造函数(会报错)
4. 使用 call/apply/bind 调用 (Explicit Context Invocation)function introduce(age) { console.log(\${this.name}, ${age}岁`); }<br>const person = { name: “Diana” };<br>// call: 参数列表<br>introduce.call(person, 25);<br>// apply: 参数数组<br>introduce.apply(person, [25]);<br>// bind: 返回新函数<br>const boundFn = introduce.bind(person);<br>boundFn(30);`this 被显式绑定为 call/apply/bind 的第一个参数- 强制指定上下文
- 函数借用
- 修复 this 丢失
- 实现继承
- callapply 立即执行
- bind 返回新函数,可延迟执行
- bind 常用于事件监听、回调函数

箭头函数的调用方式

箭头函数的调用语法与普通函数相同,但其 this 行为特殊:

调用方式this 行为示例
普通调用继承外层作用域的 thisconst obj = {
getArrow: () => this, // this 为外层(如 window)
getNormal() { return () => this; // this 为 obj }
};
作为方法无法绑定 this,始终继承外层const obj = {
name: "Eve",
arrow: () => console.log(this.name)
};
obj.arrow(); // undefined(非 obj)
call/apply/bind无效,无法改变 thisconst fn = () => console.log(this);
fn.call({ name: "Test" }); // this 仍为外层

调用方式最佳实践

建议说明
✅ 构造函数首字母大写Person,提醒使用 new
✅ 使用 bind 修复 this 丢失尤其在事件监听、setTimeout、回调中
✅ 箭头函数用于保持外层 thisarray.map(() => this.xxx)
✅ 严格模式开发避免 this 意外绑定到 window
❌ 避免在对象方法中使用箭头函数会丢失对象自身的 this

4.3 函数上下文处理

处理方式语法执行时机this 绑定效果参数传递方式典型用途代码示例
call()func.call(thisArg, arg1, arg2, ...)立即执行this 显式绑定为 thisArg逐个传参(参数列表)临时借用方法、强制指定上下文function greet() { console.log(\Hello, ${this.name}`); }<br>const user = { name: “Alice” };<br>greet.call(user); // “Hello, Alice”`
apply()func.apply(thisArg, [argsArray])立即执行this 显式绑定为 thisArg以数组形式传参处理数组参数、Math 函数借用const nums = [1, 5, 3];
console.log(Math.max.apply(null, nums)); // 5
// 等价于 Math.max(1,5,3)
bind()func.bind(thisArg, arg1, arg2, ...)返回新函数,不立即执行创建一个新函数,其 this 永久绑定为 thisArg可预设部分参数(柯里化)事件监听、回调函数、修复 this 丢失const btn = document.getElementById('btn');
function handleClick() { this.style.opacity = '0.5'; }
btn.addEventListener('click', handleClick.bind(btn));
箭头函数 (Arrow Function)() => { ... }调用时执行无自身 this,继承外层作用域的 this正常传参避免 this 丢失、保持外层上下文const obj = {
name: "Bob",
delayGreet() {
setTimeout(() => { console.log(this.name); }, 1000);
}
};
obj.delayGreet(); // "Bob"
new 操作符new Constructor()创建并执行构造函数this 指向新创建的实例对象传给构造函数的参数创建对象实例function Person(name) { this.name = name; }
const p = new Person("Charlie");
console.log(p.name); // "Charlie"
对象方法调用obj.method()调用时执行this 指向调用该方法的对象(obj正常传参对象封装行为const user = {
name: "Diana",
greet() { console.log(this.name); }
};
user.greet(); // "Diana"
Function.prototype.call 的继承借用Array.prototype.slice.call(arguments)立即执行强制指定 thisarguments传参类数组转数组(旧式写法)function example() {
const args = Array.prototype.slice.call(arguments);
return args;
}

各方式对比总结

方式是否立即执行是否创建新函数this 是否可变适用场景
call()✅ 是❌ 否❌ 执行后即完成一次性强制绑定
apply()✅ 是❌ 否❌ 执行后即完成数组参数场景
bind()❌ 否✅ 是❌ 返回函数的 this 固定需要延迟执行或重复使用
箭头函数调用时❌ 否(但为新函数)✅ 继承外层,无法用 call 改变保持外层上下文
new✅ 是✅ 是(新实例)❌ 构造函数内 this 指向新对象创建实例
对象调用调用时❌ 否✅ 取决于调用者对象方法

常见问题与解决方案

问题现象解决方案
this 丢失方法赋值后调用,this 变为 window/undefined使用 bind()const fn = obj.method.bind(obj);
异步回调中 this 错误setTimeoutPromisethis 不是期望对象- 使用箭头函数
- 使用 bind()
- 保存 const self = this;
类数组无法使用数组方法argumentsNodeListmapfilter- [...arguments]
- Array.from()
- Array.prototype.method.call()
构造函数 this 被覆盖构造函数返回对象,导致 new 失效避免在构造函数中 return 对象(除非有意)

上下文处理最佳实践

  • ✅ 优先使用 bind() 处理事件监听和回调中的 this
  • ✅ 箭头函数用于保持外层 this,特别是在 mapfiltersetTimeout 中。
  • call/apply 用于函数借用和参数适配。
  • ✅ 使用 new 时构造函数首字母大写,避免忘记。
  • ❌ 避免在对象方法中使用箭头函数,除非明确不需要 this 指向对象本身。

5. 闭包和作用域

5.1 使用闭包简化代码

使用场景闭包的作用代码示例说明
1. 创建私有变量与封装模拟私有变量,避免全局污染,实现数据隐藏function createCounter() {
let count = 0; // 外部无法直接访问
return {
increment() { count++; return count; },
decrement() { count--; return count; },
value() { return count; }
};
}
const counter = createCounter();
console.log(counter.value()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2
// count 无法被外部直接修改
- count 变量被闭包保护,只能通过返回的方法访问
- 实现了封装性,类似面向对象的私有属性
2. 函数柯里化(Currying)将多参数函数转换为一系列单参数函数,提高复用性function add(a) {
return function(b) { return a + b; };
}
// 箭头函数更简洁
const add = a => b => a + b;
const add5 = add(5);
console.log(add5(3)); // 8
console.log(add5(10)); // 15
- 利用闭包”记住”第一个参数 a
- 可创建专用函数(如 add5),简化后续调用
- 减少重复传参
3. 事件处理与回调函数保持上下文信息,避免额外绑定或全局变量function setupButtons() {
const buttons = document.querySelectorAll('.btn');
buttons.forEach((btn, index) => {
// 闭包记住 index
btn.addEventListener('click', () => {
alert(\按钮 ${index + 1} 被点击`);<br> });<br> });<br>}<br>setupButtons();`
- 每个事件监听器通过闭包记住其 index
- 无需给 DOM 添加 data-index 或使用 bind
- 代码更简洁、语义清晰
4. 模块化与命名空间创建独立作用域,暴露有限接口,避免命名冲突const MathUtils = (function() {
// 私有函数
const square = x => x * x;
const cube = x => x * x * x;
// 公开接口
return {
add(a, b) { return a + b; },
doubleSquare(x) { return 2 * square(x); }
};
})();
console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.doubleSquare(4)); // 32
// square() 无法被外部调用
- 使用 IIFE(立即执行函数)创建闭包
- 私有函数 squarecube 不暴露
- 实现简单的模块模式
5. 延迟执行与参数记忆setTimeout 等异步操作中保持变量值// ❌ 错误:不使用闭包
for (var i = 0; i < 3; i++) {
setTimeout(() => { console.log(i); }, 100);
}
// 输出 3, 3, 3

// ✅ 正确:使用闭包(IIFE)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => { console.log(index); }, 100);
})(i);
}
// 输出 0, 1, 2

// 更优:使用 let 或箭头函数 forEach
- 闭包”记住”每次循环的 i
- 避免异步回调中变量共享问题
- 箭头函数 + let 是更现代的解决方案
6. 缓存/记忆化函数(Memoization)缓存函数执行结果,避免重复计算function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("缓存命中");
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10));
// 快速计算,重复调用从缓存取值
- cache 对象被闭包保护
- 提升性能,尤其适用于递归、复杂计算
- 闭包实现数据持久化

5.2 使用执行上下文跟踪程序执行

特性说明详细内容代码示例与解释
1. 什么是执行上下文JavaScript 引擎用来跟踪函数执行和代码运行环境的内部机制- 每次函数被调用时,都会创建一个新的执行上下文
- 全局代码也在一个执行上下文中执行(全局执行上下文)
- 所有执行上下文构成一个调用栈(Call Stack)
console.log("Global context");
function foo() {
console.log("foo context");
bar();
}
function bar() {
console.log("bar context");
}
foo();
// 输出顺序反映调用栈变化
2. 执行上下文的类型不同类型的代码对应不同上下文- 全局执行上下文:脚本开始执行时创建,只有一个
- 函数执行上下文:每次调用函数时创建,可有多个
- eval 执行上下文:在 eval 函数中运行代码时创建(不推荐使用)
// 全局上下文
let a = 1;
function fn() {
// fn 的执行上下文
let b = 2;
}
fn();
3. 执行上下文的生命周期每个上下文经历两个阶段1. 创建阶段(Creation Phase)
- 创建变量对象(VO)或词法环境
- 处理函数声明提升(Hoisting)
- 确定 this
2. 执行阶段(Execution Phase)
- 变量赋值、函数调用、代码逐行执行
console.log(x);
// undefined(创建阶段已提升)
var x = 10;
function f() { console.log("f"); }
// f 在创建阶段已存在
4. 执行上下文的组成每个上下文包含三个核心部分- 变量对象(Variable Object, VO)/ 词法环境(Lexical Environment)
  存储变量、函数声明、参数
- 作用域链(Scope Chain)
  用于变量查找,包含当前上下文和外层上下文的引用
- this 绑定
  确定函数中 this 的值
let globalVar = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
console.log(innerVar, outerVar, globalVar);
}
inner();
}
outer();
// 作用域链:inner → outer → global
5. 调用栈(Call Stack)跟踪函数调用顺序的栈结构- 先进后出(LIFO)
- 全局上下文在栈底
- 每次函数调用,其上下文入栈
- 函数执行完毕,上下文出栈
function first() { second(); }
function second() { third(); }
function third() { console.trace(); }
first();
// 调用栈变化:
// [global] → [global, first] → [global, first, second] → [global, first, second, third]
6. 变量提升(Hoisting)创建阶段的行为- var 声明的变量被提升,值为 undefined
- function 声明被完整提升
- let/const 也有提升,但处于”暂时性死区”
hoistedFunc();
function hoistedFunc() { console.log("I'm hoisted!"); }
// 函数定义提升,正常执行
console.log(varVar);
var varVar;
// 变量提升,但未赋值
7. 词法环境(Lexical Environment)ES6+ 中对变量对象的现代描述- 环境记录(Environment Record):存储变量和函数声明
- 外部环境引用(Outer):指向外层词法环境,形成作用域链
let x = 10;
function foo() {
let y = 20;
function bar() {
let z = 30;
console.log(x + y + z);
}
bar();
}
foo();
// bar 的词法环境 → outer: foo 的词法环境 → outer: 全局
8. 闭包与执行上下文闭包如何影响上下文生命周期- 正常情况下,函数执行完毕后其上下文被销毁
- 但如果内部函数被外部引用(形成闭包),其词法环境不会被回收
function outer() {
let secret = "I'm secret";
return function() { console.log(secret); };
}
const inner = outer();
// outer() 执行完,但其上下文未销毁
inner(); // 仍能访问 secret

执行上下文工作流程

程序开始

创建【全局执行上下文】→ 压入调用栈

执行全局代码(变量提升、赋值等)

调用函数 A()

创建【A 的执行上下文】→ 压入栈顶

A 执行中,调用函数 B()

创建【B 的执行上下文】→ 压入栈顶

B 执行完毕 → B 上下文出栈

A 继续执行 → A 执行完毕 → A 上下文出栈

全局代码执行完毕 → 全局上下文出栈

程序结束

5.3 使用词法环境跟踪变量作用域

特性说明结构与组成代码示例与解释
1. 什么是词法环境JavaScript 中用于管理变量和函数声明存储与查找的内部结构,与代码的词法(书写)位置相关- 环境记录(Environment Record):存储当前作用域内的变量、函数、参数
- 外部环境引用(Outer Lexical Environment):指向外层函数或全局作用域的词法环境,形成作用域链
let x = 10;
function outer() {
let y = 20;
function inner() {
let z = 30;
console.log(x + y + z);
}
inner();
}
outer();

inner 的词法环境:
- 环境记录:{ z: 30 }
- 外部引用 → outer 的词法环境
2. 词法环境的创建时机在函数声明时(而非调用时)就确定了其外部引用,体现”词法”特性- 全局词法环境在脚本开始时创建
- 每个函数在定义时就确定了其 [[Outer]] 引用
- 该引用不会改变,与调用位置无关
function outer() {
let a = 1;
return function inner() {
console.log(a); // 能访问 a
};
}
const fn = outer();
// a = 1 已被闭包捕获
function runner() {
let a = 999;
fn(); // 仍然输出 1,不是 999
}
runner();

🔍 inner 定义在 outer 内,其 [[Outer]] 指向 outer,与 fn() 在哪调用无关
3. 环境记录类型不同作用域使用不同类型的环境记录- 对象环境记录:全局作用域,变量作为对象属性存储(如 window.x
- 声明性环境记录:函数/块作用域,直接存储变量绑定
// 全局:对象环境记录
var globalVar = "global";
// 函数:声明性环境记录
function fn() {
let localVar = "local";
const blockConst = true;
}
4. 变量查找过程当访问一个变量时,JS 引擎如何通过词法环境链查找1. 在当前词法环境的环境记录中查找
2. 若未找到,沿 [[Outer]] 引用到外层环境查找
3. 重复直到全局环境
4. 仍未找到则报 ReferenceError
let x = "global";
function outer() {
let x = "outer";
function inner() {
console.log(x); // 输出 "outer"
}
inner();
}
outer();

🔍 查找 x:
inner 环境记录(无)
outer 环境记录(有,值为 “outer”)✅
5. 块级作用域与词法环境let/const 在块({})中也会创建词法环境- iffor{} 等块语句会创建新的词法环境
- 支持暂时性死区(TDZ)
if (true) {
let blockVar = "block";
console.log(blockVar); // 正常
}
console.log(blockVar);
// ❌ ReferenceError

blockVar 属于 if 块的词法环境,外部无法访问
6. 闭包与词法环境闭包的本质是函数携带其词法环境- 即使外层函数执行完毕,其词法环境仍被内部函数引用
- 因此不会被垃圾回收
function counter() {
let count = 0;
return function() {
return ++count;
};
}
const inc = counter();
console.log(inc()); // 1
console.log(inc()); // 2
// count 仍存在于 counter() 的词法环境中
7. 全局词法环境所有作用域的根环境- 环境记录:包含全局变量、window 对象属性等
- 外部引用:null(最外层)
let globalA = 1;
var globalB = 2;
// 全局词法环境记录包含:{ globalA: 1, globalB: 2, ... }
8. 函数调用与词法环境栈执行上下文栈中每个上下文都关联一个词法环境- 调用函数时,创建新的执行上下文,其词法环境入栈
- 执行完毕后出栈(除非被闭包引用)
function A() {
let a = 1;
B();
}
function B() {
let b = 2;
console.log(a); // ❌ 找不到,B 的 [[Outer]] 是全局,不是 A
}
A();
// ⚠️ B 定义在全局,其 [[Outer]] 是全局环境,无法访问 A 的变量

词法环境 vs 作用域链(Scope Chain)

对比项词法环境(Lexical Environment)作用域链(Scope Chain)
定义JS 引擎内部用于存储变量和嵌套关系的结构程序员概念,指变量查找的路径(由词法环境链构成)
组成环境记录 + 外部引用一系列词法环境的引用链
可见性引擎内部机制,不可直接访问开发者可理解的概念
关系作用域链是词法环境链的体现作用域链依赖词法环境的 [[Outer]] 引用形成

词法环境工作流程

  1. 解析代码,确定函数的词法嵌套关系
  2. 创建全局词法环境
  3. 函数定义时,确定其 [[Outer]] 指向外层环境
  4. 函数调用时:
    • 创建新的词法环境(含环境记录)
    • 将其 [[Outer]] 指向定义时确定的外层环境
    • 压入执行上下文栈
  5. 变量访问:
    • 从当前环境记录开始查找
    • 逐层向上(通过 [[Outer]])直到全局
  6. 函数执行结束:
    • 若无闭包引用,词法环境被销毁
    • 若有闭包,环境保留供内部函数访问

5.4 理解变量类型

特性维度varletconst说明
1. 声明后是否可重新赋值(Reassignment)✅ 可以✅ 可以❌ 不可以var a = 1; a = 2; // ✅ 允许
let b = 1; b = 2; // ✅ 允许
const c = 1; c = 2; // ❌ 报错
📌 const 声明的变量必须初始化,且不能重新赋值
2. 是否支持变量提升(Hoisting)✅ 提升,值为 undefined✅ 提升,但处于暂时性死区(TDZ)✅ 提升,但处于暂时性死区(TDZ)console.log(x); var x = 1; // undefined
console.log(y); let y = 2; // ❌ 报错
console.log(z); const z = 3; // ❌ 报错
3. 是否支持重复声明✅ 同一作用域可重复声明❌ 不允许重复声明❌ 不允许重复声明var a = 1; var a = 2; // ✅ 合法
let b = 1; let b = 2; // ❌ 报错
const c = 1; const c = 2; // ❌ 报错
4. 作用域🌐 函数作用域(或全局)🔒 块级作用域({}🔒 块级作用域({}if (true) {
var v = 1;
let l = 2;
const c = 3;
}
console.log(v); // 1 ✅
console.log(l); // ❌ 报错
console.log(c); // ❌ 报错

数据类型与可变性(Mutability of Values)

注意const 控制的是变量绑定的不可变性,而非值的不可变性。值的可变性取决于数据类型。

数据类型是否可变(Mutable)const 声明下是否可修改属性说明代码示例
基本类型(numberstringbooleannullundefinedsymbolbigint❌ 不可变❌ 无法修改值一旦创建就不能改变,任何”修改”都是创建新值const num = 100; num = 200; // ❌ 报错(重新赋值)
const str = "hello"; str[0] = "H"; // 无效,字符串不可变
console.log(str); // 仍为 "hello"
对象(Object)✅ 可变✅ 可以修改属性const 只防止变量重新赋值,但对象内容可变const obj = { name: "Alice" };
obj.name = "Bob"; // ✅ 允许
obj.age = 25; // ✅ 允许
obj = {}; // ❌ 报错:不能重新赋值
数组(Array)✅ 可变✅ 可以修改元素数组方法如 pushpopsplice 会修改原数组const arr = [1, 2];
arr.push(3); // ✅ [1,2,3]
arr[0] = 9; // ✅ [9,2,3]
arr = []; // ❌ 报错
函数(Function)✅ 可变(罕见)✅ 属性可修改函数也是对象,可添加属性,但通常不修改const fn = function() {};
fn.displayName = "MyFunc"; // ✅ 允许

如何真正创建不可变对象?

方法说明代码示例
Object.freeze()冻结对象,使其所有属性不可修改、添加、删除const frozen = Object.freeze({ name: "Alice", items: [1,2] });
frozen.name = "Bob"; // 无效(严格模式报错)
frozen.items.push(3); // ⚠️ 子对象未冻结!
console.log(frozen.items); // [1,2,3]
⚠️ 浅冻结:嵌套对象仍可变
深冻结(递归 freeze)手动或使用库实现嵌套对象冻结function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] && typeof obj[prop] === "object") deepFreeze(obj[prop]);
});
return Object.freeze(obj);
}
使用不可变数据结构库(如 Immutable.js)提供真正不可变的 List、Map 等类型const { List } = require('immutable');
const list = List([1, 2, 3]);
const newList = list.push(4); // 返回新实例
console.log(list.size); // 3(原对象未变)

变量类型最佳实践

建议说明
✅ 优先使用 const表明变量不应被重新赋值,提升代码可读性和安全性
✅ 需要重新赋值时用 let如循环变量、累加器
❌ 避免使用 var作用域不清晰,易引发 bug
✅ 理解 const 不等于”值不可变”对象/数组内容仍可修改
✅ 需要真正不可变时使用 Object.freeze() 或不可变库尤其在 Redux、状态管理中

5.5 词法环境中注册标识符

声明方式注册时机注册位置(环境记录)是否提升重复声明行为作用域代码示例与说明
var 变量声明创建阶段(执行前)变量环境(VariableEnvironment)✅ 提升,值为 undefined✅ 同一作用域内允许重复声明🌐 函数作用域或全局console.log(a); // undefined
var a = 1; var a = 2; // 合法
a = 3;
a 在创建阶段就被注册,值为 undefined,后续赋值在执行阶段
function 函数声明创建阶段(执行前)变量环境(VariableEnvironment)✅ 完整提升(函数体也被提升)✅ 允许重复声明(后声明覆盖前声明)🌐 函数作用域或全局fn(); // "Hello"
function fn() { console.log("Hello"); }
function fn() { console.log("World"); }
// 后者覆盖前者
✅ 函数在创建阶段就完整注册,可立即调用
let 变量声明创建阶段(但受暂时性死区限制)词法环境(LexicalEnvironment)✅ 声明提升,但不能访问(TDZ)❌ 不允许重复声明🔒 块级作用域({}console.log(b);
// ❌ 报错:Cannot access 'b' before initialization
let b = 2; let b = 3;
// ❌ 报错:Identifier 'b' has already been declared
b 在词法环境中已注册,但在赋值前处于 TDZ,无法访问
const 变量声明创建阶段(但受 TDZ 限制)词法环境(LexicalEnvironment)✅ 声明提升,但必须初始化,且不能访问(TDZ)❌ 不允许重复声明🔒 块级作用域({}console.log(c); // ❌ 报错
const c = 3; const c = 4;
// ❌ 报错
const 必须在声明时初始化,且绑定不可变
函数参数创建阶段词法环境(参数作为绑定注册)✅ 参数自动注册✅ 允许同名参数(但值以后者为准)🔒 函数作用域function demo(param, param) { console.log(param); }
demo(1, 2); // 输出 2
✅ 参数在函数调用时就在词法环境中注册
class 声明创建阶段(但处于 TDZ)词法环境✅ 声明提升,但处于 TDZ❌ 不允许重复声明🔒 块级作用域console.log(MyClass); // ❌ 报错
class MyClass { }
✅ 类声明类似于 let,有提升但不能在声明前访问
import 声明创建阶段词法环境✅ 提升(静态导入)❌ 不允许重复导入同名绑定🔒 模块作用域import { foo } from './module';
import { foo } from './other';
// ❌ 若同名则报错
✅ 所有 import 在模块解析时就注册

词法环境的两个关键组成部分

JavaScript 引擎使用两个词法环境来管理标识符注册:

组成部分用途存储内容特点
词法环境(LexicalEnvironment)用于 letconstclassimport 等块级绑定- let 变量
- const 常量
- class 声明
- 函数参数
- 支持块级作用域
- 存在暂时性死区(TDZ)
变量环境(VariableEnvironment)用于 varfunction 声明- var 变量
- function 函数声明
- 函数或全局作用域
- 传统提升机制

在函数执行上下文的创建阶段,这两个环境最初可能指向同一个词法环境,但在函数体中遇到 let/const 时会分离。

标识符注册流程(创建阶段)

  1. 创建词法环境和变量环境
  2. 扫描代码,收集所有声明:
    • varfunction 声明注册到变量环境
    • letconstclass、参数等注册到词法环境
  3. 初始化绑定:
    • varundefined
    • function → 函数对象
    • let/constuninitialized(进入 TDZ)
  4. 执行阶段:
    • 遇到赋值语句时,更新对应绑定的值
    • let/const 赋值后退出 TDZ,可访问

暂时性死区(Temporal Dead Zone, TDZ)说明

情况是否在 TDZ 中行为
let x; 声明前访问 x✅ 是❌ 报错:Cannot access before initialization
const y = 1; 声明前访问 y✅ 是❌ 报错
var z; 声明前访问 z❌ 否✅ 返回 undefined(已提升)

标识符注册核心规则

规则说明
✅ 所有声明都在创建阶段注册JS 引擎先扫描代码,提前注册所有标识符
var/function → 变量环境,提升至顶部可在声明前访问(varundefined
let/const/class → 词法环境,存在 TDZ不能在声明前访问,否则报错
✅ 块级作用域由词法环境实现{} 内的 let/const 创建新词法环境
✅ 函数参数也参与注册在函数调用时注册到其词法环境中

5.6 闭包工作原理

5.6.1 使用闭包模拟私有变量

实现方式代码示例私有变量机制可访问性优点缺点适用场景
1. 函数工厂模式(Function Factory)function createCounter() {
let count = 0; // 私有变量
return {
increment() { count++; return count; },
decrement() { count--; return count; },
value() { return count; }
};
}
const counter = createCounter();
console.log(counter.value()); // 0
counter.increment();
console.log(counter.value()); // 1
// count 无法从外部访问
利用闭包:内部函数引用外层函数的局部变量 count,即使外层函数执行完毕,count 仍被保留✅ 公开方法可访问 count
❌ 外部无法直接访问 count
- 语法简单直观
- 易于理解闭包原理
- 支持多个独立实例
- 每次调用 createCounter() 创建新对象,方法未共享(内存开销)- 简单对象封装
- 计数器、状态机等小型模块
2. 模块模式(Module Pattern)const CounterModule = (
function() {
let count = 0; // 私有变量
return {
increment: function() { count++; },
decrement: function() { count--; },
getValue: function() { return count; }
};
}
)();
CounterModule.increment();
console.log(CounterModule.getValue()); // 1
// count 无法访问
IIFE(立即执行函数)创建闭包,count 被公共接口方法引用✅ 模块内的方法可访问
❌ 外部无法访问
- 创建单例模块
- 避免全局污染
- 所有方法共享私有变量
- 通常是单例,不支持多实例
- 传统写法,ES6 模块更优
- 工具库(如 MathUtils)
- 全局配置管理
3. 构造函数 + 闭包(私有属性)function Person(name) {
let privateName = name; // 私有
this.getName = function() { return privateName; };
this.setName = function(newName) { privateName = newName; };
}
const p = new Person("Alice");
console.log(p.getName()); // "Alice"
p.setName("Bob");
console.log(p.getName()); // "Bob"
// p.privateName 是 undefined
构造函数内定义变量,特权方法通过闭包访问✅ 特权方法可访问
❌ 实例属性无法访问私有变量
- 支持多实例
- 每个实例有独立私有状态
- 方法定义在构造函数中,每次创建实例都会重新创建函数(内存浪费)- 需要私有属性的类实例
- 封装敏感数据
4. ES6 类 + 闭包模拟function createPrivateScope() {
const privates = new WeakMap();
return class {
constructor(name) {
privates.set(this, { name });
}
getName() {
return privates.get(this).name;
}
setName(name) {
privates.get(this).name = name;
}
};
}
const Person = createPrivateScope();
const p = new Person("Charlie");
console.log(p.getName()); // "Charlie"
使用 WeakMap 存储实例与私有数据的映射,WeakMap 变量被闭包保护✅ 类方法可访问
❌ 外部无法访问 privates
- 支持多实例
- 私有数据真正隐藏
- 可与 class 语法结合
- 语法复杂
- 需额外数据结构(WeakMap
- 需要兼容旧环境的私有字段
- 高级封装需求
5. ES6 类私有字段 #field(现代替代)class Person {
#name; // 私有字段
constructor(name) { this.#name = name; }
getName() { return this.#name; }
setName(name) { this.#name = name; }
}
const p = new Person("Diana");
console.log(p.getName()); // "Diana"
p.#name // ❌ 语法错误
使用 # 前缀定义私有字段,由语言层面支持✅ 类内部可访问
❌ 外部完全无法访问(语法层面禁止)
- 语法简洁
- 原生支持,性能好
- 支持多实例
- 需要现代浏览器/环境支持
- 不能动态访问
✅ 推荐用于新项目
- 所有需要私有成员的类设计

闭包模拟私有变量的核心原理

原理说明
作用域隔离私有变量定义在函数内部,外部作用域无法直接访问
闭包保持引用返回的对象方法(函数)引用了外层函数的变量,形成闭包,使变量不会被垃圾回收
特权方法只有通过返回的方法才能访问或修改私有变量,实现封装

对比:闭包 vs 原生私有字段

特性闭包方式原生私有字段(#field
语法较复杂简洁(#name
性能方法可能重复创建方法共享,性能更好
可访问性外部无法直接访问外部语法错误,更安全
支持多实例✅ 是✅ 是
浏览器支持所有环境较新环境(ES2022+)
调试难度较难(变量不可见)较易(现代工具支持)

闭包最佳实践

建议说明
✅ 新项目优先使用 #field语法清晰,原生支持,性能更好
✅ 旧项目或需兼容时使用闭包如函数工厂或模块模式
✅ 避免在循环中创建闭包函数防止意外共享变量
❌ 不要滥用私有变量简单场景直接使用公共属性即可

6. 生成器和 Promise

6.1 生成器函数

6.1.1 使用迭代器对象控制生成器

特性说明对应方法返回值结构代码示例说明
1. 生成器函数定义使用 function* 语法定义,调用后返回一个生成器对象(即迭代器对象)-返回 Generator Object(同时是迭代器)function* gen() {
yield 1;
yield 2;
return 3;
}
const g = gen();
console.log(g); // Generator { }
console.log(g.next); // function next()
✅ 生成器函数返回的是一个可迭代的迭代器对象,不会立即执行函数体
2. next() 方法(核心控制方法)启动或恢复生成器执行,运行到下一个 yieldreturnnext(value?){ value: any, done: boolean }
- value: yieldreturn 的值
- done: 是否完成
function* gen() {
console.log(\"Start\");
yield \"A\";
yield \"B\";
return \"C\";
}
const g = gen();
console.log(g.next()); // { value: \"A\", done: false }
console.log(g.next()); // { value: \"B\", done: false }
console.log(g.next()); // { value: \"C\", done: true }
console.log(g.next()); // { value: undefined, done: true }
✅ 每次调用 next(),生成器执行到下一个 yield
✅ 最后一次 returndone: true
3. 向生成器传值(双向通信)next(value) 中的 value 会成为上一个 yield 表达式的返回值next(value)value 成为 yield 的返回值function* gen() {
const x = yield \"Step 1\";
console.log(x); // 输出 \"Hello\"
const y = yield \"Step 2\";
console.log(y); // 输出 \"World\"
}
const g = gen();
console.log(g.next()); // { value: \"Step 1\", done: false }
console.log(g.next(\"Hello\")); // { value: \"Step 2\", done: false }
console.log(g.next(\"World\")); // { value: undefined, done: true }
✅ 实现生成器与外部的双向通信
✅ 第一次 next() 通常不传值(无对应 yield 接收)
4. return() 方法(提前终止)立即终止生成器,跳过剩余 yield,返回给定值return(value?){ value: value, done: true }function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.return(\"End\")); // { value: \"End\", done: true }
console.log(g.next()); // { value: undefined, done: true }
✅ 强制结束生成器
✅ 常用于资源清理或中断流程
5. throw() 方法(注入错误)向生成器内部抛出异常,可在 try...catch 中捕获throw(error)若未捕获:异常向外抛出
若捕获:继续执行
function* gen() {
try {
yield 1;
} catch (e) {
console.log(\"Caught:\", e.message);
}
yield 2;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.throw(new Error(\"Boom\"))); // { value: 2, done: false }
✅ 实现错误注入与处理
✅ 增强生成器的健壮性
6. 可迭代协议支持生成器对象默认可被 for...of、展开运算符等消费Symbol.iterator返回自身(thisfunction* gen() { yield \"X\"; yield \"Y\"; }
// 方式1:for...of
for (const val of gen()) { console.log(val); }
// X, Y
// 方式2:展开
const arr = [...gen()];
// [\"X\", \"Y\"]
✅ 无需手动调用 next()
✅ 自动遍历所有 yield 值(忽略 return 值)
7. 生成器组合(yield*使用 yield* 委托给另一个生成器或可迭代对象yield* iterable遍历并逐个 yield 其值function* gen1() { yield \"A\"; yield \"B\"; }
function* gen2() { yield* gen1(); yield \"C\"; }
const g = gen2();
console.log(g.next()); // { value: \"A\", done: false }
console.log(g.next()); // { value: \"B\", done: false }
console.log(g.next()); // { value: \"C\", done: false }
✅ 实现生成器的复用与组合
✅ 可委托给数组、字符串等
生成器控制流程图示
调用 gen() → 返回生成器对象 g

g.next() → 执行到第一个 yield → 暂停,返回 {value: ..., done: false}

g.next() → 恢复执行,yield 接收传入值 → 到下一个 yield → 暂停

...

g.next() → 执行到 return → 返回 {value: ..., done: true}

后续 next() → 返回 {value: undefined, done: true}
生成器 vs 普通函数对比
特性生成器函数 (function*)普通函数 (function)
执行方式分段执行(next() 控制)一次性执行完毕
返回值迭代器对象函数返回值或 undefined
暂停能力✅ 支持 yield 暂停❌ 不可暂停
状态保持✅ 内部状态在 yield 间保持❌ 局部变量在调用后销毁
适用场景惰性求值、无限序列、异步流程控制一次性计算
典型应用场景
  • 惰性求值:生成无限序列(如斐波那契数列)
  • 异步编程:配合 co 库或 async/await 实现同步风格的异步代码
  • 状态机:管理复杂的状态流转
  • 数据流处理:逐块处理大数据,避免内存溢出

6.1.2 使用生成器

使用类别说明语法/方法代码示例详细说明
1. 定义生成器函数使用 function* 定义,调用后返回生成器对象(迭代器)function* name() { ... }
const gen = function*() { ... }
function* myGenerator() { yield 'Hello'; yield 'World'; }
const g = myGenerator();
console.log(g); // Generator { }
✅ 调用生成器函数不会立即执行函数体
✅ 返回一个可迭代的迭代器对象
2. yield 表达式暂停执行并返回值,等待下一次 next() 调用yield value;
const received = yield value;
function* gen() {
yield 1;
const x = yield 2;
console.log(x); // 接收 next() 传入的值
yield 3;
}
const g = gen();
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.next(\"Hi\"); // 输出 \"Hi\", {value: 3, done: false}
yield 是双向通道:
- 向外返回值
- 接收 next(value) 传入的值
✅ 第一个 next() 通常不传值
3. next() 方法启动或恢复生成器执行generator.next(value?)function* counter() {
let i = 0;
while (true) { yield i++; }
}
const c = counter();
console.log(c.next().value); // 0
console.log(c.next().value); // 1
console.log(c.next().value); // 2
✅ 返回 { value: any, done: boolean }
done: true 表示执行完毕
✅ 可实现无限序列
4. return() 方法提前终止生成器,跳过剩余 yieldgenerator.return(value?)function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
g.next(); // {value:1, done:false}
console.log(g.return(\"End\")); // {value:\"End\", done:true}
✅ 强制结束,done: true
✅ 常用于资源清理或中断流程
5. throw() 方法向生成器内部抛出异常generator.throw(error)function* gen() {
try {
yield 1;
} catch (e) {
console.log(\"Error:\", e.message);
}
yield 2;
}
const g = gen();
g.next();
g.throw(new Error(\"Oops\"));
// 输出错误
g.next(); // {value:2, done:false}
✅ 可在生成器内用 try...catch 捕获
✅ 实现错误注入与处理
6. for...of 循环自动遍历所有 yield 值(忽略 returnfor (const val of generator)function* colors() {
yield \"Red\";
yield \"Green\";
yield \"Blue\";
}
for (const color of colors()) {
console.log(color);
}
// Red, Green, Blue
✅ 不需要手动调用 next()
✅ 无法获取 return 的值
✅ 遇到 throw() 会终止
7. 展开运算符将生成器的 yield 值展开为数组[...generator]function* numbers() {
yield 1;
yield 2;
yield 3;
}
const arr = [...numbers()];
// [1, 2, 3]
✅ 适用于有限生成器
❌ 无限生成器会导致死循环
8. yield* 委托委托给另一个生成器或可迭代对象yield* iterablefunction* gen1() {
yield 'A';
yield 'B';
}
function* gen2() {
yield* gen1();
yield 'C';
}
console.log([...gen2()]);
// ['A','B','C']
✅ 实现生成器组合复用
✅ 可委托给数组、字符串等:yield* [1,2,3]
9. 异步流程控制使用生成器 + Promise 实现同步风格的异步代码结合 co 模式或手动控制function* asyncTask() {
const data1 = yield fetch('/api/1');
const data2 = yield fetch('/api/2');
return [data1, data2];
}
// 需配合执行器函数运行
✅ 曾是 async/await 出现前的主流方案
✅ 现在多用于学习原理或特殊场景
10. 惰性求值与无限序列生成不立即计算的序列,节省内存结合 while(true)yieldfunction* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
✅ 只在需要时计算下一个值
✅ 可表示数学上的无限序列
生成器对象的返回值格式
属性类型说明
valueanyyieldreturn 的值
donebooleanfalse:未完成,可继续
true:已结束

示例:{ value: 42, done: false }

生成器 vs 普通函数对比
特性生成器函数普通函数
执行方式分段执行(next() 控制)一次性执行完毕
暂停能力✅ 支持 yield 暂停❌ 不可暂停
状态保持✅ 局部变量在 yield 间保留❌ 调用后销毁
返回值迭代器对象函数返回值
适用场景惰性求值、状态机、异步流程一次性计算
生成器最佳实践
建议说明
✅ 优先使用 async/await 处理异步语法更简洁,是现代标准
✅ 使用生成器实现惰性序列或状态机发挥其暂停和状态保持优势
✅ 避免在生成器中做复杂副作用保持其可预测性
✅ 结合 yield* 提高复用性模块化生成器逻辑

6.1.3 生成器内部构成

构成部分说明关键特征作用示例/补充说明
1. 生成器函数(Generator Function)function*使用 function* 声明的特殊函数,是生成器的”模板”或”构造器”- 函数名前有 *
- 调用后返回生成器对象
- 不立即执行函数体
定义生成器的逻辑和 yield 序列function* gen() {
yield 1;
yield 2;
}
const g = gen(); // g 是生成器对象
gen 是函数,g 是实例
2. 生成器对象(Generator Object)生成器函数调用后返回的实例,同时是迭代器和可迭代对象- 实现了 Iterator 接口(有 next()return()throw()
- 具有 Symbol.iterator 方法,返回自身
控制生成器的执行流程,对外提供交互接口console.log(typeof g.next); // \"function\"
console.log(g[Symbol.iterator]() === g); // true
✅ 可用于 for...of、展开等
3. 执行上下文(Execution Context)每个生成器对象拥有独立的调用栈上下文,保存局部变量、作用域链等- 独立于其他生成器实例
- 在 yield 时暂停但不销毁
- 恢复时从中断点继续
保持函数内部状态(如变量、执行位置)function* counter() {
let i = 0;
while (true) { yield i++; }
}
const c1 = counter();
const c2 = counter();
console.log(c1.next().value); // 0
console.log(c2.next().value); // 0(独立状态)
4. 指令指针(Program Counter / Instruction Pointer)内部指针,记录当前执行到哪一条语句- 初始指向函数体开始
- 每次 next() 向前移动到下一个 yieldreturn
- yield 时暂停,指针停在此处
控制代码执行的”暂停-恢复”机制function* gen() {
console.log(\"A\"); // ← 指针可能在此
yield 1; // ← 暂停点
console.log(\"B\");
yield 2;
}
✅ 指针在 yield 处暂停
5. yield 表达式生成器中的关键字,用于暂停执行并输出值- 可出现在任何表达式位置
- 支持双向通信:
  - 向外返回值
  - 接收 next(value) 传入的值
实现惰性求值和协程通信const received = yield \"out\";
// \"out\" 是输出值
// received 是 next() 传入的输入值
yield 是生成器的”暂停点”
6. 状态机(State Machine)生成器本质上是一个自动状态机,内部维护当前状态状态包括:
- suspended(暂停)
- executing(执行中)
- completed(完成)
决定 next()return()throw() 的行为g.next();
// → suspended → executing → suspended with value
✅ 引擎根据状态决定下一步操作
7. 返回值协议(Iterator Result Protocol)所有方法返回统一格式的对象{ value: any, done: boolean }
- value: 当前产出的值
- done: 是否已结束
标准化与外部的交互格式g.next(); // { value: 1, done: false }
g.return(); // { value: undefined, done: true }
g.throw(); // 若未捕获:抛出异常
8. 委托机制(yield*允许一个生成器委托给另一个可迭代对象或生成器- yield* iterable
- 递归展开并逐个 yield 其值
实现生成器组合与复用function* gen1() { yield 'A'; }
function* gen2() { yield* gen1(); yield 'B'; }
yield* 内部也是通过调用 next() 实现
9. 闭包环境(Lexical Environment)生成器函数定义时的词法环境被保留- 可访问外层函数的变量
- 结合 yield 形成闭包
支持更复杂的逻辑封装function outer(x) {
return function*() { yield x; };
}
const g = outer(42);
g.next(); // { value: 42, done: false }
生成器对象的生命周期
阶段内部状态触发动作说明
创建suspended调用 gen()分配执行上下文,指针指向函数开始
执行/恢复executing调用 next()return()throw()执行代码直到下一个 yieldreturn 或异常
暂停suspended遇到 yield保存上下文,返回 {value, done: false}
完成completed遇到 returnreturn()/throw() 未捕获后续所有调用返回 {value: undefined, done: true}
生成器与普通函数对比(内部机制)
特性生成器函数普通函数
执行方式协作式多任务(yield 主动让出)抢占式(执行完才返回)
上下文生命周期yield 时暂停但保留调用结束即销毁
返回值机制多次返回(yield)+ 最终返回(return仅一次返回
控制权外部通过 next() 控制内部自主控制

总结:生成器 = 生成器函数(模板) + 生成器对象(实例) + 暂停/恢复机制(yield) + 状态机 + 迭代器协议

6.2 使用 Promise 处理异步任务

6.2.1 回调函数带来的问题

问题类型说明具体表现代码示例产生原因解决方案
1. 回调地狱(Callback Hell / Pyramid of Doom)多层嵌套回调导致代码难以阅读和维护代码向右”金字塔式”缩进,逻辑分散getUser(id, (user) => {
getProfile(user.id, (profile) => {
getPosts(profile.userId, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
});
});
});
});
异步操作需按顺序执行,每层依赖上一层结果,导致嵌套加深✅ 使用 Promise + .then() / async/await
✅ 模块化拆分函数
2. 控制反转(Inversion of Control)将回调函数交给第三方库或函数执行,失去对执行流程的控制无法确定回调何时、如何、调用几次// 你无法控制 thirdPartyAPI 是否:
// - 立即调用?延迟调用?
// - 调用一次还是多次?
// - 会抛出异常?
thirdPartyAPI(data, (err, result) => {
/* 你的逻辑 */
});
回调被”托管”给外部函数,执行权被转移✅ 使用 Promise(控制权回归)
✅ 封装为 Promise 并使用 try/catch
3. 错误处理困难错误需通过回调参数传递(如 callback(err, data)),难以统一捕获需在每一层手动检查 err,容易遗漏getUser(id, (err, user) => {
if (err) return handleError(err);
getProfile(user.id, (err, profile) => {
if (err) return handleError(err);
// ... 多层重复检查
});
});
错误未通过异常机制抛出,而是作为参数传递✅ 使用 Promise + .catch()async/await + try/catch
✅ 统一错误处理中间件
4. 调试困难嵌套回调导致调用栈不完整,堆栈信息难以追踪调试器难以定位异步错误源头setTimeout(() => {
setTimeout(() => {
throw new Error(\"Who called me?\");
}, 100);
}, 100);
❌ 堆栈信息可能只显示 setTimeout,丢失上下文
异步回调打断了同步调用栈✅ 使用现代调试工具(如 Chrome DevTools 的异步堆栈追踪)
✅ 使用 async/await 提供更清晰的堆栈
5. 无法使用 returnthrow回调中 return 无法返回到外层函数,throw 可能未被捕获return 只能退出当前回调,不能传递结果function getData() {
asyncOp((data) => {
return data;
// ❌ 无法返回给 getData()
});
// getData() 实际返回 undefined
}
回调是独立函数调用,与外层函数执行流分离✅ 使用 Promise + resolve(data)
✅ 使用 async/await 返回值
6. 难以组合与复用多个回调操作难以并行、串行或组合处理需手动管理多个异步任务的同步// 如何同时加载用户和配置?
// 回调方式需手动计数或使用库
loadUser((user) => {
loadConfig((config) => {
initApp(user, config);
});
});
缺乏统一的异步操作抽象✅ 使用 Promise.all()Promise.race()
✅ 使用 async/await 并行控制
7. 内存泄漏风险回调函数持有外部变量引用,可能导致闭包内存泄漏未及时清理事件监听或定时器function attachHandler() {
const hugeData = new Array(1e6).fill(\"data\");
button.addEventListener('click', () => {
console.log(hugeData.length);
// 一直引用 hugeData
});
// 未移除监听,即使 hugeData 不再需要
}
闭包保持对变量的引用,阻止垃圾回收✅ 及时移除事件监听器(removeEventListener
✅ 清除定时器(clearTimeout
✅ 避免在回调中引用大对象
对比:回调函数 vs 现代异步方案
特性回调函数Promise / async/await
可读性差(嵌套深)好(扁平化,类似同步)
错误处理手动检查 err 参数try/catch 统一捕获
控制权交给第三方(控制反转)自身控制(.then() / await
组合性差(需手动管理)强(Promise.all 等)
调试体验差(堆栈断裂)较好(现代工具支持)
返回值无法通过 return 传递可自然返回
回调最佳实践
建议说明
✅ 新项目优先使用 async/await语法清晰,错误处理简单,控制权明确
✅ 将旧回调 API 封装为 Promise使用 util.promisify(Node.js)或手动包装
✅ 避免深层嵌套拆分为独立函数,提升可维护性
✅ 始终处理错误即使使用回调,也应检查 err 参数

6.2.2 Promise 简介

类别说明详细内容示例代码
1. 什么是 PromisePromise 是 JavaScript 中用于处理异步操作的内置对象,代表一个尚未完成但将来会完成的操作的结果。- 是 ES6(ECMAScript 2015)引入的标准
- 解决了回调地狱(Callback Hell)问题
- 提供了更清晰的异步编程模型
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(\"Success!\");
// 或 reject(\"Error!\");
}, 1000);
});
2. Promise 的三种状态Promise 对象有且仅有以下三种状态,状态一旦改变不可逆。- Pending(等待中):初始状态,尚未完成
- Fulfilled(已成功):操作成功完成,调用 resolve()
- Rejected(已失败):操作失败,调用 reject()
console.log(promise); // Pending
console.log(promise); // Fulfilled 或 Rejected
3. 创建 Promise使用 new Promise(executor) 构造函数创建。- executor 是一个函数,接收两个参数:
  - resolve(value):将状态变为 fulfilled
  - reject(reason):将状态变为 rejected
const fetchData = new Promise((resolve, reject) => {
const success = Math.random() > 0.5;
if (success) {
resolve({ data: \"Hello\" });
} else {
reject(new Error(\"Network failed\"));
}
});
4. 消费 Promise(核心方法)使用 .then().catch().finally() 处理结果。- .then(onFulfilled, onRejected):成功时执行 onFulfilled,可选失败时执行 onRejected
- .catch(onRejected):专门处理错误
- .finally(onFinally):无论成功失败都执行,常用于清理
fetchData
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log(\"请求完成\"));
5. 链式调用(Promise Chaining).then() 返回新的 Promise,支持链式操作,避免嵌套。- 每个 .then() 可以返回值或新的 Promise
- 返回值会传递给下一个 .then()
- 返回 Promise 会等待其完成
fetchData
.then(data => {
return data.data.toUpperCase();
})
.then(upper => console.log(upper))
.catch(err => console.log(err));
6. 静态方法Promise 提供多个有用的静态方法,用于组合多个 Promise。- Promise.resolve(value):返回已成功的 Promise
- Promise.reject(reason):返回已失败的 Promise
- Promise.all(iterable):所有成功才成功
- Promise.race(iterable):首个完成即返回
- Promise.allSettled(iterable):等待所有完成
- Promise.any(iterable):首个成功即返回
Promise.all([p1, p2, p3])
.then(results => console.log(results));

Promise.race([p1, p2])
.then(first => console.log(first));
7. 错误处理错误会沿着 Promise 链向后传递,直到被 .catch() 捕获。- 未捕获的 Promise 错误会触发 unhandledrejection 事件
- 建议始终使用 .catch()async/await 中的 try/catch
someAsync()
.then(() => {
throw new Error(\"Oops\");
})
.catch(err => console.log(err));
// 正确捕获
8. 与 async/await 的关系async/await 是基于 Promise 的语法糖,使异步代码更像同步代码。- async 函数自动返回 Promise
- await 等待 Promise 完成
async function getData() {
try {
const result = await fetchData;
console.log(result);
} catch (error) {
console.error(error);
}
}
9. 优点Promise 相比传统回调的优势。- ✅ 解决回调地狱,代码更扁平
- ✅ 错误处理统一(.catch()
- ✅ 支持链式调用和组合操作
- ✅ 状态不可逆,避免重复执行
10. 注意事项使用 Promise 时需注意的问题。- ❌ 不要忘记 .catch(),否则错误静默失败
- ❌ 避免在 .then() 中不返回值导致 undefined
- ❌ Promise.all() 中任一失败会导致整体失败,需根据场景选择 allSettled
- ⚠️ executor 函数是立即执行的
// 错误示例:缺少 catch
someAsync().then(data => console.log(data));
// 正确:添加 catch 或使用 async/await
Promise 状态流转图
Pending(初始)

   ├─ resolve(value) → Fulfilled(成功)
   │                     │
   │                     └─ .then(onFulfilled) 触发

   └─ reject(reason) → Rejected(失败)

                         └─ .catch(onRejected) 或 .then(null, onRejected) 触发

6.2.3 使用 Promise

使用类别方法/语法说明代码示例注意事项
1. 创建 Promisenew Promise(executor)手动封装异步操作,如定时器、自定义请求const delay = (ms) => {
return new Promise(
(resolve) => { setTimeout(resolve, ms); }
);
};
delay(1000).then(() => console.log(\"1秒后执行\"));
executor 函数立即执行
❌ 必须调用 resolvereject,否则 Promise 会永远 pending
2. 包装异步 API将回调函数 API 转为 Promise常用于 setTimeoutXMLHttpRequest、Node.js 回调等// 包装 setTimeout
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 包装 Node.js fs.readFile
const fs = require('fs');
const readFile = (path) => new Promise(
(resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
err ? reject(err) : resolve(data);
});
}
);
✅ 提升代码可读性
✅ 可使用 util.promisify(Node.js)自动转换
3. .then() 成功处理promise.then(onFulfilled)处理 Promise 成功结果,支持链式调用fetch('/api/user')
.then(response => response.json())
.then(user => console.log(user.name));
✅ 每个 .then() 返回新 Promise
✅ 可返回值或新 Promise,供下一级使用
4. .catch() 错误处理promise.catch(onRejected)捕获 Promise 链中的任何错误fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.error(\"请求失败:\", error));
✅ 建议链的末尾添加 .catch()
❌ 忽略 catch 会导致 unhandledrejection
5. .finally() 清理操作promise.finally(onFinally)无论成功失败都执行,常用于加载状态关闭let loading = true;
fetchData()
.then(data => { /* 处理数据 */ })
.catch(err => { /* 处理错误 */ })
.finally(() => { loading = false; console.log(\"请求结束\"); });
✅ 不接收参数,不改变结果
✅ 适合关闭加载动画、清理资源
6. 链式调用(Chaining)多个 .then() 串联实现异步操作的顺序执行getUser(1)
.then(user => getProfile(user.id))
.then(profile => getPosts(profile.userId))
.then(posts => console.log(posts))
.catch(err => console.error(err));
✅ 避免回调地狱
✅ 每一步的返回值传递给下一步
7. 组合多个 PromisePromise.all(iterable)所有 Promise 都成功才成功,任一失败则整体失败const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');
Promise.all([p1, p2])
.then(([usersRes, postsRes]) => {
return Promise.all([usersRes.json(), postsRes.json()]);
})
.then(([users, posts]) => { console.log(users, posts); })
.catch(err => console.error(err));
✅ 适合并行请求
❌ 一个失败则全部失败
8. 等待所有完成Promise.allSettled(iterable)等待所有 Promise 完成(无论成功失败)const promises = [
fetch('/api/a'),
fetch('/api/b').then(() => { throw new Error(); }),
fetch('/api/c')
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(\\请求 \${i} 成功:\, result.value);
} else {
console.log(\\请求 \${i} 失败:\, result.reason);
}
});
});
✅ 适合”全部尝试”场景
✅ 不因单个失败中断
9. 返回第一个完成的Promise.race(iterable)返回第一个完成(成功或失败)的 Promiseconst slow = new Promise(r => setTimeout(() => r(\"慢\"), 2000));
const fast = new Promise(r => setTimeout(() => r(\"快\"), 500));
Promise.race([slow, fast])
.then(result => console.log(result));
// 输出 \"快\"
✅ 用于超时控制
❌ 失败也算”完成”
10. 返回第一个成功的Promise.any(iterable)返回第一个成功的 Promise,全部失败才失败const fail1 = Promise.reject(\"失败1\");
const success = new Promise(r => setTimeout(() => r(\"成功\"), 1000));
const fail2 = Promise.reject(\"失败2\");
Promise.any([fail1, success, fail2])
.then(result => console.log(result));
// \"成功\"
✅ 适合”多源请求,取最快成功”
❌ 全部失败时抛出 AggregateError
11. 快速创建 PromisePromise.resolve(value)
Promise.reject(reason)
快速创建已解决或已拒绝的 Promise// 返回已成功的 Promise
const p1 = Promise.resolve(42);
// 返回已失败的 Promise
const p2 = Promise.reject(new Error(\"出错\"));
p1.then(v => console.log(v)); // 42
p2.catch(e => console.log(e.message)); // \"出错\"
✅ 常用于函数返回值标准化
✅ 在 .then() 中可直接返回值
12. 与 async/await 结合async 函数 + await使用同步语法处理 Promise,更清晰async function getData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(\"错误:\", error);
}
}
getData();
✅ 推荐现代写法
✅ 必须在 async 函数内使用 await
Promise 使用最佳实践
建议说明
✅ 始终使用 .catch()try/catch避免未捕获的 Promise 错误
✅ 优先使用 async/await语法更简洁,调试更方便
✅ 合理选择组合方法根据业务需求选择 allallSettledraceany
✅ 避免 Promise 地狱不要嵌套 .then(),应使用链式或 async/await
✅ 及时清理副作用如取消请求、清除定时器

7. 面向对象与原型

7.1 理解原型

JavaScript 原型(Prototype)详解表:

类别术语说明关键特性代码示例注意事项
1. 核心概念原型(Prototype)JavaScript 中实现继承和共享属性的机制。每个函数都有一个 prototype 属性,指向一个对象,该对象包含可被实例继承的属性和方法。是 JavaScript 基于原型的继承的核心
所有对象都有原型(除了 null)
属性查找遵循原型链
✅ 原型不是类,而是对象
✅ 理解原型是掌握 JS 面向对象的基础
原型对象(Prototype Object)函数的 prototype 属性所指向的对象,用于存放实例共享的属性和方法。默认包含 constructor 属性,指向函数本身
可手动添加方法或属性供实例继承
function Person(name) { this.name = name; }
Person.prototype.greet = function() {
console.log(\Hello, I’m ${this.name}`);<br>};`
✅ 添加到原型的方法会被所有实例共享
✅ 避免在原型中定义引用类型(如数组)的默认值
实例(Instance)通过 new 构造函数创建的对象,会继承构造函数原型上的属性和方法。实例通过 [[Prototype]] 内部链接指向原型对象
可通过 __proto__Object.getPrototypeOf() 访问
const p1 = new Person("Alice");
p1.greet(); // "Hello, I'm Alice"
✅ 实例优先使用自身属性,再查找原型
✅ 不要修改 __proto__(已废弃)
2. 关键属性与方法prototype(函数专属)函数对象的一个属性,指向其原型对象,用于构造实例时设置原型。仅函数有此属性(箭头函数除外)
用于构造实例的原型
console.log(Person.prototype);
// { greet: f, constructor: Person }
❌ 普通对象没有 prototype 属性
__proto__(实例属性)实例的非标准但广泛支持的属性,指向其原型对象。[[Prototype]] 的可访问形式
已被 Object.getPrototypeOf() 取代
console.log(p1.__proto__ === Person.prototype);
// true
⚠️ 不推荐在生产中使用,应使用 Object.getPrototypeOf()
Object.getPrototypeOf(obj)标准方法,返回对象的原型。推荐使用的标准 API
支持所有对象
console.log(Object.getPrototypeOf(p1) === Person.prototype);
// true
✅ 替代 __proto__
Object.setPrototypeOf(obj, prototype)标准方法,设置对象的原型(性能较差,慎用)。动态修改原型
影响性能,避免频繁使用
const animal = { speak() { console.log("Animal"); } };
Object.setPrototypeOf(p1, animal);
p1.speak(); // "Animal"
⚠️ 仅在必要时使用,影响 V8 优化
constructor原型对象上的属性,默认指向构造函数。可用于反向查找构造函数
可被手动修改
console.log(Person.prototype.constructor === Person); // true
console.log(p1.constructor === Person); // true
⚠️ 手动重写原型时需修复 constructor
3. 原型链(Prototype Chain)查找机制当访问对象属性时,先查找自身,若不存在则沿 [[Prototype]] 链向上查找,直到 null实现属性继承
链的终点是 Object.prototype → null
console.log(p1.toString());
// 继承自 Object.prototype
✅ 查找是单向向上的
✅ 性能:链越长,查找越慢
链的终点Object.prototype[[Prototype]]null,表示链的结束。所有对象都继承 Object.prototype 的方法(如 toStringhasOwnPropertyconsole.log(Object.getPrototypeOf(Object.prototype));
// null
✅ 可通过 Object.create(null) 创建无原型链的对象
4. 创建方式构造函数模式使用 new 调用函数创建实例,自动设置原型。实例的 [[Prototype]] 指向构造函数的 prototypefunction Car(brand) { this.brand = brand; }
Car.prototype.run = function() { console.log("Running"); };
const c1 = new Car("Tesla");
✅ 最常见的面向对象方式
Object.create(proto)创建新对象,显式指定其原型。第一个参数为原型对象
可创建”纯净”对象(Object.create(null)
const personProto = { greet() { console.log("Hi"); } };
const p2 = Object.create(personProto);
p2.greet(); // "Hi"
✅ 更直接的原型继承方式
ES6 class语法糖,底层仍基于原型。class 的方法自动定义在原型上
更清晰的语法
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(\${this.name} makes a sound`); }<br>}<br>const a1 = new Animal(“Dog”);`
✅ 推荐现代写法
✅ 本质仍是原型机制
5. 实际应用共享方法将方法定义在原型上,避免每个实例重复创建。节省内存
提高性能
// 推荐:定义在原型上
Person.prototype.greet = function() { ... };
// 不推荐:每个实例都创建新函数
function Person(name) { this.greet = function() { ... }; }
✅ 推荐做法
继承实现通过原型链实现对象间的继承。子类原型指向父类实例或使用 Object.create()function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
✅ 经典继承模式
✅ ES6 class + extends 更简单

原型关系图(文字描述):

实例对象 (p1)
   ↓ [[Prototype]] / __proto__
构造函数的 prototype 对象 (Person.prototype)
   ↓ [[Prototype]] / __proto__
Object.prototype
   ↓ [[Prototype]]
   null

7.2 构造器

7.2.1 实例属性

JavaScript 构造函数的实例属性详解表:

类别术语说明关键特性代码示例注意事项
1. 实例属性定义在构造函数内部通过 this 创建的属性,每个实例独立拥有,不共享。每个实例都有自己的一份副本
修改一个实例的属性不影响其他实例
优先级高于原型属性(屏蔽效应)
function Person(name, age) {
this.name = name;
this.age = age;
this.hobbies = ["reading"];
}
const p1 = new Person("Alice", 25);
const p2 = new Person("Bob", 30);
p1.hobbies.push("coding");
console.log(p2.hobbies); // ["reading"]
✅ 实例属性是对象最核心的数据载体
✅ 适合存储每个实例独有的数据
存储位置存储在实例对象本身上,可通过 Object.keys()for...in 枚举。不在原型上
是对象自身的”自有属性”(own property)
console.log(p1.hasOwnProperty("name")); // true
console.log(Object.keys(p1));
// ["name", "age", "hobbies"]
✅ 使用 hasOwnProperty() 可判断是否为实例属性
与原型属性的关系当访问属性时,JS 引擎优先查找实例属性,若不存在再查找原型链。实例属性会”屏蔽”同名的原型属性
删除实例属性后可恢复访问原型属性
function Dog(name) { this.name = name; }
Dog.prototype.name = "Unknown";
const d1 = new Dog("Max");
console.log(d1.name); // "Max"
delete d1.name;
console.log(d1.name); // "Unknown"
✅ 理解”属性屏蔽”机制对调试很重要
2. 创建方式在构造函数中使用 this最常见的方式,在 new 调用时为每个实例初始化属性。构造函数执行时 this 指向新实例
可接收参数动态赋值
function Book(title, author) {
this.title = title;
this.author = author;
this.isRead = false;
}
const b1 = new Book("JS Guide", "John");
✅ 推荐方式
实例化后动态添加可在创建实例后随时添加新属性。动态语言特性
属性在添加时才存在
const b2 = new Book("ES6", "Jane");
b2.pages = 300; // 动态添加
console.log(b2.pages); // 300
⚠️ 可能影响代码可预测性,建议在构造函数中统一定义
3. 与原型属性对比内存占用每个实例都保存一份,内存占用高。不共享,独立存储// 每个 Person 实例都有自己的 name 和 age
// 占用更多内存
✅ 适合存储唯一数据
❌ 避免在实例上定义大量重复方法
共享性❌ 不共享,各实例独立。修改 p1.age 不影响 p2.agep1.age = 26;
console.log(p2.age); // 30(不变)
✅ 数据隔离
适用场景存储实例独有的状态数据,如姓名、年龄、ID、配置等。每个对象的值不同✅ 推荐用于状态管理
4. 与方法的关系实例方法(不推荐)将方法定义在实例上(通过 this),每个实例都创建新函数。内存浪费
无法复用
function BadPerson(name) {
this.greet = function() {
console.log(\Hi, I’m ${name}`);<br> };<br>}`
❌ 浪费内存,应将方法定义在原型上
正确做法方法应定义在 prototype 上,实现共享。所有实例共享同一个函数Person.prototype.greet = function() {
console.log(\Hello, I’m ${this.name}`);<br>};`
✅ 节省内存,推荐做法

实例属性 vs 原型属性对比表:

特性实例属性原型属性
定义位置构造函数内部使用 this构造函数的 prototype 对象上
存储位置实例对象自身原型对象上
共享性❌ 每个实例独立一份✅ 所有实例共享
内存占用高(重复存储)低(只存一份)
修改影响仅影响当前实例影响所有实例(除非被实例属性屏蔽)
适用场景实例独有的数据(如 name, id)共享的方法或默认值
访问方式直接访问或 hasOwnProperty()通过原型链访问

总结:实例属性的核心要点:

要点说明
定义方式在构造函数中使用 this.property = value
独立性每个实例拥有独立副本,互不影响
优先级高于原型属性(屏蔽机制)
最佳实践用于存储实例状态,方法应放在原型上
现代替代ES6 class 中的 constructor 内定义的属性也是实例属性
class Person {
  constructor(name, age) {
    this.name = name; // 实例属性
    this.age = age;
  }
}

7.2.2 副作用

JavaScript 中的副作用(Side Effects)详解表:

类别术语定义关键特征代码示例潜在影响如何避免/管理
1. 基本概念副作用(Side Effect)指函数在执行过程中,除了返回值之外,对外部环境或状态产生可观察的影响。函数不”纯净”
执行结果依赖或改变外部状态
多次调用相同输入可能产生不同输出
降低可预测性
难以测试和调试
引发意外行为
✅ 尽量将副作用隔离
✅ 使用纯函数处理核心逻辑
纯函数(Pure Function)无副作用的函数:相同输入 ⇒ 相同输出,且不修改外部状态。无 I/O 操作
不修改全局变量或参数
不触发事件
function add(a, b) { return a + b; }✅ 易于测试、缓存、并行执行✅ 推荐用于业务逻辑计算
2. 常见副作用类型修改全局变量函数内部更改了全局作用域中的变量。破坏封装性
导致状态混乱
let counter = 0;
function increment() {
counter++;
return counter;
}
多个函数共享状态,难以追踪
并发时可能出错
✅ 将状态作为参数传入/返回
✅ 使用闭包封装状态
修改参数(引用类型)函数直接修改传入的对象或数组。调用者可能未预期到数据被改
数据不可控变更
function addItem(arr, item) {
arr.push(item); // 修改原数组
return arr;
}
难以追踪 bug✅ 返回新数组(concatmap、展开运算符)
DOM 操作函数直接读取或修改页面 DOM 结构。改变用户界面状态
依赖浏览器环境
function updateHeader(text) {
document.getElementById("header").innerText = text;
}
难以单元测试
与 UI 强耦合
✅ 将 DOM 操作封装在独立函数中
✅ 在框架中通过状态驱动 UI
网络请求(I/O)发起 fetchXMLHttpRequest 等异步请求。依赖外部服务
可能失败或超时
async function fetchUser(id) {
const res = await fetch(\/api/users/${id}`);<br> return res.json();<br>}`
异步、不可靠
需要错误处理
✅ 使用 Promise/async-await 处理
✅ Mock 请求用于测试
定时器操作设置或清除 setTimeoutsetInterval延迟执行,影响程序流程function delayedLog(msg) {
setTimeout(() => {
console.log(msg); // 副作用
}, 1000);
}
可能造成内存泄漏(未清除)
难以控制执行顺序
✅ 记录 timer ID 并提供清除机制
console 输出/日志使用 console.log()console.error() 等输出信息。影响开发环境
生产环境应移除
function debug(data) {
console.log("Debug:", data); // 副作用
}
污染控制台
可能暴露敏感信息
✅ 使用日志库并配置级别
✅ 构建时移除 console 语句
抛出异常使用 throw 主动抛出错误。中断正常执行流
需要调用者捕获
function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}
控制流跳转
未捕获会导致程序崩溃
✅ 在必要时使用
✅ 配合 try/catch 处理
读取外部状态读取全局变量、环境变量、时间等非参数数据。函数行为依赖外部环境function greeting() {
const hour = new Date().getHours();
if (hour < 12) return "Good morning";
else return "Good afternoon";
}
相同输入可能返回不同结果
难以测试
✅ 将外部依赖作为参数传入

副作用的影响总结:

影响维度说明
可测试性❌ 难以进行单元测试(依赖外部状态)
可预测性❌ 相同输入可能产生不同输出
可维护性❌ 状态混乱,bug 难以追踪
可复用性❌ 函数与上下文强耦合,不易复用
并发安全❌ 多线程/异步环境下可能引发竞态条件(Race Condition)

管理副作用的最佳实践:

实践说明
✅ 函数式编程原则尽量使用纯函数处理业务逻辑,将副作用隔离到程序边缘
✅ 副作用隔离将 I/O、DOM、网络等操作封装在独立模块或函数中
✅ 依赖注入将外部依赖(如时间、API 客户端)作为参数传入,便于 Mock 测试
✅ 使用状态管理在大型应用中使用 Redux、Vuex 等集中管理状态变更
✅ Immutability(不可变性)使用 Object.freeze() 或 Immutable.js 避免意外修改对象
✅ 函数封装将有副作用的代码包装成明确命名的函数,提高可读性

从有副作用到更可控的设计示例:

// ❌ 有副作用:修改参数、读取全局、DOM 操作
let users = [];
function renderUserList() {
  users.sort(); // 修改全局
  document.body.innerHTML = `<ul>${users.map(u => `<li>${u}</li>`).join('')}</ul>`;
}
// ✅ 改进:纯函数 + 副作用分离
function getSortedUsers(users) { // 纯函数
  return [...users].sort(); // 不修改原数组
}
function render(list) { // 副作用集中
  document.body.innerHTML = `<ul>${list.map(u => `<li>${u}</li>`).join('')}</ul>`;
}
// 使用
const sorted = getSortedUsers(userList);
render(sorted);

7.2.3 使用构造函数实现对象类型

使用构造函数实现对象类型的完整指南:

类别术语/概念说明关键特性代码示例注意事项
1. 基本定义构造函数(Constructor Function)一种特殊函数,用于创建和初始化特定类型的对象实例。通过 new 关键字调用。函数名通常大写首字母
内部使用 this 绑定实例属性
隐式返回新对象
function Person(name, age) {
this.name = name;
this.age = age;
}
const p1 = new Person("Alice", 25);
✅ 必须使用 new 调用
❌ 直接调用会污染全局对象
2. 创建实例new 操作符的执行过程四步机制:1. 创建空对象 2. 将该对象的 [[Prototype]] 指向 Constructor.prototype 3. 将 this 绑定到该对象并执行构造函数体 4. 返回该对象function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result && typeof result === 'object' ? result : obj;
}
✅ 理解 new 的机制有助于掌握原型继承
3. 实例属性在构造函数中定义属性直接绑定在实例上,每个实例独立拥有。使用 this.property = value
不共享,内存开销较高
可被修改而不影响其他实例
function Car(brand, color) {
this.brand = brand;
this.color = color;
this.started = false;
}
✅ 适合存储独有状态
❌ 避免在此处定义方法(应放原型上)
4. 共享方法Constructor.prototype构造函数的 prototype 属性指向一个对象,所有实例共享其上的方法。方法定义一次,被所有实例继承
节省内存
支持动态添加
Car.prototype.start = function() {
this.started = true;
console.log(\${this.brand} started.`);<br>};`
✅ 推荐将方法放在原型上
✅ 可后续扩展功能
修复 constructor手动重写 prototype 时需恢复 constructor 指向。默认 prototype.constructor 指向构造函数
重写后需手动设置
Car.prototype = {
start() { /*...*/ },
drive() { /*...*/ }
};
Car.prototype.constructor = Car; // ✅ 修复
⚠️ 否则 instanceof 和反射可能出错
5. 类型识别instanceof 运算符检查对象是否为某构造函数的实例。基于原型链检查
返回布尔值
console.log(p1 instanceof Person); // true
console.log(c1 instanceof Car); // true
✅ 安全的类型判断方式
.constructor 属性实例可通过 constructor 属性访问其构造函数。来自原型链console.log(p1.constructor === Person); // true⚠️ 可被修改,不如 instanceof 可靠
6. 继承实现原型链继承子类原型指向父类实例,实现方法继承。子类可访问父类原型方法
构造函数需手动调用父类初始化
function ElectricCar(brand, color, battery) {
Car.call(this, brand, color);
this.battery = battery;
}
ElectricCar.prototype = Object.create(Car.prototype);
ElectricCar.prototype.constructor = ElectricCar;
✅ 经典继承模式
⚠️ 父类构造函数中的引用类型会被共享
7. 静态属性与方法静态成员属于构造函数本身,不属于任何实例。通过构造函数直接访问
用于工具方法或常量
Car.wheels = 4;
Car.getInfo = function() {
return "A car has 4 wheels.";
};
✅ 适合定义通用工具
❌ 实例无法直接访问
8. ES6 Class 替代写法class 语法糖ES6 提供更清晰的语法,底层仍基于构造函数和原型。constructor 方法对应构造函数
类中定义的方法自动在原型上
支持 extends 实现继承
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(\Hi, I’m ${this.name}`);<br> }<br>}<br>class Student extends Person {<br> constructor(name, age, grade) {<br> super(name, age);<br> this.grade = grade;<br> }<br> study() {<br> console.log(`${this.name} is studying.`);<br> }<br>}`
✅ 推荐现代写法
✅ 更易读、易维护
9. 实际应用创建自定义类型对象如用户、商品、订单等业务实体。封装数据和行为
支持多实例
function Product(name, price) {
this.name = name;
this.price = price;
}
Product.prototype.getDiscountPrice = function(rate) {
return this.price * (1 - rate);
};
✅ 适用于需要复用结构和行为的场景
10. 注意事项避免遗漏 new直接调用构造函数会导致 this 指向全局对象。严格模式下会报错
可能污染全局环境
// 错误
const p = Person("Bob", 30);
// this → window(非严格模式)
✅ 使用 class 避免此问题
✅ 或使用工厂函数模式
性能优化方法应定义在原型上,避免重复创建。实例方法定义在 this 上会浪费内存// ❌ 反模式
function BadPerson(name) {
this.greet = function() { /*...*/ };
}
// ✅ 正确
Person.prototype.greet = function() { /*...*/ };
✅ 始终将方法放在原型上

构造函数 vs 工厂函数 vs Class 对比:

特性构造函数工厂函数ES6 Class
调用方式new Constructor()factory()new Class()
this 绑定自动绑定新对象需手动创建对象自动绑定
原型支持手动管理 prototype无内置原型机制内置 extends 继承
类型识别instanceof 可用typeof 或自定义标记instanceof 可用
推荐程度✅(传统)✅(函数式风格)✅✅✅(现代首选)

总结:构造函数实现对象类型的核心要点:

要点说明
本质JavaScript 中”类”的前身,通过函数 + new + 原型实现对象类型
实例属性在构造函数中用 this 定义
共享方法定义在 Constructor.prototype
继承通过原型链(Object.create() + 修复 constructor
现代替代class 是语法糖,更推荐使用

7.3 继承

7.3.1 构造器重载

JavaScript 构造器”重载”详解表:

类别说明是否原生支持实现方式/技巧代码示例注意事项
1. 原生支持?JavaScript 不支持构造函数重载。❌ 否函数名唯一
无法像 Java/C++ 那样定义多个同名但参数不同的构造函数
function Person(name) { this.name = name; }
// 以下定义会覆盖上面的!
function Person(name, age) { this.name = name; this.age = age; }
⚠️ 后定义的函数会完全替换前一个
❌ 不能通过参数数量区分不同构造逻辑
2. 模拟重载方式通过参数判断实现多态初始化✅ 可模拟检查 arguments 或参数值
使用 typeof 判断参数类型
根据条件分支设置不同属性
function Person() {
const args = arguments;
if (args.length === 1 && typeof args[0] === 'string')
{ this.name = args[0]; this.age = null; }
else if (args.length === 2)
{ this.name = args[0]; this.age = args[1]; }
else
{ this.name = "Unknown"; this.age = 0; }
}
✅ 灵活,可实现多种初始化逻辑
⚠️ 代码可能变得复杂,需良好注释
通过对象字面量配置参数(推荐)✅ 推荐方式接收一个配置对象作为唯一参数
支持任意字段的可选性与默认值
function Person(config) {
this.name = config.name;
}
使用工厂函数替代构造函数✅ 替代方案定义多个工厂函数,返回不同配置的实例
更灵活,无需 new
function createPersonWithName(name) { return { name, age: 0 }; }
function createPersonFull(name, age) { return { name, age }; }
✅ 完全避免 new 的问题
❌ 返回的对象没有原型链
3. ES6 默认参数 + 解构现代简洁写法✅ 推荐结合默认值和解构赋值,使参数更清晰function Person({ name = "Unknown", age = 0, city = "N/A" } = {}) {
this.name = name;
this.age = age;
this.city = city;
}
✅ 语法最简洁
✅ 自动处理 undefined 和缺失字段
4. Class 中的情况ES6 class 也不支持重载❌ 否constructor 只能有一个
重写会报错(SyntaxError
class Person {
constructor(name) { this.name = name; }
// ❌ SyntaxError: Duplicate constructor
constructor(age) { this.age = age; }
}
✅ 仍需使用配置对象等模拟方式

总结:JavaScript 构造器”重载”的可行策略:

策略适用场景推荐程度
参数判断法简单场景,参数类型差异明显⭐⭐☆
配置对象法大多数场景,尤其是参数多或可选⭐⭐⭐⭐⭐(强烈推荐)
工厂函数法需要多种创建逻辑,或避免 new⭐⭐⭐⭐
ES6 解构 + 默认值现代项目,结合配置对象使用⭐⭐⭐⭐⭐(最佳实践)

结论:

  • ❌ JavaScript 原生不支持构造器重载。
  • ✅ 但可以通过参数检查、配置对象、工厂函数等方式模拟重载行为。
  • ✅ 推荐使用配置对象 + 解构默认值的方式,代码清晰、可维护性强。

最佳实践示例(推荐写法):

function User({ id, name = "Guest", email, role = "user" } = {}) {
  this.id = id;
  this.name = name;
  this.email = email;
  this.role = role;
}
const u1 = new User(); // 使用全部默认值
const u2 = new User({ name: "Alice", email: "a@b.com" });

7.3.2 instanceof 操作符

类别术语/概念说明语法格式代码示例注意事项
1. 基本定义instanceof用于检测构造函数的 prototype 是否出现在对象的原型链中。object instanceof Constructorfunction Person(name) { this.name = name; }
const p1 = new Person("Alice");
console.log(p1 instanceof Person); // true
✅ 用于判断对象是否为某构造函数的实例
✅ 基于原型链进行判断
2. 工作原理原型链查找机制instanceof 会沿着对象的 [[Prototype]] 链向上查找,检查是否存在 Constructor.prototypeconst arr = [1, 2, 3];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
✅ 返回 true 只要原型链中存在对应 prototype
3. 使用场景判断自定义对象类型判断由构造函数或 class 创建的对象类型。obj instanceof MyConstructorclass Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // true
✅ 支持继承关系判断(多态)
内置对象类型判断检测是否为数组、正则、日期等内置对象。obj instanceof BuiltInTypeconst date = new Date();
console.log(date instanceof Date); // true
const arr = [];
console.log(arr instanceof Array); // true
✅ 比 typeof 更精确
typeof [] 返回 "object"
4. 特殊情况跨执行上下文失效在 iframe 或多个 window 环境中,不同全局环境的构造函数不共享原型链。const iframe = document.getElementById('frame').contentWindow;
const arr = new iframe.Array(1, 2, 3);
console.log(arr instanceof Array); // false!
⚠️ instanceof 在跨 frame 时可能失效
✅ 此时应使用 Array.isArray() 等方法
基本类型(原始值)原始值使用 instanceof 返回 falseconsole.log("hello" instanceof String); // false
console.log(new String("hello") instanceof String); // true
instanceof 只对对象有效
✅ 原始值没有原型链
nullundefined不能使用 instanceof 判断。console.log(null instanceof Object); // false
console.log(undefined instanceof Object); // false
✅ 它们不是任何构造函数的实例
5. 对比vs typeoftypeof 返回数据类型的字符串,但对对象细分不足。typeof valueconsole.log(typeof []); // "object"
console.log(typeof {}); // "object"
typeof 快速但不精确
instanceof 更适合对象类型细分
vs Object.prototype.toString.call()最精确的类型判断方式,可识别所有内置类型。Object.prototype.toString.call(obj)console.log(Object.prototype.toString.call([]));
// "[object Array]"
✅ 跨上下文安全
✅ 推荐用于库开发
vs Array.isArray()专门用于判断数组,比 instanceof 更可靠。Array.isArray(obj)console.log(Array.isArray(arr)); // true(跨 frame 下也有效)✅ 推荐替代 instanceof Array
6. 自定义行为Symbol.hasInstance可通过静态方法自定义 instanceof 的判断逻辑。Constructor[Symbol.hasInstance](obj)function MyType() {}
MyType[Symbol.hasInstance] = function(obj) {
return obj.hasMyType === true;
};
const obj = { hasMyType: true };
console.log(obj instanceof MyType); // true
✅ 高级用法,用于自定义类型检查逻辑

instanceof 使用总结:

场景推荐方法原因
判断是否为数组Array.isArray(arr)跨上下文安全,语义清晰
判断是否为正则obj instanceof RegExpObject.prototype.toStringinstanceof 在同域下可用
判断自定义类实例obj instanceof MyClass语义清晰,支持继承
通用精确类型判断Object.prototype.toString.call(obj)最可靠,适用于所有内置类型
判断原始值类型typeof value直接有效

7.4 Class

7.4.1 使用

JavaScript class 使用方法详解表:

类别语法/特性说明关键点代码示例注意事项
1. 基本定义class ClassName { ... }定义一个类,是构造函数的语法糖,底层仍基于原型。类声明不会被提升
必须使用 new 调用
类体内默认启用严格模式
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const p1 = new Person("Alice", 25);
✅ 类名通常大写首字母
❌ 不能直接调用 Person(...)
2. 构造函数constructor()类的构造方法,用于初始化实例属性。每个类最多一个 constructor
若省略,会自动添加空构造函数
使用 this 绑定实例属性
class Car {
constructor(brand) {
this.brand = brand;
this.started = false;
}
}
✅ 必须通过 super() 调用父类构造函数(在继承中)
3. 实例方法methodName() { ... }定义在类原型上的方法,所有实例共享。自动添加到 ClassName.prototype
支持 this 指向当前实例
class Person {
constructor(name) { this.name = name; }
greet() {
console.log(\Hello, I’m ${this.name}`);<br> }<br>}`
✅ 推荐将方法定义在此处(节省内存)
❌ 不要在此定义函数表达式
4. 静态方法static methodName() { ... }属于类本身,不属于任何实例。通过类名调用:ClassName.method()
不能通过实例调用
常用于工具函数或工厂方法
class MathUtils {
static add(a, b) { return a + b; }
}
console.log(MathUtils.add(2, 3)); // 5
✅ 适合定义与类相关但不依赖实例的逻辑
5. 静态属性(ES2022+)static propertyName = value类的静态属性。直接绑定在类上
可用于配置或常量
class Config {
static API_URL = "https://api.example.com";
static VERSION = "1.0";
}
⚠️ 需注意浏览器兼容性
✅ 可替代构造函数外的赋值
6. 实例属性(提案语法)propertyName = value在类中直接定义实例属性(无需在 constructor 中)。使用类字段语法
更简洁的初始化方式
class Counter {
count = 0; // 实例属性
increment = () => { this.count++; };
}
⚠️ 非标准语法,需 Babel/TypeScript 转译
✅ 提升可读性
7. Getters / Settersget propertyName() / set propertyName(value)定义属性的读取和赋值行为。用于封装、验证、计算属性
外部使用像普通属性
class Person {
constructor(name) { this._name = name; }
get name() { return this._name.toUpperCase(); }
set name(value) {
if (value.length === 0) throw new Error("Name cannot be empty");
this._name = value;
}
}
✅ 实现数据验证和封装
⚠️ 避免在 getter 中产生副作用
8. 继承(extends)class Child extends Parent实现类之间的继承。子类可继承父类的属性和方法
支持多层继承
使用 super() 调用父类构造函数
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(\${this.name} makes a sound`); }<br>}<br>class Dog extends Animal {<br> constructor(name, breed) {<br> super(name);<br> this.breed = breed;<br> }<br> speak() { console.log(`${this.name} barks`); }<br>}`
super() 必须在 this 之前调用
✅ 可重写父类方法(多态)
9. 私有成员(##fieldName定义私有属性或方法,外部不可访问。使用井号 # 前缀
真正的封装机制
只能在类内部访问
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount > 0) this.#balance += amount;
}
getBalance() { return this.#balance; }
}
✅ 实现数据隐藏
⚠️ 仅现代浏览器支持
10. 静态块static { ... }在类定义时执行一次性初始化逻辑。可访问私有成员
用于设置静态字段或执行初始化代码
class Config {
static API_KEY;
static {
const env = JSON.parse(process.env.CONFIG);
this.API_KEY = env.apiKey;
}
}
✅ ES2022 新特性
✅ 适合复杂静态初始化
11. 类表达式const ClassName = class { ... }类也可以像函数一样作为表达式使用。可命名或匿名
支持立即执行
const Person = class {
constructor(name) { this.name = name; }
};
const createClass = (name) => class {
getName() { return name; }
};
✅ 灵活,可用于工厂模式或条件定义

Class 与传统构造函数对比:

特性class 语法传统构造函数
语法清晰度✅ 简洁,面向对象风格❌ 冗长,需手动操作 prototype
继承实现extends + super(),简单直观Object.create() + 手动修复 constructor
静态方法static 关键字Constructor.method = function() {}
私有成员#privateField(原生支持)闭包模拟,不直观
构造函数调用检查自动报错(必须 new可能污染全局(this 指向 window
推荐程度✅✅✅(现代首选)⚠️(旧项目兼容)

最佳实践总结:

建议说明
✅ 使用 class 替代传统构造函数语法更清晰,易于维护
✅ 合理使用 get/set 封装属性实现数据验证和计算属性
✅ 优先使用 static 方法处理工具逻辑避免污染实例
✅ 在继承中正确调用 super()避免 this 错误
✅ 使用 # 实现私有成员提高封装性和安全性
✅ 避免在 class 中定义箭头函数方法(除非必要)每个实例都会创建新函数,浪费内存

结论:

  • class 是 JavaScript 中实现面向对象编程的现代标准方式。
  • 它是语法糖,底层仍基于原型机制,但提供了更清晰、安全、易读的语法。
  • 支持继承、静态成员、私有字段、getter/setter 等高级特性。
  • 推荐在所有新项目中使用 class 替代传统的构造函数+原型模式。

7.4.2 继承

JavaScript class 继承详解表:

类别特性/语法说明关键机制代码示例注意事项
1. 基本语法class Child extends Parent使用 extends 关键字实现类继承。子类继承父类的实例属性和方法
支持多层继承(链式继承)
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(\${this.name} makes a sound`); }<br>}<br>class Dog extends Animal {<br> constructor(name, breed) {<br> super(name);<br> this.breed = breed;<br> }<br>}`
✅ 必须调用 super()this 之前
2. 调用父类构造函数super(...args)在子类 constructor 中调用父类构造函数以初始化继承的属性。super() 必须在 this 访问前调用
不调用会报错
class Cat extends Animal {
constructor(name, color) {
// this.color = color; ❌ 错误
super(name); // ✅ 先调用 super
this.color = color;
}
}
⚠️ super() 必须是构造函数中第一句
✅ 可传递参数给父类
3. 方法重写(Override)在子类中定义同名方法子类可以重写父类的方法,实现多态。子类方法优先调用
可通过 super.method() 调用父类版本
class Dog extends Animal {
speak() {
console.log(\${this.name} barks`);<br> }<br>}<br>class Bird extends Animal {<br> speak() {<br> super.speak(); // 调用父类方法<br> console.log(`${this.name} chirps`);<br> }<br>}`
✅ 实现多态行为
super.method() 可复用父类逻辑
4. 访问父类成员super.property / super.method()在子类中访问父类的属性或方法。super 指向父类的原型class Animal {
greet() { return "Hello"; }
}
class Dog extends Animal {
greet() { return super.greet() + ", I'm a dog"; }
}
✅ 用于扩展父类功能
⚠️ 仅在类方法中可用
5. 静态成员继承static 方法和属性可被继承子类会继承父类的静态成员。静态方法通过类名调用
继承链基于构造函数的原型
class Animal {
static planet = "Earth";
static info() { return "This is an animal"; }
}
class Dog extends Animal {}
console.log(Dog.planet); // "Earth"
Dog.__proto__ === Animal
6. 实例类型判断instanceof.constructor检查对象是否为某类的实例。基于原型链检查const d = new Dog("Max");
console.log(d instanceof Dog); // true
console.log(d instanceof Animal); // true
✅ 支持多态判断
✅ 继承关系中 instanceof 返回 true
7. 私有成员私有字段和方法不被继承# 开头的私有成员不能在子类中访问。私有成员仅在定义它们的类中可用class Animal {
#privateMethod() { return "secret"; }
}
class Dog extends Animal {
test() {
// this.#privateMethod(); ❌ 报错
}
}
✅ 实现真正的封装
❌ 子类无法访问父类私有成员
8. 多重继承模拟JavaScript 不支持多继承一个类只能 extends 一个父类。可通过 Mixin 模式模拟多继承function flyable(Base) {
return class extends Base {
fly() { console.log("Flying..."); }
};
}
class Bird extends flyable(Animal) {}
✅ 使用 Mixin 实现功能组合
❌ 不能直接 extends A, B
9. 继承内置对象可继承 ArrayErrorMap自定义类可继承内置构造函数。增强内置对象功能class MyArray extends Array {
sum() { return this.reduce((a, b) => a + b, 0); }
}
const arr = new MyArray(1, 2, 3);
console.log(arr.sum()); // 6
✅ 可扩展内置类型
✅ 保持原有行为
10. 抽象类模拟JavaScript 无原生 abstract可通过抛出错误模拟抽象类。在父类构造函数或方法中抛出异常class Shape {
constructor() {
if (new.target === Shape)
throw new Error("Shape is abstract");
}
draw() {
throw new Error("draw() must be implemented");
}
}
✅ 模拟强制子类实现
❌ 非语言级支持(TypeScript 支持)

Class 继承机制核心原理:

机制说明
原型链继承Child.prototype.__proto__ === Parent.prototype
实例 → Child.prototypeParent.prototypeObject.prototype
构造函数继承通过 super() 调用父类构造函数,初始化实例属性
静态继承Child.__proto__ === Parent,实现静态成员继承
super 实现super 是一个指向父类或父类原型的引用,用于调用父类方法或构造函数

继承最佳实践:

建议说明
✅ 使用 extends 实现单继承标准、清晰的继承方式
✅ 子类构造函数中必须调用 super()否则会报错
✅ 合理使用 super.method() 复用父类逻辑避免重复代码
✅ 使用 Mixin 实现功能组合模拟多继承,提升代码复用
✅ 优先使用组合而非深度继承避免”继承地狱”
✅ 利用 instanceof 进行类型判断安全、可靠

总结:Class 继承的核心要点:

要点说明
语法class Child extends Parent
调用父构造super(...args) 必须在 this 之前
方法重写子类定义同名方法即可重写
访问父方法使用 super.method()
静态继承子类继承父类的 static 成员
私有成员不被子类继承,仅本类可访问
多继承不支持,可用 Mixin 模拟
内置对象继承支持,可扩展 ArrayError

8. 处理集合

8.1 数组

JavaScript 数组处理方法详解表:

操作类别方法名功能说明语法格式是否改变原数组返回值代码示例
1. 添加/删除元素push()在数组末尾添加一个或多个元素arr.push(element1, ..., elementN)✅ 改变新长度(number)const arr = [1, 2];
arr.push(3, 4); // arr: [1, 2, 3, 4]
pop()移除并返回数组最后一个元素arr.pop()✅ 改变被移除的元素(any)const last = arr.pop();
// last: 4, arr: [1, 2, 3]
unshift()在数组开头添加一个或多个元素arr.unshift(element1, ..., elementN)✅ 改变新长度(number)arr.unshift(0);
// arr: [0, 1, 2, 3]
shift()移除并返回数组第一个元素arr.shift()✅ 改变被移除的元素(any)const first = arr.shift();
// first: 0, arr: [1, 2, 3]
splice()在指定位置添加/删除元素arr.splice(start, deleteCount, item1, ...)✅ 改变被删除元素组成的数组arr.splice(1, 1, 'a');
// 删除1个,插入'a' → [1, 'a', 3]
2. 查找与检测indexOf()返回指定元素首次出现的索引,未找到返回 -1arr.indexOf(searchElement, fromIndex)❌ 不变索引(number)arr.indexOf('a'); // 1
lastIndexOf()返回指定元素最后一次出现的索引arr.lastIndexOf(searchElement)❌ 不变索引(number)arr.lastIndexOf(3); // 2
includes()检查数组是否包含某元素arr.includes(searchElement)❌ 不变布尔值(boolean)arr.includes('a'); // true
find()返回满足条件的第一个元素arr.find(callback)❌ 不变元素(any)或 undefinedarr.find(x => x > 2); // 3
findIndex()返回满足条件的第一个元素的索引arr.findIndex(callback)❌ 不变索引(number)或 -1arr.findIndex(x => x === 'a'); // 1
some()检查是否有至少一个元素满足条件arr.some(callback)❌ 不变布尔值(boolean)arr.some(x => x === 'a'); // true
every()检查是否所有元素都满足条件arr.every(callback)❌ 不变布尔值(boolean)arr.every(x => typeof x === 'number'); // false
3. 遍历与迭代forEach()对每个元素执行一次回调函数arr.forEach(callback)❌ 不变undefinedarr.forEach(x => console.log(x));
map()创建一个新数组,结果是原数组每个元素调用函数的返回值arr.map(callback)❌ 不变新数组(Array)const doubled = arr.map(x => x * 2);
filter()创建一个新数组,包含所有满足条件的元素arr.filter(callback)❌ 不变新数组(Array)const nums = arr.filter(x => typeof x === 'number');
reduce()从左到右归约数组为一个值arr.reduce(callback, initialValue)❌ 不变累积结果(any)const sum = arr.filter(x => typeof x === 'number')
.reduce((a, b) => a + b, 0); // 4
reduceRight()从右到左归约数组为一个值arr.reduceRight(callback)❌ 不变累积结果(any)const rightSum = arr.reduceRight((a, b) => a + b, 0);
4. 转换与提取slice()提取数组的一部分并返回新数组arr.slice(start, end)❌ 不变新数组(Array)const part = arr.slice(1, 3); // ['a', 3]
concat()合并两个或多个数组,返回新数组arr.concat(arr2, ..., valueN)❌ 不变新数组(Array)const newArr = arr.concat([4, 5]);
join()将数组元素连接成字符串arr.join(separator)❌ 不变字符串(string)const str = arr.join('-'); // "1-a-3"
toString()将数组转换为字符串(逗号分隔)arr.toString()❌ 不变字符串(string)arr.toString(); // "1,a,3"
flat()扁平化嵌套数组arr.flat(depth)❌ 不变新数组(Array)[1, [2, 3]].flat(); // [1, 2, 3]
flatMap()mapflat(1)arr.flatMap(callback)❌ 不变新数组(Array)[[1, 2], [3]].flatMap(x => x); // [1, 2, 3]
5. 排序与反转sort()对数组元素进行排序(默认转字符串)arr.sort(compareFn)✅ 改变排序后的数组(引用)[3, 1, 2].sort(); // [1, 2, 3]
reverse()反转数组元素顺序arr.reverse()✅ 改变反转后的数组(引用)[1, 2, 3].reverse(); // [3, 2, 1]
6. 其他实用方法fill()用固定值填充数组arr.fill(value, start, end)✅ 改变修改后的数组[1, 2, 3].fill(0); // [0, 0, 0]
copyWithin()复制数组内部的一段到另一位置arr.copyWithin(target, start, end)✅ 改变修改后的数组[1, 2, 3, 4].copyWithin(0, 2); // [3, 4, 3, 4]
Array.from()从类数组或可迭代对象创建新数组Array.from(arrayLike, mapFn)新数组(Array)Array.from('abc'); // ['a', 'b', 'c']
Array.of()创建具有可变参数的新数组Array.of(element0, ..., elementN)新数组(Array)Array.of(1, 2, 3); // [1, 2, 3]
keys() / values() / entries()返回数组的键、值、键值对迭代器arr.keys() / arr.values() / arr.entries()❌ 不变迭代器(Iterator)for (const [i, v] of arr.entries()) { ... }

总结:改变原数组 vs 不改变原数组

特性改变原数组的方法不改变原数组的方法
常见方法push, pop, unshift, shift, splice, sort, reverse, fill, copyWithinmap, filter, slice, concat, join, find, indexOf, forEach, reduce
函数式编程友好✅ 推荐用于纯函数和不可变数据处理
性能考虑直接修改,可能影响其他引用创建新数组,内存开销略高

8.2 Map

方法名功能说明语法格式参数说明是否改变原 Map返回值代码示例
new Map()创建一个新的 Map 对象new Map([iterable])iterable:可选,键值对数组或可迭代对象新的 Map 实例const map = new Map();
const map2 = new Map([['name', 'Alice'], ['age', 25]]);
set(key, value)添加或更新一个键值对map.set(key, value)key:键(任意类型)
value:值(任意类型)
✅ 改变当前 Map 实例(支持链式调用)map.set('city', 'Beijing');
map.set(1, 'number key');
get(key)获取指定键对应的值map.get(key)key:要查找的键❌ 不变值(any),若键不存在则返回 undefinedconsole.log(map.get('name')); // "Alice"
console.log(map.get('unknown')); // undefined
has(key)检查 Map 是否包含某个键map.has(key)key:要检查的键❌ 不变布尔值(boolean)console.log(map.has('age')); // true
console.log(map.has('email')); // false
delete(key)删除指定键的键值对map.delete(key)key:要删除的键✅ 改变布尔值:删除成功为 true,否则为 falseconst result = map.delete('city'); // true
clear()清空所有键值对map.clear()无参数✅ 改变undefinedmap.clear();
console.log(map.size); // 0
size获取 Map 中键值对的数量(只读属性)map.size❌ 不变数字(number)console.log(map.size);
keys()返回一个包含所有键的迭代器map.keys()无参数❌ 不变迭代器(Iterator)for (const key of map.keys()) { console.log(key); }
values()返回一个包含所有值的迭代器map.values()无参数❌ 不变迭代器(Iterator)for (const value of map.values()) { console.log(value); }
entries()返回一个包含所有键值对的迭代器map.entries()无参数❌ 不变迭代器(Iterator)for (const [key, value] of map.entries()) { console.log(\${key}: ${value}`); }`
[Symbol.iterator]()Map 默认可迭代,等同于 entries()for...of map无参数❌ 不变迭代器(Iterator)for (const [k, v] of map) { console.log(k, v); }
forEach(callback)对每个键值对执行回调函数map.forEach((value, key, map) => { ... })callback 接收三个参数:value、key、map❌ 不变undefinedmap.forEach((v, k) => {
console.log(\${k} = ${v}`);<br>});`

Map 与 Object 对比:

特性MapObject
键的类型任意类型(对象、函数、原始值等)只能是字符串或 Symbol
键的顺序保持插入顺序ES6 之前无序,现代引擎通常有序但不保证
性能大量增删改查时性能更优小规模使用良好,大规模操作可能较慢
获取大小map.size(直接属性)需手动计算 Object.keys(obj).length
继承属性干扰不会受原型链上属性影响可能受到 Object.prototype 上属性影响
序列化不能直接 JSON.stringify()可直接 JSON.stringify()
初始化方式支持可迭代对象初始化字面量 {} 或构造函数
适用场景高频增删查、键为对象/非字符串、需稳定顺序配置对象、数据结构简单、需 JSON 序列化

使用建议与最佳实践:

场景推荐使用
✅ 键是对象或函数Map
✅ 频繁添加/删除键值对Map
✅ 需要稳定的键值对顺序Map
✅ 键是动态的且类型多样Map
✅ 数据需要 JSON 序列化Object
✅ 简单配置或结构化数据Object
✅ 与 API 交互或表单数据Object

8.3 Set

方法名功能说明语法格式参数说明是否改变原 Set返回值代码示例
new Set()创建一个新的 Set 实例new Set([iterable])iterable:可选,可迭代对象新的 Set 实例const set = new Set();
const set2 = new Set([1, 2, 2, 3]); // 自动去重 → {1, 2, 3}
add(value)添加一个值到 Set 中set.add(value)value:要添加的值(任意类型)✅ 改变当前 Set 实例(支持链式调用)set.add(4);
set.add('hello').add({}); // 链式调用
delete(value)从 Set 中删除指定值set.delete(value)value:要删除的值✅ 改变布尔值:删除成功为 true,否则为 falseconst result = set.delete(4); // true
has(value)检查 Set 是否包含某个值set.has(value)value:要检查的值❌ 不变布尔值(boolean)console.log(set.has('hello')); // true
console.log(set.has(99)); // false
clear()清空 Set 中所有值set.clear()无参数✅ 改变undefinedset.clear();
console.log(set.size); // 0
size获取 Set 中值的数量(只读属性)set.size❌ 不变数字(number)console.log(set.size);
values()返回一个包含所有值的迭代器set.values()无参数❌ 不变迭代器(Iterator)for (const val of set.values()) { console.log(val); }
keys()返回一个包含所有值的迭代器(Set 的键和值相同)set.keys()无参数❌ 不变迭代器(Iterator)(与 values() 相同)
entries()返回一个包含 [value, value] 键值对的迭代器set.entries()无参数❌ 不变迭代器(Iterator)for (const [key, value] of set.entries()) {
console.log(key, value); // key === value
}
[Symbol.iterator]()Set 默认可迭代,等同于 values()for...of set无参数❌ 不变迭代器(Iterator)for (const item of set) { console.log(item); }
forEach(callback)对每个值执行回调函数set.forEach((value, key, set) => { ... })callback 接收三个参数:value、key、set❌ 不变undefinedset.forEach((v, k) => { console.log(\${k} = ${v}`); }); // k === v`

Set 常用操作与转换:

操作目标实现方式代码示例
数组去重Set 自动去重特性const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
Set 转数组展开运算符 ...Array.from()const arr = [...set]; // 或 Array.from(set)
数组转 Setnew Set(array)const set = new Set([1, 2, 3]);
合并多个 Set使用展开运算符const merged = new Set([...set1, ...set2]);
求交集过滤一个 Set 中存在于另一个 Set 的值const intersection = new Set([...set1].filter(x => set2.has(x)));
求差集过滤一个 Set 中不存在于另一个 Set 的值const difference = new Set([...set1].filter(x => !set2.has(x)));
求并集合并两个 Setconst union = new Set([...set1, ...set2]);

Set 与 Array 对比:

特性SetArray
元素唯一性✅ 自动去重,值唯一❌ 允许重复元素
性能(查找/删除)✅ O(1) 平均时间复杂度❌ O(n) 线性查找
插入顺序✅ 保持插入顺序✅ 保持插入顺序
索引访问❌ 不支持 set[0]✅ 支持 arr[0]
初始化new Set(iterable)字面量 []new Array()
序列化不能直接 JSON.stringify()可直接 JSON.stringify()
适用场景去重、集合运算、高频查找/删除有序列表、索引访问、数据存储

使用建议:

场景推荐使用
✅ 去除数组重复元素new Set(arr)
✅ 高频的”是否存在”检查Set.has(value)(比 Array.includes() 更快)
✅ 集合运算(交集、并集、差集)Set 配合 filter/map
✅ 存储唯一值(如用户ID、标签)Set
✅ 需要索引访问或保持下标Array
✅ 数据需要 JSON 序列化Array 或手动转换 Set
✅ 简单数据列表Array

9. 正则表达式

9.1 正则表达式进阶

一、正则表达式创建方式

创建方式语法格式说明代码示例
字面量语法/pattern/flags编译时创建,性能高,适合静态正则const regex = /hello/gi;
构造函数语法new RegExp('pattern', 'flags')运行时创建,适合动态生成正则const pattern = 'hello';
const regex = new RegExp(pattern, 'gi');

建议: 静态正则用字面量,动态拼接用 new RegExp()

二、正则表达式修饰符(Flags)

修饰符全称功能说明示例说明
gglobal全局匹配,查找所有匹配项(而非第一个)/abc/g 匹配字符串中所有 “abc”
iignoreCase忽略大小写/hello/i 匹配 “Hello”, “HELLO”
mmultiline多行模式,^$ 匹配每行的开始和结束/^start/m 匹配每行以 “start” 开头
uunicode启用 Unicode 模式,正确处理码位大于 U+FFFF 的字符/𠮷/u 正确匹配 emoji 或汉字扩展
sdotAll使 . 匹配包括换行符在内的所有字符/a.b/s 可匹配 “a\nb”

ES2018 新增:d(hasIndices,返回匹配索引)、v(Unicode 属性类增强,实验性)。

三、字符串上的正则方法(不改变原字符串)

方法名功能说明语法格式返回值代码示例
str.match(regex)获取匹配结果str.match(/pattern/g)有 g:匹配字符串数组
无 g:匹配对象(含 index, groups)
无匹配:null
'hello123'.match(/\d+/); // ["123", index: 5, ...]
'hello123'.match(/\d+/g); // ["123"]
str.matchAll(regex)返回所有匹配的迭代器(支持捕获组)str.matchAll(/pattern/g)迭代器,每个项为匹配对象for (const match of 'a1b2'.matchAll(/\d/g)) { console.log(match); }
str.search(regex)返回第一个匹配项的索引str.search(/pattern/)索引(number),未找到返回 -1'hello'.search(/ll/); // 2
str.replace(pattern, replacement)替换匹配内容str.replace(/pattern/g, 'new')
或使用函数:str.replace(/pattern/g, (match) => '...')
新字符串(string)'a1b2'.replace(/\d/g, 'X'); // "aXbX"
'price: 10'.replace(/(\d+)/, (_, num) => num * 2); // "price: 20"
str.replaceAll(pattern, replacement)替换所有匹配项(ES2021)str.replaceAll(/pattern/g, 'new')新字符串(string)'a1a2'.replaceAll('a', 'X'); // "X1X2"

四、正则对象上的方法

方法名功能说明语法格式返回值代码示例
regex.test(str)检查字符串是否匹配regex.test('string')布尔值(boolean)/\d+/.test('abc123'); // true
regex.exec(str)执行匹配,返回详细结果对象regex.exec('string')匹配对象或 null(g 模式可循环匹配)const r = /\d+/g;
let m;
while (m = r.exec('a1b2c3')) { console.log(m[0]); } // "1", "2", "3"

⚠️ exec 在 g 模式下会维护 lastIndex,可用于逐次匹配。

五、常用正则模式语法

模式功能说明示例
.匹配任意单个字符(除换行符,除非使用 s/a.c/ 匹配 “abc”, “a2c”
^匹配字符串开始/^Hello/ 匹配以 “Hello” 开头
$匹配字符串结束/world$/ 匹配以 “world” 结尾
\d数字(0-9)/\d+/ 匹配连续数字
\w单词字符(字母、数字、下划线)/\w+/ 匹配单词
\s空白字符(空格、制表符、换行等)/\s+/ 匹配空白
*零次或多次/a*/ 匹配 "", “a”, “aaa”
+一次或多次/a+/ 匹配 “a”, “aa”,不匹配 ""
?零次或一次/colou?r/ 匹配 “color” 或 “colour”
{n}恰好 n 次/\d{3}/ 匹配三位数字
{n,}至少 n 次/\d{2,}/ 匹配至少两位数字
{n,m}n 到 m 次/\d{2,4}/ 匹配 2~4 位数字
[]字符类,匹配其中任意一个字符/[aeiou]/ 匹配任意元音字母
[^]否定字符类/[^0-9]/ 匹配非数字字符
()捕获组,用于提取或引用/(abc)\1/ 匹配 “abcabc”
(?:)非捕获组/(?:abc)+/ 匹配一个或多个 “abc”,但不捕获
(?=...)正向先行断言(lookahead)/JavaScript(?=:)/ 匹配后跟 ”:” 的 “JavaScript”
(?!...)负向先行断言/JavaScript(?!script)/ 匹配不后跟 “script” 的 “JavaScript”
(?<=...)正向后行断言(ES2018)/(?<=\$)\d+/ 匹配 $ 后的数字
(?<!...)负向后行断言(ES2018)/(?<!\$)\d+/ 匹配非 $ 后的数字

六、实用场景示例

需求正则表达式示例代码
验证邮箱/^\S+@\S+\.\S+$//^\S+@\S+\.\S+$/.test('user@example.com'); // true
验证手机号(中国)/^1[3-9]\d{9}$//^1[3-9]\d{9}$/.test('13812345678'); // true
提取所有数字/\d+/g'a1b2c3'.match(/\d+/g); // ["1", "2", "3"]
替换多个空格为单个空格/\s+/g'a b'.replace(/\s+/g, ' '); // "a b"
检查是否包含特殊字符/[^A-Za-z0-9]//[^A-Za-z0-9]/.test('hello!'); // true
提取 URL 中的域名/https?:\/\/([^\/\s]+)/const match = url.match(/https?:\/\/([^\/\s]+)/);
match && match[1]; // "example.com"

总结:正则操作选择指南

操作目标推荐方法
✅ 检查是否匹配regex.test(str)
✅ 获取第一个匹配详情str.match(/pattern/)regex.exec(str)
✅ 获取所有匹配str.match(/pattern/g)str.matchAll(/pattern/g)
✅ 替换所有匹配str.replaceAll(/pattern/g, 'new')str.replace(/pattern/g, 'new')
✅ 提取带捕获组的信息regex.exec()str.matchAll()
✅ 遍历所有匹配(含索引)for...of 配合 str.matchAll()

9.2 正则表达式编译

📌 说明: JavaScript 中的正则表达式在创建时会被引擎”编译”为内部可执行的指令结构(类似字节码),以便后续高效匹配。虽然语法上没有显式的 compile() 方法,但”编译”是隐式发生的。

类别项目说明关键机制代码示例注意事项
1. 编译定义什么是”编译”将正则表达式字符串或字面量解析为浏览器/引擎内部的可执行状态机(如 NFA/DFA),用于快速执行匹配操作。解析模式字符串
构建状态转移图
优化匹配路径
✅ 并非传统语言的编译成机器码,而是构建高效的匹配引擎数据结构
2. 创建与编译时机字面量 /pattern/flags在脚本加载/解析时编译一次,之后重复使用同一实例。编译发生在代码解析阶段
多次使用不重新编译
function test() {
const re = /hello/g; // 只编译一次
return re.test("hello");
}
✅ 高频调用函数中推荐使用字面量(避免重复编译)
✅ 性能最优
构造函数 new RegExp()每次执行到该语句时动态编译,适合模式动态生成。运行时解析字符串
每次调用都可能触发编译
function containsDigit(str) {
const re = new RegExp('\\d');
return re.test(str);
}
⚠️ 频繁调用时性能较低
✅ 适用于动态模式
3. 编译缓存是否缓存编译结果字面量:是(单个实例)
构造函数:部分引擎会缓存
V8 等现代引擎会对 new RegExp('pat', 'g') 做弱缓存
但不如字面量稳定
const r1 = new RegExp('abc', 'i');
const r2 = new RegExp('abc', 'i');
console.log(r1 === r2); // false
❌ 不要依赖构造函数的缓存
✅ 高频使用建议缓存变量
4. 编译错误错误类型如果正则语法无效,会在创建时抛出 SyntaxError模式解析失败时立即报错// 抛出 SyntaxError
const re = /[a-]/; // 无效字符类
✅ 错误发生在创建阶段而非使用阶段
✅ 动态构造时建议加 try/catch
5. 内部属性source编译后的原始模式字符串(只读)/hello/.source; // "hello"✅ 用于调试或拼接
flags编译时解析的修饰符字符串/abc/gim.flags; // "gim"✅ ES2015 新增
global, ignoreCase, multiline, dotAll, unicode, sticky布尔属性,表示对应 flag 是否启用/a/g.global; // true
/a/u.unicode; // true
✅ 用于条件判断
lastIndex用于 gy 模式的匹配位置指针const re = /a/g;
re.exec('aa');
console.log(re.lastIndex); // 1
⚠️ g 模式下多次匹配会更新此值
6. 性能对比字面量 vs 构造函数字面量:编译一次,性能高
构造函数:每次运行可能重新编译,性能较低
const DIGIT = /\d+/g; // 推荐
const WORD = new RegExp('\\w+', 'g'); // 推荐:缓存
✅ 将正则赋值给常量以复用
❌ 避免在循环中频繁 new RegExp()
7. 编译优化最佳实践静态字面量 + 缓存动态正则const EMAIL = /^[^\s@]+@[^\s@]+.[^\s@]+/;
const createSearchRegex = (term) => {
` const escaped = term.replace(/[.*+?^{}()
[]\]/g, ’\$&’);<br> return new RegExp(escaped, ‘gi’);<br>};<br>// ❌ 差:循环中重复创建<br>for (let s of strings) {<br> if (s.match(new RegExp(pattern))) { … }<br>}`

正则”编译”流程图解:

源代码

[ /pattern/flags ] 或 [ new RegExp('pattern', 'flags') ]

       ┌───────────────┐
       │ 语法解析       │ → 若错误,抛出 SyntaxError
       └───────────────┘

       ┌──────────────────┐
       │ 构建状态机/NFA    │ → "编译"核心步骤
       └──────────────────┘

       ┌──────────────────┐
       │ 生成 RegExp 对象  │ ← 包含 source, flags, lastIndex 等
       └──────────────────┘

      可用于 exec(), test(), match() 等方法

总结:JavaScript 正则编译要点

要点说明
编译是隐式的无需手动 compile(),创建即编译
字面量更高效编译一次,全局复用,推荐高频使用
构造函数用于动态场景模式来自变量或用户输入时使用
注意 lastIndex 副作用g 模式下多次调用 exec 会改变状态
合理缓存将正则存储在变量中复用,避免重复创建
错误早发现语法错误在创建时抛出,便于调试

9.3 正则表达式捕获

类别语法功能说明是否创建捕获组代码示例匹配结果说明
1. 普通捕获组(pattern)将括号内的子模式匹配内容”捕获”并存储,可通过索引访问。✅ 是const str = "John Doe";
const regex = /(\w+)\s+(\w+)/;
const match = str.match(regex);
match[0] → “John Doe” (完整匹配)
match[1] → “John” (第1个捕获组)
match[2] → “Doe” (第2个捕获组)
match.index → 0
match.input → “John Doe”
2. 非捕获组(?:pattern)分组但不捕获,仅用于逻辑分组或量词作用,不生成编号。❌ 否const regex = /(?:\w+),\s*(\w+)/g;
let m;
while (m = regex.exec("apple, banana, cherry")) {
console.log(m[1]);
}
输出:banana, cherry
✅ 只有后面的 (\w+) 被捕获
3. 命名捕获组(ES2018)(?<name>pattern)使用名称而非索引访问捕获内容,提升可读性和维护性。✅ 是const str = "2025-09-09";
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = str.match(regex);
match.groups.year → “2025”
match.groups.month → “09”
match.groups.day → “09”
match[1] → “2025” (仍可用索引)
4. 反向引用(Backreference)\1, \2, ...\k<name>在正则内部引用前面已捕获的内容。// 匹配重复单词
const regex1 = /(\b\w+)\s+\1/i;
regex1.test("hello hello"); // true
// 命名反向引用
const regex2 = /(?<word>\b\w+)\s+\k<word>/i;
\1 表示与第一个捕获组完全相同的内容
⚠️ 大小写敏感(除非用 i 标志)
5. 嵌套捕获组(outer(inn)er)捕获组可以嵌套,编号按左括号 ( 出现顺序从左到右分配。✅ 是const str = "abc";
const regex = /((a)(b))(c)/;
const match = str.match(regex);
match[0] → “abc”
match[1] → “ab”
match[2] → “a”
match[3] → “b”
match[4] → “c”
6. 捕获空值(pattern)?捕获组是可选的,若未匹配,对应捕获结果为 undefined✅ 是const regex = /colou?(r)/;若 str = “color”,match[1] → “r”
若 pattern 不匹配,则为 undefined
7. 在 replace 中使用捕获$1, $2, ...$<name>在替换字符串中引用捕获组内容。const str = "John Doe";
const regex = /(\w+)\s+(\w+)/;
str.replace(regex, "$2, $1"); // "Doe, John"
// 命名捕获
const regex2 = /(?<first>\w+)\s+(?<last>\w+)/;
str.replace(regex2, "$<last>, $<first>");
$1 表示第一个捕获组
$<name> 表示命名捕获组
8. match() 的捕获行为str.match(regex)无 g 标志:返回包含捕获组的数组
有 g 标志:只返回完整匹配项,忽略捕获组
const str = "a1b2";
const regex = /(\w)(\d)/g;
str.match(regex); // ["a1", "b2"] (无捕获)
str.match(/(\w)(\d)/); // ["a1", "a", "1", ...]
⚠️ g 模式下 match() 不返回捕获组信息,应使用 matchAll()
9. matchAll() 返回完整捕获str.matchAll(regex)返回迭代器,每个项为完整匹配对象(含 groups 和索引捕获)。✅ 推荐用于全局捕获const str = "a1b2c3";
const regex = /(\w)(\d)/g;
for (const match of str.matchAll(regex)) {
console.log(match[0], match[1], match[2]);
}
输出:a1 a 1, b2 b 2, c3 c 3
10. exec() 的捕获regex.exec(str)每次执行返回一个匹配对象(含捕获组),可用于循环获取所有匹配。✅ 支持全局遍历const regex = /(\d+)/g;
let match;
while (match = regex.exec("a1b2c3")) {
console.log(match[1]); // "1", "2", "3"
}
⚠️ g 模式下 lastIndex 会更新

捕获组编号规则说明:

示例正则:(a)(b(c)(d))e(f)?,匹配目标:“abcdef”

分组内容说明
完整匹配”abcdef”match[0] = "abcdef"
第1组 (a)”a”match[1] = "a"
第2组 (b(c)(d))”bcd”match[2] = "bcd"
第3组 (c)”c”match[3] = "c"
第4组 (d)”d”match[4] = "d"
第5组 (f)?”f” 或 undefinedmatch[5] = "f"undefined

🔢 编号规则: 按左括号 ( 从左到右出现的顺序分配编号,无论是否嵌套。

命名捕获组使用建议:

场景推荐做法
✅ 结构复杂、易混淆的正则使用 (?<name>...) 提高可读性
✅ 需要后期维护或团队协作命名比数字索引更清晰
✅ 与后端或其他语言交互确保命名兼容性
❌ 简单模式可继续使用 (pattern)

总结:捕获机制对比

特性普通捕获 ()非捕获 (?:)命名捕获 (?<name>)
是否生成编号✅ 是❌ 否✅ 是(同时有编号和名称)
是否可被引用\1❌ 不能\1\k<name>
在 replace 中使用$1❌ 不能$1$<name>
性能略低(需存储)更高(不存储)略低于普通捕获
可读性低(依赖位置)高(语义明确)

10. 代码模块化

10.1 实现模块

JavaScript 模块实现方式详解表:

实现方式出现时间/标准核心机制语法示例加载方式浏览器原生支持主要优点主要缺点适用场景
1. 全局对象模式早期 JavaScript将函数和变量挂载到 window(浏览器)或 global(Node.js)上function myModule() { ... }
window.myModule = myModule;
<script src="...">✅ 是简单直接命名冲突、污染全局、依赖管理困难早期项目、简单脚本
2. IIFE(立即调用函数表达式)ES5 及之前使用函数作用域隔离变量,通过返回值暴露接口const MyModule = (function() {
const privateVar = "secret";
return { publicMethod() { ... } };
})();
<script src="...">✅ 是避免全局污染、支持私有成员依赖需手动管理、模块间通信复杂中小型项目、库开发
3. CommonJSNode.js 默认模块系统(2009)使用 require() 同步加载模块,module.exports 导出// math.js
module.exports = { add(a, b) { return a + b; } };
// app.js
const math = require('./math');
require() 同步加载❌ 浏览器不原生支持
✅ Node.js 原生
简单直观、支持动态加载同步加载不适合浏览器、不支持静态分析Node.js 后端开发、工具脚本
4. AMDRequireJS 推广(2010)异步加载模块,支持浏览器环境define([], function() {
return { add(a, b) { return a + b; } };
});
define() 定义
require() 使用
❌ 不原生支持
需 RequireJS
异步加载、适合浏览器、支持依赖前置语法复杂、已被现代标准取代旧版前端项目
5. UMD混合模式(2010s)兼容 CommonJS、AMD 和全局对象条件判断加载方式:支持多种环境✅ 兼容性好代码冗长、维护复杂发布库时兼容多环境
6. ES6 Modules(ESM)ES2015(ES6)标准使用 import/export 语法,支持静态分析export function add(a, b) { return a + b; }
import { add } from './math.js';
import 静态导入
export 导出
✅ 现代浏览器和 Node.js 原生静态分析、tree-shaking、语法简洁需正确文件扩展名现代前端项目、Node.js、库开发
7. 动态导入ES2020运行时动态加载,返回 Promiseimport('./math.js').then(math => { ... });
const math = await import('./math.js');
import('module-path')✅ 现代浏览器和 Node.js按需加载、代码分割、懒加载返回 Promise,需异步处理路由懒加载、条件加载、性能优化
8. Node.js 中的 ESMNode.js v12+.mjs 文件或 "type": "module"// package.json
{ "type": "module" }
import/export✅ Node.js(需配置)统一前后端模块标准与 CommonJS 不兼容新建 Node.js 项目

各模块系统互操作说明(Node.js 环境):

场景实现方式
ESM 中导入 CommonJS 模块import fs from 'fs'; // 默认导入整个对象
CommonJS 中导入 ESM 模块❌ 不支持直接 require()
✅ 必须使用 import() 动态导入(异步)

模块文件扩展名说明:

扩展名说明
.js默认为 CommonJS(Node.js)
package.json"type": "module",则为 ESM
.mjs强制为 ESM 模块(Node.js)
.cjs强制为 CommonJS 模块(Node.js)
.ts, .jsx, .vue构建工具处理后转为 JS 模块

现代前端构建工具中的模块处理:

工具模块支持说明
Webpack支持 ESM、CommonJS、AMD自动打包、支持 tree-shaking、代码分割
Vite原生支持 ESM利用浏览器原生 import,启动极快
Rollup主打 ESM适合库打包,输出更干净的代码
Parcel零配置支持多种模块自动识别和处理模块类型

总结:JavaScript 模块演进与选择建议

阶段推荐方案
✅ 现代浏览器项目ES6 Modules + 构建工具(Vite/Webpack)
✅ 旧浏览器兼容使用构建工具将 ESM 转为 IIFE 或 CommonJS
✅ 浏览器端直接使用(无构建)ES6 Modules(使用 <script type="module">
✅ Node.js 新项目ES6 Modules(配置 "type": "module"
✅ 旧版 Node.js 项目CommonJS
✅ 发布 npm 库提供 ESM 和 CommonJS 两种格式
✅ 实现代码懒加载动态导入 import()

💡 提示:

  • 浏览器中使用 ESM 需通过 <script type="module"> 加载。
  • importexport 只能在模块顶层使用,不能在条件语句中。
  • 推荐使用现代工具链(Vite、Webpack)来管理模块。

10.2 ES6 模块

ES6 模块全流程详解表:

阶段操作说明语法/工具示例代码关键要点
1. 创建模块(定义导出内容)将功能封装在 .js 文件中,使用 export 导出变量、函数、类等export const/var/function/class
export default value
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default function subtract(a, b) { return a - b; }
✅ 支持多个命名导出
✅ 只能有一个 default 导出
✅ 可混合使用
2. 打包模块(用于生产环境)使用构建工具将模块打包成更小、兼容性更好的文件Vite / Webpack / Rollup可生成多种格式(ESM/CJS),支持压缩、混淆、tree-shaking✅ 构建工具可输出多种格式
✅ Vite 构建速度极快
✅ Rollup 适合库打包
3. 发布模块(上传到 npm)将打包好的模块发布到 npm 仓库npm login / npm publish需确保 package.json 包含:name、version、main、module、files✅ 模块名需在 npm 中唯一
✅ 可设置 private: true 防止误发
4. 导入模块(在其他项目中使用)通过 import 语法引入模块import { name } from 'path'
import DefaultName from 'path'
import * as ns from 'path'
import subtract, { add, multiply, PI } from './math-utils.js';
const math = await import('./math-utils.js'); // 动态导入
✅ 静态导入在顶层使用
✅ 支持重命名:import { add as sum }
✅ 动态导入返回 Promise

浏览器中直接使用 ES6 模块:

场景HTML 示例说明
本地模块导入<script type="module">
import { add } from './math-utils.js';
console.log(add(1, 2));
</script>
✅ 必须设置 type="module"
✅ 遵循 CORS 策略
导入 CDN 上的模块<script type="module">
import _ from 'https://cdn.skypack.dev/lodash-es';
</script>
✅ 可直接从 CDN 导入 ESM 兼容包

Node.js 中使用 ES6 模块:

方式示例要求
使用 .mjs 扩展名node main.mjs文件必须为 .mjs
"type": "module"{ "type": "module" }所有 .js 文件被视为 ESM
互操作:ESM 中导入 CJSimport fs from 'fs';✅ 支持
互操作:CJS 中导入 ESM❌ 不支持 require()
✅ 必须使用动态导入

总结:ES6 模块最佳实践

建议说明
✅ 优先使用命名导出更清晰,便于 tree-shaking
✅ 谨慎使用默认导出适用于只有一个主要导出的模块
✅ 构建时输出多格式同时提供 ESM 和 CJS 格式
✅ 使用现代构建工具Vite / Rollup / Webpack
✅ 发布前测试本地引用使用 npm linkfile: 协议
✅ 版本语义化(SemVer)遵循主版本.次版本.修订号规则

💡 提示:

  • import 是静态的,编译时确定依赖关系。
  • export 只是”声明”要导出的内容,不创建新变量。
  • 推荐使用 Vite 进行快速原型开发和构建。

11. BOM 与 DOM 对比

BOM 与 DOM 对比总览表:

特性BOM(Browser Object Model)DOM(Document Object Model)
全称浏览器对象模型文档对象模型
定义提供与浏览器窗口交互的对象和接口,用于控制浏览器行为将 HTML/XML 文档表示为树状结构,允许程序动态访问和操作文档
核心对象window, navigator, location, history, screen, framesdocument, Element, Node, Text, Attribute
是否标准❌ 非官方标准,存在兼容性差异✅ W3C 官方标准,各浏览器实现较为统一
主要用途控制浏览器窗口、导航、获取设备信息、弹窗、会话存储等操作网页内容、响应事件、修改样式、动态渲染
顶层对象window 是全局对象document 是 DOM 入口点,挂在 window.document
是否可选✅ 可以不存在(如 Node.js 环境无 BOM)✅ 在浏览器中总是存在

典型 API 示例:

// === BOM ===
// 导航到新页面
window.location.href = "https://example.com";
// 弹出警告框
window.alert("Hello!");
// 获取用户代理
navigator.userAgent;
// 前进/后退
history.back();
history.forward();
// 屏幕信息
screen.width, screen.height;
// === DOM ===
// 获取元素
const el = document.getElementById("myId");
// 修改内容
el.innerHTML = "New Content";
// 创建新元素
const p = document.createElement("p");
p.textContent = "Hello DOM!";
document.body.appendChild(p);
// 监听事件
el.addEventListener("click", () => alert("Clicked!"));
// 修改样式
el.style.color = "red";

核心对象详解表:

对象所属模型功能说明常用属性/方法
windowBOM浏览器窗口的全局对象,JS 的全局执行环境window.innerWidth/Heightwindow.open()window.setTimeout()window.localStorage
documentBOM & DOM连接 BOM 与 DOM 的桥梁document.titledocument.URLdocument.getElementById()document.querySelector()document.createElement()
locationBOM表示当前页面 URL 信息,并提供导航功能location.hreflocation.protocollocation.reload()location.replace()
historyBOM管理浏览器会话历史记录history.back()history.forward()history.go(-2)history.pushState()
navigatorBOM提供浏览器和设备相关信息navigator.userAgentnavigator.platformnavigator.geolocation
screenBOM提供用户屏幕信息screen.width/heightscreen.availWidth/availHeight
ElementDOM表示 HTML 元素节点element.idelement.getAttribute()element.addEventListener()
NodeDOM所有 DOM 节点的基类node.nodeTypenode.childNodesnode.parentNode

BOM 与 DOM 的关系图解:

        +------------------+
        |    window        |  ← BOM 核心(全局对象)
        +------------------+
                  |
                  | 包含

        +------------------+
        |   document       |  ← BOM 成员,同时也是 DOM 根节点
        +------------------+
                  |
                  | 表示

        +------------------+
        | HTML Document    |  ← 实际的网页文档
        +------------------+
                  |
                  | 解析为

        +------------------+      +------------------+
        | DOM Tree         | ↔→→→ | JavaScript API   |
        | (节点树)          |      | (操作文档)       |
        +------------------+      +------------------+

🔗 关键点: document 对象是 BOM 的一部分,但它所管理的内容(即网页结构)构成了 DOM。通过 document,JavaScript 可以进入并操作 DOM。

实际开发中的使用场景对比:

需求使用对象代码示例
修改网页标题document(DOM)document.title = "新标题";
跳转到另一个页面location(BOM)location.href = "/home";
获取浏览器宽度window(BOM)const width = window.innerWidth;
动态添加一个按钮document(DOM)const btn = document.createElement("button");
btn.textContent = "点击我";
document.body.appendChild(btn);
记录用户浏览历史history(BOM)history.pushState({}, "", "/page2");
判断用户使用的设备navigator(BOM)if (navigator.userAgent.includes("Mobile")) { ... }
存储用户偏好localStorage(BOM)localStorage.setItem("theme", "dark");

总结:一句话区分 BOM 与 DOM

模型一句话概括
BOM”控制浏览器做什么” —— 管理窗口、导航、设备信息、存储等
DOM”控制网页长什么样” —— 管理 HTML 文档的内容、结构和样式

💡 提示:

  • 所有 DOM 操作最终都通过 window.document 进行。
  • BOM 提供了更广泛的浏览器控制能力,但兼容性需注意。
  • 现代前端框架(如 React、Vue)底层仍基于 DOM 进行虚拟 DOM 更新。

12. BOM 操作

JavaScript BOM 操作一览表:

BOM 对象属性/方法功能说明示例代码
windowwindow.innerWidth / window.innerHeight获取浏览器窗口的视口宽度和高度const width = window.innerWidth;
const height = window.innerHeight;
window.open(url, name, features)打开新窗口或标签页window.open('https://example.com', '_blank', 'width=600,height=400');
window.close()关闭当前窗口const newWin = window.open();
newWin.close();
window.alert(message)显示警告对话框window.alert("操作成功!");
window.confirm(message)显示确认对话框,返回 true/falseif (window.confirm("确定要删除吗?")) { ... }
window.prompt(message, default)显示输入对话框,返回用户输入const name = window.prompt("请输入您的姓名:", "张三");
window.setTimeout(func, delay)延迟指定毫秒后执行函数window.setTimeout(() => { console.log("3秒后执行"); }, 3000);
window.setInterval(func, interval)每隔指定毫秒重复执行const timer = window.setInterval(() => { ... }, 1000);
clearInterval(timer);
window.scrollTo(x, y)
window.scrollTo(options)
滚动到文档指定位置window.scrollTo(0, 100);
window.scrollTo({ top: 500, behavior: 'smooth' });
window.scrollX / window.scrollY获取页面当前滚动偏移量console.log(\已滚动: ${window.scrollY}px`);`
locationlocation.href获取或设置完整的 URLlocation.href = "https://example.com"; // 跳转
location.protocol获取协议(如 http:https:console.log(location.protocol);
location.host获取主机名和端口console.log(location.host);
location.hostname获取主机名(不含端口)console.log(location.hostname);
location.port获取端口号console.log(location.port);
location.pathname获取 URL 路径部分console.log(location.pathname);
location.search获取查询字符串(? 开头)console.log(location.search); // ?id=123
location.hash获取锚点部分(# 开头)console.log(location.hash); // #section1
location.reload()重新加载当前页面location.reload();
location.replace(url)替换当前页面,不在历史中留下记录location.replace("/new-page");
location.assign(url)导航到新页面(等同设置 href)location.assign("/home");
historyhistory.back()返回上一页history.back();
history.forward()前进到下一页history.forward();
history.go(n)前进或后退 n 步history.go(-2); // 后退两页
history.go(1); // 前进一步
history.pushState(state, title, url)添加一条新历史记录(不刷新)history.pushState({page: 2}, "", "/page2");
history.replaceState(state, title, url)替换当前历史记录history.replaceState({page: 3}, "", "/page3");
window.onpopstate监听浏览器前进/后退按钮window.onpopstate = (event) => { ... };
navigatornavigator.userAgent获取用户代理字符串if (navigator.userAgent.includes("Mobile")) { ... }
navigator.platform获取操作系统平台console.log(navigator.platform);
navigator.language获取浏览器首选语言console.log(navigator.language); // zh-CN
navigator.onLine检查设备是否联网if (navigator.onLine) { ... } else { ... }
navigator.geolocation获取用户地理位置(需授权)navigator.geolocation.getCurrentPosition( ... )
navigator.cookieEnabled检查 Cookie 是否启用if (navigator.cookieEnabled) { ... }
screenscreen.width / screen.height获取屏幕总宽度/高度console.log(\屏幕尺寸: ${screen.width}x${screen.height}`);`
screen.availWidth / screen.availHeight获取屏幕可用宽度/高度console.log(\可用: ${screen.availWidth}x${screen.availHeight}`);`
screen.colorDepth获取屏幕颜色深度console.log(\颜色深度: ${screen.colorDepth}位`);`
frameswindow.frames.length获取 <iframe> 数量console.log(\有 ${window.frames.length} 个 iframe`);`
window.frames[0]获取第一个 <iframe> 的 window 对象window.frames[0].postMessage("Hello", "*");
localStoragelocalStorage.setItem(key, value)存储数据(持久化)localStorage.setItem("username", "Alice");
localStorage.getItem(key)读取数据const name = localStorage.getItem("username");
localStorage.removeItem(key)删除指定键的数据localStorage.removeItem("tempData");
localStorage.clear()清除所有数据localStorage.clear();
sessionStoragesessionStorage.setItem(key, value)存储数据(会话级)sessionStorage.setItem("token", "abc123");
sessionStorage.getItem(key)读取会话数据const token = sessionStorage.getItem("token");
sessionStorage.removeItem(key)删除会话数据sessionStorage.removeItem("draft");
sessionStorage.clear()清除当前会话所有数据sessionStorage.clear();

补充说明:

  • 所有 BOM 对象均挂载在 window 对象下,window 是全局对象,因此 window.alert() 可简写为 alert()
  • BOM 并非 W3C 标准,不同浏览器实现可能存在差异,使用时需注意兼容性。
  • 现代 Web 开发中,BOM 常用于实现路由导航、响应式布局、用户行为追踪、本地存储等功能。

13. DOM 操作

13.1 向 DOM 中注入 HTML

13.1.1 HTML 转换为 DOM

✅ HTML 转换为 DOM 的处理流程

阶段处理步骤详细说明示例(输入 → 输出)
1. 字节流接收(Bytes Received)浏览器从服务器接收 HTML 文件的原始字节流网络层接收到 HTML 文件的二进制数据(如 UTF-8 编码的字节序列)。48 65 6C 6C 6F 20 3C 68 74 6D 6C 3E ...
2. 编码解码(Decoding)将字节流根据指定编码(如 UTF-8)转换为字符浏览器根据 <meta charset="UTF-8"> 或 HTTP 响应头确定编码方式,将字节解码为 Unicode 字符。... <html> ...(字符串形式)
3. 分词 / 词法分析(Tokenization)将字符流分解为有意义的”标记”(Tokens)解析器逐字符读取 HTML,识别出开始标签、结束标签、属性、文本内容、注释等标记。输入:<div class="box">Hello</div>
输出标记序列:
- <div(开始标签)
- class="box"(属性)
- >(标签结束)
- Hello(文本节点)
- </div>(结束标签)
4. 构建 DOM 树(Tree Construction)根据标记创建 DOM 节点,并按父子关系组织成树状结构解析器根据 HTML 的嵌套结构,使用栈结构管理当前上下文,逐步构建节点之间的层级关系。输入:标记流
输出:Document → html → head / body → div (class="box") → TextNode "Hello"
5. 处理外部资源(Resource Loading)遇到 <script><link><img> 等标签时,发起额外请求- CSS:暂停渲染,下载并解析 CSS 构建 CSSOM
- JS:默认阻塞 HTML 解析,下载并执行脚本
- 图片/字体:异步加载
<link rel="stylesheet" href="style.css"><script src="app.js"></script><img src="logo.png"> → 触发 3 个额外网络请求
6. 执行脚本(Script Execution)当遇到 <script> 标签时,暂停解析,执行 JavaScriptJS 可访问当前已构建的 DOM 部分(但不一定完整),可动态修改 DOM 结构。<p id="msg">Loading...</p><script>document.getElementById('msg').textContent = 'Loaded';</script> → 修改 DOM 文本内容
7. 构建完成(DOMContentLoaded)HTML 解析完毕,DOM 树构建完成,触发 DOMContentLoaded 事件此时 DOM 已就绪,可被 JavaScript 完全访问和操作,不等待 CSS、图片等资源加载完成。document.addEventListener('DOMContentLoaded', () => { console.log('DOM 已构建完成'); });
8. 最终稳定状态(load 事件)所有资源(图片、样式、脚本等)全部加载完成触发 window.onload 事件,表示页面完全加载。window.addEventListener('load', () => { console.log('页面完全加载'); });

✅ 关键概念补充说明表

术语定义在流程中的作用
Tokenizer(分词器)将字符流拆分为标记(Token)的组件是解析的第一步,决定如何理解 HTML 内容
Tree Constructor(树构造器)根据 Token 构建 DOM 节点树的组件将扁平的标记流转化为具有父子关系的树形结构
Document ObjectDOM 的根节点,代表整个 HTML 文档所有 DOM 操作的入口点,通过 document 访问
Node(节点)DOM 树中的基本单位,包括元素节点、文本节点、注释节点等每个 HTML 标签或文本都会生成一个或多个节点
Parser Blocking<script> 默认会阻塞 HTML 解析直到脚本下载并执行完毕影响页面渲染性能,可通过 asyncdefer 属性优化

✅ 示例:HTML 到 DOM 的转换过程

原始 HTML 代码:

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>示例页面</title>
</head>
<body>
  <h1>Hello World</h1>
  <p class="intro">这是一个段落。</p>
</body>
</html>

对应 DOM 树结构:

节点类型节点名称属性/内容子节点
Document#documenthtml
Elementhtmllang="zh"head, body
Elementheadmeta, title
Elementmetacharset="UTF-8"
ElementtitleTextNode: “示例页面”
Elementbodyh1, p
Elementh1TextNode: “Hello World”
Elementpclass="intro"TextNode: “这是一个段落。“
TextNode#text”示例页面”
TextNode#text”Hello World”
TextNode#text”这是一个段落。”

✅ 总结:HTML → DOM 的核心流程

字节流 → 字符串 → 标记(Tokens) → 节点(Nodes) → DOM 树

  • 整个过程由浏览器的 HTML 解析引擎(如 WebKit、Blink、Gecko)自动完成。
  • 开发者可通过 document 对象访问和操作最终生成的 DOM。
  • 理解该流程有助于优化页面加载性能(如减少关键路径阻塞、合理放置脚本)。

13.1.2 DOM 元素插入 HTML

✅ DOM 元素插入 HTML 的完整流程

阶段操作步骤使用方法 / API示例代码关键说明
1. 创建新 DOM 元素(Create Element)在内存中创建一个新的元素节点,尚未添加到文档中document.createElement(tagName)const div = document.createElement('div');- 仅在 JS 内存中存在
- 不会触发页面重绘或回流
- 可提前设置属性、类名等
2. 配置元素属性与内容(Set Properties & Content)为新元素设置属性、文本内容、HTML 内容或样式element.setAttribute(name, value)
element.textContent / element.innerHTML
element.className / element.id
element.style.property
div.className = 'box';
div.id = 'myDiv';
div.setAttribute('data-role', 'container');
div.textContent = '这是一个动态插入的盒子';
div.style.color = 'blue';
✅ 推荐使用 textContent 防止 XSS
✅ 若需插入标签,使用 innerHTML(注意安全)
3. 选择插入位置的目标父元素(Select Parent)找到要将新元素插入其中的父容器document.getElementById(id)
document.querySelector(selector)
document.body
const container = document.getElementById('container');
const container = document.querySelector('.main');
必须确保目标元素已存在于 DOM 中,否则返回 null
4. 插入元素到 DOM 树(Append to DOM)将创建好的元素正式添加到文档结构中,使其可见parent.appendChild(child)
parent.insertBefore(newChild, referenceChild)
parent.append(...nodes)(支持多个节点)
container.appendChild(div);
container.insertBefore(div, p);
container.append(div, span, textNode);
✅ 此时元素进入 DOM 树
✅ 浏览器开始计算布局(回流)和绘制(重绘)
5. (可选)监听事件(Attach Event Listeners)为新插入的元素绑定交互行为element.addEventListener(event, handler)div.addEventListener('click', function() { alert('你点击了动态元素!'); });即使元素是动态创建的,也可正常绑定事件
6. 浏览器渲染更新(Reflow & Repaint)浏览器重新计算布局(reflow)并绘制屏幕(repaint),用户可见新元素自动触发- 回流(Reflow):元素尺寸/位置变化,影响布局
- 重绘(Repaint):样式变化但不改变布局(如颜色)
- 频繁操作会影响性能
7. 元素可被访问与操作(Accessible in DOM)新元素已成为 DOM 的一部分,可通过选择器访问document.querySelectorgetElementByIdconst insertedDiv = document.getElementById('myDiv');
console.log(insertedDiv.textContent);
插入后即可像静态 HTML 元素一样操作

✅ 常见插入方式对比表

方法语法功能说明适用场景
appendChild()parent.appendChild(child)将子元素添加到父元素的最后最常用,简单追加
insertBefore()parent.insertBefore(newNode, referenceNode)在指定子节点之前插入新节点精确控制插入位置
append()parent.append(...nodes)添加一个或多个节点(包括文本)到末尾支持批量插入,更现代
prepend()parent.prepend(...nodes)添加一个或多个节点到开头插入到容器最前面
before() / after()target.before(node) / target.after(node)在当前元素之前或之后插入(同级)插入兄弟节点,无需父元素引用

💡 示例:

const h1 = document.querySelector('h1');
const note = document.createElement('span');
note.textContent = '(备注)';
h1.after(note); // 在 <h1> 后面插入

✅ 性能优化建议

建议说明
✅ 批量操作先在内存中构建完整结构,再一次性插入 DOM,减少回流次数
✅ 使用 DocumentFragment创建一个虚拟容器,先将多个元素添加进去,再整体插入 DOM
✅ 避免频繁读取布局信息offsetHeightgetBoundingClientRect() 会强制同步回流
✅ 使用事件委托对大量动态元素,不要逐个绑定事件,而是绑定到父元素

🌟 DocumentFragment 示例:

const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const item = document.createElement('li');
  item.textContent = `项目 ${i}`;
  fragment.appendChild(item);
}
document.ul.appendChild(fragment); // 仅一次回流

✅ 总结:DOM 插入核心流程链

🔗 创建 → 配置 → 定位 → 插入 → 渲染 → 可操作

  • 所有动态插入的元素在浏览器中与静态 HTML 元素无异。
  • 理解插入流程有助于编写高效、安全的前端代码。
  • 现代框架(React/Vue)底层仍基于此机制实现虚拟 DOM 更新。

13.2 DOM 操作

✅ JavaScript DOM 操作方法一览表

操作类别方法 / 属性语法格式功能说明返回值 / 示例
一、元素查询(获取节点)getElementById()document.getElementById(id)根据 id 获取单个元素const el = document.getElementById('header'); → 返回一个 Elementnull
getElementsByClassName()element.getElementsByClassName(className)根据类名获取元素集合(实时 HTMLCollectionconst items = document.getElementsByClassName('item'); → 返回 HTMLCollection
getElementsByTagName()element.getElementsByTagName(tagName)根据标签名获取元素集合const divs = document.getElementsByTagName('div'); → 返回 HTMLCollection
querySelector()element.querySelector(selector)使用 CSS 选择器匹配第一个符合条件的元素const firstBtn = document.querySelector('.btn.primary'); → 返回第一个匹配的 Elementnull
querySelectorAll()element.querySelectorAll(selector)使用 CSS 选择器匹配所有符合条件的元素const allLinks = document.querySelectorAll('a[href^="https"]'); → 返回 NodeList(静态)
二、元素创建与插入createElement()document.createElement(tagName)创建一个新的元素节点const div = document.createElement('div');
createTextNode()document.createTextNode(text)创建一个文本节点const text = document.createTextNode('Hello');
appendChild()parent.appendChild(child)将子节点添加到父节点的末尾parent.appendChild(div);
insertBefore()parent.insertBefore(newNode, referenceNode)在指定子节点之前插入新节点parent.insertBefore(newDiv, existingChild);
append()parent.append(...nodes)添加一个或多个节点(包括文本)到末尾parent.append(div, span, 'text');
prepend()parent.prepend(...nodes)添加一个或多个节点到开头parent.prepend(header);
before()target.before(...nodes)在当前元素之前插入同级节点h1.before(note);
after()target.after(...nodes)在当前元素之后插入同级节点h1.after(subtitle);
三、元素内容与属性操作innerHTMLelement.innerHTML获取或设置元素的 HTML 内容div.innerHTML = '<p>新内容</p>';
⚠️ 存在 XSS 风险
textContentelement.textContent获取或设置元素的纯文本内容(推荐安全方式)div.textContent = 'Hello World';
innerTextelement.innerText获取或设置可渲染的文本内容(受样式影响)console.log(el.innerText);
getAttribute()element.getAttribute(name)获取指定属性的值const href = link.getAttribute('href');
setAttribute()element.setAttribute(name, value)设置指定属性的值img.setAttribute('src', 'image.jpg');
hasAttribute()element.hasAttribute(name)判断元素是否包含某属性if (input.hasAttribute('required')) { ... }
removeAttribute()element.removeAttribute(name)移除指定属性button.removeAttribute('disabled');
datasetelement.dataset.key访问 data-* 自定义属性(驼峰命名)<div data-user-id="123">div.dataset.userId; // "123"
四、元素类名与样式操作classNameelement.className获取或设置整个 class 字符串el.className = 'box highlight';
classListelement.classList.add/remove/toggle/contains()操作类名的现代 API(推荐使用)el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('visible');
if (el.classList.contains('error')) { ... }
styleelement.style.property直接设置内联样式(驼峰命名)div.style.color = 'red';
div.style.backgroundColor = '#fff';
五、元素删除与替换removeChild()parent.removeChild(child)从父元素中移除指定子元素parent.removeChild(oldDiv);
remove()element.remove()直接删除元素自身(无需父节点引用)oldElement.remove(); ✅ 现代推荐
replaceChild()parent.replaceChild(newChild, oldChild)用新节点替换旧节点parent.replaceChild(newDiv, oldDiv);
六、节点关系与遍历parentNodenode.parentNode获取父节点const parent = child.parentNode;
childNodesnode.childNodes获取所有子节点(包括文本、注释)const children = element.childNodes;NodeList
childrenelement.children获取所有元素子节点(仅 Elementconst elems = container.children;HTMLCollection
firstChild / lastChildnode.firstChild / node.lastChild获取第一个/最后一个子节点(任意类型)const first = node.firstChild;
firstElementChild / lastElementChildelement.firstElementChild / lastElementChild获取第一个/最后一个元素子节点const firstEl = container.firstElementChild;
previousSibling / nextSiblingnode.previousSibling / nextSibling获取前一个/后一个兄弟节点(任意类型)const prev = current.previousSibling;
previousElementSibling / nextElementSiblingelement.previousElementSibling / nextElementSibling获取前一个/后一个元素兄弟节点const nextBtn = btn.nextElementSibling;
七、事件处理addEventListener()element.addEventListener(event, handler, options)绑定事件监听器(推荐方式)btn.addEventListener('click', function(e) { console.log('点击了按钮'); });
removeEventListener()element.removeEventListener(event, handler)移除事件监听器(需引用同一函数)btn.removeEventListener('click', handleClick);
dispatchEvent()element.dispatchEvent(event)手动触发一个事件const event = new Event('custom'); el.dispatchEvent(event);
八、其他实用方法closest()element.closest(selector)向上查找最近的匹配祖先元素const card = button.closest('.card');
matches()element.matches(selector)判断元素是否匹配指定选择器if (el.matches('.active')) { ... }
focus() / blur()element.focus() / element.blur()聚焦或失焦元素input.focus();
scrollIntoView()element.scrollIntoView(options)滚动使元素进入视口section.scrollIntoView({ behavior: 'smooth' });

✅ 总结:DOM 操作最佳实践

建议说明
✅ 优先使用 querySelector / querySelectorAll语法灵活,支持复杂选择器
✅ 使用 classList 替代 className更安全、易读
✅ 使用 textContent 替代 innerHTML(除非必要)防止 XSS 攻击
✅ 使用 addEventListener 绑定事件支持多个监听器,便于解绑
✅ 动态批量插入时使用 DocumentFragment减少页面回流,提升性能
✅ 删除元素优先使用 element.remove()语法简洁,无需父节点

💡 提示:

  • 所有 DOM 方法均作用于节点(Node)或元素(Element)。
  • 现代前端框架(React/Vue/Angular)底层仍基于这些原生 API 实现虚拟 DOM 更新。

13.3 特性

13.3.1 样式

✅ JavaScript 操作 DOM 样式的方法一览表

操作类别方法 / 属性语法格式功能说明示例代码
一、内联样式操作(直接设置 style 属性)element.styleelement.style.property直接设置元素的内联样式(即 style="..."),优先级高const box = document.getElementById('box');
box.style.color = 'red';
box.style.backgroundColor = 'yellow';
box.style.fontSize = '16px';
box.style.display = 'none';
✅ 注意:使用驼峰命名法(如 backgroundColor
element.style.cssTextelement.style.cssText = '...'一次性设置多个内联样式(字符串形式)box.style.cssText = 'color: blue; font-size: 18px; padding: 10px;';
⚠️ 会覆盖原有内联样式
element.setAttribute('style', ...)element.setAttribute('style', value)通过属性方式设置整个 style 字符串box.setAttribute('style', 'color: green; font-weight: bold;');
二、类名操作(推荐方式,样式与行为分离)element.classNameelement.className = '...'获取或设置整个 class 属性值(字符串)box.className = 'highlight active'; // 覆盖所有原有类名
element.classListelement.classList.add/remove/toggle/contains()现代化类名操作 API,推荐使用box.classList.add('visible'); // 添加类
box.classList.remove('hidden'); // 移除类
box.classList.toggle('active'); // 切换类
if (box.classList.contains('error')) { ... } // 判断是否包含
三、获取最终计算样式(读取浏览器渲染后的样式)window.getComputedStyle()getComputedStyle(element, pseudo)获取元素最终计算后的样式值(包含继承、CSS 规则等)const style = getComputedStyle(box);
console.log(style.color); // "rgb(255, 0, 0)"
console.log(style.fontSize); // "16px"
console.log(style.marginTop); // "10px"
✅ 只读,不可修改
getComputedStyle(element).property直接读取某个样式属性const width = getComputedStyle(box).width;
console.log(width); // "200px"
四、访问与修改样式表(操作 <style><link>document.styleSheetsdocument.styleSheets[index]获取页面中所有样式表的集合const sheets = document.styleSheets;
console.log(sheets.length); // 样式表数量
sheet.cssRules / sheet.rulessheet.cssRules[index]获取样式表中的所有 CSS 规则const sheet = document.styleSheets[0];
const rules = sheet.cssRules;
sheet.insertRule()sheet.insertRule(rule, index)向样式表中插入一条新规则sheet.insertRule('p { color: green; }', 0); // 插入到第一条
sheet.deleteRule()sheet.deleteRule(index)删除指定索引的 CSS 规则sheet.deleteRule(0); // 删除第一条规则
五、元素尺寸与位置(布局相关)offsetWidth / offsetHeightelement.offsetWidth / offsetHeight获取元素的布局尺寸(含 padding、border)console.log(box.offsetWidth); // 220 (width + padding + border)
clientWidth / clientHeightelement.clientWidth / clientHeight获取内容区+内边距尺寸(不含 border、滚动条)console.log(box.clientWidth); // 200 (width + padding)
scrollWidth / scrollHeightelement.scrollWidth / scrollHeight获取元素总内容尺寸(含溢出不可见部分)console.log(box.scrollHeight); // 内容实际高度(可能 > clientHeight)
offsetTop / offsetLeftelement.offsetTop / offsetLeft获取元素相对于定位父元素的偏移位置console.log(box.offsetTop); // 距离最近定位祖先的顶部距离
getBoundingClientRect()element.getBoundingClientRect()获取元素在视口中的精确位置和尺寸const rect = box.getBoundingClientRect();
console.log(rect.top, rect.left, rect.width, rect.height);
// 相对于视口的位置
六、滚动控制element.scrollTop / scrollLeftelement.scrollTop = value获取或设置元素内容的垂直/水平滚动偏移量box.scrollTop = 100; // 滚动100px
console.log(box.scrollTop); // 当前滚动位置
element.scrollTo()element.scrollTo(x, y)options滚动到指定位置box.scrollTo({ top: 200, behavior: 'smooth' });
element.scrollIntoView()element.scrollIntoView(options)滚动使元素进入视口box.scrollIntoView({ behavior: 'smooth', block: 'center' });

✅ 内联样式 vs 类名操作 对比表

特性内联样式 style类名操作 classList
优先级最高(行内样式)取决于 CSS 规则
维护性差(样式与 JS 混合)好(样式集中在 CSS 文件)
性能频繁修改会触发多次回流/重绘推荐批量通过类控制,性能更优
复用性高(CSS 类可复用)
推荐场景动态计算的样式(如拖拽位置)通用样式切换(如主题、状态)

✅ 总结:DOM 样式操作最佳实践

建议说明
✅ 优先使用 classList 控制样式实现样式与逻辑分离,易于维护
✅ 避免频繁读写 offsetWidth 等布局属性会强制同步回流,影响性能
✅ 使用 getComputedStyle() 读取真实样式而非依赖 style 属性
✅ 动画优先使用 CSS 而非 JS 修改 style利用 GPU 加速,性能更好
✅ 批量样式修改建议通过切换类实现减少 DOM 重排(reflow)次数

13.3.2 样式属性

✅ DOM 样式属性操作一览表

CSS 属性名JavaScript 对应属性名语法格式功能说明代码示例
colorcolorelement.style.color设置文本颜色elem.style.color = 'red'; // 或 '#ff0000', 'rgb(255,0,0)'
background-colorbackgroundColorelement.style.backgroundColor设置背景颜色elem.style.backgroundColor = 'blue';
font-sizefontSizeelement.style.fontSize设置字体大小elem.style.fontSize = '16px'; // 也可用 '1.2em', '20pt'
font-weightfontWeightelement.style.fontWeight设置字体粗细elem.style.fontWeight = 'bold'; // 或 'normal', '600'
font-familyfontFamilyelement.style.fontFamily设置字体族elem.style.fontFamily = 'Arial, sans-serif';
text-aligntextAlignelement.style.textAlign设置文本对齐方式elem.style.textAlign = 'center'; // 'left', 'right', 'justify'
line-heightlineHeightelement.style.lineHeight设置行高elem.style.lineHeight = '1.5'; // 或 '20px'
displaydisplayelement.style.display设置元素显示方式elem.style.display = 'none'; // 隐藏元素
elem.style.display = 'block'; // 块级显示
elem.style.display = 'inline-flex';
visibilityvisibilityelement.style.visibility控制可见性(保留空间)elem.style.visibility = 'hidden'; // 隐藏但占位
elem.style.visibility = 'visible';
width / heightwidth / heightelement.style.width / height设置宽高elem.style.width = '200px';
elem.style.height = '100px';
max-width / max-heightmaxWidth / maxHeightelement.style.maxWidth / maxHeight设置最大宽高elem.style.maxWidth = '500px';
marginmarginelement.style.margin设置外边距(四边)elem.style.margin = '10px'; // 统一设置
elem.style.margin = '5px 10px'; // 上下 左右
margin-topmarginTopelement.style.marginTop设置上外边距elem.style.marginTop = '20px';
paddingpaddingelement.style.padding设置内边距(四边)elem.style.padding = '15px';
borderborderelement.style.border设置边框(宽度、样式、颜色)elem.style.border = '2px solid black';
border-radiusborderRadiuselement.style.borderRadius设置圆角elem.style.borderRadius = '8px'; // 或 '50%' 圆形
box-shadowboxShadowelement.style.boxShadow设置阴影效果elem.style.boxShadow = '2px 2px 5px rgba(0,0,0,0.3)';
opacityopacityelement.style.opacity设置透明度elem.style.opacity = '0.5'; // 0=全透明, 1=不透明
positionpositionelement.style.position设置定位方式elem.style.position = 'absolute'; // 'relative', 'fixed', 'sticky'
top / right / bottom / lefttop / right / bottom / leftelement.style.top配合 position 设置偏移elem.style.position = 'absolute';
elem.style.top = '10px';
elem.style.left = '20px';
z-indexzIndexelement.style.zIndex设置层叠顺序(仅对定位元素有效)elem.style.zIndex = '1000';
floatcssFloat(标准)/ styleFloat(IE)element.style.cssFloat设置浮动(现代布局多用 Flex/Grid)elem.style.cssFloat = 'left';
flexflexelement.style.flex设置 Flex 子项的伸缩属性container.style.display = 'flex';
item.style.flex = '1';
grid-template-columnsgridTemplateColumnselement.style.gridTemplateColumns设置 Grid 布局列结构grid.style.display = 'grid';
grid.style.gridTemplateColumns = '1fr 2fr';
transformtransformelement.style.transform设置 2D/3D 变换elem.style.transform = 'rotate(45deg) scale(1.2)';
transitiontransitionelement.style.transition设置过渡动画elem.style.transition = 'all 0.3s ease-in-out';
cursorcursorelement.style.cursor设置鼠标指针样式elem.style.cursor = 'pointer'; // 'wait', 'move', 'not-allowed'

✅ 使用说明与注意事项

要点说明
✅ 驼峰命名法所有包含连字符(-)的 CSS 属性,在 JS 中需转换为驼峰命名法(如 background-colorbackgroundColor
✅ 值为字符串所有 style 属性的值都必须是字符串,即使是数字也需加单位(如 '100px'),无单位的(如 opacity)可直接赋值数字或字符串
✅ 内联样式优先级高通过 style 设置的样式相当于 style="...",优先级高于外部 CSS 规则
✅ 推荐使用 classList对于复杂的样式切换,建议通过 element.classList.add('highlight') 操作类名,而非直接写样式
✅ 批量设置建议用 cssText若需一次性设置多个样式,可使用:elem.style.cssText = 'color: red; font-size: 16px;';(会覆盖原有内联样式)

💡 示例:使用 cssText 批量设置样式

const box = document.getElementById('box');
box.style.cssText = `
  width: 200px;
  height: 100px;
  background-color: #007bff;
  color: white;
  border-radius: 8px;
  text-align: center;
  line-height: 100px;
`;

13.3.3 获取样式

✅ DOM 获取样式的方法一览表

方法 / 属性语法格式功能说明返回值类型代码示例
element.styleelement.style.property获取元素的内联样式(即通过 style="..."element.style.xxx = ... 设置的样式)CSSStyleDeclaration(仅包含内联样式)const elem = document.getElementById('box');
console.log(elem.style.color); // 仅当有内联 style="color:red" 时返回值
⚠️ 无法获取 CSS 规则或外部样式表中的样式
window.getComputedStyle()getComputedStyle(element, pseudo)获取元素最终渲染后的计算样式(包含所有 CSS 规则、继承、默认样式等)CSSStyleDeclaration(只读)const style = getComputedStyle(elem);
console.log(style.color); // 真实颜色(无论来源)
console.log(style.fontSize); // 计算后的字体大小(如 "16px")
✅ 推荐用于精确读取样式
element.classNameelement.className获取元素的 class 属性值(字符串形式)stringconsole.log(elem.className); // 输出: "btn primary active"
element.classListelement.classList获取一个 DOMTokenList 对象,可遍历或操作类名DOMTokenListconsole.log(elem.classList); // 输出: ['btn', 'primary', 'active']
console.log(elem.classList.length); // 类数量
console.log(elem.classList[0]); // 第一个类名
element.getAttribute('class')element.getAttribute('class')通过属性方式获取 classstringnullconst classes = elem.getAttribute('class');
console.log(classes); // "btn primary"
element.idelement.id获取元素的 id 属性stringconsole.log(elem.id); // 如 "header"
element.getAttribute('style')element.getAttribute('style')获取内联 style 属性的原始字符串stringnullconsole.log(elem.getAttribute('style')); // 输出: "color: red; font-size: 16px;"
getComputedStyle().getPropertyValue()getComputedStyle(elem).getPropertyValue('property-name')使用标准 CSS 属性名(带连字符)获取计算样式值stringconst color = getComputedStyle(elem).getPropertyValue('color');
const margin = getComputedStyle(elem).getPropertyValue('margin-top');
✅ 与 CSS 写法一致,更直观

✅ 方法对比与使用建议

方法能否获取外部 CSS 样式?是否包含继承样式?是否只读?推荐使用场景
element.style❌ 否❌ 否✅ 是仅用于读取或设置内联样式
getComputedStyle()✅ 是✅ 是✅ 是(只读)✅ 推荐:获取元素最终渲染样式(最准确)
element.className❌ 否❌ 否✅ 是快速获取类名字符串
element.classList❌ 否❌ 否✅ 是操作类名(添加/删除/判断)
getAttribute('class')❌ 否❌ 否✅ 是className 类似,属性方式访问
getAttribute('style')❌ 否❌ 否✅ 是获取原始 style 字符串
getPropertyValue()✅ 是✅ 是✅ 是(只读)getComputedStyle 配合使用,支持标准 CSS 名

✅ 示例:对比 stylegetComputedStyle

<!-- HTML 代码 -->
<div id="box" class="highlight" style="color: red;">内容</div>
/* CSS 代码 */
.highlight {
  background-color: yellow;
  font-size: 18px;
}
// JavaScript 代码
const box = document.getElementById('box');

console.log(box.style.color);        // "red" ✅(内联)
console.log(box.style.backgroundColor); // "" ❌(非内联,返回空)
console.log(box.style.fontSize);     // "" ❌

const computed = getComputedStyle(box);
console.log(computed.color);               // "rgb(255, 0, 0)"
console.log(computed.backgroundColor);     // "rgb(255, 255, 0)" ✅
console.log(computed.fontSize);            // "18px" ✅

💡 总结:

  • 若需获取真实渲染样式 → 使用 getComputedStyle(element)
  • 若只需获取类名或内联样式 → 使用 classNamestyle
  • 避免依赖 element.style 读取非内联样式,因其无法获取 CSS 文件中的规则。

13.3.4 转换像素值

✅ DOM 转换像素值的方法一览表

方法 / 技巧语法格式功能说明返回值类型代码示例
window.getComputedStyle() + 读取属性getComputedStyle(element).property获取元素任意样式的计算后像素值(自动转为 pxstring(如 "16px"const elem = document.querySelector('.text');
const fontSize = getComputedStyle(elem).fontSize;
console.log(fontSize); // "16px"(无论原单位是 em/rem/%)
✅ 最常用、最可靠的方式
手动解析并转换 em / rem自定义函数emrem 字符串转换为像素值(基于当前上下文)number(像素数值)function emToPx(value, base) { return parseFloat(value) * base; }
console.log(emToPx('1.5em', 16)); // 24
⚠️ 需手动管理基准值(如 font-size
使用临时元素测量法创建隐藏元素进行实际渲染测量通过 DOM 渲染机制将任意单位转换为精确像素值number见下方详细示例
✅ 精确,适用于复杂单位
Element.getBoundingClientRect()element.getBoundingClientRect()获取元素在视口中的实际布局尺寸(单位为 pxDOMRect 对象(含 width, height, top, left 等)const rect = element.getBoundingClientRect();
console.log(rect.width); // 实际宽度(px)
console.log(rect.height); // 实际高度(px)
✅ 包含边框、内边距的实际渲染尺寸
offsetWidth / offsetHeightelement.offsetWidth / offsetHeight获取元素的布局尺寸(含 paddingborder,单位为 px)numberconsole.log(myDiv.offsetWidth); // 如 200(px)
✅ 快速获取整数像素尺寸
parseFloat() + 单位剥离parseFloat("10px")提取带单位字符串中的数值部分(常用于后续计算)numberconst val = '15.5em';
const num = parseFloat(val); // 15.5
🔧 常配合其他方法使用
利用 canvas 或 SVG 进行测量(高级)使用图形上下文测量文本或布局在特定场景下精确测量文本宽度等numberconst canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '16px Arial';
const width = ctx.measureText('Hello').width; // 文本像素宽度
✅ 适合文本尺寸测量

✅ 常见单位转换说明

原始单位是否可通过 getComputedStyle 转为 px说明
em✅ 是相对于父元素字体大小
rem✅ 是相对于根元素(<html>)字体大小
%✅ 是相对于父元素对应尺寸(如 width: 50% 相对于父容器宽度)
vh / vw✅ 是相对于视口高度/宽度
pt / in / cm✅ 是浏览器会自动换算为 px(基于 DPI)
auto⚠️ 特殊返回实际计算值(如 0px 或根据内容推算的值)

✅ 示例:将任意 CSS 值转换为像素值

function toPixels(cssValue, element = document.body) {
  const temp = document.createElement('div');
  temp.style.setProperty('position', 'absolute');
  temp.style.setProperty('visibility', 'hidden');
  temp.style.setProperty('font-size', cssValue); // 假设用于字体
  element.appendChild(temp);

  const computed = getComputedStyle(temp).fontSize;
  element.removeChild(temp);

  return parseFloat(computed); // 返回纯数字(如 16)
}

// 使用示例
console.log(toPixels('2em'));   // → 32 (假设基准为16px)
console.log(toPixels('1.5rem')); // → 24
console.log(toPixels('100%'));   // → 16

✅ 总结:推荐策略

场景推荐方法
获取元素真实样式像素值getComputedStyle(element).property
获取元素宽高尺寸offsetWidth / offsetHeightgetBoundingClientRect()
动态转换任意单位(如 em → px)✅ 临时元素测量法 或 getComputedStyle 配合隐藏元素
文本宽度测量canvas.measureText()
简单提取数值parseFloat(value)

💡 提示:

  • 所有通过 getComputedStyle 获取的尺寸均为最终渲染值,已转换为 px
  • 避免在循环中频繁调用 getComputedStyle 或创建临时元素,以免影响性能。
  • parseFloat 只提取数字,不会自动转换单位,需结合上下文计算。

13.3.5 获取元素高度、宽度、颜色等

✅ 测试 DOM 元素可视化信息的方法一览表

测试目标核心方法 / 属性说明返回值类型代码示例注意事项
高度(Height)offsetHeight获取元素布局高度(含 padding + border,不含 marginnumber(像素值)const height = elem.offsetHeight;
console.log(height); // 如 100
✅ 最常用,包含边框和内边距
clientHeight获取内容区+内边距高度(不含 border 和滚动条)numberconst clientH = elem.clientHeight;适用于内容可视区域
scrollHeight获取元素总内容高度(含溢出不可见部分)numberconst scrollH = elem.scrollHeight;用于判断是否滚动
getBoundingClientRect().height获取元素在视口中的渲染高度(含 CSS 变换影响)numberconst rect = elem.getBoundingClientRect();
console.log(rect.height);
✅ 精确,受 transform 影响
宽度(Width)offsetWidth获取元素布局宽度(含 padding + bordernumberconst width = elem.offsetWidth;offsetHeight 对应
clientWidth获取内容区+内边距宽度numberconst clientW = elem.clientWidth;常用于响应式判断
scrollWidth获取元素总内容宽度(含横向溢出)numberconst scrollW = elem.scrollWidth;判断水平滚动
getBoundingClientRect().width获取元素在视口中的实际渲染宽度numberconst rect = elem.getBoundingClientRect();
console.log(rect.width);
✅ 支持旋转、缩放等变换
坐标(位置)offsetTop / offsetLeft相对于最近定位祖先元素的偏移numberconsole.log(elem.offsetTop, elem.offsetLeft);⚠️ 依赖 position 上下文
getBoundingClientRect()获取元素相对于视口(viewport)的位置和尺寸DOMRect 对象const rect = elem.getBoundingClientRect();
console.log(rect.top, rect.left); // 距离视口顶部/左侧
console.log(rect.right, rect.bottom);
✅ 推荐:精确、不受滚动影响
clientTop / clientLeft元素边框内侧到内容区的偏移(通常是 border-widthnumberconsole.log(elem.clientTop); // 上边框宽度较少使用
颜色(Color)getComputedStyle(elem).color获取元素的最终计算颜色(文本颜色)string(如 "rgb(255, 0, 0)"const color = getComputedStyle(elem).color;
console.log(color); // 标准化为 rgb/rgba 格式
✅ 必须使用 getComputedStyle
getComputedStyle(elem).backgroundColor获取背景颜色stringconst bg = getComputedStyle(elem).backgroundColor;
console.log(bg); // "rgb(0, 0, 255)" 或 "transparent"
注意:透明背景返回 "transparent"
window.getComputedStyle(elem).getPropertyValue('color')使用标准 CSS 名获取颜色stringconst color = getComputedStyle(elem).getPropertyValue('color');语法更直观(支持连字符)
可见性(Visibility)getComputedStyle(elem).display检查是否被 display: none 隐藏stringconst isVisible = getComputedStyle(elem).display !== 'none';✅ 最常用
getComputedStyle(elem).visibility检查是否 visibility: hiddenstringconst isHidden = getComputedStyle(elem).visibility === 'hidden';隐藏但占位
getBoundingClientRect() 面积判断通过宽高是否为 0 判断是否可见booleanconst rect = elem.getBoundingClientRect();
const isVisible = rect.width > 0 && rect.height > 0 && rect.top >= 0;
防止 opacity: 0 或移出视口
透明度(Opacity)getComputedStyle(elem).opacity获取元素及其子元素的透明度stringconst opacity = parseFloat(getComputedStyle(elem).opacity);
console.log(opacity); // 0.0 ~ 1.0
注意:返回字符串,需 parseFloat
边框(Border)getComputedStyle(elem).borderWidth获取边框宽度(如 "1px"stringconst border = getComputedStyle(elem).borderTopWidth;返回带单位的字符串
getComputedStyle(elem).borderColor获取边框颜色stringconst color = getComputedStyle(elem).borderColor;同样为 rgb() 格式

✅ 综合示例:完整测试一个元素的可视化信息

function inspectElement(selector) {
  const elem = document.querySelector(selector);
  if (!elem) return console.log('Element not found');

  const style = getComputedStyle(elem);
  const rect = elem.getBoundingClientRect();

  console.log('--- 可视化信息 ---');
  console.log('Width (layout):', elem.offsetWidth, 'px');
  console.log('Height (layout):', elem.offsetHeight, 'px');
  console.log('Client Width:', elem.clientWidth, 'px');
  console.log('Client Height:', elem.clientHeight, 'px');
  console.log('Position (viewport):', `top=${rect.top}, left=${rect.left}`);
  console.log('Color:', style.color);
  console.log('Background:', style.backgroundColor);
  console.log('Font Size:', style.fontSize);
  console.log('Display:', style.display);
  console.log('Visibility:', style.visibility);
  console.log('Opacity:', parseFloat(style.opacity));
  console.log('Border:', style.border);
  console.log('Is Visible:', rect.width > 0 && rect.height > 0 && style.display !== 'none');
}

// 使用
inspectElement('#myButton');

✅ 测试建议与最佳实践

建议说明
✅ 优先使用 getComputedStyle()获取真实渲染样式,不受内联 style 限制
✅ 坐标使用 getBoundingClientRect()精确、不受滚动和定位影响
✅ 颜色/字体等样式必须用 getComputedStyleelem.style.color 无法获取 CSS 文件中的值
✅ 宽高优先用 offsetWidth/Height简单直接,适合大多数布局测试
✅ 在 DOMContentLoadedwindow.onload 后执行确保 DOM 和样式已加载完成
✅ 自动化测试中可结合 Puppeteer / Playwright直接注入脚本获取这些值进行断言

13.4 避免频繁处理布局

✅ 如何避免频繁处理布局:完整指南

问题原因说明优化策略正确示例错误示例
布局抖动(Layout Thrashing)在一个循环或函数中,交替读取和修改 DOM 布局属性,导致浏览器强制同步回流多次✅ 分离读取与写入:
1. 先批量读取所有需要的布局信息(如 offsetHeight
2. 再批量修改样式或 DOM
// ✅ 正确:先读,后写
const heights = [];
const items = document.querySelectorAll('.item');
// 1. 批量读取
for (let item of items) { heights.push(item.offsetHeight); }
// 2. 批量写入
for (let i = 0; i < items.length; i++) { items[i].style.height = heights[i] + 10 + 'px'; }
// ❌ 错误:读写交替
for (let item of items) {
// 读取 → 触发回流
let h = item.offsetHeight;
// 修改 → 触发下次回流
item.style.height = h + 10 + 'px';
// 下次循环又读 → 再回流
}
// ❌ 每次循环都强制回流!
频繁访问布局属性(如 offsetTop, clientWidth每次访问这些属性都会强制浏览器同步计算布局,即使样式未变✅ 缓存值:将读取的布局值存储在变量中,避免重复访问// ✅ 缓存 rect
const rect = element.getBoundingClientRect();
console.log(rect.top);
console.log(rect.left);
// 复用,不触发新回流
// ❌ 重复访问
console.log(element.getBoundingClientRect().top);
console.log(element.getBoundingClientRect().left);
// 调用两次,可能触发两次回流
在循环中修改 DOM 或样式每次修改都可能触发重排或重绘,尤其在长列表中✅ 批量操作:
- 使用 documentFragment 创建 DOM
- 使用 classNamecssText 批量设置样式
// ✅ 批量创建
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
container.appendChild(fragment); // 仅触发一次回流
// ❌ 逐个添加
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
container.appendChild(div);
// 每次都可能回流
}
使用 getComputedStyle 过于频繁getComputedStyle() 是只读但代价高的操作,会强制回流✅ 减少调用次数
✅ 合并读取
const style = getComputedStyle(elem);
const color = style.color;
const fontSize = style.fontSize; // 复用 style 对象
console.log(getComputedStyle(elem).color);
console.log(getComputedStyle(elem).fontSize);
// 两次调用,两次潜在回流
动画中使用 JS 修改布局属性(如 top, left, width每帧修改都会触发回流和重绘,性能差✅ 使用 transform:利用 GPU 加速,不触发布局重排// ✅ 推荐:使用 transform
elem.style.transform = \translateX(${x}px)`;`
// ❌ 避免:修改 top/left
elem.style.left = x + 'px'; // 触发回流
监听 resizescroll 事件未节流这些事件触发频率极高(每秒数十次)✅ 使用节流(throttle)或防抖(debounce)function throttle(func, delay) {
let inThrottle;
return function() {
if (!inThrottle) {
func.apply(this, arguments);
inThrottle = true;
setTimeout(() => inThrottle = false, delay);
}
}
}
window.addEventListener('scroll', throttle(handleScroll, 100));
window.addEventListener('scroll', handleScroll);
// ❌ 每次滚动都执行

✅ 推荐的高性能模式

模式说明适用场景
读写分离(Read-Write Batch)先集中读取所有布局信息,再集中修改列表项高度同步、动态布局计算
使用 requestAnimationFrame将 DOM 操作安排在浏览器下一次重绘前执行,避免中间状态动画、滚动效果、高频更新
使用 CSS 类代替内联样式通过切换 class 来应用样式,减少 JS 直接操作样式状态切换(如 active, hidden
虚拟滚动(Virtual Scrolling)只渲染可视区域内的元素,避免长列表重排表格、聊天记录、无限滚动
使用 transformopacity这两个属性由合成器(compositor)处理,不触发回流或重绘动画、过渡效果

✅ 示例:使用 requestAnimationFrame 优化滚动处理

let ticking = false;

function updateUI() {
  const top = window.scrollY;
  header.style.transform = `translateY(${top > 50 ? -100 : 0}px)`;
}

function requestTick() {
  if (!ticking) {
    requestAnimationFrame(updateUI);
    ticking = true;
  }
}

window.addEventListener('scroll', requestTick);

✅ 这样即使用户快速滚动,也只会每帧最多执行一次布局更新。

✅ 总结:避免布局频繁处理的关键原则

  • 🔹 不要在循环中读写布局属性
  • 🔹 先读取,后修改(分离读写)
  • 🔹 缓存 getBoundingClientRect()getComputedStyle() 等返回值
  • 🔹 使用 transform 替代 top/left/width/height 进行动画
  • 🔹 批量操作 DOM(使用 DocumentFragment
  • 🔹 对 scrollresize 等高频事件使用节流
  • 🔹 优先使用 CSS 而非 JavaScript 控制样式

遵循这些原则,可显著提升页面性能,避免卡顿,尤其是在复杂 UI 或移动设备上。

14. 事件

14.1 事件循环

✅ JavaScript 事件循环核心机制一览表

组成部分说明常见示例执行顺序规则备注
调用栈(Call Stack)跟踪当前正在执行的函数调用。先进后出(LIFO)。foo()bar() 等函数执行同步代码立即入栈执行✅ 执行同步任务,栈空时处理异步回调
堆(Heap)存放对象等非结构化数据的内存区域const obj = {}; // DOM 节点数据存储区域,不参与执行顺序
消息队列 / 宏任务队列(MacroTask Queue)存放异步宏任务的回调函数setTimeoutsetIntervalsetImmediate(Node.js)、I/O、UI 渲染事件、postMessagerequestAnimationFrame 回调每次事件循环只取一个宏任务执行⚠️ 浏览器在两个宏任务之间可能会进行渲染
微任务队列(MicroTask Queue)存放高优先级的异步微任务回调Promise.then/catch/finallyMutationObserverqueueMicrotask()process.nextTick()(Node.js)在当前宏任务结束后,立即执行所有微任务(清空微任务队列)✅ 微任务优先级高于宏任务,且会全部执行完再继续
渲染(Rendering)浏览器更新 UI 的过程(重排、重绘)页面视觉更新在每个宏任务之后,浏览器可能进行渲染(如果需要)🔁 渲染频率通常为 60fps(每 ~16.67ms 一次)

✅ 事件循环执行流程(每轮)

步骤操作说明
1执行同步代码从全局开始,依次执行调用栈中的同步任务
2遇到异步任务 → 分类入队- setTimeout → 宏任务队列
- Promise.then → 微任务队列
3当前宏任务执行完毕调用栈清空
4执行所有微任务清空微任务队列(先进先出),期间产生的新微任务也会被执行
5可能进行 UI 渲染浏览器检查是否需要更新页面(在宏任务和微任务都完成后)
6从宏任务队列取下一个任务开始下一轮事件循环

🔄 此过程不断重复,构成”事件循环”。

✅ 示例代码与执行顺序分析

console.log('1'); // 同步

setTimeout(() => {
  console.log('2'); // 宏任务1
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务1
});

Promise.resolve().then(() => {
  console.log('4'); // 微任务2
});

console.log('5'); // 同步

执行顺序:

输出阶段说明
1同步执行第一行
5同步执行第五行
3微任务宏任务(同步代码)结束后执行
4微任务同轮微任务队列中继续执行
2宏任务下一轮事件循环开始执行

✅ 最终输出:1 → 5 → 3 → 4 → 2

✅ 宏任务 vs 微任务 对比表

特性宏任务(MacroTask)微任务(MicroTask)
执行时机每轮事件循环取一个执行当前宏任务结束后,立即清空整个队列
优先级较低较高(在宏任务之间执行)
是否阻塞渲染是(长宏任务会卡住 UI)否(但大量微任务会影响性能)
是否可嵌套是(setTimeout 中可再放 setTimeout是(then 中可再 then
典型 APIsetTimeout, setInterval, I/O, UI eventsPromise, queueMicrotask, MutationObserver
浏览器渲染时机在宏任务之间可能发生渲染不渲染(微任务执行期间 UI 冻结)

✅ 特殊情况:queueMicrotask()

queueMicrotask(() => {
  console.log('This is a microtask');
});
  • Promise.then 同级,但不依赖 Promise 状态
  • 用于将任务放入微任务队列,确保在当前宏任务结束后尽快执行

✅ 与浏览器渲染的关系

场景是否可能触发渲染
同步代码执行中❌ 否
微任务执行期间❌ 否(UI 冻结)
宏任务执行结束后✅ 是(浏览器检查并渲染)
requestAnimationFrame 回调前✅ 是(rAF 在渲染前执行)

💡 提示:使用 requestAnimationFrame 可在下一次渲染前执行动画更新,避免闪烁。

✅ 总结:关键原则

原则说明
✅ 微任务优先于宏任务当前任务结束后,必须清空微任务队列
✅ 宏任务之间可能渲染UI 更新发生在宏任务切换时
✅ 避免长任务阻塞过长的同步或微任务会卡住 UI
✅ 合理使用异步setTimeout(fn, 0) 可将任务推到下一轮(降级为宏任务)
✅ 微任务不是”越快越好”大量微任务会导致页面响应延迟

示例 1:仅含宏任务的示例

console.log('全局同步代码开始');

setTimeout(() => {
  console.log('宏任务 1: setTimeout 1');
}, 0);

setTimeout(() => {
  console.log('宏任务 2: setTimeout 2');
}, 0);

console.log('全局同步代码结束');

🔍 执行流程分析:

  • 执行同步代码:
    • 输出:全局同步代码开始
    • 遇到第一个 setTimeout → 回调加入宏任务队列
    • 遇到第二个 setTimeout → 回调加入宏任务队列
    • 输出:全局同步代码结束
  • 当前宏任务(全局脚本)执行完毕,调用栈清空。
  • 事件循环从宏任务队列中取出第一个宏任务执行:
    • 输出:宏任务 1: setTimeout 1
  • 下一轮事件循环,取出第二个宏任务执行:
    • 输出:宏任务 2: setTimeout 2

✅ 最终输出顺序:

全局同步代码开始
全局同步代码结束
宏任务 1: setTimeout 1
宏任务 2: setTimeout 2

示例 2:含宏任务和微任务的示例

console.log('全局同步代码开始');

setTimeout(() => {
  console.log('宏任务 1: setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('微任务 1: Promise.then');
});

Promise.resolve().then(() => {
  console.log('微任务 2: Promise.then');
});

console.log('全局同步代码结束');

🔍 执行流程分析:

  • 执行同步代码:
    • 输出:全局同步代码开始
    • 遇到 setTimeout → 回调加入宏任务队列
    • 遇到第一个 Promise.then → 回调加入微任务队列
    • 遇到第二个 Promise.then → 回调加入微任务队列
    • 输出:全局同步代码结束
  • 当前宏任务(全局脚本)执行完毕,调用栈清空。
  • 事件循环检查微任务队列,并清空所有微任务:
    • 输出:微任务 1: Promise.then
    • 输出:微任务 2: Promise.then
  • 微任务队列清空后,进入下一轮事件循环,从宏任务队列中取出任务执行:
    • 输出:宏任务 1: setTimeout

✅ 最终输出顺序:

全局同步代码开始
全局同步代码结束
微任务 1: Promise.then
微任务 2: Promise.then
宏任务 1: setTimeout

✅ 对比总结

特性仅宏任务示例宏任务 + 微任务示例
同步代码输出全局同步代码开始全局同步代码结束相同
异步任务类型只有 setTimeout(宏任务)setTimeout(宏任务) + Promise.then(微任务)
异步执行时机下一轮事件循环开始执行微任务在当前轮结束时执行,宏任务在下一轮执行
最终顺序同步 → 宏任务1 → 宏任务2同步 → 微任务1 → 微任务2 → 宏任务

14.2 计时器

14.2.1 延迟执行和间隔执行

✅ 一、延迟执行:setTimeout

用于在指定延迟后执行一次的函数。

属性说明
语法const timerId = setTimeout(callback, delay, ...args);
参数说明- callback:延迟执行的函数
- delay:延迟时间(毫秒),最小值通常为 4ms(嵌套或非用户触发时)
- ...args:传递给回调函数的参数
返回值number 类型的定时器 ID,可用于取消执行(clearTimeout(timerId)
执行特点- 只执行一次
- 延迟时间是最小等待时间,不保证精确执行(受事件循环、任务队列影响)
- 回调函数在下一个事件循环的宏任务阶段执行
代码示例console.log('开始');
setTimeout(() => { console.log('2秒后执行'); }, 2000);
console.log('结束');
输出:开始结束2秒后执行
常见用途- 延迟加载资源
- 防抖(Debounce)函数实现
- 模拟异步操作延迟
- 页面跳转前的等待提示
取消执行使用 clearTimeout(timerId) 可在延迟期间取消执行:
const id = setTimeout(() => { /* 不会执行 */ }, 3000);
clearTimeout(id); // 取消
最小延迟限制- 浏览器通常限制最小延迟为 4ms(HTML5 规范)
- setTimeout(fn, 0) 实际延迟约为 4ms
- 在 setTimeout 嵌套调用中,某些浏览器会强制最小延迟
与事件循环关系属于宏任务,在当前调用栈清空后,经过指定延迟,被加入宏任务队列等待执行

✅ 二、间隔执行:setInterval

用于每隔指定时间重复执行的函数,直到被取消。

属性说明
语法const intervalId = setInterval(callback, interval, ...args);
参数说明- callback:重复执行的函数
- interval:重复间隔时间(毫秒)
- ...args:传递给回调函数的参数
返回值number 类型的间隔定时器 ID,可用于停止执行(clearInterval(intervalId)
执行特点- 重复执行,直到被 clearInterval 明确停止
- 每次执行间隔不保证精确(受回调执行时间、事件循环阻塞影响)
- 如果前一次回调未完成,下一次不会并行执行(JavaScript 是单线程)
代码示例let count = 0;
const id = setInterval(() => {
console.log(\第 ${++count} 次执行`);<br> if (count >= 3) { clearInterval(id); // 停止 }<br>}, 1000);<br>输出:第 1 次执行第 2 次执行第 3 次执行`
常见用途- 实现倒计时、时钟显示
- 轮询服务器状态(Polling)
- 动画效果(但推荐使用 requestAnimationFrame
- 心跳检测
取消执行必须使用 clearInterval(intervalId) 显式停止,否则会持续执行,可能导致内存泄漏:
const id = setInterval(tick, 1000);
// ... 条件满足后
clearInterval(id);
执行间隔偏差- 如果回调函数执行时间较长(如 800ms),而间隔设置为 1000ms,则实际间隔为 1000 + 800 = 1800ms
- 事件循环阻塞也会导致跳过或延迟执行
与事件循环关系属于宏任务,每次执行后,浏览器会在 interval 时间后再次将其加入宏任务队列

✅ 对比总结:setTimeout vs setInterval

特性setTimeoutsetInterval
执行次数仅执行一次重复执行,直到取消
典型用途延迟、防抖、一次性任务轮询、倒计时、周期性任务
是否需要手动停止否(自动结束)✅ 是(必须调用 clearInterval
时间精度受事件循环影响,最小约 4ms累积误差明显,不适合高精度定时
内存泄漏风险较低较高(忘记 clearInterval 会导致持续执行)
替代方案对于动画,推荐使用 requestAnimationFrame
对于轮询,可结合 setTimeout 实现更可控的递归调用
递归 setTimeout 替代 setInterval✅ 推荐用于需要固定间隔的场景:
function repeat() {
// 任务
setTimeout(repeat, 1000);
// 保证每次任务完成后才开始下一次
}
repeat();
setInterval 无法保证固定间隔,可能累积延迟

✅ 最佳实践建议

建议说明
✅ 使用 clearTimeout / clearInterval 及时清理避免内存泄漏,尤其在组件销毁时(如 React 的 useEffect 清理)
✅ 避免过短的延迟或间隔(如 1ms)浏览器会强制最小延迟,且频繁执行影响性能
✅ 高精度需求使用 performance.now()配合 requestAnimationFrame 实现更精确的动画计时
✅ 轮询任务考虑使用 setTimeout 递归setInterval 更可控,避免任务堆积
✅ 注意 this 上下文在回调中使用箭头函数或 .bind() 确保 this 正确

14.2.2 事件循环中执行计时器

✅ 事件循环中执行计时器:核心机制详解

组成部分说明在事件循环中的角色
计时器(Timers)setTimeoutsetInterval 是 JavaScript 中最常用的计时器 API,用于延迟或周期性执行代码。属于宏任务(MacroTask),由事件循环统一调度。

✅ 计时器在事件循环中的执行流程

步骤操作详细说明
1注册计时器当代码执行到 setTimeout(fn, 1000) 时,浏览器(或 Node.js)启动一个内部计时器,开始倒计时。
2等待延迟时间计时器在后台等待指定毫秒数(如 1000ms),不占用主线程。
3延迟到期 → 回调入队时间到后,回调函数被放入宏任务队列(不是立即执行!)。⚠️ 如果队列中已有其他任务,需等待。
4事件循环检查宏任务队列当前宏任务(如同步代码)执行完毕,调用栈为空。
5取出并执行计时器回调事件循环从宏任务队列中取出最早加入的计时器回调,推入调用栈执行。
6执行微任务队列计时器回调执行完毕后,立即清空微任务队列(如 Promise.then)。
7可能进行 UI 渲染浏览器在宏任务(包括计时器)执行后,可能进行页面渲染。
8进入下一轮事件循环取下一个宏任务执行,循环继续。

🔁 关键点:计时器回调是宏任务,必须等待当前任务完成,并在延迟到期后排队等待执行。

setTimeoutsetInterval 在事件循环中的行为对比

特性setTimeoutsetInterval
任务类型宏任务宏任务
执行次数仅一次重复执行,直到被 clearInterval
回调入队时机延迟时间到后,立即将回调加入宏任务队列每个间隔时间到后,尝试将回调加入宏任务队列
执行间隔保证❌ 不保证精确(受事件循环阻塞影响)❌ 不保证固定间隔:
- 如果回调执行时间 > 间隔,后续任务会堆积或跳过
- 事件循环繁忙时,可能延迟执行
任务堆积风险低(只执行一次)高(可能产生多个待执行回调)
代码示例console.log('A');
setTimeout(() => { console.log('B'); }, 0);
console.log('C'); // 输出: A → C → B
let i = 0;
setInterval(
() => { console.log(i++);
if (i > 3) clearInterval(this); }, 100);
// 每 ~100ms 输出一次(不精确)
取消方法clearTimeout(timerId)clearInterval(intervalId)

✅ 计时器的最小延迟限制

环境最小延迟说明
浏览器(标准)4msHTML5 规范规定,setTimeoutsetInterval 的最小延迟为 4ms(即使设置为 0)
嵌套 setTimeout可能增加到 10ms+某些浏览器为防止脚本滥用,对嵌套计时器施加更长的最小延迟
非用户触发的定时器4ms ~ 数秒在页面后台标签页中,浏览器可能将最小延迟增加到数秒以节省资源
Node.js1msNode.js 环境下最小延迟为 1ms,但实际精度仍受系统调度影响

💡 示例:setTimeout(fn, 0) 实际延迟约为 4ms,不是”立即执行”。

✅ 计时器与微任务的交互(关键示例)

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

执行顺序分析:

步骤操作输出
1执行同步代码1
2遇到 setTimeout → 回调入宏任务队列
3遇到 Promise.then → 回调入微任务队列
4执行剩余同步代码4
5当前宏任务结束 → 执行所有微任务3
6微任务清空 → 取下一个宏任务(setTimeout)执行2

✅ 最终输出:1 → 4 → 3 → 2

🔍 结论:即使 setTimeout 延迟为 0,也晚于微任务执行,因为微任务在宏任务之间优先执行。

setInterval 的执行偏差问题

假设设置 setInterval(fn, 100),但 fn 执行耗时 80ms:

时间线事件
t=0ms注册 setInterval
t=100ms第一次回调入队 → 执行(耗时 80ms)→ 完成于 t=180ms
t=200ms第二次回调应入队,但主线程仍在执行(直到 t=180ms)→ 实际入队时间 ≈ t=180ms
t=180ms第二次回调入队 → 执行 → 完成于 t=260ms
t=300ms第三次回调入队 → 执行 → 完成于 t=340ms

📌 问题:实际执行间隔为 180ms、160ms… 而非固定的 100ms,且可能跳过某些执行。

✅ 更优的替代方案:递归 setTimeout

function repeat() {
  // 执行任务
  console.log('执行任务');

  // 任务完成后,再设置下一次
  setTimeout(repeat, 1000);
}

repeat(); // 每次任务完成后等待 1 秒,保证**固定间隔**

✅ 优点:

  • 避免任务堆积
  • 保证每次执行间隔固定(从上次完成开始计时)
  • 更可控,易于取消

✅ 最佳实践总结

建议说明
✅ 理解”最小延迟”而非”精确延迟”setTimeout(fn, 100) 表示”至少 100ms 后执行”
✅ 避免依赖计时器实现高精度动画使用 requestAnimationFrame
✅ 及时清理计时器防止内存泄漏,尤其在 SPA 组件卸载时
✅ 用 Promise + setTimeout 实现延迟async function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
await delay(1000);
✅ 在性能敏感场景避免高频 setInterval改用递归 setTimeout 或事件驱动

14.2.3 处理计算复杂度高的任务

🎯 问题背景

假设我们需要处理一个包含 100,000 个元素的数组,对每个元素进行复杂计算(如模拟耗时操作)。如果用同步循环处理:

// ❌ 同步处理:会完全阻塞主线程,页面卡死数秒
const data = new Array(100000).fill(1);
console.log('开始处理');
for (let i = 0; i < data.length; i++) {
  data[i] = expensiveOperation(data[i]);
}
console.log('处理完成');

⚠️ 用户会看到页面完全无响应,无法滚动、点击等。

✅ 解决方案:使用 setTimeout 拆分任务(时间分片)

我们将大任务拆分为多个小批次(如每批 1000 个),每处理完一批,用 setTimeout(fn, 0) 让出控制权,允许浏览器处理其他任务(如渲染、事件响应),然后再继续下一批。

✅ 示例:使用 setTimeout 实现任务分片

// 模拟复杂计算(耗时操作)
function expensiveOperation(value) {
  // 模拟 CPU 密集型任务
  let result = 0;
  for (let i = 0; i < 1000; i++) {
    result += Math.sqrt(value * i);
  }
  return result;
}

// 要处理的数据
const data = new Array(100000).fill(1);
let index = 0; // 当前处理到的索引
const CHUNK_SIZE = 1000; // 每批处理数量

// 进度显示元素(可选)
const progress = document.createElement('div');
progress.textContent = '处理进度:0%';
document.body.appendChild(progress);

// 核心:使用 setTimeout 实现异步分片处理
function processNextChunk() {
  const start = index;
  const end = Math.min(index + CHUNK_SIZE, data.length);

  // 处理当前批次
  for (let i = start; i < end; i++) {
    data[i] = expensiveOperation(data[i]);
  }

  // 更新进度
  index = end;
  const percent = ((index / data.length) * 100).toFixed(1);
  progress.textContent = `处理进度:${percent}%`;

  // 判断是否还有任务
  if (index < data.length) {
    // ✅ 使用 setTimeout 让出控制权,避免阻塞
    setTimeout(processNextChunk, 0);
  } else {
    console.log('✅ 所有任务处理完成!');
    progress.textContent += ' ✔️';
  }
}

// 开始处理
console.log('🚀 开始异步处理大任务...');
processNextChunk();

🔍 执行流程详解

阶段操作效果
1processNextChunk() 第一次调用处理前 1000 个元素
2批次处理完成更新进度条
3index < data.length 成立执行 setTimeout(processNextChunk, 0)
4当前宏任务结束事件循环继续,浏览器可进行 UI 渲染、响应用户输入
5下一轮事件循环processNextChunk 回调执行,处理下一批
6重复 1-5 步直到所有数据处理完成

✅ 效果对比

方案用户体验主线程阻塞适用场景
同步循环页面完全卡死,无法交互严重(数秒)小数据量(< 1000)
setTimeout 分片页面可滚动、可点击,进度可见极短(每批 < 16ms)大数据量、复杂计算

✅ 进阶优化:动态调整批次大小

根据设备性能动态调整 CHUNK_SIZE,避免低端设备仍出现卡顿:

let CHUNK_SIZE = 1000;
const TARGET_FRAME_TIME = 16; // 目标:每帧处理时间 < 16ms(60fps)

function processNextChunk() {
  const start = performance.now();

  const end = Math.min(index + CHUNK_SIZE, data.length);
  for (let i = index; i < end; i++) {
    data[i] = expensiveOperation(data[i]);
  }

  const duration = performance.now() - start;
  index = end;

  // 动态调整批次大小
  if (duration > TARGET_FRAME_TIME) {
    CHUNK_SIZE = Math.max(100, CHUNK_SIZE - 100); // 太慢则减小
  } else if (duration < TARGET_FRAME_TIME / 2) {
    CHUNK_SIZE += 100; // 太快则增大
  }

  // 更新 UI
  updateProgress();

  if (index < data.length) {
    setTimeout(processNextChunk, 0);
  }
}

✅ 更优替代方案(推荐)

方案说明优势
requestIdleCallback浏览器在空闲时期执行回调✅ 系统级优化,不占用关键渲染时间
Web Workers将任务放到后台线程执行✅ 完全不阻塞主线程,适合 CPU 密集型任务
requestAnimationFrame用于动画相关的分片✅ 与屏幕刷新同步

💡 示例:使用 Web Worker

// worker.js
self.onmessage = function(e) {
  const result = e.data.map(expensiveOperation);
  self.postMessage(result);
};
// main.js
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = (e) => {
  console.log('处理完成', e.data);
};

✅ 总结:使用计时器处理高复杂度任务的关键原则

原则说明
✅ 拆分任务将大任务分解为小批次(如每批 100~1000 项)
✅ 让出控制权每批后使用 setTimeout(fn, 0) 释放主线程
✅ 更新 UI在批次间更新进度条或状态,提升用户体验
✅ 避免无限递归使用条件判断确保任务最终完成
✅ 考虑更优方案对于极高复杂度任务,优先使用 Web Workers

通过 setTimeout 实现任务分片,是一种简单有效的时间分片(Time Slicing)技术,能显著提升单线程 JavaScript 处理复杂任务时的响应性和用户体验。

14.3 事件处理

14.3.1 事件处理

✅ 一、事件处理核心机制概览

组成部分说明
事件(Event)用户或浏览器触发的动作,如点击、按键、加载完成等。
事件目标(Event Target)触发事件的 DOM 元素(如按钮、输入框)。
事件监听器(Event Listener)响应事件的函数,也称”事件处理器”或”回调函数”。
事件流(Event Flow)事件在 DOM 树中传播的路径,包括:
1. 捕获阶段(Capture):从 window → 目标元素(由外向内)
2. 目标阶段(Target):到达目标元素
3. 冒泡阶段(Bubble):从目标元素 → document(由内向外)

✅ 二、事件处理的三种方式对比

方式语法示例优点缺点使用场景
HTML 内联事件处理<button onclick="handleClick()">点击</button>简单直观,快速原型❌ 逻辑与结构耦合
❌ 不推荐用于生产环境
❌ 不利于维护和测试
this 指向不明确
DOM 元素属性绑定btn.onclick = function() { /* 处理 */ };简单,兼容性好❌ 同一事件只能绑定一个处理函数
❌ 无法控制事件阶段(默认冒泡)
小型脚本或简单交互
addEventListener 方法btn.addEventListener('click', handler, options);✅ 可绑定多个监听器
✅ 支持捕获/冒泡阶段
✅ 可配置选项(如 once, passive
✅ 可移除(removeEventListener
稍复杂✅ 推荐用于所有现代开发

✅ 三、addEventListener 详细参数说明

参数类型说明常用值/示例
typestring事件类型'click', 'keydown', 'load', 'submit'
listenerfunction事件处理函数,接收 Event 对象function handleClick(e) { console.log(e.target); }
optionsObjectboolean配置选项(推荐使用对象){ capture, once, passive, signal }

options 详细配置:

选项类型说明示例
capturebooleantrue 表示在捕获阶段执行监听器
false(默认)表示在冒泡阶段执行
addEventListener('click', fn, { capture: true })
oncebooleantrue 表示监听器只执行一次,之后自动移除addEventListener('load', init, { once: true })
passivebooleantrue 表示监听器不会调用 preventDefault(),提升滚动性能addEventListener('touchstart', handle, { passive: true })
signalAbortSignal通过 AbortController 控制监听器的移除见下方示例

✅ 四、事件对象(Event Object)常用属性与方法

属性/方法说明典型用途
e.target触发事件的原始元素(可能不是监听器绑定的元素)区分事件委托中的具体目标
e.currentTarget当前绑定监听器的元素(即 this确认监听器作用域
e.type事件类型(如 'click'动态判断事件类型
e.preventDefault()阻止事件的默认行为(如表单提交、链接跳转)表单验证、自定义右键菜单
e.stopPropagation()阻止事件继续传播(停止冒泡或捕获)防止事件冒泡到父元素
e.stopImmediatePropagation()阻止事件传播,且阻止同一元素上的其他监听器执行精确控制事件流
e.bubbles事件是否支持冒泡(如 click 是,focus 否)判断事件传播能力
e.isTrusted是否为用户真实操作触发(true)或脚本触发(false安全验证

✅ 五、事件处理流程(以点击事件为例)

步骤操作说明
1用户点击按钮事件被触发
2事件从 window 开始捕获如果有捕获阶段监听器,按顺序执行
3到达目标元素(按钮)执行目标阶段的监听器
4事件开始冒泡向父元素、祖先元素传播
5父元素监听器执行可通过 e.stopPropagation() 阻止继续冒泡
6到达 document冒泡结束
7浏览器执行默认行为(如跳转)除非被 preventDefault() 阻止

✅ 六、高级技巧与最佳实践

技巧说明代码示例
事件委托(Event Delegation)将事件监听器绑定到父元素,利用冒泡机制处理子元素事件,减少监听器数量document.querySelector('#list').addEventListener('click', (e) => { if (e.target.tagName === 'LI') { console.log('点击了列表项:', e.target.textContent); } });
使用 AbortController 移除监听器更优雅地清理事件监听器,尤其在组件销毁时const controller = new AbortController();
btn.addEventListener('click', handler, { signal: controller.signal });
// 需要时移除
controller.abort(); // 自动移除所有关联监听器
防抖(Debounce)与节流(Throttle)控制高频事件(如 resize, scroll)的执行频率window.addEventListener('resize', debounce(handleResize, 100));
使用 once 选项确保初始化逻辑只执行一次btn.addEventListener('click', init, { once: true });
使用 passive 提升性能尤其在移动端滚动事件中elem.addEventListener('touchstart', handle, { passive: true });

✅ 七、常见问题与解决方案

问题原因解决方案
事件未触发选择器错误、元素未加载使用 DOMContentLoaded 或检查元素是否存在
事件监听器未移除导致内存泄漏使用 removeEventListenerAbortController
事件冒泡导致意外行为父元素也有监听器使用 e.stopPropagation()
默认行为未阻止如表单提交刷新页面使用 e.preventDefault()
this 指向错误使用普通函数时 this 指向监听元素使用箭头函数或 .bind()

✅ 总结:事件处理关键原则

原则说明
✅ 优先使用 addEventListener支持多监听器、灵活配置
✅ 合理使用事件委托减少内存占用,提升性能
✅ 及时清理监听器防止内存泄漏(SPA 中尤其重要)
✅ 控制高频事件频率使用防抖/节流优化 scrollresize
✅ 理解事件流机制正确使用捕获、冒泡、阻止传播

14.3.2 通过 DOM 代理事件

✅ 一、事件代理

事件代理(Event Delegation)是一种利用事件冒泡(Event Bubbling)机制的编程模式:不为每个子元素单独绑定事件监听器,而是将监听器绑定到父元素上,通过检查 event.target 来判断具体是哪个子元素触发了事件,并执行相应逻辑。

✅ 二、事件代理的核心原理

机制说明
事件冒泡大多数 DOM 事件(如 clickkeydown)会从触发元素(目标)向上传播到根节点(document)。
event.target指向实际触发事件的元素(可能是子元素)。
event.currentTarget指向当前绑定监听器的元素(即父元素)。
委托逻辑父元素监听事件 → 检查 e.target 是否匹配预期子元素 → 执行对应操作。

✅ 三、实现事件代理的步骤

步骤操作说明
1选择合适的父元素选择一个稳定的、包含所有目标子元素的共同祖先(如 <ul><table><div class="container">)。
2绑定事件监听器到父元素使用 addEventListener 将事件绑定到父元素。
3在回调中获取 event.target通过 e.target 确定用户实际点击的元素。
4判断目标元素是否匹配使用 classList.contains()tagNamedatasetmatches() 等方法判断。
5执行对应逻辑根据匹配结果执行删除、编辑、跳转等操作。
6(可选)阻止默认行为或冒泡使用 e.preventDefault()e.stopPropagation() 控制行为。

✅ 四、完整代码示例:动态待办事项列表

HTML 结构:

<div id="app">
  <h2>待办事项</h2>
  <input type="text" id="new-task" placeholder="输入任务">
  <button id="add-btn">添加</button>

  <ul id="task-list">
    <li data-id="1">
      <span>学习 JavaScript</span>
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
    </li>
    <li data-id="2">
      <span>掌握事件代理</span>
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
    </li>
  </ul>
</div>

JavaScript 实现(事件代理):

// 获取父元素
const taskList = document.getElementById('task-list');
const newTaskInput = document.getElementById('new-task');
const addBtn = document.getElementById('add-btn');

// ✅ 步骤1:为父元素 ul 绑定 click 事件
taskList.addEventListener('click', function(e) {
  // ✅ 步骤2:获取实际点击的元素
  const target = e.target;

  // ✅ 步骤3:判断点击的是哪个类型的按钮
  if (target.classList.contains('delete')) {
    // 删除任务
    const li = target.parentElement;
    const taskId = li.dataset.id;
    li.remove();
    console.log(`任务 ${taskId} 已删除`);
  }

  if (target.classList.contains('edit')) {
    // 编辑任务
    const li = target.parentElement;
    const span = li.querySelector('span');
    const newText = prompt('编辑任务:', span.textContent);
    if (newText) {
      span.textContent = newText;
    }
  }
});

// 添加新任务(无需绑定事件!)
addBtn.addEventListener('click', () => {
  const text = newTaskInput.value.trim();
  if (text) {
    const li = document.createElement('li');
    li.dataset.id = Date.now(); // 模拟唯一 ID
    li.innerHTML = `
      <span>${text}</span>
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
    `;
    taskList.appendChild(li);
    newTaskInput.value = '';
    // ✅ 新按钮自动被代理处理!
  }
});

✅ 五、识别目标元素的常用方法

方法示例用途
element.classList.contains('class')e.target.classList.contains('delete')判断是否有特定类名
element.tagNamee.target.tagName === 'BUTTON'判断标签类型(注意:返回大写)
element.matches(selector)e.target.matches('.delete, .edit')判断是否匹配 CSS 选择器(推荐)
element.dataset.keyli.dataset.id获取自定义数据属性
element.getAttribute('name')e.target.getAttribute('data-action')获取任意属性值

💡 推荐使用 matches(),更灵活:

if (e.target.matches('button.delete')) {
  // 处理删除
}

✅ 六、支持的事件类型

并非所有事件都冒泡,适合事件代理的常见事件:

事件类型是否冒泡代理是否适用替代方案(如不冒泡)
click✅ 是✅ 推荐
mousedown / mouseup✅ 是✅ 推荐
keydown / keyup✅ 是✅ 推荐
input / change✅ 是✅ 推荐
focus / blur❌ 否❌ 不适用使用 focusin / focusout(会冒泡)
mouseenter / mouseleave❌ 否❌ 不适用使用 mouseover / mouseout(会冒泡)

✅ 七、高级技巧与最佳实践

技巧说明示例
使用 closest() 定位祖先当点击的是子元素(如 <span> 内的文字),用 closest() 找到最近的匹配父元素if (e.target.closest('.delete')) { /* 处理 */ }
批量处理多种操作使用 data-action 属性统一管理行为<button data-action="delete">删</button>
<button data-action="edit">编</button>
const action = e.target.dataset.action;
避免过度代理不要将所有事件都绑定到 document,选择最近的稳定容器✅ 推荐:<ul>
❌ 避免:document(除非全局事件)
清理监听器在单页应用中,组件销毁时应移除监听器parent.removeEventListener('click', handler);
结合防抖/节流对高频事件(如 scroll)使用节流window.addEventListener('scroll', throttle(handleScroll, 100));

✅ 八、常见应用场景

场景说明
动态列表/表格添加、删除、编辑行项目(如待办事项、商品列表)
导航菜单处理多个菜单项的点击跳转
模态框/对话框点击遮罩层关闭对话框
无限滚动/分页动态加载内容后无需重新绑定事件
表单控件统一处理多个输入框的 inputchange 事件

✅ 九、事件代理 vs 直接绑定对比

特性事件代理直接绑定
监听器数量1 个(父元素)N 个(每个子元素)
内存占用高(N 大时明显)
动态元素支持✅ 自动支持❌ 需手动重新绑定
代码维护性高(逻辑集中)低(分散)
性能✅ 更优(尤其 N 大时)差(绑定/解绑开销大)
适用场景动态、大量子元素静态、少量元素

✅ 总结

DOM 事件代理是前端开发中一项重要的性能优化和代码组织技巧:

  • ✅ 核心思想:以一敌百,用一个监听器管理多个子元素。
  • ✅ 依赖机制:事件冒泡 + event.target
  • ✅ 关键优势:支持动态元素、节省内存、提升性能。
  • ✅ 推荐使用:在处理列表、表格、菜单、动态内容时优先采用。

14.3.3 自定义事件

✅ 一、什么是自定义事件?

自定义事件是开发者可以自行定义名称和行为的事件,不依赖于用户操作(如点击、输入),而是用于在代码不同部分之间进行解耦通信。

🎯 用途:组件间通信、状态变更通知、模块化开发、MVVM 框架实现等。

✅ 二、创建和触发自定义事件的步骤

步骤操作方法
1创建自定义事件使用 new CustomEvent() 构造函数
2触发事件使用 element.dispatchEvent(event)
3监听事件使用 element.addEventListener('事件名称', 回调)

✅ 三、CustomEvent 构造函数语法

new CustomEvent(type, options)
参数说明
type事件名称(字符串),如 'data-loaded''user-login'
options可选对象,包含:
- detail: 传递给事件的数据(任意类型)
- bubbles: 是否冒泡(默认 false
- cancelable: 是否可被取消(默认 false

✅ 四、完整示例:模拟用户登录通知

场景:

  • 用户登录成功后,触发一个 'user-login' 自定义事件。
  • 其他模块(如导航栏、消息中心)监听该事件并更新 UI。

HTML:

<div id="app">
  <button id="login-btn">登录</button>
  <div id="status">当前状态:未登录</div>
  <div id="greeting"></div>
</div>

JavaScript:

const loginBtn = document.getElementById('login-btn');
const status = document.getElementById('status');
const greeting = document.getElementById('greeting');

// ✅ 1. 定义事件监听器(可以分布在不同模块)
// 模块1:更新状态栏
document.addEventListener('user-login', function(e) {
  status.textContent = '当前状态:已登录';
  status.style.color = 'green';
});

// 模块2:显示欢迎语
document.addEventListener('user-login', function(e) {
  const userData = e.detail; // 获取传递的数据
  greeting.textContent = `欢迎回来,${userData.name}!`;
});

// 模块3:记录日志(可阻止默认行为)
document.addEventListener('user-login', function(e) {
  console.log('用户登录:', e.detail);
  // 可选择阻止后续逻辑
  // e.preventDefault(); // 需设置 cancelable: true
});

// ✅ 2. 触发自定义事件(例如登录成功后)
loginBtn.addEventListener('click', () => {
  // 模拟登录成功
  const userData = {
    id: 123,
    name: 'Alice',
    role: 'admin',
    loginTime: new Date()
  };

  // 创建自定义事件,携带用户数据
  const loginEvent = new CustomEvent('user-login', {
    detail: userData,        // 传递数据
    bubbles: true,           // 允许冒泡
    cancelable: true         // 允许被 preventDefault()
  });

  // 触发事件
  document.dispatchEvent(loginEvent);

  console.log('✅ "user-login" 事件已触发');
});

✅ 五、事件对象中的 detail 属性

event.detail 是自定义事件的核心,用于传递任意数据:

// 触发时
const event = new CustomEvent('data-updated', {
  detail: {
    items: [1, 2, 3],
    timestamp: Date.now(),
    source: 'api'
  }
});
element.dispatchEvent(event);

// 监听时
element.addEventListener('data-updated', (e) => {
  console.log(e.detail.items);     // [1, 2, 3]
  console.log(e.detail.timestamp); // 时间戳
});

detail 可以是:对象、数组、字符串、数字等。

✅ 六、冒泡与捕获(bubbles 选项)

bubbles: truebubbles: false(默认)
事件会从触发元素向上冒泡到 document事件仅在目标元素触发,不传播
父元素也能监听到该事件只有直接绑定的元素能收到
// 允许冒泡,父元素可监听
const event = new CustomEvent('child-action', {
  bubbles: true,
  detail: { action: 'click' }
});
childElement.dispatchEvent(event);

// 父元素也能监听
parentElement.addEventListener('child-action', (e) => {
  console.log('子元素触发了:', e.detail.action);
});

✅ 七、阻止事件(cancelable 选项)

如果设置 cancelable: true,监听器可以调用 e.preventDefault() 来阻止后续逻辑。

const riskyEvent = new CustomEvent('before-delete', {
  detail: { id: 123 },
  cancelable: true
});

document.dispatchEvent(riskyEvent);

// 在监听器中判断是否被阻止
if (!riskyEvent.defaultPrevented) {
  console.log('执行删除操作...');
} else {
  console.log('删除被取消');
}

监听器中:

document.addEventListener('before-delete', (e) => {
  if (!confirm('确定要删除吗?')) {
    e.preventDefault(); // 阻止删除
  }
});

✅ 八、使用 Event 构造函数(无数据传递)

如果不需要传递数据,可以直接使用 Event

const simpleEvent = new Event('loading-start');
element.dispatchEvent(simpleEvent);

element.addEventListener('loading-start', () => {
  console.log('加载开始...');
});

✅ 九、自定义事件的典型应用场景

场景说明
组件通信在 Web Components 或模块间传递消息
状态管理模拟简单的发布-订阅模式(Pub/Sub)
插件系统插件触发钩子事件(如 'plugin-ready'
表单验证验证通过后触发 'form-valid' 事件
动画控制动画开始/结束时通知其他模块

✅ 十、最佳实践

建议说明
✅ 使用语义化事件名'user-login''data-saved',避免模糊命名
✅ 用 detail 传递数据不要修改事件对象其他属性
✅ 合理设置 bubbles需要父元素响应时设为 true
✅ 清理监听器(SPA)使用 removeEventListener 避免内存泄漏
✅ 避免过度使用简单场景可直接调用函数,复杂通信再用事件

✅ 总结

自定义事件是 JavaScript 中实现松耦合、可扩展代码的重要工具:

  • ✅ 使用 new CustomEvent() 创建事件
  • ✅ 使用 dispatchEvent() 触发事件
  • ✅ 使用 addEventListener() 监听事件
  • ✅ 通过 detail 传递数据,bubbles 控制传播,cancelable 支持阻止

15. 跨浏览器开发

15.1 注意事项

✅ 跨浏览器开发注意事项总览

编号类别问题描述受影响浏览器解决方案/最佳实践示例/工具
1DOM API 兼容性某些现代 DOM 方法在旧浏览器中不存在IE、旧版 Safari/Edge使用特性检测 + Polyfillif (element.classList) { element.classList.add('active'); } else { element.className += ' active'; }
2事件处理差异addEventListener vs attachEvent,事件对象属性不同IE8 及以下特性检测,封装跨浏览器事件绑定function addEvent(el, type, handler) { if (el.addEventListener) { ... } else if (el.attachEvent) { ... } }
3CSS 兼容性缺少现代 CSS 特性或需要厂商前缀IE、旧版 Android 浏览器使用 Autoprefixer,提供降级样式display: -webkit-flex; /* Safari */ display: flex;
4JavaScript 语法支持ES6+ 语法(箭头函数、letconstPromise)不被支持IE、旧版浏览器使用 Babel 编译,引入 PolyfillBabel + @babel/preset-env
5HTML5 元素支持<article>, <section>, <video> 等不被识别IE8 及以下使用 html5shiv 或条件注释<!--[if lt IE 9]><script src="html5shiv.js"></script><![endif]-->
6盒模型差异IE 的怪异模式使用非标准盒模型IE6/7使用 box-sizing: border-box 重置*, *::before, *::after { box-sizing: border-box; }
7CSS Flexbox/Grid 支持旧浏览器对 Flexbox/Grid 支持不完整IE11 及以下、旧版 Safari使用 Autoprefixer,提供 floatinline-block 降级布局Autoprefixer + 回退布局
8Fetch API 支持fetch() 不被 IE 支持IE 所有版本使用 axios 或 whatwg-fetch Polyfillimport 'whatwg-fetch'; fetch('/api/data').then(...);
9Promise 支持原生 Promise 不可用IE、旧版 Safari使用 Polyfill(如 es6-promise<script src="es6-promise.auto.min.js"></script>
10Storage APIlocalStoragesessionStorage 可能被禁用或不支持旧版浏览器、隐私模式检测支持性,提供备用存储方案try { localStorage.setItem('test', '1'); } catch(e) { /* 使用 cookie 或内存存储 */ }
11移动端点击延迟移动浏览器有 300ms 点击延迟旧版 iOS/Android使用 touchstart 事件或引入 fastclicknpm install fastclick
12字体与图标加载自定义字体格式兼容性差旧版 IE提供多种字体格式(.woff2, .woff, .ttf, .eot@font-face { src: url('font.eot'); src: url('font.woff2') format('woff2'), url('font.woff') format('woff'); }
13条件注释(IE专属)仅 IE 支持条件注释IE10 及以下用于加载 IE 专用 CSS/JS<!--[if IE 8]><link rel="stylesheet" href="ie8.css"><![endif]-->
14用户代理检测navigator.userAgent 被修改或不可靠所有浏览器❌ 避免 UA 检测,✅ 推荐使用特性检测使用 Modernizr 进行特性检测
15跨域请求(CORS)旧浏览器对 CORS 支持有限IE10 以下使用 JSONP(不推荐)或服务端代理服务端中转请求
16动画与性能requestAnimationFrame 不支持旧版浏览器使用 Polyfill 或 setTimeout 回退const raf = window.requestAnimationFrame || ...
17表单控件样式各浏览器默认样式差异大所有浏览器使用 CSS Reset 或 Normalize.css<link rel="stylesheet" href="normalize.min.css">
18安全特性支持Content-Security-Policy, HTTPS 强制等旧浏览器服务端配置 + 渐进增强使用 HTTPS,设置 CSP 头
19Web API 支持IntersectionObserver, ResizeObserver 等新 API旧版浏览器使用 Polyfillimport 'intersection-observer';
20构建工具配置代码需适配多浏览器所有项目配置 browserslist"browserslist": ["> 1%", "not dead", "not ie <= 11"]

✅ 跨浏览器开发最佳实践总结

实践说明
✅ 特性检测 > 浏览器检测检查功能是否存在,而非判断浏览器类型
✅ 渐进增强(Progressive Enhancement)核心功能在所有浏览器可用,高级功能在现代浏览器中增强
✅ 使用 Polyfill为缺失的 API 提供模拟实现
✅ 自动化工具使用 Babel、Autoprefixer、PostCSS 等自动处理兼容性
✅ 真实设备测试在 IE、Safari、移动端等真实环境中测试
✅ 使用 Modernizr检测 HTML5/CSS3 特性支持情况
✅ 关注 Can I Use查询 API 的浏览器支持率(caniuse.com)

15.2 开发问题

15.2.1 浏览器 Bug 和差异

✅ 浏览器 Bug 与差异对照表

编号类别问题描述受影响浏览器具体表现/差异解决方案/规避方法
1盒模型(Box Model)IE6/7 在怪异模式下使用非标准盒模型IE6, IE7width 包含 paddingborder,导致布局错乱使用 box-sizing: border-box 统一模型;避免怪异模式(使用标准 DOCTYPE)
2display: inline-block旧版浏览器不支持或行为异常IE6, IE7, 旧版 Firefox元素不换行但无法设置宽高,或出现额外间距IE6/7:使用 display: inline; zoom: 1; 模拟;其他:使用 flex 或 grid 布局替代
3min-height / max-heightIE6 不支持 min-heightIE6min-height 被忽略,元素无法保持最小高度使用 height + !importantmin-height: 100px; height: auto !important; height: 100px;
4PNG 透明度支持IE6 不支持 PNG-24 透明通道IE6PNG 图片背景显示为灰色或背景色使用 PNG8 格式或滤镜
5position: fixed旧版 iOS Safari 和 IE 不支持iOS < 5, IE6-IE8元素无法固定在视口,随页面滚动使用 position: absolute + JavaScript 模拟,或使用 Polyfill(如 fixed-sticky)
6floatmargin 双倍边距 BugIE6 的经典 BugIE6浮动元素的 margin 在特定方向上被加倍使用 display: inline; 修复
7z-index 层叠上下文问题各浏览器对层叠上下文处理不一致所有浏览器(尤其 IE)z-index 不生效,元素层级混乱确保父元素未创建独立层叠上下文;避免过度使用 z-index
8:focus 样式支持Safari 不支持非表单元素的 :focusSafari(旧版)<div tabindex="0"> 无法通过 :focus 设置样式使用 JavaScript 模拟焦点状态
9text-overflow: ellipsis需要特定条件才能生效所有浏览器在某些布局下省略号不显示必须同时设置:white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
10vertical-align 对齐差异不同浏览器默认对齐方式不同Chrome, Firefox, Safari, IE图片或内联元素垂直对齐不一致显式设置 vertical-align: middle; 或使用 flex 布局替代
11line-height 继承问题字体缩放时行高计算差异所有浏览器行高在嵌套元素中表现不一致使用无单位数值(如 1.5)而非像素值
12input 元素默认样式各浏览器默认边框、内边距不同Chrome, Safari, Firefox, IE输入框外观不一致使用 CSS Reset 或 Normalize.css
13placeholder 样式控制伪元素选择器不统一Chrome, Firefox, Safari无法统一设置占位符颜色、字体使用浏览器前缀:::-webkit-input-placeholder::-moz-placeholder:-ms-input-placeholder
14CSS3 动画性能软件渲染 vs 硬件加速旧版 Android, 低端设备动画卡顿、掉帧使用 transformopacity 触发 GPU 加速:transform: translateZ(0);
15Flexbox 布局差异IE10-11 对 Flexbox 支持不完整IE10, IE11flex: 1 行为异常,align-items 不生效使用 Autoprefixer 添加前缀;测试并提供 float 或 inline-block 回退方案
16Event 对象属性event.target vs event.srcElementIE8 及以下e.target 不存在,需用 e.srcElement封装事件处理函数
17addEventListener 支持IE8 及以下使用 attachEventIE8 及以下addEventListener 报错封装跨浏览器事件绑定
18localStorage 支持隐私模式下可能禁用Safari(隐私模式)localStorage 抛出异常使用 try-catch 包裹
19CSS 渐变语法旧版浏览器需要厂商前缀Safari < 6, Android < 4.4渐变不显示或显示为纯色使用 Autoprefixer 自动生成前缀
20HTML5 语义标签IE8 及以下不识别IE8 及以下<article>, <section> 无法设置样式使用 html5shiv 或动态创建元素

15.2.2 浏览器 Bug 修复

✅ 浏览器 Bug 修复方法对照表

编号Bug 类型问题描述受影响浏览器修复方法示例代码/说明
1IE6 双倍 margin Bug浮动元素的 margin 在浮动方向上被加倍IE6使用 display: inline; 修复.float-left { float: left; margin-left: 10px; display: inline; } — 原理:触发 IE6 的 “hasLayout” 机制
2IE6 不支持 min-heightmin-height 被忽略IE6使用 height + !important 模拟.box { min-height: 100px; height: auto !important; height: 100px; }
3IE6 PNG 透明背景变灰PNG-24 透明通道不支持IE6使用滤镜或降级为 PNG8filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='image.png');
4position: fixed 不支持元素无法固定在视口iOS < 5, IE6-IE8使用 position: absolute + JS 模拟window.onscroll = function() { elem.style.top = window.pageYOffset + 'px'; }; 或使用 Polyfill
5inline-block 不兼容旧版浏览器不支持该属性IE6, IE7使用 display: inline; zoom: 1; 模拟.inline-block { display: inline-block; zoom: 1; *display: inline; }
6:focus 样式不生效非表单元素无法获得焦点样式Safari(旧版)添加 tabindex 属性<div tabindex="0" class="focusable">可聚焦元素</div>
7text-overflow: ellipsis 不显示省略号未生效所有浏览器(条件不满足)补全必需 CSS 属性.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } — 三者缺一不可
8z-index 层级失效子元素层级被父元素限制所有浏览器(层叠上下文问题)避免父元素创建独立层叠上下文修复:移除父元素的 z-index 或调整结构
9placeholder 颜色无法修改占位符文字颜色默认为浅灰Chrome, Firefox, Safari使用浏览器前缀伪元素::-webkit-input-placeholder { color: #999; } :-moz-placeholder { color: #999; opacity: 1; } :-ms-input-placeholder { color: #999; }
10addEventListener 不支持IE8 及以下使用旧 APIIE8 及以下封装跨浏览器事件绑定函数function addEvent(el, type, handler) { if (el.addEventListener) { ... } else if (el.attachEvent) { ... } }
11event.target 不存在IE8 及以下使用 srcElementIE8 及以下使用兼容性判断function getTarget(e) { return e.target || e.srcElement; }
12localStorage 抛出异常Safari 隐私模式禁用Safari(隐私模式)使用 try-catch 包裹try { localStorage.setItem('test', '1'); } catch(e) { console.warn('localStorage 不可用'); }
13Flexbox 布局错乱IE10-11 对 Flexbox 支持不完整IE10, IE11添加厂商前缀,测试回退方案使用 Autoprefixer:display: -ms-flexbox; /* IE10 */ display: flex;
14CSS3 渐变不显示旧版浏览器需前缀Safari < 6, Android < 4.4使用 Autoprefixer 或手动添加.gradient { background: -webkit-linear-gradient(top, #fff, #000); background: linear-gradient(to bottom, #fff, #000); }
15HTML5 标签无法设置样式IE8 及以下不识别语义标签IE8 及以下使用 html5shiv 或动态创建<!--[if lt IE 9]><script src="html5shiv.js"></script><![endif]-->
16line-height 继承异常字体缩放时行高计算错误所有浏览器使用无单位数值body { line-height: 1.5; } — 推荐使用无单位值
17input 默认样式不一致各浏览器边框、内边距不同所有浏览器使用 CSS Reset 或 Normalize.cssinput, textarea, button { margin: 0; padding: 0; border: none; font: inherit; }
18vertical-align 对齐错位图片或内联元素垂直对齐不一致所有浏览器显式设置对齐方式img, .inline-block { vertical-align: middle; } 或使用 flex 布局替代
19动画卡顿(无硬件加速)软件渲染导致性能差旧版 Android, 低端设备使用 transform 触发 GPU 加速.animate { transform: translateZ(0); /* 或 translate3d(0,0,0) */ }
20box-sizing 不支持IE7 及以下不支持IE7 及以下使用条件注释或避免使用降级方案:用 padding 模拟 border;推荐使用 Modernizr 检测并加载 polyfill

16. 补环境

16.1 Node.js 环境下执行浏览器中的 JS 代码

🔥 Node.js 本身无法原生提供完整的浏览器环境,因为它是服务端运行时,缺少 DOM 渲染引擎、事件循环机制、安全沙箱、图形系统等。

可以使用 Playwright 控制真实浏览器,并在其上下文中执行 JS 代码。

使用 Playwright 的原因:

  • 支持 Chromium、Firefox、WebKit(多浏览器兼容测试)
  • API 更现代、稳定、性能更好
  • 自动等待、设备模拟、网络拦截更强大
  • 由 Microsoft 开发,持续维护

🔧 步骤 1:安装 Playwright

npm init -y
npm install playwright

📁 步骤 2:准备一段”浏览器中的 JS 代码”

// browser-code.js
function simulateUserAction() {
  // 使用真实浏览器 API
  console.log('User Agent:', navigator.userAgent);
  console.log('支持 fetch:', typeof fetch !== 'undefined');
  console.log('页面可见性:', document.visibilityState);

  // DOM 操作
  const div = document.createElement('div');
  div.id = 'injected-content';
  div.textContent = `Injected at: ${new Date().toISOString()}`;
  document.body.appendChild(div);

  // 存储数据
  localStorage.setItem('test-key', 'test-value');

  // 使用现代 API
  if (window.IntersectionObserver) {
    new IntersectionObserver(entries => {
      entries.forEach(entry => {
        console.log('元素交叉状态:', entry.isIntersecting);
      });
    }).observe(div);
  }

  // 返回当前页面状态
  return {
    url: location.href,
    title: document.title,
    localStorage: { ...localStorage },
    html: document.documentElement.outerHTML
  };
}

// 执行并返回结果
simulateUserAction();

🚀 步骤 3:在 Node.js 中使用 Playwright 执行该代码

// run-in-real-browser.js
const { chromium } = require('playwright');
const fs = require('fs');

(async () => {
  // 1. 启动真实浏览器(headless: true 表示无界面)
  const browser = await chromium.launch({
    headless: false, // 设为 false 可看到浏览器窗口(调试用)
    timeout: 30000
  });

  const context = await browser.newContext({
    // 模拟真实用户环境
    viewport: { width: 1280, height: 720 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    javaScriptEnabled: true
  });

  const page = await context.newPage();

  // 2. 设置初始页面内容(模拟你的网页)
  await page.setContent(`
    <!DOCTYPE html>
    <html lang="zh">
      <head><title>Test Page</title></head>
      <body><h1>原始内容</h1></body>
    </html>
  `);

  // 3. 读取浏览器 JS 代码
  const browserCode = fs.readFileSync('./browser-code.js', 'utf8');

  // 4. 在真实浏览器上下文中执行代码
  const result = await page.evaluate(`(async () => {
    ${browserCode}
  })()`); // 使用 async 包裹,支持 await

  // 5. 输出执行结果(来自真实浏览器)
  console.log('✅ 执行成功!结果来自真实浏览器环境:');
  console.log('URL:', result.url);
  console.log('Title:', result.title);
  console.log('LocalStorage:', result.localStorage);
  console.log('\n最终 HTML:');
  console.log(result.html);

  // 6. 截图验证(可选)
  await page.screenshot({ path: 'final-state.png' });

  // 7. 关闭浏览器
  await browser.close();
})();

✅ 输出示例

✅ 执行成功!结果来自真实浏览器环境:
URL: http://localhost/
Title: Test Page
LocalStorage: { 'test-key': 'test-value' }

最终 HTML:
<!DOCTYPE html>
<html lang="zh">
  <head><title>Test Page</title></head>
  <body>
    <h1>原始内容</h1>
    <div id="injected-content">Injected at: 2025-09-09T07:30:00.000Z</div>
  </body>
</html>

✅ 补环境涉及内容

特性是否真实复现说明
window✅ 是原生 Chromium 的 window 对象
document✅ 是完整 DOM,支持所有方法和属性
navigator✅ 是包含 userAgentplatformgeolocation
location / history✅ 是支持路由操作
localStorage / sessionStorage✅ 是持久化存储可用
fetch / WebSocket✅ 是网络请求正常工作
Canvas / SVG✅ 是图形渲染支持
IntersectionObserver✅ 是现代 API 完整支持
MutationObserver✅ 是DOM 变更监听
requestAnimationFrame✅ 是动画帧支持
CSS 渲染与布局✅ 是真实重排重绘
事件系统✅ 是支持 clickscrollkeydown
同源策略 / CORS✅ 是安全机制完整

✅ 替代方案对比

方案是否真实环境适用场景
Playwright / Puppeteer✅ 是(推荐)自动化测试、爬虫、SSR、真实行为模拟
jsdom + 手动补全❌ 否(仅模拟)单元测试、轻量 DOM 操作、SSR 预渲染
node-webcompat(实验性)⚠️ 部分尝试模拟更多 API,仍不完整
Electron 渲染进程✅ 是桌面应用内嵌,但太重

✅ 高级技巧:注入代码前准备环境

如果你的浏览器代码依赖某些全局变量,可以在 page.evaluate 前注入:

// 注入 polyfill 或全局变量
await page.addInitScript(() => {
  // 模拟 window.config
  window.APP_CONFIG = { env: 'production', debug: false };

  // 拦截 alert
  window.alert = msg => console.log('[ALERT]', msg);
});

// 然后执行你的业务代码
const result = await page.evaluate(yourBrowserCode);

✅ 总结:如何在 Node.js 中复现浏览器 JS 执行?

目标推荐方案
尽可能真实复现线上环境✅ 使用 Playwright / Puppeteer 控制真实浏览器
获取 windowdocument通过 page.evaluate() 在浏览器上下文中访问
执行浏览器 JS 代码使用 page.evaluate(code)
获取执行结果函数返回值会自动序列化回 Node.js
支持所有 Web API是,因为是真实浏览器
性能与资源较高,适合任务型使用,不适合高频调用

16.2 浏览器环境下执行 Node.js 中的 JS 代码

Chrome DevTools 的 “代码段(Snippets)” 功能允许你保存和执行 JavaScript 代码片段。但它是在当前页面的浏览器上下文中运行的。

✅ 注意事项与限制(在浏览器 Snippets 中运行 Node.js 风格代码)

编号注意项说明解决方案 / 替代方案
1无法使用 Node.js 内置模块fspathoschild_process 等模块在浏览器中不存在使用浏览器 API 替代(如 fetch 代替 http);手动实现轻量逻辑(如路径拼接);不能读写本地文件
2require 不存在浏览器不支持 CommonJS 的 require()使用 ES6 import(仅限模块);将依赖打包(如用 Webpack);手动定义依赖对象
3__dirname__filename 无效这些是 Node.js 特有的全局变量删除或替换为字符串常量
4process 对象缺失process.envprocess.cwd() 等无法使用模拟:const process = { env: { NODE_ENV: 'development' } };
5模块系统差异Node.js 使用 CommonJS,浏览器原生支持 ES Modules使用打包工具(Webpack/Vite)统一模块格式
6全局对象不同Node.js 有 global,浏览器有 window使用 typeof window !== 'undefined' 判断环境
7文件路径 vs URLNode.js 用文件路径,浏览器用 URL避免硬编码路径,使用相对 URL 或配置
8异步行为差异Node.js 事件循环包含 I/O,浏览器包含渲染通常不影响简单逻辑,但复杂调度需注意

✅ 示例:在 Snippets 中”模拟”Node.js 代码

假设有一段 Node.js 代码:

// node-code.js(Node.js 环境)
const fs = require('fs');
const path = require('path');

function greet(name) {
  console.log(`Hello, ${name}! Current dir: ${process.cwd()}`);
  return `Welcome, ${name}`;
}

greet('Alice');

❌ 在浏览器 Snippets 中直接运行会报错:

// ❌ 错误:浏览器中执行
const fs = require('fs'); // Uncaught ReferenceError: require is not defined

✅ 正确做法:剥离 Node.js 特有逻辑

// ✅ 在 Snippets 中可运行的版本
// 模拟 process
const process = { env: { NODE_ENV: 'browser' }, cwd: () => '/' };

// 模拟 path(简化版)
const path = {
  join: (...args) => args.join('/').replace(/\/+/g, '/')
};

function greet(name) {
  console.log(`Hello, ${name}! Running in browser.`);
  return `Welcome, ${name}`;
}

// 执行
greet('Alice');

✅ 更高级:使用打包工具预处理

如果在浏览器中运行复杂的 Node.js 逻辑:

方案:使用 Webpack / Vite / Browserify

步骤 1:将 Node.js 代码打包为浏览器可用的版本

npx browserify node-code.js -o browser-bundle.js

步骤 2:在 HTML 页面中引入:

<script src="browser-bundle.js"></script>

步骤 3:再在 Snippets 中调用暴露的函数

✅ 开发者工具使用技巧

技巧说明
使用 Snippets 调试通用逻辑如算法、数据处理、字符串操作等与环境无关的代码
模拟 Node.js 环境变量在 Snippets 开头定义 processglobal
结合 console.log 调试观察变量、执行流程
保存为 Snippet 便于复用适合调试工具函数
不要依赖系统 API如文件读写、网络请求(除非用 fetch