语言特性
1. JavaScript 简介
1.1 核心语言特性
| 名称 | 说明 | 代码示例 |
|---|---|---|
| 变量声明 | 使用 var、let 或 const 声明变量。var 存在函数作用域和变量提升问题,let 和 const 是 ES6 引入的块级作用域变量,推荐使用。 | let name = "Alice";const age = 25;var oldStyle = "deprecated"; |
| 数据类型 | JavaScript 有 7 种原始数据类型:string、number、boolean、null、undefined、symbol (ES6)、bigint (ES2020),以及引用类型 object。 | typeof "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()); |
| 数组 | 有序的值集合,提供丰富的内置方法(如 map、filter、reduce)进行数据操作。 | const numbers = [1, 2, 3, 4];const doubled = numbers.map(n => n * 2);console.log(doubled); // [2, 4, 6, 8] |
| 条件语句 | 根据条件执行不同代码块,包括 if...else、switch 等。 | const score = 85;if (score >= 90) { console.log("A");} else if (score >= 80) { console.log("B");} else { console.log("C");} |
| 循环 | 用于重复执行代码,常见有 for、while、for...of、for...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 引入的简写函数语法,不绑定自己的 this、arguments、super 或 new.target。 | const 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 模块系统,使用 import 和 export 实现代码的模块化组织和复用。 | // math.jsexport const PI = 3.14;export function add(a, b) { return a + b; }// main.jsimport { 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)的一部分,但与引擎协同工作,负责处理异步操作的回调执行。 | 协调 setTimeout、Promise.then 等异步任务的执行顺序。 |
| 内联缓存 (Inline Caching) | 优化属性访问速度的技术。缓存对象属性的查找结果,避免重复查找原型链。 | 第一次访问 obj.prop 时查找并缓存位置,后续访问直接使用缓存位置。 |
| 隐藏类 (Hidden Classes) | V8 使用的技术,为相似对象动态创建”隐藏类”,使属性访问接近 C++ 等静态语言的速度。 | 对象属性顺序影响性能:{a:1, b:2} 和 {b:2, a:1} 不共享隐藏类。 |
总结:现代引擎通过 解释器 + JIT 编译器 + 优化技术(如内联缓存、隐藏类)+ 自动内存管理 的组合,实现了接近原生代码的执行性能。
2. 运行时的页面构建过程
2.1 Web 页面的生命周期
注意:以下代码示例可以在浏览器的开发者工具界面,源代码/来源 → 代码段 → 新建 → 编辑保存新代码段 → 右键 → 运行,在控制台的左下角中,点选用户消息,即可在与页面的交互过程中,看到监控事件的相关输出。
| 阶段 | 说明 | JavaScript 事件 | 代码示例 |
|---|---|---|---|
| 创建 (Creation) | 浏览器开始创建 window 和 document 对象,但页面内容尚未加载。 | 无直接事件,但全局脚本在此阶段开始解析。 | // 此处代码在页面构建时执行console.log("Script parsed"); |
| 加载 (Loading) | HTML 文档被加载和解析,DOM 树构建完成。外部资源(如图片、样式表、脚本)开始加载。 | DOMContentLoaded、load | // DOM 构建完成,不等待资源document.addEventListener('DOMContentLoaded', () => { console.log('DOM ready!');});// 所有资源(图片等)加载完成window.addEventListener('load', () => { console.log('Page fully loaded');}); |
| 交互 (Interactive) | 用户可以与页面交互(如点击、输入),但某些资源可能仍在加载。document.readyState 变为 "interactive"。 | readystatechange | document.addEventListener('readystatechange', () => { if (document.readyState === 'interactive') { console.log('Document is interactive'); }}); |
| 完全加载 (Complete) | 页面所有资源(包括图片、iframe 等)都已加载完毕。document.readyState 变为 "complete"。 | readystatechange | document.addEventListener('readystatechange', () => { if (document.readyState === 'complete') { console.log('Document is complete'); }}); |
| 活动 (Active) | 页面在前台运行,用户可正常交互。这是页面的主要运行阶段。 | 无特定事件,但可监听各种用户事件(click、scroll 等)。 | window.addEventListener('click', () => { console.log('User clicked');}); |
| 可见性变更 (Visibility Change) | 页面在可见与不可见状态间切换(如切换标签页、最小化窗口)。 | visibilitychange | document.addEventListener('visibilitychange', () => { if (document.hidden) { console.log('Page is hidden'); } else { console.log('Page is visible'); }}); |
| 页面切换/卸载 (Page Navigation / Unload) | 用户离开当前页面(如关闭标签、跳转到新页面)。此阶段适合执行清理操作,但不能进行异步操作。 | beforeunload、unload、pagehide | // 用户尝试离开页面前(可询问是否离开)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.onbeforeunload、window.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 尝试访问布局属性(如 offsetTop、clientWidth),会强制触发同步布局(性能警告:Forced reflow)。 | element.offsetWidth、element.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 构建完成后执行,适合初始化代码。 | DOMContentLoaded(defer 脚本在此事件前执行) |
| 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.readyState、window.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 | 鼠标按钮在元素上按下时触发(早于 click) | elem.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) | // 不推荐使用 |
beforeinput | 在 input 事件前触发,可取消输入 | 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('页面完全加载完成'); }); |
DOMContentLoaded | HTML 文档解析完成,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'); }); |
hashchange | URL 的 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 | 被拖拽元素在目标区域上移动时触发(必须阻止默认行为才能触发 drop) | dropZone.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 | 接收到跨窗口消息时触发(postMessage) | window.addEventListener('message', (e) => { if (e.origin !== 'https://trusted.com') return; console.log('收到消息:', e.data); }); |
关键注意事项
- 元素选择:所有示例都使用
document.getElementById()或类似方法先获取元素,避免undefined错误。 - 事件绑定时机:如果脚本在 DOM 之前加载,应使用
DOMContentLoaded事件来确保元素存在。 - 阻止默认行为:使用
e.preventDefault()来阻止链接跳转、表单提交、右键菜单等。 - 事件冒泡:使用
e.stopPropagation()阻止事件向上冒泡。 - 性能:
mousemove、scroll、resize等高频事件建议节流(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 对象- 语法简洁 | 回调函数、事件处理、数组方法(map、filter 等) | 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); // 1console.log(gen.next().value); // 2 | - 使用 function* 和 yield- 可暂停和恢复执行 - 返回迭代器对象 | 处理异步流程、惰性求值、无限序列 | - 用于 redux-saga 等库 - 与 for...of、yield* 配合使用 |
总结对比
| 方式 | 是否提升 | 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)- 不适用于 null 或 false(它们是有效值) |
| 剩余参数 (Rest Parameters) | function sum(...numbers) { return numbers.reduce((a, b) => a + b, 0);}console.log(sum(1, 2, 3)); // 6console.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 实例,可直接使用 map、filter 等方法 |
| 解构参数 (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- 是类数组,不是真数组(无 map、filter)- 建议用 ...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 或配置函数 | - 普通参数 → 默认参数 → 剩余参数(顺序不能乱) - 剩余参数必须在最后 |
参数传递机制
| 机制 | 说明 | 示例 |
|---|---|---|
| 值传递 | 基本类型(string、number、boolean、null、undefined、symbol、bigint)按值传递 | 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 | - 无法直接使用数组方法如 push、map、filter 等- 可通过 [...arguments] 或 Array.from(arguments) 转为真数组 |
| 动态性 | 实时反映函数调用时传入的参数,即使函数定义没有显式参数 | function dynamic() { return arguments.length; }console.log(dynamic()); // 0console.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 更利于引擎优化 |
| 没有数组方法 | 不能直接调用 map、filter、forEach 等 | function badMap() { // ❌ 报错:arguments.map is not a function arguments.map(x => x * 2);} | 必须先转换为数组才能使用数组方法 |
arguments vs ...rest 总结对比
| 特性 | arguments | ...rest |
|---|---|---|
| 类型 | 类数组对象 | 真正的 Array |
| 是否存在 | 普通函数中自动存在 | 需显式定义 ...paramName |
| 箭头函数支持 | ❌ 不支持 | ✅ 支持 |
| 数组方法 | 不能直接使用 | 可直接使用 map、filter 等 |
| 性能 | 可能影响优化 | 更优,利于引擎优化 |
| 可读性 | 较差,需额外转换 | 清晰,语义明确 |
| 推荐程度 | ❌ 不推荐新代码使用 | ✅ 推荐使用 |
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(丢失绑定) |
显式绑定(call、apply、bind) | 使用 call()、apply() 或 bind() 强制指定 this 值 | function 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,继承外层函数或全局作用域的 this | const 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; |
| 严格模式影响 | 严格模式下,独立函数调用的 this 为 undefined,而非 window | "use strict";function test() { console.log(this); }test(); // undefined | - 更安全,避免污染全局对象 |
this 不能被赋值改变 | this 是关键字,不能通过 this = obj 修改 | function bad() { this = {}; // ❌ 语法错误 } | this 的值由调用方式决定,无法在函数体内赋值修改 |
this 绑定优先级(从高到低)
| 顺序 | 绑定方式 | 说明 |
|---|---|---|
| 1 | new 绑定 | new Foo() |
| 2 | 显式绑定 | call、apply、bind |
| 3 | 隐式绑定 | obj.method() |
| 4 | 默认绑定 | func() |
new会覆盖bind的绑定。
this 最佳实践
| 建议 | 说明 |
|---|---|
✅ 使用 bind() 防止 this 丢失 | 尤其在回调、事件处理、setTimeout 中 |
✅ 箭头函数用于保持外层 this | 适合在 map、filter、Promise.then 中使用 |
| ✅ 构造函数命名首字母大写 | 如 Person,提醒使用 new |
| ✅ 严格模式开发 | 避免意外绑定全局对象 |
| ❌ 避免在对象方法中使用箭头函数 | 会丢失 this 对象引用 |
4.2 调用方式
| 调用方式 | 语法示例 | this 的绑定规则 | 使用场景 | 注意事项 |
|---|---|---|---|---|
| 1. 作为普通函数调用 (Function Invocation) | function greet() { console.log(\Hello, ${this.name}`); }<br>greet(); // 直接调用` | - 非严格模式:this 指向全局对象(浏览器中为 window)- 严格模式( "use strict"):this 为 undefined | 通用函数执行 | "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 丢失- 实现继承 | - call 和 apply 立即执行- bind 返回新函数,可延迟执行- bind 常用于事件监听、回调函数 |
箭头函数的调用方式
箭头函数的调用语法与普通函数相同,但其 this 行为特殊:
| 调用方式 | this 行为 | 示例 |
|---|---|---|
| 普通调用 | 继承外层作用域的 this | const 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 | 无效,无法改变 this | const fn = () => console.log(this);fn.call({ name: "Test" }); // this 仍为外层 |
调用方式最佳实践
| 建议 | 说明 |
|---|---|
| ✅ 构造函数首字母大写 | 如 Person,提醒使用 new |
✅ 使用 bind 修复 this 丢失 | 尤其在事件监听、setTimeout、回调中 |
✅ 箭头函数用于保持外层 this | 如 array.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) | 立即执行 | 强制指定 this 为 arguments | 传参 | 类数组转数组(旧式写法) | 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 错误 | setTimeout、Promise 中 this 不是期望对象 | - 使用箭头函数 - 使用 bind()- 保存 const self = this; |
| 类数组无法使用数组方法 | arguments、NodeList 无 map、filter | - [...arguments]- Array.from()- Array.prototype.method.call() |
构造函数 this 被覆盖 | 构造函数返回对象,导致 new 失效 | 避免在构造函数中 return 对象(除非有意) |
上下文处理最佳实践
- ✅ 优先使用
bind()处理事件监听和回调中的this。 - ✅ 箭头函数用于保持外层
this,特别是在map、filter、setTimeout中。 - ✅
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()); // 0counter.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)); // 8console.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)); // 5console.log(MathUtils.doubleSquare(4)); // 32// square() 无法被外部调用 | - 使用 IIFE(立即执行函数)创建闭包 - 私有函数 square、cube 不暴露- 实现简单的模块模式 |
| 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 在块({})中也会创建词法环境 | - if、for、{} 等块语句会创建新的词法环境- 支持暂时性死区(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()); // 1console.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]] 引用形成 |
词法环境工作流程
- 解析代码,确定函数的词法嵌套关系
- 创建全局词法环境
- 函数定义时,确定其
[[Outer]]指向外层环境 - 函数调用时:
- 创建新的词法环境(含环境记录)
- 将其
[[Outer]]指向定义时确定的外层环境 - 压入执行上下文栈
- 变量访问:
- 从当前环境记录开始查找
- 逐层向上(通过
[[Outer]])直到全局
- 函数执行结束:
- 若无闭包引用,词法环境被销毁
- 若有闭包,环境保留供内部函数访问
5.4 理解变量类型
| 特性维度 | var | let | const | 说明 |
|---|---|---|---|---|
| 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; // undefinedconsole.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 声明下是否可修改属性 | 说明 | 代码示例 |
|---|---|---|---|---|
基本类型(number、string、boolean、null、undefined、symbol、bigint) | ❌ 不可变 | ❌ 无法修改 | 值一旦创建就不能改变,任何”修改”都是创建新值 | 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) | ✅ 可变 | ✅ 可以修改元素 | 数组方法如 push、pop、splice 会修改原数组 | 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); // undefinedvar 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 initializationlet 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) | 用于 let、const、class、import 等块级绑定 | - let 变量- const 常量- class 声明- 函数参数 | - 支持块级作用域 - 存在暂时性死区(TDZ) |
| 变量环境(VariableEnvironment) | 用于 var 和 function 声明 | - var 变量- function 函数声明 | - 函数或全局作用域 - 传统提升机制 |
在函数执行上下文的创建阶段,这两个环境最初可能指向同一个词法环境,但在函数体中遇到
let/const时会分离。
标识符注册流程(创建阶段)
- 创建词法环境和变量环境
- 扫描代码,收集所有声明:
- 将
var和function声明注册到变量环境 - 将
let、const、class、参数等注册到词法环境
- 将
- 初始化绑定:
var→undefinedfunction→ 函数对象let/const→uninitialized(进入 TDZ)
- 执行阶段:
- 遇到赋值语句时,更新对应绑定的值
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 → 变量环境,提升至顶部 | 可在声明前访问(var 为 undefined) |
✅ 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()); // 0counter.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() 方法(核心控制方法) | 启动或恢复生成器执行,运行到下一个 yield 或 return | next(value?) | { value: any, done: boolean }- value: yield 或 return 的值- 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✅ 最后一次 return 后 done: 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 | 返回自身(this) | function* gen() { yield \"X\"; yield \"Y\"; }// 方式1:for...offor (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); // 0console.log(c.next().value); // 1console.log(c.next().value); // 2 | ✅ 返回 { value: any, done: boolean }✅ done: true 表示执行完毕✅ 可实现无限序列 |
4. return() 方法 | 提前终止生成器,跳过剩余 yield | generator.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 值(忽略 return) | for (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* iterable | function* 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) 和 yield | function* fibonacci() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; }}const fib = fibonacci();console.log(fib.next().value); // 0console.log(fib.next().value); // 1console.log(fib.next().value); // 1 | ✅ 只在需要时计算下一个值 ✅ 可表示数学上的无限序列 |
生成器对象的返回值格式
| 属性 | 类型 | 说明 |
|---|---|---|
value | any | yield 或 return 的值 |
done | boolean | false:未完成,可继续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); // 0console.log(c2.next().value); // 0(独立状态) |
| 4. 指令指针(Program Counter / Instruction Pointer) | 内部指针,记录当前执行到哪一条语句 | - 初始指向函数体开始 - 每次 next() 向前移动到下一个 yield 或 return- 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() | 执行代码直到下一个 yield、return 或异常 |
| 暂停 | suspended | 遇到 yield | 保存上下文,返回 {value, done: false} |
| 完成 | completed | 遇到 return 或 return()/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. 无法使用 return 和 throw | 回调中 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. 什么是 Promise | Promise 是 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); // Pendingconsole.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 函数是立即执行的 | // 错误示例:缺少 catchsomeAsync().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. 创建 Promise | new Promise(executor) | 手动封装异步操作,如定时器、自定义请求 | const delay = (ms) => { return new Promise( (resolve) => { setTimeout(resolve, ms); } );};delay(1000).then(() => console.log(\"1秒后执行\")); | ✅ executor 函数立即执行❌ 必须调用 resolve 或 reject,否则 Promise 会永远 pending |
| 2. 包装异步 API | 将回调函数 API 转为 Promise | 常用于 setTimeout、XMLHttpRequest、Node.js 回调等 | // 包装 setTimeoutconst wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));// 包装 Node.js fs.readFileconst 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. 组合多个 Promise | Promise.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) | 返回第一个完成(成功或失败)的 Promise | const 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. 快速创建 Promise | Promise.resolve(value)Promise.reject(reason) | 快速创建已解决或已拒绝的 Promise | // 返回已成功的 Promiseconst p1 = Promise.resolve(42);// 返回已失败的 Promiseconst p2 = Promise.reject(new Error(\"出错\"));p1.then(v => console.log(v)); // 42p2.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 | 语法更简洁,调试更方便 |
| ✅ 合理选择组合方法 | 根据业务需求选择 all、allSettled、race、any |
| ✅ 避免 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); // trueconsole.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 的方法(如 toString、hasOwnProperty) | console.log(Object.getPrototypeOf(Object.prototype));// null | ✅ 可通过 Object.create(null) 创建无原型链的对象 | |
| 4. 创建方式 | 构造函数模式 | 使用 new 调用函数创建实例,自动设置原型。 | 实例的 [[Prototype]] 指向构造函数的 prototype | function 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")); // trueconsole.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.age | p1.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 | ✅ 返回新数组(concat、map、展开运算符) | |
| DOM 操作 | 函数直接读取或修改页面 DOM 结构。 | 改变用户界面状态 依赖浏览器环境 | function updateHeader(text) { document.getElementById("header").innerText = text;} | 难以单元测试 与 UI 强耦合 | ✅ 将 DOM 操作封装在独立函数中 ✅ 在框架中通过状态驱动 UI | |
| 网络请求(I/O) | 发起 fetch、XMLHttpRequest 等异步请求。 | 依赖外部服务 可能失败或超时 | async function fetchUser(id) { const res = await fetch(\/api/users/${id}`);<br> return res.json();<br>}` | 异步、不可靠 需要错误处理 | ✅ 使用 Promise/async-await 处理 ✅ Mock 请求用于测试 | |
| 定时器操作 | 设置或清除 setTimeout、setInterval。 | 延迟执行,影响程序流程 | 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); // trueconsole.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 Constructor | function Person(name) { this.name = name; }const p1 = new Person("Alice");console.log(p1 instanceof Person); // true | ✅ 用于判断对象是否为某构造函数的实例 ✅ 基于原型链进行判断 |
| 2. 工作原理 | 原型链查找机制 | instanceof 会沿着对象的 [[Prototype]] 链向上查找,检查是否存在 Constructor.prototype。 | — | const arr = [1, 2, 3];console.log(arr instanceof Array); // trueconsole.log(arr instanceof Object); // true | ✅ 返回 true 只要原型链中存在对应 prototype |
| 3. 使用场景 | 判断自定义对象类型 | 判断由构造函数或 class 创建的对象类型。 | obj instanceof MyConstructor | class Animal {}class Dog extends Animal {}const dog = new Dog();console.log(dog instanceof Animal); // trueconsole.log(dog instanceof Dog); // true | ✅ 支持继承关系判断(多态) |
| 内置对象类型判断 | 检测是否为数组、正则、日期等内置对象。 | obj instanceof BuiltInType | const date = new Date();console.log(date instanceof Date); // trueconst 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 返回 false。 | — | console.log("hello" instanceof String); // falseconsole.log(new String("hello") instanceof String); // true | ✅ instanceof 只对对象有效✅ 原始值没有原型链 | |
null 和 undefined | 不能使用 instanceof 判断。 | — | console.log(null instanceof Object); // falseconsole.log(undefined instanceof Object); // false | ✅ 它们不是任何构造函数的实例 | |
| 5. 对比 | vs typeof | typeof 返回数据类型的字符串,但对对象细分不足。 | typeof value | console.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 RegExp 或 Object.prototype.toString | instanceof 在同域下可用 |
| 判断自定义类实例 | 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 / Setters | get 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); // trueconsole.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. 继承内置对象 | 可继承 Array、Error、Map 等 | 自定义类可继承内置构造函数。 | 增强内置对象功能 | 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.prototype→Parent.prototype→Object.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 模拟 |
| 内置对象继承 | 支持,可扩展 Array、Error 等 |
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() | 返回指定元素首次出现的索引,未找到返回 -1 | arr.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)或 undefined | arr.find(x => x > 2); // 3 | |
findIndex() | 返回满足条件的第一个元素的索引 | arr.findIndex(callback) | ❌ 不变 | 索引(number)或 -1 | arr.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) | ❌ 不变 | undefined | arr.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() | 先 map 再 flat(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),若键不存在则返回 undefined | console.log(map.get('name')); // "Alice"console.log(map.get('unknown')); // undefined |
has(key) | 检查 Map 是否包含某个键 | map.has(key) | key:要检查的键 | ❌ 不变 | 布尔值(boolean) | console.log(map.has('age')); // trueconsole.log(map.has('email')); // false |
delete(key) | 删除指定键的键值对 | map.delete(key) | key:要删除的键 | ✅ 改变 | 布尔值:删除成功为 true,否则为 false | const result = map.delete('city'); // true |
clear() | 清空所有键值对 | map.clear() | 无参数 | ✅ 改变 | undefined | map.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 | ❌ 不变 | undefined | map.forEach((v, k) => { console.log(\${k} = ${v}`);<br>});` |
Map 与 Object 对比:
特性 Map Object 键的类型 任意类型(对象、函数、原始值等) 只能是字符串或 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,否则为 false | const result = set.delete(4); // true |
has(value) | 检查 Set 是否包含某个值 | set.has(value) | value:要检查的值 | ❌ 不变 | 布尔值(boolean) | console.log(set.has('hello')); // trueconsole.log(set.has(99)); // false |
clear() | 清空 Set 中所有值 | set.clear() | 无参数 | ✅ 改变 | undefined | set.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 | ❌ 不变 | undefined | set.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) |
| 数组转 Set | new 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))); |
| 求并集 | 合并两个 Set | const union = new Set([...set1, ...set2]); |
Set 与 Array 对比:
特性 Set Array 元素唯一性 ✅ 自动去重,值唯一 ❌ 允许重复元素 性能(查找/删除) ✅ 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)
| 修饰符 | 全称 | 功能说明 | 示例说明 |
|---|---|---|---|
g | global | 全局匹配,查找所有匹配项(而非第一个) | /abc/g 匹配字符串中所有 “abc” |
i | ignoreCase | 忽略大小写 | /hello/i 匹配 “Hello”, “HELLO” |
m | multiline | 多行模式,^ 和 $ 匹配每行的开始和结束 | /^start/m 匹配每行以 “start” 开头 |
u | unicode | 启用 Unicode 模式,正确处理码位大于 U+FFFF 的字符 | /𠮷/u 正确匹配 emoji 或汉字扩展 |
s | dotAll | 使 . 匹配包括换行符在内的所有字符 | /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。 | 模式解析失败时立即报错 | // 抛出 SyntaxErrorconst 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 | 用于 g 或 y 模式的匹配位置指针 | 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 → 0match.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” 或 undefined match[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. CommonJS | Node.js 默认模块系统(2009) | 使用 require() 同步加载模块,module.exports 导出 | // math.jsmodule.exports = { add(a, b) { return a + b; } };// app.jsconst math = require('./math'); | require() 同步加载 | ❌ 浏览器不原生支持 ✅ Node.js 原生 | 简单直观、支持动态加载 | 同步加载不适合浏览器、不支持静态分析 | Node.js 后端开发、工具脚本 |
| 4. AMD | RequireJS 推广(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 | 运行时动态加载,返回 Promise | import('./math.js').then(math => { ... });const math = await import('./math.js'); | import('module-path') | ✅ 现代浏览器和 Node.js | 按需加载、代码分割、懒加载 | 返回 Promise,需异步处理 | 路由懒加载、条件加载、性能优化 |
| 8. Node.js 中的 ESM | Node.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">加载。import和export只能在模块顶层使用,不能在条件语句中。- 推荐使用现代工具链(Vite、Webpack)来管理模块。
10.2 ES6 模块
ES6 模块全流程详解表:
| 阶段 | 操作说明 | 语法/工具 | 示例代码 | 关键要点 |
|---|---|---|---|---|
| 1. 创建模块(定义导出内容) | 将功能封装在 .js 文件中,使用 export 导出变量、函数、类等 | export const/var/function/classexport 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 中导入 CJS | import fs from 'fs'; | ✅ 支持 |
| 互操作:CJS 中导入 ESM | ❌ 不支持 require()✅ 必须使用动态导入 | — |
总结:ES6 模块最佳实践
建议 说明 ✅ 优先使用命名导出 更清晰,便于 tree-shaking ✅ 谨慎使用默认导出 适用于只有一个主要导出的模块 ✅ 构建时输出多格式 同时提供 ESM 和 CJS 格式 ✅ 使用现代构建工具 Vite / Rollup / Webpack ✅ 发布前测试本地引用 使用 npm link或file:协议✅ 版本语义化(SemVer) 遵循主版本.次版本.修订号规则
💡 提示:
import是静态的,编译时确定依赖关系。export只是”声明”要导出的内容,不创建新变量。- 推荐使用 Vite 进行快速原型开发和构建。
11. BOM 与 DOM 对比
BOM 与 DOM 对比总览表:
| 特性 | BOM(Browser Object Model) | DOM(Document Object Model) |
|---|---|---|
| 全称 | 浏览器对象模型 | 文档对象模型 |
| 定义 | 提供与浏览器窗口交互的对象和接口,用于控制浏览器行为 | 将 HTML/XML 文档表示为树状结构,允许程序动态访问和操作文档 |
| 核心对象 | window, navigator, location, history, screen, frames | document, 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";
核心对象详解表:
| 对象 | 所属模型 | 功能说明 | 常用属性/方法 |
|---|---|---|---|
window | BOM | 浏览器窗口的全局对象,JS 的全局执行环境 | window.innerWidth/Height、window.open()、window.setTimeout()、window.localStorage |
document | BOM & DOM | 连接 BOM 与 DOM 的桥梁 | document.title、document.URL、document.getElementById()、document.querySelector()、document.createElement() |
location | BOM | 表示当前页面 URL 信息,并提供导航功能 | location.href、location.protocol、location.reload()、location.replace() |
history | BOM | 管理浏览器会话历史记录 | history.back()、history.forward()、history.go(-2)、history.pushState() |
navigator | BOM | 提供浏览器和设备相关信息 | navigator.userAgent、navigator.platform、navigator.geolocation |
screen | BOM | 提供用户屏幕信息 | screen.width/height、screen.availWidth/availHeight |
Element | DOM | 表示 HTML 元素节点 | element.id、element.getAttribute()、element.addEventListener() |
Node | DOM | 所有 DOM 节点的基类 | node.nodeType、node.childNodes、node.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 对象 | 属性/方法 | 功能说明 | 示例代码 |
|---|---|---|---|
window | window.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/false | if (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`);` | |
location | location.href | 获取或设置完整的 URL | location.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"); | |
history | history.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) => { ... }; | |
navigator | navigator.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) { ... } | |
screen | screen.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}位`);` | |
frames | window.frames.length | 获取 <iframe> 数量 | console.log(\有 ${window.frames.length} 个 iframe`);` |
window.frames[0] | 获取第一个 <iframe> 的 window 对象 | window.frames[0].postMessage("Hello", "*"); | |
localStorage | localStorage.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(); | |
sessionStorage | sessionStorage.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> 标签时,暂停解析,执行 JavaScript | JS 可访问当前已构建的 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 Object | DOM 的根节点,代表整个 HTML 文档 | 所有 DOM 操作的入口点,通过 document 访问 |
| Node(节点) | DOM 树中的基本单位,包括元素节点、文本节点、注释节点等 | 每个 HTML 标签或文本都会生成一个或多个节点 |
| Parser Blocking | <script> 默认会阻塞 HTML 解析直到脚本下载并执行完毕 | 影响页面渲染性能,可通过 async 或 defer 属性优化 |
✅ 示例: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 | #document | — | html |
| Element | html | lang="zh" | head, body |
| Element | head | — | meta, title |
| Element | meta | charset="UTF-8" | 无 |
| Element | title | — | TextNode: “示例页面” |
| Element | body | — | h1, p |
| Element | h1 | — | TextNode: “Hello World” |
| Element | p | class="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.innerHTMLelement.className / element.idelement.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.querySelector、getElementById 等 | const 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 |
| ✅ 避免频繁读取布局信息 | 如 offsetHeight、getBoundingClientRect() 会强制同步回流 |
| ✅ 使用事件委托 | 对大量动态元素,不要逐个绑定事件,而是绑定到父元素 |
🌟 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'); → 返回一个 Element 或 null |
getElementsByClassName() | element.getElementsByClassName(className) | 根据类名获取元素集合(实时 HTMLCollection) | const 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'); → 返回第一个匹配的 Element 或 null | |
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); | |
| 三、元素内容与属性操作 | innerHTML | element.innerHTML | 获取或设置元素的 HTML 内容 | div.innerHTML = '<p>新内容</p>';⚠️ 存在 XSS 风险 |
textContent | element.textContent | 获取或设置元素的纯文本内容(推荐安全方式) | div.textContent = 'Hello World'; | |
innerText | element.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'); | |
dataset | element.dataset.key | 访问 data-* 自定义属性(驼峰命名) | <div data-user-id="123"> → div.dataset.userId; // "123" | |
| 四、元素类名与样式操作 | className | element.className | 获取或设置整个 class 字符串 | el.className = 'box highlight'; |
classList | element.classList.add/remove/toggle/contains() | 操作类名的现代 API(推荐使用) | el.classList.add('active');el.classList.remove('hidden');el.classList.toggle('visible');if (el.classList.contains('error')) { ... } | |
style | element.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); | |
| 六、节点关系与遍历 | parentNode | node.parentNode | 获取父节点 | const parent = child.parentNode; |
childNodes | node.childNodes | 获取所有子节点(包括文本、注释) | const children = element.childNodes; → NodeList | |
children | element.children | 获取所有元素子节点(仅 Element) | const elems = container.children; → HTMLCollection | |
firstChild / lastChild | node.firstChild / node.lastChild | 获取第一个/最后一个子节点(任意类型) | const first = node.firstChild; | |
firstElementChild / lastElementChild | element.firstElementChild / lastElementChild | 获取第一个/最后一个元素子节点 | const firstEl = container.firstElementChild; | |
previousSibling / nextSibling | node.previousSibling / nextSibling | 获取前一个/后一个兄弟节点(任意类型) | const prev = current.previousSibling; | |
previousElementSibling / nextElementSibling | element.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.style | element.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.cssText | element.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.className | element.className = '...' | 获取或设置整个 class 属性值(字符串) | box.className = 'highlight active'; // 覆盖所有原有类名 |
element.classList | element.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.styleSheets | document.styleSheets[index] | 获取页面中所有样式表的集合 | const sheets = document.styleSheets;console.log(sheets.length); // 样式表数量 |
sheet.cssRules / sheet.rules | sheet.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 / offsetHeight | element.offsetWidth / offsetHeight | 获取元素的布局尺寸(含 padding、border) | console.log(box.offsetWidth); // 220 (width + padding + border) |
clientWidth / clientHeight | element.clientWidth / clientHeight | 获取内容区+内边距尺寸(不含 border、滚动条) | console.log(box.clientWidth); // 200 (width + padding) | |
scrollWidth / scrollHeight | element.scrollWidth / scrollHeight | 获取元素总内容尺寸(含溢出不可见部分) | console.log(box.scrollHeight); // 内容实际高度(可能 > clientHeight) | |
offsetTop / offsetLeft | element.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 / scrollLeft | element.scrollTop = value | 获取或设置元素内容的垂直/水平滚动偏移量 | box.scrollTop = 100; // 滚动100pxconsole.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 对应属性名 | 语法格式 | 功能说明 | 代码示例 |
|---|---|---|---|---|
color | color | element.style.color | 设置文本颜色 | elem.style.color = 'red'; // 或 '#ff0000', 'rgb(255,0,0)' |
background-color | backgroundColor | element.style.backgroundColor | 设置背景颜色 | elem.style.backgroundColor = 'blue'; |
font-size | fontSize | element.style.fontSize | 设置字体大小 | elem.style.fontSize = '16px'; // 也可用 '1.2em', '20pt' |
font-weight | fontWeight | element.style.fontWeight | 设置字体粗细 | elem.style.fontWeight = 'bold'; // 或 'normal', '600' |
font-family | fontFamily | element.style.fontFamily | 设置字体族 | elem.style.fontFamily = 'Arial, sans-serif'; |
text-align | textAlign | element.style.textAlign | 设置文本对齐方式 | elem.style.textAlign = 'center'; // 'left', 'right', 'justify' |
line-height | lineHeight | element.style.lineHeight | 设置行高 | elem.style.lineHeight = '1.5'; // 或 '20px' |
display | display | element.style.display | 设置元素显示方式 | elem.style.display = 'none'; // 隐藏元素elem.style.display = 'block'; // 块级显示elem.style.display = 'inline-flex'; |
visibility | visibility | element.style.visibility | 控制可见性(保留空间) | elem.style.visibility = 'hidden'; // 隐藏但占位elem.style.visibility = 'visible'; |
width / height | width / height | element.style.width / height | 设置宽高 | elem.style.width = '200px';elem.style.height = '100px'; |
max-width / max-height | maxWidth / maxHeight | element.style.maxWidth / maxHeight | 设置最大宽高 | elem.style.maxWidth = '500px'; |
margin | margin | element.style.margin | 设置外边距(四边) | elem.style.margin = '10px'; // 统一设置elem.style.margin = '5px 10px'; // 上下 左右 |
margin-top | marginTop | element.style.marginTop | 设置上外边距 | elem.style.marginTop = '20px'; |
padding | padding | element.style.padding | 设置内边距(四边) | elem.style.padding = '15px'; |
border | border | element.style.border | 设置边框(宽度、样式、颜色) | elem.style.border = '2px solid black'; |
border-radius | borderRadius | element.style.borderRadius | 设置圆角 | elem.style.borderRadius = '8px'; // 或 '50%' 圆形 |
box-shadow | boxShadow | element.style.boxShadow | 设置阴影效果 | elem.style.boxShadow = '2px 2px 5px rgba(0,0,0,0.3)'; |
opacity | opacity | element.style.opacity | 设置透明度 | elem.style.opacity = '0.5'; // 0=全透明, 1=不透明 |
position | position | element.style.position | 设置定位方式 | elem.style.position = 'absolute'; // 'relative', 'fixed', 'sticky' |
top / right / bottom / left | top / right / bottom / left | element.style.top 等 | 配合 position 设置偏移 | elem.style.position = 'absolute';elem.style.top = '10px';elem.style.left = '20px'; |
z-index | zIndex | element.style.zIndex | 设置层叠顺序(仅对定位元素有效) | elem.style.zIndex = '1000'; |
float | cssFloat(标准)/ styleFloat(IE) | element.style.cssFloat | 设置浮动(现代布局多用 Flex/Grid) | elem.style.cssFloat = 'left'; |
flex | flex | element.style.flex | 设置 Flex 子项的伸缩属性 | container.style.display = 'flex';item.style.flex = '1'; |
grid-template-columns | gridTemplateColumns | element.style.gridTemplateColumns | 设置 Grid 布局列结构 | grid.style.display = 'grid';grid.style.gridTemplateColumns = '1fr 2fr'; |
transform | transform | element.style.transform | 设置 2D/3D 变换 | elem.style.transform = 'rotate(45deg) scale(1.2)'; |
transition | transition | element.style.transition | 设置过渡动画 | elem.style.transition = 'all 0.3s ease-in-out'; |
cursor | cursor | element.style.cursor | 设置鼠标指针样式 | elem.style.cursor = 'pointer'; // 'wait', 'move', 'not-allowed' |
✅ 使用说明与注意事项
| 要点 | 说明 |
|---|---|
| ✅ 驼峰命名法 | 所有包含连字符(-)的 CSS 属性,在 JS 中需转换为驼峰命名法(如 background-color → backgroundColor) |
| ✅ 值为字符串 | 所有 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.style | element.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.className | element.className | 获取元素的 class 属性值(字符串形式) | string | console.log(elem.className); // 输出: "btn primary active" |
element.classList | element.classList | 获取一个 DOMTokenList 对象,可遍历或操作类名 | DOMTokenList | console.log(elem.classList); // 输出: ['btn', 'primary', 'active']console.log(elem.classList.length); // 类数量console.log(elem.classList[0]); // 第一个类名 |
element.getAttribute('class') | element.getAttribute('class') | 通过属性方式获取 class 值 | string 或 null | const classes = elem.getAttribute('class');console.log(classes); // "btn primary" |
element.id | element.id | 获取元素的 id 属性 | string | console.log(elem.id); // 如 "header" |
element.getAttribute('style') | element.getAttribute('style') | 获取内联 style 属性的原始字符串 | string 或 null | console.log(elem.getAttribute('style')); // 输出: "color: red; font-size: 16px;" |
getComputedStyle().getPropertyValue() | getComputedStyle(elem).getPropertyValue('property-name') | 使用标准 CSS 属性名(带连字符)获取计算样式值 | string | const 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 名 |
✅ 示例:对比 style 与 getComputedStyle
<!-- 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)- 若只需获取类名或内联样式 → 使用
className或style- 避免依赖
element.style读取非内联样式,因其无法获取 CSS 文件中的规则。
13.3.4 转换像素值
✅ DOM 转换像素值的方法一览表
| 方法 / 技巧 | 语法格式 | 功能说明 | 返回值类型 | 代码示例 |
|---|---|---|---|---|
window.getComputedStyle() + 读取属性 | getComputedStyle(element).property | 获取元素任意样式的计算后像素值(自动转为 px) | string(如 "16px") | const elem = document.querySelector('.text');const fontSize = getComputedStyle(elem).fontSize;console.log(fontSize); // "16px"(无论原单位是 em/rem/%)✅ 最常用、最可靠的方式 |
手动解析并转换 em / rem 值 | 自定义函数 | 将 em 或 rem 字符串转换为像素值(基于当前上下文) | number(像素数值) | function emToPx(value, base) { return parseFloat(value) * base; }console.log(emToPx('1.5em', 16)); // 24⚠️ 需手动管理基准值(如 font-size) |
| 使用临时元素测量法 | 创建隐藏元素进行实际渲染测量 | 通过 DOM 渲染机制将任意单位转换为精确像素值 | number | 见下方详细示例 ✅ 精确,适用于复杂单位 |
Element.getBoundingClientRect() | element.getBoundingClientRect() | 获取元素在视口中的实际布局尺寸(单位为 px) | DOMRect 对象(含 width, height, top, left 等) | const rect = element.getBoundingClientRect();console.log(rect.width); // 实际宽度(px)console.log(rect.height); // 实际高度(px)✅ 包含边框、内边距的实际渲染尺寸 |
offsetWidth / offsetHeight | element.offsetWidth / offsetHeight | 获取元素的布局尺寸(含 padding、border,单位为 px) | number | console.log(myDiv.offsetWidth); // 如 200(px)✅ 快速获取整数像素尺寸 |
parseFloat() + 单位剥离 | parseFloat("10px") | 提取带单位字符串中的数值部分(常用于后续计算) | number | const val = '15.5em';const num = parseFloat(val); // 15.5🔧 常配合其他方法使用 |
| 利用 canvas 或 SVG 进行测量(高级) | 使用图形上下文测量文本或布局 | 在特定场景下精确测量文本宽度等 | number | const 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/offsetHeight或getBoundingClientRect()动态转换任意单位(如 em → px) ✅ 临时元素测量法 或 getComputedStyle配合隐藏元素文本宽度测量 ✅ canvas.measureText()简单提取数值 ✅ parseFloat(value)💡 提示:
- 所有通过
getComputedStyle获取的尺寸均为最终渲染值,已转换为px。- 避免在循环中频繁调用
getComputedStyle或创建临时元素,以免影响性能。parseFloat只提取数字,不会自动转换单位,需结合上下文计算。
13.3.5 获取元素高度、宽度、颜色等
✅ 测试 DOM 元素可视化信息的方法一览表
| 测试目标 | 核心方法 / 属性 | 说明 | 返回值类型 | 代码示例 | 注意事项 |
|---|---|---|---|---|---|
| 高度(Height) | offsetHeight | 获取元素布局高度(含 padding + border,不含 margin) | number(像素值) | const height = elem.offsetHeight;console.log(height); // 如 100 | ✅ 最常用,包含边框和内边距 |
clientHeight | 获取内容区+内边距高度(不含 border 和滚动条) | number | const clientH = elem.clientHeight; | 适用于内容可视区域 | |
scrollHeight | 获取元素总内容高度(含溢出不可见部分) | number | const scrollH = elem.scrollHeight; | 用于判断是否滚动 | |
getBoundingClientRect().height | 获取元素在视口中的渲染高度(含 CSS 变换影响) | number | const rect = elem.getBoundingClientRect();console.log(rect.height); | ✅ 精确,受 transform 影响 | |
| 宽度(Width) | offsetWidth | 获取元素布局宽度(含 padding + border) | number | const width = elem.offsetWidth; | 与 offsetHeight 对应 |
clientWidth | 获取内容区+内边距宽度 | number | const clientW = elem.clientWidth; | 常用于响应式判断 | |
scrollWidth | 获取元素总内容宽度(含横向溢出) | number | const scrollW = elem.scrollWidth; | 判断水平滚动 | |
getBoundingClientRect().width | 获取元素在视口中的实际渲染宽度 | number | const rect = elem.getBoundingClientRect();console.log(rect.width); | ✅ 支持旋转、缩放等变换 | |
| 坐标(位置) | offsetTop / offsetLeft | 相对于最近定位祖先元素的偏移 | number | console.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-width) | number | console.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 | 获取背景颜色 | string | const bg = getComputedStyle(elem).backgroundColor;console.log(bg); // "rgb(0, 0, 255)" 或 "transparent" | 注意:透明背景返回 "transparent" | |
window.getComputedStyle(elem).getPropertyValue('color') | 使用标准 CSS 名获取颜色 | string | const color = getComputedStyle(elem).getPropertyValue('color'); | 语法更直观(支持连字符) | |
| 可见性(Visibility) | getComputedStyle(elem).display | 检查是否被 display: none 隐藏 | string | const isVisible = getComputedStyle(elem).display !== 'none'; | ✅ 最常用 |
getComputedStyle(elem).visibility | 检查是否 visibility: hidden | string | const isHidden = getComputedStyle(elem).visibility === 'hidden'; | 隐藏但占位 | |
getBoundingClientRect() 面积判断 | 通过宽高是否为 0 判断是否可见 | boolean | const rect = elem.getBoundingClientRect();const isVisible = rect.width > 0 && rect.height > 0 && rect.top >= 0; | 防止 opacity: 0 或移出视口 | |
| 透明度(Opacity) | getComputedStyle(elem).opacity | 获取元素及其子元素的透明度 | string | const opacity = parseFloat(getComputedStyle(elem).opacity);console.log(opacity); // 0.0 ~ 1.0 | 注意:返回字符串,需 parseFloat |
| 边框(Border) | getComputedStyle(elem).borderWidth | 获取边框宽度(如 "1px") | string | const border = getComputedStyle(elem).borderTopWidth; | 返回带单位的字符串 |
getComputedStyle(elem).borderColor | 获取边框颜色 | string | const 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简单直接,适合大多数布局测试 ✅ 在 DOMContentLoaded或window.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) | 每次访问这些属性都会强制浏览器同步计算布局,即使样式未变 | ✅ 缓存值:将读取的布局值存储在变量中,避免重复访问 | // ✅ 缓存 rectconst rect = element.getBoundingClientRect();console.log(rect.top);console.log(rect.left);// 复用,不触发新回流 | // ❌ 重复访问console.log(element.getBoundingClientRect().top);console.log(element.getBoundingClientRect().left);// 调用两次,可能触发两次回流 |
| 在循环中修改 DOM 或样式 | 每次修改都可能触发重排或重绘,尤其在长列表中 | ✅ 批量操作: - 使用 documentFragment 创建 DOM- 使用 className 或 cssText 批量设置样式 | // ✅ 批量创建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 加速,不触发布局重排 | // ✅ 推荐:使用 transformelem.style.transform = \translateX(${x}px)`;` | // ❌ 避免:修改 top/leftelem.style.left = x + 'px'; // 触发回流 |
监听 resize 或 scroll 事件未节流 | 这些事件触发频率极高(每秒数十次) | ✅ 使用节流(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) | 只渲染可视区域内的元素,避免长列表重排 | 表格、聊天记录、无限滚动 |
使用 transform 和 opacity | 这两个属性由合成器(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)- 🔹 对
scroll、resize等高频事件使用节流- 🔹 优先使用 CSS 而非 JavaScript 控制样式
遵循这些原则,可显著提升页面性能,避免卡顿,尤其是在复杂 UI 或移动设备上。
14. 事件
14.1 事件循环
✅ JavaScript 事件循环核心机制一览表
| 组成部分 | 说明 | 常见示例 | 执行顺序规则 | 备注 |
|---|---|---|---|---|
| 调用栈(Call Stack) | 跟踪当前正在执行的函数调用。先进后出(LIFO)。 | foo()、bar() 等函数执行 | 同步代码立即入栈执行 | ✅ 执行同步任务,栈空时处理异步回调 |
| 堆(Heap) | 存放对象等非结构化数据的内存区域 | const obj = {}; // DOM 节点 | — | 数据存储区域,不参与执行顺序 |
| 消息队列 / 宏任务队列(MacroTask Queue) | 存放异步宏任务的回调函数 | setTimeout、setInterval、setImmediate(Node.js)、I/O、UI 渲染事件、postMessage、requestAnimationFrame 回调 | 每次事件循环只取一个宏任务执行 | ⚠️ 浏览器在两个宏任务之间可能会进行渲染 |
| 微任务队列(MicroTask Queue) | 存放高优先级的异步微任务回调 | Promise.then/catch/finally、MutationObserver、queueMicrotask()、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) |
| 典型 API | setTimeout, setInterval, I/O, UI events | Promise, 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
| 特性 | setTimeout | setInterval |
|---|---|---|
| 执行次数 | 仅执行一次 | 重复执行,直到取消 |
| 典型用途 | 延迟、防抖、一次性任务 | 轮询、倒计时、周期性任务 |
| 是否需要手动停止 | 否(自动结束) | ✅ 是(必须调用 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) | setTimeout 和 setInterval 是 JavaScript 中最常用的计时器 API,用于延迟或周期性执行代码。 | 属于宏任务(MacroTask),由事件循环统一调度。 |
✅ 计时器在事件循环中的执行流程
| 步骤 | 操作 | 详细说明 |
|---|---|---|
| 1 | 注册计时器 | 当代码执行到 setTimeout(fn, 1000) 时,浏览器(或 Node.js)启动一个内部计时器,开始倒计时。 |
| 2 | 等待延迟时间 | 计时器在后台等待指定毫秒数(如 1000ms),不占用主线程。 |
| 3 | 延迟到期 → 回调入队 | 时间到后,回调函数被放入宏任务队列(不是立即执行!)。⚠️ 如果队列中已有其他任务,需等待。 |
| 4 | 事件循环检查宏任务队列 | 当前宏任务(如同步代码)执行完毕,调用栈为空。 |
| 5 | 取出并执行计时器回调 | 事件循环从宏任务队列中取出最早加入的计时器回调,推入调用栈执行。 |
| 6 | 执行微任务队列 | 计时器回调执行完毕后,立即清空微任务队列(如 Promise.then)。 |
| 7 | 可能进行 UI 渲染 | 浏览器在宏任务(包括计时器)执行后,可能进行页面渲染。 |
| 8 | 进入下一轮事件循环 | 取下一个宏任务执行,循环继续。 |
🔁 关键点:计时器回调是宏任务,必须等待当前任务完成,并在延迟到期后排队等待执行。
✅ setTimeout 与 setInterval 在事件循环中的行为对比
| 特性 | setTimeout | setInterval |
|---|---|---|
| 任务类型 | 宏任务 | 宏任务 |
| 执行次数 | 仅一次 | 重复执行,直到被 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) |
✅ 计时器的最小延迟限制
| 环境 | 最小延迟 | 说明 |
|---|---|---|
| 浏览器(标准) | 4ms | HTML5 规范规定,setTimeout 和 setInterval 的最小延迟为 4ms(即使设置为 0) |
嵌套 setTimeout | 可能增加到 10ms+ | 某些浏览器为防止脚本滥用,对嵌套计时器施加更长的最小延迟 |
| 非用户触发的定时器 | 4ms ~ 数秒 | 在页面后台标签页中,浏览器可能将最小延迟增加到数秒以节省资源 |
| Node.js | 1ms | Node.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();
🔍 执行流程详解
| 阶段 | 操作 | 效果 |
|---|---|---|
| 1 | processNextChunk() 第一次调用 | 处理前 1000 个元素 |
| 2 | 批次处理完成 | 更新进度条 |
| 3 | index < 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 详细参数说明
| 参数 | 类型 | 说明 | 常用值/示例 |
|---|---|---|---|
type | string | 事件类型 | 'click', 'keydown', 'load', 'submit' 等 |
listener | function | 事件处理函数,接收 Event 对象 | function handleClick(e) { console.log(e.target); } |
options | Object 或 boolean | 配置选项(推荐使用对象) | { capture, once, passive, signal } |
options 详细配置:
| 选项 | 类型 | 说明 | 示例 |
|---|---|---|---|
capture | boolean | true 表示在捕获阶段执行监听器false(默认)表示在冒泡阶段执行 | addEventListener('click', fn, { capture: true }) |
once | boolean | true 表示监听器只执行一次,之后自动移除 | addEventListener('load', init, { once: true }) |
passive | boolean | true 表示监听器不会调用 preventDefault(),提升滚动性能 | addEventListener('touchstart', handle, { passive: true }) |
signal | AbortSignal | 通过 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 或检查元素是否存在 |
| 事件监听器未移除 | 导致内存泄漏 | 使用 removeEventListener 或 AbortController |
| 事件冒泡导致意外行为 | 父元素也有监听器 | 使用 e.stopPropagation() |
| 默认行为未阻止 | 如表单提交刷新页面 | 使用 e.preventDefault() |
this 指向错误 | 使用普通函数时 this 指向监听元素 | 使用箭头函数或 .bind() |
✅ 总结:事件处理关键原则
| 原则 | 说明 |
|---|---|
✅ 优先使用 addEventListener | 支持多监听器、灵活配置 |
| ✅ 合理使用事件委托 | 减少内存占用,提升性能 |
| ✅ 及时清理监听器 | 防止内存泄漏(SPA 中尤其重要) |
| ✅ 控制高频事件频率 | 使用防抖/节流优化 scroll、resize 等 |
| ✅ 理解事件流机制 | 正确使用捕获、冒泡、阻止传播 |
14.3.2 通过 DOM 代理事件
✅ 一、事件代理
事件代理(Event Delegation)是一种利用事件冒泡(Event Bubbling)机制的编程模式:不为每个子元素单独绑定事件监听器,而是将监听器绑定到父元素上,通过检查 event.target 来判断具体是哪个子元素触发了事件,并执行相应逻辑。
✅ 二、事件代理的核心原理
| 机制 | 说明 |
|---|---|
| 事件冒泡 | 大多数 DOM 事件(如 click、keydown)会从触发元素(目标)向上传播到根节点(document)。 |
event.target | 指向实际触发事件的元素(可能是子元素)。 |
event.currentTarget | 指向当前绑定监听器的元素(即父元素)。 |
| 委托逻辑 | 父元素监听事件 → 检查 e.target 是否匹配预期子元素 → 执行对应操作。 |
✅ 三、实现事件代理的步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 选择合适的父元素 | 选择一个稳定的、包含所有目标子元素的共同祖先(如 <ul>、<table>、<div class="container">)。 |
| 2 | 绑定事件监听器到父元素 | 使用 addEventListener 将事件绑定到父元素。 |
| 3 | 在回调中获取 event.target | 通过 e.target 确定用户实际点击的元素。 |
| 4 | 判断目标元素是否匹配 | 使用 classList.contains()、tagName、dataset、matches() 等方法判断。 |
| 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.tagName | e.target.tagName === 'BUTTON' | 判断标签类型(注意:返回大写) |
element.matches(selector) | e.target.matches('.delete, .edit') | 判断是否匹配 CSS 选择器(推荐) |
element.dataset.key | li.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)); |
✅ 八、常见应用场景
| 场景 | 说明 |
|---|---|
| 动态列表/表格 | 添加、删除、编辑行项目(如待办事项、商品列表) |
| 导航菜单 | 处理多个菜单项的点击跳转 |
| 模态框/对话框 | 点击遮罩层关闭对话框 |
| 无限滚动/分页 | 动态加载内容后无需重新绑定事件 |
| 表单控件 | 统一处理多个输入框的 input 或 change 事件 |
✅ 九、事件代理 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: true | bubbles: 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 注意事项
✅ 跨浏览器开发注意事项总览
| 编号 | 类别 | 问题描述 | 受影响浏览器 | 解决方案/最佳实践 | 示例/工具 |
|---|---|---|---|---|---|
| 1 | DOM API 兼容性 | 某些现代 DOM 方法在旧浏览器中不存在 | IE、旧版 Safari/Edge | 使用特性检测 + Polyfill | if (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) { ... } } |
| 3 | CSS 兼容性 | 缺少现代 CSS 特性或需要厂商前缀 | IE、旧版 Android 浏览器 | 使用 Autoprefixer,提供降级样式 | display: -webkit-flex; /* Safari */ display: flex; |
| 4 | JavaScript 语法支持 | ES6+ 语法(箭头函数、let、const、Promise)不被支持 | IE、旧版浏览器 | 使用 Babel 编译,引入 Polyfill | Babel + @babel/preset-env |
| 5 | HTML5 元素支持 | <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; } |
| 7 | CSS Flexbox/Grid 支持 | 旧浏览器对 Flexbox/Grid 支持不完整 | IE11 及以下、旧版 Safari | 使用 Autoprefixer,提供 float 或 inline-block 降级布局 | Autoprefixer + 回退布局 |
| 8 | Fetch API 支持 | fetch() 不被 IE 支持 | IE 所有版本 | 使用 axios 或 whatwg-fetch Polyfill | import 'whatwg-fetch'; fetch('/api/data').then(...); |
| 9 | Promise 支持 | 原生 Promise 不可用 | IE、旧版 Safari | 使用 Polyfill(如 es6-promise) | <script src="es6-promise.auto.min.js"></script> |
| 10 | Storage API | localStorage、sessionStorage 可能被禁用或不支持 | 旧版浏览器、隐私模式 | 检测支持性,提供备用存储方案 | try { localStorage.setItem('test', '1'); } catch(e) { /* 使用 cookie 或内存存储 */ } |
| 11 | 移动端点击延迟 | 移动浏览器有 300ms 点击延迟 | 旧版 iOS/Android | 使用 touchstart 事件或引入 fastclick | npm 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 头 |
| 19 | Web API 支持 | IntersectionObserver, ResizeObserver 等新 API | 旧版浏览器 | 使用 Polyfill | import '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, IE7 | width 包含 padding 和 border,导致布局错乱 | 使用 box-sizing: border-box 统一模型;避免怪异模式(使用标准 DOCTYPE) |
| 2 | display: inline-block | 旧版浏览器不支持或行为异常 | IE6, IE7, 旧版 Firefox | 元素不换行但无法设置宽高,或出现额外间距 | IE6/7:使用 display: inline; zoom: 1; 模拟;其他:使用 flex 或 grid 布局替代 |
| 3 | min-height / max-height | IE6 不支持 min-height | IE6 | min-height 被忽略,元素无法保持最小高度 | 使用 height + !important:min-height: 100px; height: auto !important; height: 100px; |
| 4 | PNG 透明度支持 | IE6 不支持 PNG-24 透明通道 | IE6 | PNG 图片背景显示为灰色或背景色 | 使用 PNG8 格式或滤镜 |
| 5 | position: fixed | 旧版 iOS Safari 和 IE 不支持 | iOS < 5, IE6-IE8 | 元素无法固定在视口,随页面滚动 | 使用 position: absolute + JavaScript 模拟,或使用 Polyfill(如 fixed-sticky) |
| 6 | float 与 margin 双倍边距 Bug | IE6 的经典 Bug | IE6 | 浮动元素的 margin 在特定方向上被加倍 | 使用 display: inline; 修复 |
| 7 | z-index 层叠上下文问题 | 各浏览器对层叠上下文处理不一致 | 所有浏览器(尤其 IE) | z-index 不生效,元素层级混乱 | 确保父元素未创建独立层叠上下文;避免过度使用 z-index |
| 8 | :focus 样式支持 | Safari 不支持非表单元素的 :focus | Safari(旧版) | <div tabindex="0"> 无法通过 :focus 设置样式 | 使用 JavaScript 模拟焦点状态 |
| 9 | text-overflow: ellipsis | 需要特定条件才能生效 | 所有浏览器 | 在某些布局下省略号不显示 | 必须同时设置:white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| 10 | vertical-align 对齐差异 | 不同浏览器默认对齐方式不同 | Chrome, Firefox, Safari, IE | 图片或内联元素垂直对齐不一致 | 显式设置 vertical-align: middle; 或使用 flex 布局替代 |
| 11 | line-height 继承问题 | 字体缩放时行高计算差异 | 所有浏览器 | 行高在嵌套元素中表现不一致 | 使用无单位数值(如 1.5)而非像素值 |
| 12 | input 元素默认样式 | 各浏览器默认边框、内边距不同 | Chrome, Safari, Firefox, IE | 输入框外观不一致 | 使用 CSS Reset 或 Normalize.css |
| 13 | placeholder 样式控制 | 伪元素选择器不统一 | Chrome, Firefox, Safari | 无法统一设置占位符颜色、字体 | 使用浏览器前缀:::-webkit-input-placeholder、::-moz-placeholder、:-ms-input-placeholder |
| 14 | CSS3 动画性能 | 软件渲染 vs 硬件加速 | 旧版 Android, 低端设备 | 动画卡顿、掉帧 | 使用 transform 和 opacity 触发 GPU 加速:transform: translateZ(0); |
| 15 | Flexbox 布局差异 | IE10-11 对 Flexbox 支持不完整 | IE10, IE11 | flex: 1 行为异常,align-items 不生效 | 使用 Autoprefixer 添加前缀;测试并提供 float 或 inline-block 回退方案 |
| 16 | Event 对象属性 | event.target vs event.srcElement | IE8 及以下 | e.target 不存在,需用 e.srcElement | 封装事件处理函数 |
| 17 | addEventListener 支持 | IE8 及以下使用 attachEvent | IE8 及以下 | addEventListener 报错 | 封装跨浏览器事件绑定 |
| 18 | localStorage 支持 | 隐私模式下可能禁用 | Safari(隐私模式) | localStorage 抛出异常 | 使用 try-catch 包裹 |
| 19 | CSS 渐变语法 | 旧版浏览器需要厂商前缀 | Safari < 6, Android < 4.4 | 渐变不显示或显示为纯色 | 使用 Autoprefixer 自动生成前缀 |
| 20 | HTML5 语义标签 | IE8 及以下不识别 | IE8 及以下 | <article>, <section> 无法设置样式 | 使用 html5shiv 或动态创建元素 |
15.2.2 浏览器 Bug 修复
✅ 浏览器 Bug 修复方法对照表
| 编号 | Bug 类型 | 问题描述 | 受影响浏览器 | 修复方法 | 示例代码/说明 |
|---|---|---|---|---|---|
| 1 | IE6 双倍 margin Bug | 浮动元素的 margin 在浮动方向上被加倍 | IE6 | 使用 display: inline; 修复 | .float-left { float: left; margin-left: 10px; display: inline; } — 原理:触发 IE6 的 “hasLayout” 机制 |
| 2 | IE6 不支持 min-height | min-height 被忽略 | IE6 | 使用 height + !important 模拟 | .box { min-height: 100px; height: auto !important; height: 100px; } |
| 3 | IE6 PNG 透明背景变灰 | PNG-24 透明通道不支持 | IE6 | 使用滤镜或降级为 PNG8 | filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='image.png'); |
| 4 | position: fixed 不支持 | 元素无法固定在视口 | iOS < 5, IE6-IE8 | 使用 position: absolute + JS 模拟 | window.onscroll = function() { elem.style.top = window.pageYOffset + 'px'; }; 或使用 Polyfill |
| 5 | inline-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> |
| 7 | text-overflow: ellipsis 不显示 | 省略号未生效 | 所有浏览器(条件不满足) | 补全必需 CSS 属性 | .truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } — 三者缺一不可 |
| 8 | z-index 层级失效 | 子元素层级被父元素限制 | 所有浏览器(层叠上下文问题) | 避免父元素创建独立层叠上下文 | 修复:移除父元素的 z-index 或调整结构 |
| 9 | placeholder 颜色无法修改 | 占位符文字颜色默认为浅灰 | Chrome, Firefox, Safari | 使用浏览器前缀伪元素 | ::-webkit-input-placeholder { color: #999; } :-moz-placeholder { color: #999; opacity: 1; } :-ms-input-placeholder { color: #999; } |
| 10 | addEventListener 不支持 | IE8 及以下使用旧 API | IE8 及以下 | 封装跨浏览器事件绑定函数 | function addEvent(el, type, handler) { if (el.addEventListener) { ... } else if (el.attachEvent) { ... } } |
| 11 | event.target 不存在 | IE8 及以下使用 srcElement | IE8 及以下 | 使用兼容性判断 | function getTarget(e) { return e.target || e.srcElement; } |
| 12 | localStorage 抛出异常 | Safari 隐私模式禁用 | Safari(隐私模式) | 使用 try-catch 包裹 | try { localStorage.setItem('test', '1'); } catch(e) { console.warn('localStorage 不可用'); } |
| 13 | Flexbox 布局错乱 | IE10-11 对 Flexbox 支持不完整 | IE10, IE11 | 添加厂商前缀,测试回退方案 | 使用 Autoprefixer:display: -ms-flexbox; /* IE10 */ display: flex; |
| 14 | CSS3 渐变不显示 | 旧版浏览器需前缀 | Safari < 6, Android < 4.4 | 使用 Autoprefixer 或手动添加 | .gradient { background: -webkit-linear-gradient(top, #fff, #000); background: linear-gradient(to bottom, #fff, #000); } |
| 15 | HTML5 标签无法设置样式 | IE8 及以下不识别语义标签 | IE8 及以下 | 使用 html5shiv 或动态创建 | <!--[if lt IE 9]><script src="html5shiv.js"></script><![endif]--> |
| 16 | line-height 继承异常 | 字体缩放时行高计算错误 | 所有浏览器 | 使用无单位数值 | body { line-height: 1.5; } — 推荐使用无单位值 |
| 17 | input 默认样式不一致 | 各浏览器边框、内边距不同 | 所有浏览器 | 使用 CSS Reset 或 Normalize.css | input, textarea, button { margin: 0; padding: 0; border: none; font: inherit; } |
| 18 | vertical-align 对齐错位 | 图片或内联元素垂直对齐不一致 | 所有浏览器 | 显式设置对齐方式 | img, .inline-block { vertical-align: middle; } 或使用 flex 布局替代 |
| 19 | 动画卡顿(无硬件加速) | 软件渲染导致性能差 | 旧版 Android, 低端设备 | 使用 transform 触发 GPU 加速 | .animate { transform: translateZ(0); /* 或 translate3d(0,0,0) */ } |
| 20 | box-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 | ✅ 是 | 包含 userAgent、platform、geolocation 等 |
location / history | ✅ 是 | 支持路由操作 |
localStorage / sessionStorage | ✅ 是 | 持久化存储可用 |
fetch / WebSocket | ✅ 是 | 网络请求正常工作 |
| Canvas / SVG | ✅ 是 | 图形渲染支持 |
IntersectionObserver | ✅ 是 | 现代 API 完整支持 |
MutationObserver | ✅ 是 | DOM 变更监听 |
requestAnimationFrame | ✅ 是 | 动画帧支持 |
| CSS 渲染与布局 | ✅ 是 | 真实重排重绘 |
| 事件系统 | ✅ 是 | 支持 click、scroll、keydown 等 |
| 同源策略 / 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 控制真实浏览器 |
获取 window、document | 通过 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 内置模块 | fs、path、os、child_process 等模块在浏览器中不存在 | 使用浏览器 API 替代(如 fetch 代替 http);手动实现轻量逻辑(如路径拼接);不能读写本地文件 |
| 2 | require 不存在 | 浏览器不支持 CommonJS 的 require() | 使用 ES6 import(仅限模块);将依赖打包(如用 Webpack);手动定义依赖对象 |
| 3 | __dirname 和 __filename 无效 | 这些是 Node.js 特有的全局变量 | 删除或替换为字符串常量 |
| 4 | process 对象缺失 | process.env、process.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 URL | Node.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 开头定义 process、global 等 |
结合 console.log 调试 | 观察变量、执行流程 |
| 保存为 Snippet 便于复用 | 适合调试工具函数 |
| 不要依赖系统 API | 如文件读写、网络请求(除非用 fetch) |