JavaScript核心知识点详解
好的,我们来详细扩展说明这几个核心的 JavaScript 知识点。
1. 事件循环 (Event Loop)
事件循环是 JavaScript 处理异步操作的核心机制。它允许单线程的 JavaScript 执行非阻塞的 I/O 操作。
宏任务 vs. 微任务
- 宏任务: 由浏览器/Node.js 环境发起的任务。例如:
setTimeout,setIntervalsetImmediate(Node.js 特有)- I/O 操作 (如读取文件、网络请求)
- UI 渲染 (浏览器)
- 整体的
script代码本身
- 微任务: 由 JavaScript 自身发起的任务,通常与“承诺”相关。例如:
Promise.then(),Promise.catch(),Promise.finally()process.nextTick(Node.js 特有,优先级最高)MutationObserver(浏览器)
执行规则 (浏览器)
- 执行一个宏任务(通常是最老的或最先入队的),例如执行全局的脚本。
- 执行过程中遇到微任务,就将其放入微任务队列;遇到宏任务,就将其放入宏任务队列。
- 当前宏任务执行完毕后,立即清空整个微任务队列(执行所有微任务)。
- 进行 UI 渲染(如果需要)。
- 从宏任务队列中取出下一个宏任务,开始新一轮的循环。
核心:一个宏任务 → 所有微任务 → (渲染) → 下一个宏任务。
Node.js 与浏览器的差异
Node.js 的事件循环基于 libuv 库,其阶段划分比浏览器更复杂。
Node.js 事件循环阶段 (简化版):
- Timers: 执行
setTimeout和setInterval的回调。 - Pending callbacks: 执行延迟到下一个循环的 I/O 回调。
- Idle, Prepare: 内部使用。
- Poll: 检索新的 I/O 事件;执行 I/O 相关的回调(如读取文件)。必要时会在此阶段阻塞。
- Check: 执行
setImmediate的回调。 - Close callbacks: 执行关闭事件的回调(如
socket.on('close', ...))。
process.nextTick() 在一个阶段结束后、下一个阶段开始前执行,它拥有一个独立的队列,优先级高于微任务。
主要差异总结:
| 特性 | 浏览器 | Node.js |
|---|---|---|
| 阶段 | 相对简单(宏任务、微任务、渲染) | 分为多个明确阶段(Timers, Poll, Check等) |
setImmediate |
不支持 | 支持,在 Check 阶段执行 |
process.nextTick |
不支持 | 支持,优先级最高(在各阶段间执行) |
| 微任务执行时机 | 在每个宏任务之后 | 在事件循环的每个阶段之后 |
复杂异步代码执行顺序示例
console.log('1. Script Start'); // 同步代码,宏任务1开始
setTimeout(() => {
console.log('2. setTimeout'); // 回调是宏任务
Promise.resolve().then(() => {
console.log('3. Promise inside setTimeout'); // 微任务
});
}, 0);
Promise.resolve()
.then(() => {
console.log('4. Promise 1'); // 微任务
})
.then(() => {
console.log('5. Promise 2'); // 微任务
});
console.log('6. Script End'); // 同步代码,宏任务1结束
// 预期输出顺序:
// 1. Script Start
// 6. Script End
// 4. Promise 1
// 5. Promise 2
// 2. setTimeout
// 3. Promise inside setTimeout
执行流程图示:
flowchart TD
A["开始执行宏任务 (主脚本)"] --> B["输出 'Script Start'"]
B --> C["遇到 setTimeout<br>将其回调放入宏任务队列"]
C --> D["遇到 Promise.resolve().then<br>将其回调放入微任务队列"]
D --> E["输出 'Script End'<br>主脚本(宏任务)执行完毕"]
E --> F["清空微任务队列"]
F --> G["执行第一个Promise回调<br>输出 'Promise 1'"]
G --> H["执行then返回的新Promise回调<br>输出 'Promise 2'"]
H --> I["微任务队列清空"]
I --> J["取出并执行下一个宏任务(setTimeout回调)"]
J --> K["输出 'setTimeout'"]
K --> L["遇到Promise.resolve().then<br>将其回调放入微任务队列"]
L --> M["setTimeout回调(宏任务)执行完毕"]
M --> N["清空微任务队列"]
N --> O["执行Promise回调<br>输出 'Promise inside setTimeout'"]
O --> P[结束]
2. 内存管理与垃圾回收
JavaScript 的内存分配和释放是自动的,这个过程称为垃圾回收 (Garbage Collection)。
V8 垃圾回收机制
V8 采用了分代式垃圾回收,将内存分为两个“代”:
-
新生代 (New Space):
- 存放生存时间短的对象(如局部变量)。
- 使用 Scavenge 算法(一种复制算法),速度快。
- 分为
From-Space和To-Space。垃圾回收时,将存活对象从From复制到To,然后清空From。经历多次回收仍存活的对象会被移到老生代。
-
老生代 (Old Space):
- 存放生存时间较长或较大的对象(如全局变量、闭包变量)。
- 使用 标记-清除 (Mark-Sweep) 和 标记-压缩 (Mark-Compact) 算法。
- 标记-清除:从根对象(全局变量、当前调用栈)开始,标记所有可达对象。然后遍历整个堆,清除未被标记的对象。
- 标记-压缩:在标记-清除的基础上,将存活的对象向一端移动,解决内存碎片问题。
引用计数是一种旧的算法(无法处理循环引用,已基本被淘汰)。
常见内存泄漏场景
- 意外的全局变量:在非严格模式下,给未声明的变量赋值会创建全局变量。
function foo() { bar = 'leak'; // window.bar = 'leak' this.baz = 'leak'; // 如果foo是普通函数,this指向window } - 遗忘的定时器或回调函数:
const data = getHugeData(); setInterval(() => { const node = document.getElementById('Node'); if(node) { node.innerHTML = JSON.stringify(data); // data 一直被定时器引用,无法释放 } }, 1000); - 脱离DOM的引用:保存了DOM节点的引用,即使节点已从DOM树移除。
const elements = { button: document.getElementById('myButton') }; document.body.removeChild(document.getElementById('myButton')); // 由于 elements.button 仍引用着该DOM节点,它无法被回收 - 闭包:闭包会保留其词法作用域中变量的引用。如果不慎引用了不再需要的大对象,就会导致泄漏。
function outer() { const bigData = new Array(1000000); return function inner() { console.log('I have a big data reference'); // bigData 一直被inner函数引用 }; }
使用 Chrome DevTools 排查内存泄漏
- Performance 面板:录制一段时间内的性能,观察 JS Heap 内存曲线是否持续攀升而不下降。
- Memory 面板:
- Heap Snapshot:拍摄堆内存快照,查看对象分布,比较多次快照找出增多的对象。
- Allocation instrumentation on timeline:实时查看内存分配的时间线,定位分配内存的函数。
- Allocation sampling:采样内存分配,统计哪个函数分配了最多的内存。
3. 原型与继承
JavaScript 使用原型链来实现继承。
原型链
- 每个函数都有一个
prototype属性(原型对象)。 - 每个对象都有一个
__proto__属性(指向其构造函数的原型对象)。 - 当访问一个对象的属性时,如果对象自身没有,就会通过
__proto__向上查找其原型对象,直到找到或到达链条尽头 (null)。
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {}
// 关键:继承 Parent 的原型
Child.prototype = new Parent(); // 或 Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child; // 修复构造函数指向
const child = new Child();
child.sayName(); // 'Parent'
// 查找路径:child -> child.__proto__ (Parent实例) -> Parent实例.__proto__ (Parent.prototype)
Class 语法糖的本质
ES6 的 class 本质上是基于原型的继承的语法糖,更清晰易读。
class Parent {
constructor() {
this.name = 'Parent';
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent { // extends 关键字建立了原型链
constructor() {
super(); // 相当于调用 Parent.call(this)
}
}
// 其原型链结构与上面的函数实现完全相同
继承的多种实现及优缺点
-
原型链继承
- 优点:简单。
- 缺点:所有子类实例共享原型属性(引用类型会互相影响);无法向父类传参。
-
构造函数继承
function Child() { Parent.call(this); // “借调”父类构造函数 }- 优点:解决了共享属性和传参问题。
- 缺点:方法都在构造函数中定义,无法复用;无法继承父类原型上的方法。
-
组合继承(最常用)
function Child() { Parent.call(this); // 继承实例属性 } Child.prototype = new Parent(); // 继承原型方法 Child.prototype.constructor = Child;- 优点:融合两者优点,是 JavaScript 中最常用的继承模式。
- 缺点:调用了两次父类构造函数。
-
原型式继承 (
Object.create())- 优点:无需创建构造函数,基于现有对象创建新对象。
- 缺点:与原型链继承类似,属性共享。
-
寄生组合式继承(最优解)
function inheritPrototype(Child, Parent) { const prototype = Object.create(Parent.prototype); // 创建父类原型的副本 prototype.constructor = Child; Child.prototype = prototype; } function Child() { Parent.call(this); } inheritPrototype(Child, Parent);- 优点:只调用一次父类构造函数,避免了在子类原型上创建不必要的属性,效率最高。
extends的原理与此类似。
- 优点:只调用一次父类构造函数,避免了在子类原型上创建不必要的属性,效率最高。
4. ES6+ 核心特性
-
Promise
- 作用:解决“回调地狱”,提供更优雅的异步编程方式。
- 状态:
pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变就不可逆。 - API:
.then(),.catch(),.finally(),Promise.all(),Promise.race(),Promise.resolve(),Promise.reject().
-
Async/Await
- 作用:以同步代码的书写方式处理异步操作,是 Promise 的语法糖。
async函数总是返回一个 Promise。await后面可以跟一个 Promise,它会暂停函数执行,等待 Promise 解决后再继续。
-
Generator
- 作用:可以暂停和恢复的函数。用
function*声明,使用yield暂停。 - 是 Async/Await 实现的基础。
function* gen() { yield 'hello'; yield 'world'; } const g = gen(); g.next(); // { value: 'hello', done: false } - 作用:可以暂停和恢复的函数。用
-
Module (ES Module)
- 作用:语言层面的模块化方案,取代 CommonJS、AMD 等。
export:导出模块。import:导入模块。- 静态化,编译时就能确定依赖关系,支持 Tree Shaking。
-
箭头函数
- 语法更简洁:
(params) => { expression }。 - 没有自己的
this,其this继承自外层词法作用域(定义时决定)。 - 没有
arguments对象,不能用作构造函数(不能new)。
- 语法更简洁:
-
解构赋值
- 从数组或对象中提取值,对变量进行赋值。
// 数组 const [a, b] = [1, 2]; // 对象 const { name, age } = { name: 'Alice', age: 30 }; // 函数参数 function foo({ id, data }) { ... }
挣钱养家

浙公网安备 33010602011771号