跳到主要内容JavaScript 核心概念与机制速通 | 极客日志JavaScriptNode.js大前端
JavaScript 核心概念与机制速通
本文系统讲解了 JavaScript 的核心概念,涵盖面向对象(原型链、class、属性定义)、作用域与闭包、Proxy 代理、模块系统、异步编程(回调、Promise、async/await、Workers)以及浏览器渲染机制、DOM 事件模型、执行模型、事件循环和内存管理等关键知识点。通过代码示例和对比分析,帮助开发者深入理解 JS 底层原理与最佳实践。
云间漫步2 浏览 面向对象
原型链
- JavaScript 使用对象实现继承。所有对象都有一个称为原型的内置属性,它指向的也是一个对象,所以原型对象也会有它自己的原型对象,这个链条就叫原型链,原型链终止于拥有 null 作为原型的对象上。
- 当你试图访问一个对象的属性时:如果在对象中找不到该属性,那么就会沿着原型链上的对象逐个搜索,直到找到该属性或者到达链的末端,仍然找不到则返回 undefined。
设置原型
const a = { name: 'a', __proto__: { name: 'b' } }
const personPrototype = {
greet() { console.log("hello!"); },
};
const carl = Object.create(personPrototype);
console.log(carl.greet());
const personPrototype2 = {
greet() { console.log(`你好,我的名字是 ${this.name}!`); },
};
function Person(name) { this.name = name; }
Object.assign(Person.prototype, personPrototype2);
自有属性:直接在对象中定义的属性,被称为自有属性。
const a = { : , : , : { : , : } }
.(.(a, ));
.(.(a, ));
.(.(a, ));
.(a.());
.(a.());
.(a.());
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
name
'a'
sex
'男'
__proto__
name
'b'
age
18
console
log
Object
hasOwn
'name'
console
log
Object
hasOwn
'age'
console
log
Object
hasOwn
'sex'
console
log
hasOwnProperty
'name'
console
log
hasOwnProperty
'age'
console
log
hasOwnProperty
'sex'
属性遮蔽:当一个对象自身拥有某个属性时,会遮蔽其原型链上同名的属性(由于原型链查找机制)。
const a = { name: 'a', __proto__: { name: 'b' } }
console.log(a.name);
console.log(a.__proto__.name);
内置的原型属性名称没有标准,但是实际上所有浏览器都使用 proto 作为原型属性名。访问原型的标准方法是 Object.getPrototypeOf()。
console.log(Object.getPrototypeOf(new Object()))
class
- class 是一个创建原型链的语法糖,它让 JavaScript 的面向对象看起来更像经典的面向对象实现,在引擎底层仍然是使用的原型。
私有成员:私有属性和方法必须在类声明中定义,并且以 #开头,私有字段不能被 for…in、Object.keys()、JSON.stringify() 等访问。也不能在外部调用。
class Person {
#id;
constructor(id) {
this.#id = id;
}
#countingMoney() { return 1000 + '元'; }
introduceSelf() {
console.log(`我是身份证是 ${this.#id} 我有 ${this.#countingMoney()}`);
}
}
const person = new Person('123456789');
person.introduceSelf();
构造函数使用 constructor 关键字来声明,在子类的构造函数中须先使用 super() 来调用父类的构造函数,并传递父类构造函数期望的参数。
class Person {
name;
constructor(name) {
this.name = name;
}
introduceSelf() {
console.log(`嗨!我是 ${this.name}`);
}
}
class Professor extends Person {
teaches;
constructor(name, teaches) {
super(name);
this.teaches = teaches;
}
introduceSelf() {
console.log(`我的名字叫 ${this.name}, 我是你们 ${this.teaches} 课程的教授.`);
}
grade() {
const grade = Math.floor(Math.random() * (5 - 1) + 1);
console.log(grade);
}
}
const walsh = new Professor("Walsh", "Psychology");
walsh.introduceSelf();
walsh.grade();
定义对象属性
const obj = {};
obj.name = "Alice";
Object.defineProperty(obj, 'name', { value: "Alice", writable: true, enumerable: true, configurable: true });
添加一个访问器属性:具有 get 和 set(不能同时有 value 和 writable)
let obj = {};
let _age = 0;
Object.defineProperty(obj, 'age', {
enumerable: true,
configurable: true,
get() { return _age; },
set(v) { _age = v; },
});
console.log(obj.age);
obj.age = 10;
console.log(obj.age);
let obj = {};
Object.defineProperty(obj, 'id', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.id);
obj.id = 456;
console.log(obj.id);
delete obj.id;
console.log(obj.id);
Object.defineProperty() 静态方法会可以在一个对象上定义一个新属性,或修改已有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
作用域
- 作用域是当前的执行上下文中的值和表达式是否可见(可被访问)。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。
- JavaScript 的作用域分以下三种:
- 全局作用域:脚本模式运行所有代码的默认作用域。
- 模块作用域:模块模式中运行代码的作用域。
- 函数作用域:由函数创建的作用域。
- 块级作用域:用一对花括号创建出来的作用域(只对 let 和 const 声明有效,对 var 声明无效)。
闭包
- 闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
- 速记:函数记住了它被创建时所在作用域中的变量,即使该函数在其原始作用域之外被调用,仍能访问这些变量。
- 闭包也是模块模式的基础,可用于实现命名空间和单例。
每个函数实例管理着它自己的作用域和闭包,在函数内创建不必要的闭包会对处理速度和内存消耗产生负面影响。例如,在创建一个新对象或类时,方法通常应该关联到对象的原型上,而不是定义到对象的构造函数中。
function MyObject(name, message) {
this.name = name.toString();
this.getName = function () { return this.name; };
}
function MyObject(name, message) {
this.name = name.toString();
}
MyObject.prototype.getName = function () { return this.name; };
const makeCounter = function () {
let privateCounter = 0;
function changeBy(val) { privateCounter += val; }
return {
increment() { changeBy(1); },
decrement() { changeBy(-1); },
value() { return privateCounter; },
};
};
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1.value());
counter1.increment();
counter1.increment();
console.log(counter1.value());
counter1.decrement();
console.log(counter1.value());
console.log(counter2.value());
Proxy
- Object.defineProperty 有个致命的缺陷:无法监听动态新增或删除的属性,也无法监听数组索引的变化,这正是 Vue2 响应式系统饱受诟病的原因。
- ES6 引入的 Proxy 和 Reflect 彻底改变了这一局面。它们提供了对整个对象的拦截能力,让真正的响应式成为可能。Vue3 正是基于此重构了其核心。
- Reflect 是一个内置的全局对象,它提供了与 Proxy 拦截器方法一一对应的静态方法。它是为了将原本分散在 Object 上的方法(如 Object.defineProperty)和操作符(如 in, delete)统一到一个命名空间下。
- 些内部操作(如 == 相等比较)无法被拦截,这是语言设计的安全边界。
- Proxy 会在每次操作时引入函数调用开销,不适合对性能极度敏感的热路径(如游戏主循环中的每帧计算)。
- Proxy 不支持 IE 浏览器,Babel polyfill 也不支持 Proxy,这也是 Vue3 不支持 IE 的核心原因。
- Object.defineProperty 定义时有额外开销,但访问时无额外开销。Proxy 每次操作都有拦截开销。
核心语法:创建一个对象的代理,从而实现对基本操作的拦截(如属性查找、赋值、枚举、函数调用等)。
const target = {
message: 'Hello, Proxy!',
count: 0,
[Symbol('secret')]: 'top secret'
};
const handler = {
get(target, prop, receiver) {
console.log(`[get] 读取属性:${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[set] 设置属性:${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
has(target, prop) {
console.log(`[has] 检查属性是否存在:${String(prop)}`);
return Reflect.has(target, prop);
},
deleteProperty(target, prop) {
console.log(`[deleteProperty] 删除属性:${String(prop)}`);
return Reflect.deleteProperty(target, prop);
},
ownKeys(target) {
console.log(`[ownKeys] 获取自身所有键`);
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(target, prop) {
console.log(`[getOwnPropertyDescriptor] 获取属性描述符:${String(prop)}`);
return Reflect.getOwnPropertyDescriptor(target, prop);
},
defineProperty(target, prop, descriptor) {
console.log(`[defineProperty] 定义属性:${String(prop)}`, descriptor);
return Reflect.defineProperty(target, prop, descriptor);
},
preventExtensions(target) {
console.log(`[preventExtensions] 禁止扩展对象`);
return Reflect.preventExtensions(target);
},
isExtensible(target) {
console.log(`[isExtensible] 检查对象是否可扩展`);
return Reflect.isExtensible(target);
},
getPrototypeOf(target) {
console.log(`[getPrototypeOf] 获取原型`);
return Reflect.getPrototypeOf(target);
},
setPrototypeOf(target, prototype) {
console.log(`[setPrototypeOf] 设置原型`);
return Reflect.setPrototypeOf(target, prototype);
},
apply(target, thisArg, args) {
console.log(`[apply] 调用函数`, { thisArg, args });
return Reflect.apply(target, thisArg, args);
},
construct(target, args, newTarget) {
console.log(`[construct] 使用 new 构造实例`, { args, newTarget });
return Reflect.construct(target, args, newTarget);
}
};
const proxy = new Proxy(target, handler);
模块系统
- 模块化是构建可维护、可扩展大型 JavaScript 应用的基石。过去,我们依赖社区方案(如 CommonJS、AMD)来组织代码。如今,ECMAScript 模块(ES Modules, ESM)已成为语言标准,并得到所有现代浏览器和 Node.js 的原生支持。
- ESM 的模块依赖关系在代码解析阶段就已确定,而非运行时。
- 实时绑定:ESM 导入的不是值的拷贝,而是对导出值的只读实时引用。如果模块内部的值发生变化,导入方能立即看到更新(前提是导出的是变量而非常量)。
- 在 Node.js 中,.mjs 文件总是被当作 ESM 处理,而.cjs 文件总是被当作 CJS 处理。对于.js 文件,其解析方式取决于 package.json 中的 type 字段('module'或'commonjs')。
- 自动延迟执行:模块脚本总是像带有 defer 属性一样,在文档解析完成后、DOMContentLoaded 事件触发前执行。
- 严格模式:模块内部自动启用严格模式。
- CORS 限制:通过 file://协议直接打开 HTML 文件会因 CORS 策略失败,必须通过本地服务器(如 http-server)进行测试。
- import() 作为一个函数,允许你在运行时按需加载模块。它返回一个 Promise,是实现懒加载和代码分割的关键。
- 现在,使用者只需导入 shapes/index.js 即可访问所有形状类。
export { default as Circle } from './circle.js';
export { default as Square } from './square.js';
export { default as Triangle } from './triangle.js';
export { drawCircle, calculateArea } from './circle.js';
document.getElementById('loadChart').addEventListener('click', async () => {
const { renderChart } = await import('./chartModule.js');
renderChart(data);
});
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { greet } from './utils.js';
greet('World');
</script>
</head>
<body>
<script type="module" src="./main.js"></script>
</body>
</html>
import { PI, add, Calculator } from './mathUtils.js';
import multiply from './mathUtils.js';
import multiply, { PI as piConstant } from './mathUtils.js';
import * as math from './mathUtils.js';
math.multiply(2, 3);
math.add(1, 2);
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Calculator { ... }
export default function multiply(a, b) { return a * b; }
| 特性 | ES Modules (ESM) | CommonJS (CJS) |
|---|
| 加载时机 | 编译时(静态) | 运行时(动态) |
| 依赖分析 | 构建工具/引擎可在执行前分析依赖图 | 无法静态分析,依赖在 require() 时才确定 |
| 值 vs 绑定 | 实时绑定(Live Binding) | 值拷贝(Copy of Value) |
| this 上下文 | undefined(严格模式) | 指向 module.exports |
| 循环依赖 | 通过绑定机制处理,但需注意初始化顺序 | 返回 module.exports 的当前快照 |
异步编程
回调函数
- 早期的异步 API 通过事件回调来实现,例如 XMLHttpRequest 通过监听不同的事件并执行对应的回调函数来处理数据。
- 回调函数就是一个被传递到另一个函数中的会在适当的时候被调用的函数:回调函数曾经是 JavaScript 中实现异步函数的主要方式。
- 回调函数的问题在于如果函数之间有先后顺序时,需要一层层的处理函数调用,并且异步只能在每一层处理无法统一处理。
Promise
- Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。
- Promise 对象有以下两个特点:
- 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
- Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
- resolve 函数的作用是,将 Promise 对象的状态从'未完成'变为'成功'。
- reject 函数的作用是,将 Promise 对象的状态从'未完成'变为'失败'。
- Promise 实例生成以后可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数,这两个函数都是可选的。
- finally() 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
- all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
- 1)只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
- 2)只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
- Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
- 只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
- ES2020 引入了 Promise.allSettled() 方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做'Settled',包含了'fulfilled'和'rejected'两种情况。
- ES2021 引入了 Promise.any() 方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。
- 有时需要将现有对象转为 Promise 对象,Promise.resolve() 方法就起到这个作用。
- Promise.reject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。
const p = Promise.reject('出错了');
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(s) {
console.log(s)
});
Promise.any([fetch('https://v8.dev/').then(() => 'home'), fetch('https://v8.dev/blog').then(() => 'blog'), fetch('https://v8.dev/docs').then(() => 'docs')]).then((first) => {
console.log(first);
}).catch((error) => {
console.log(error);
});
const promises = [fetch('/api-1'), fetch('/api-2'), fetch('/api-3'),];
await Promise.allSettled(promises);
removeLoadingIndicator();
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(console.log).catch(console.error);
const p = Promise.all([p1, p2, p3]);
Promise.prototype.finally()
promise
.then(result => { ··· })
.catch(error => { ··· })
.finally(() => { ··· });
const promise = new Promise(function(resolve, reject) {
if () {
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
}, function(error) {
});
async 函数
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
workers
- Worker 给了你在不同线程中运行某些任务的能力。
- Workers 和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互,这意味着 workers 不能访问 DOM(窗口、文档、页面元素等等),也不能直接访问彼此的变量。
- dedicated workers
- shared workers
- 可以由运行在不同窗口中的多个不同脚本共享。
- 要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口)。
- service workers
- Service worker 的行为就像代理服务器,缓存资源以便于 web 应用程序可以在用户离线时工作。他们是渐进式 Web 应用的关键组件。
浏览器
渲染机制
- 核心步骤:
- 第一步:处理 HTML 标记并构造 DOM 树。
- 遇到
- async:脚本异步下载,下载完立即执行(可能打断 HTML 解析)。
- defer:脚本异步下载,等 HTML 解析完再按顺序执行。
- 第二步:处理 CSS 并构建 CSSOM 树。
- CSSOM 是阻塞渲染的:在 CSSOM 构建完成前,页面不会渲染。
- 第三步:将 DOM 和 CSSOM 组合成渲染树。
- 只包含需要显示的节点(不包括 display: none 的元素)。
- 第四步:计算布局。
- 每个渲染树节点在窗口中的精确位置和大小,这个过程也叫回流。
- DOM 结构变化、窗口大小改变、读取某些布局属性(如 offsetWidth)都会触发回流。
- 第五步:绘制。
- 将渲染树转换为屏幕上的像素,包括颜色、边框、阴影等。
- 拆分为多个图层(Layers),便于后续合成。
- 第六步:合成。
- 将多个图层按正确顺序合成最终图像,利用 GPU 加速提升性能。
- 使用 transform、opacity 等属性可触发合成层(Compositing Layer),避免重绘/回流。
- 预加载器:现代浏览器在主线程解析 HTML 构建 DOM 的同时,会启动一个轻量级的预加载扫描器,它快速扫描 HTML 字节流(无需完整解析 DOM),识别出需要的资源标签如 link、script、img、video、audio、source、iframe 等并理解发起网络请求。
- 预加载的资源会进入浏览器缓存,主解析器使用时直接命中。
- 回流:当 DOM 的几何尺寸或布局发生变化时,浏览器需要重新计算元素的位置、大小及其在渲染树中的结构,这个过程称为回流。
- 重绘:当元素的外观样式(如颜色、背景、边框颜色等)发生变化,但不影响布局时,浏览器只需重新绘制该元素的像素,称为重绘。
- 减少回流与重绘:
- 避免连续读写布局属性,因为会触发强制同步回流,性能极差!
- 将多个单独修改样式操作合并为批量操作。
- 将元素暂时移出文档流,修改后再放回。
- 能用 transform 就不用 left/top。
可以使用 rel="preload"显式预加载资源。
<link rel="preload" as="font" href="font.woff2" type="font/woff2" crossorigin>
<link rel="preload" as="image" href="hero.webp">
<link rel="preload" as="script" href="critical.js">
DOM 事件模型
- 捕获阶段(Capturing Phase):事件从 window 开始,逐级向下传递到目标元素的父级。此阶段默认不触发监听器。
- 目标阶段(Target Phase):事件到达目标元素本身。
- 冒泡阶段(Bubbling Phase):事件从目标元素开始,逐级向上传递回 window。
- 除非特别指定,否则我们添加的事件监听器都默认在冒泡阶段执行。
-
事件委托
- 事件委托的优势:
- 内存高效:只需一个监听器。
- 自动适配动态内容:新添加的
- 无需额外绑定。
- 代码简洁:逻辑集中,易于维护。
- 阻止冒泡:Event 对象的 stopPropagation() 函数,可以阻止事件向其他元素传递。
利用事件冒泡机制,将监听器委托给父容器。父容器通过 event.target 判断实际被点击的子元素。
<ul id="list">
<li data-id="1">Item 1</li>
<li data-id="2">Item 2</li>
</ul>
document.getElementById('list').addEventListener('click', (event) => {
if (event.target.matches('li')) {
const id = event.target.dataset.id;
console.log(`Clicked item ${id}`);
}
});
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => {
console.log('Parent (Bubble)');
});
parent.addEventListener('click', () => {
console.log('Parent (Capture)');
}, true);
child.addEventListener('click', () => {
console.log('Child (Target)');
});
--- 输出结果 ---
Parent (Capture) // 捕获阶段
Child (Target) // 目标阶段
Parent (Bubble) // 冒泡阶段
window ↓ (捕获)
document ↓ (捕获)
<html> ↓ (捕获)
<body> ↓ (捕获)
<div> ← 监听器在此(冒泡阶段触发)
↓ (捕获)
<button>Click me</button> ← 事件目标
↓ (冒泡)
</div>
↓ (冒泡)
<body>
↓ (冒泡)
<html>
↓ (冒泡)
document
↓ (冒泡)
window
本地存储
- Cookies:
- Secure 属性的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,有助于防范中间人攻击。
- HttpOnly 属性的 cookie 无法被 Document.cookie API 访问,此类 Cookie 仅作用于服务器。有助于缓解跨站点脚本(XSS)攻击
- Domain 属性指定了哪些主机可以接受 Cookie。如果不指定,该属性默认为同一 host,不包含子域名。如果指定了 Domain,则一般包含子域名。
- Path 属性指定了一个 URL 路径,该 URL 路径必须存在于请求的 URL 中,以便发送 Cookie 标头。以字符 %x2F ('/') 作为路径分隔符,并且子路径也会被匹配。
- SameSite 属性允许服务器指定跨站点请求发送情况:
- 设置值 Strict cookie 仅发送到它来源的站点。
- 设置值 Lax 与 Strict 相似,只是在用户导航到 cookie 的源站点时发送 cookie。
- 设置值 None 指定浏览器会在同站请求和跨站请求下继续发送 cookie,但仅在安全的上下文中(即,如果 SameSite=None 还必须设置 Secure 属性)
- Web Storage API:
- sessionStorage:会话期间可用。
- localStorage:永久存储。
- Storage API 的操作都是同步的,会阻塞主页面。
- IndexedDB_API:
- IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。然而,不像 RDBMS 使用固定列表,IndexedDB 是一个基于 JavaScript 的面向对象数据库。IndexedDB 允许你存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。
JS 执行模型
- 域(Realm)是 JavaScript 执行环境的隔离单元,每一段 JavaScript 代码在加载时都会与一个域相关联,即使从另一个领域调用也不会改变。域由以下信息组成:
- 固有对象列表,如 Array、Array.prototype 等。
- 全局声明的变量、globalThis 的值以及全局对象。
- 模板字面数组的缓存,因为对同一标记的模板字面表达式的求值总是会导致标记接收到相同的数组对象。
Agent 是 ECMAScript 规范中定义的一个逻辑执行单元,代表一个独立的能够执行 JavaScript 代码的引擎实例。它维护着自己的代码执行设施:堆(对象)、队列(作业)、栈(执行上下文)。每个 Agent 可以拥有多个域(Realm),多个 Agent 可以通过共享内存进行通信,形成一个 Agent 集群。
事件循环
- 核心概念:
- 执行栈(Call Stack):存储当前正在执行的函数调用。JavaScript 是单线程的,同一时间只能执行一个函数。
- 堆(Heap):存放对象等动态分配的数据结构。
- 消息队列(Message Queue / Task Queue):存放待处理的异步回调函数。当 Web API(如 setTimeout、fetch)完成工作后,会将回调推入此队列。
- 任务优先级和分类
- 执行流程
- 从宏任务队列中取出第一个宏任务并执行。
- 执行过程中产生的所有微任务,都被推入微任务队列。
- 当前宏任务执行完毕后,立即清空微任务队列(全部执行完)。
- (浏览器环境)进行 UI 渲染(如果需要)。
- 开始下一次事件循环,回到步骤 1。
- 避免性能陷阱:queueMicrotask
- Window 接口的 queueMicrotask() 方法,可以向任务队列增加微任务。如果在微任务中不断添加新的微任务,会导致宏任务(如用户交互、UI 渲染)被无限期阻塞,造成页面卡死。
- 输出结果:1 → 5 → 4 → 2 → 3
- 宏任务 1(script 整体)开始执行。
console.log('1') → 输出 1。
- 遇到 setTimeout,将其回调(打印'2')交给 Web API,稍后推入宏任务队列。
- 遇到 Promise.resolve().then(...),将 then 回调(打印'4')推入微任务队列。
console.log('5') → 输出 5。
- 宏任务 1 执行完毕。
- 清空微任务队列。
- 执行微任务:
console.log('4') → 输出 4。
- (假设此时需要渲染)
- 下一次事件循环开始,宏任务 2(setTimeout 回调)开始执行。
console.log('2') → 输出 2。
- 执行过程中遇到新的 Promise.resolve().then(...),将其回调(打印'3')推入微任务队列。
- 宏任务 2 执行完毕。
- 清空微任务队列。
- 执行微任务:
console.log('3') → 输出 3。
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
});
console.log('5');
| 任务类型 | 常见来源 | 执行时机 |
|---|
| 宏任务 (Macrotask) | script 整体代码、setTimeout、setInterval、setImmediate (Node.js)、I/O、UI 渲染 | 每次事件循环只执行一个宏任务 |
| 微任务 (Microtask) | Promise.then/catch/finally、queueMicrotask()、MutationObserver | 在当前宏任务执行完毕后,UI 渲染之前,清空整个微任务队列 |
内存管理
- JavaScript 是在创建对象时自动分配内存,并在不再使用时自动释放内存(垃圾回收)。
- 垃圾回收算法:以前 JavaScript 使用引用计数垃圾回收算法,但是该算法无法解决循环引用的问题,现在浏览器都是标记清除垃圾回收器算法,过去几年中针对该算法的分代/增量/并发/并行垃圾回收等改进都是在该算法之上的优化。
- 引用计数垃圾回收算法:对象有个变量引用加 1,少一个变量引用减 1,当引用数量为 0,那么该对象称作'垃圾'或者可回收的。
- 标记清除垃圾回收器算法:法假定有一组叫做根的对象。在 JavaScript 中根是全局对象。垃圾回收器将定期从这些根开始,找到从这些根能引用到的所有对象,然后找到从这些对象能引用到的所有对象。从根开始,垃圾回收器将找到所有可到达的对象并收集所有不能到达的对象。
- 弱引用值:WeakMap 和 WeakSet 和非 weak 版的 Map 和 Set 功能一样,
- WeakMap 和 WeakSet 仅能存储对象或 symbol。这是因为仅对象是可垃圾回收的——原始值总是被复制的。
- WeakMap 和 WeakSet 是不可迭代的。
- WeakRef 是对象的弱引用。
- FinalizationRegistry 提供了一个更强的机制观察垃圾回收。它让你注册对象以及对象被垃圾回收时得到通知。
参考资料