跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScriptNode.js大前端

JavaScript 核心概念与机制速通

综述由AI生成系统讲解了 JavaScript 的核心概念,涵盖面向对象(原型链、class、属性定义)、作用域与闭包、Proxy 代理、模块系统、异步编程(回调、Promise、async/await、Workers)以及浏览器渲染机制、DOM 事件模型、执行模型、事件循环和内存管理等关键知识点。通过代码示例和对比分析,帮助开发者深入理解 JS 底层原理与最佳实践。

云间漫步发布于 2026/3/28更新于 2026/5/2529 浏览
JavaScript 核心概念与机制速通

面向对象

原型链

  • JavaScript 使用对象实现继承。所有对象都有一个称为原型的内置属性,它指向的也是一个对象,所以原型对象也会有它自己的原型对象,这个链条就叫原型链,原型链终止于拥有 null 作为原型的对象上。
  • 当你试图访问一个对象的属性时:如果在对象中找不到该属性,那么就会沿着原型链上的对象逐个搜索,直到找到该属性或者到达链的末端,仍然找不到则返回 undefined。

设置原型

// 创建字面量对象时通过__proto__
const a = { name: 'a', __proto__: { name: 'b' } }
// 使用 Object.create 创建对象时指定原型
const personPrototype = {
  greet() { console.log("hello!"); },
};
const carl = Object.create(personPrototype);
console.log(carl.greet()); // hello!
// 指定构造函数的 prototype
const personPrototype2 = {
  greet() { console.log(`你好,我的名字是 ${this.name}!`); },
};
function Person(name) { this.name = name; }
Object.assign(Person.prototype, personPrototype2);
// 或
// Person.prototype.greet = personPrototype2.greet;

自有属性:直接在对象中定义的属性,被称为自有属性。

const a = { : , : , : { : , :  } }

.(.(a, )); 
.(.(a, )); 
.(.(a, )); 
.(a.()); 
.(a.()); 
.(a.()); 
name
'a'
sex
'男'
__proto__
name
'b'
age
18
// 推荐使用 Object.hasOwn 检查自有属性
console
log
Object
hasOwn
'name'
// true:自有属性
console
log
Object
hasOwn
'age'
// false:非自有属性
console
log
Object
hasOwn
'sex'
// true:自有属性
console
log
hasOwnProperty
'name'
// true:自有属性
console
log
hasOwnProperty
'age'
// false:非自有属性
console
log
hasOwnProperty
'sex'
// true:自有属性

属性遮蔽:当一个对象自身拥有某个属性时,会遮蔽其原型链上同名的属性(由于原型链查找机制)。

const a = { name: 'a', __proto__: { name: 'b' } }
console.log(a.name); // 打印 a,这种现象叫属性遮蔽
console.log(a.__proto__.name); // 打印 b

内置的原型属性名称没有标准,但是实际上所有浏览器都使用 proto 作为原型属性名。访问原型的标准方法是 Object.getPrototypeOf()。

console.log(Object.getPrototypeOf(new Object()))

class

  • class 是一个创建原型链的语法糖,它让 JavaScript 的面向对象看起来更像经典的面向对象实现,在引擎底层仍然是使用的原型。

私有成员:私有属性和方法必须在类声明中定义,并且以 #开头,私有字段不能被 for…in、Object.keys()、JSON.stringify() 等访问。也不能在外部调用。

// 定义了 Person 类
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();
// 报错:属性 "#id" 在类 "Person" 外部不可访问,因为它具有专用标识符。
// console.log(person.#id); // 报错
// console.log(person.#countingMoney()); // 报错

构造函数使用 constructor 关键字来声明,在子类的构造函数中须先使用 super() 来调用父类的构造函数,并传递父类构造函数期望的参数。

// 定义了 Person 类
class Person {
  name;
  // 构造函数
  constructor(name) {
    this.name = name;
  }
  // 公共方法
  introduceSelf() {
    console.log(`嗨!我是 ${this.name}`);
  }
}
// 定义了 Professor 类,继承自 Person 方法
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, 我是你们 Psychology 课程的教授.
walsh.grade(); // 打印:随机的分数

定义对象属性

  • obj:操作的对象。

  • prop:操作的属性名。

  • descriptor:属性描述

  • 数据描述符:如果描述符没有 value、writable、get 和 set 中的任何一个,则被视为数据描述符。

通过 . 或者 [] 操作符设置属性等价于

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); // 打印 0
obj.age = 10;
console.log(obj.age); // 打印 10

添加一个只读属性

let obj = {};
Object.defineProperty(obj, 'id', {
  value: 123,
  writable: false, // 不可修改
  enumerable: true, // 可以被枚举
  configurable: false // 不能删除或重新定义
});
console.log(obj.id); // 打印 123
obj.id = 456;
console.log(obj.id); // 仍然打印 123
delete obj.id;
console.log(obj.id); // 仍然打印 123

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()); // 打印结果是 0.
counter1.increment(); // counter1 的私有变量 privateCounter 加 1.
counter1.increment(); // counter1 的私有变量 privateCounter 加 1.
console.log(counter1.value()); // 打印结果是 2.
counter1.decrement(); // counter1 的私有变量 privateCounter 减 1.
console.log(counter1.value()); // 打印结果是 1.
console.log(counter2.value()); // 打印结果是 0,counter2 的词法环境和 counter1 是互不影响的。

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'
};
// 拦截器处理器(Handler)
const handler = {
  // 1. get —— 拦截属性读取 (obj.prop 或 obj['prop'])
  get(target, prop, receiver) {
    console.log(`[get] 读取属性:${String(prop)}`);
    return Reflect.get(target, prop, receiver);
  },
  // 2. set —— 拦截属性赋值 (obj.prop = value)
  set(target, prop, value, receiver) {
    console.log(`[set] 设置属性:${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },
  // 3. has —— 拦截 in 操作符 ('prop' in obj)
  has(target, prop) {
    console.log(`[has] 检查属性是否存在:${String(prop)}`);
    return Reflect.has(target, prop);
  },
  // 4. deleteProperty —— 拦截 delete 操作 (delete obj.prop)
  deleteProperty(target, prop) {
    console.log(`[deleteProperty] 删除属性:${String(prop)}`);
    return Reflect.deleteProperty(target, prop);
  },
  // 5. ownKeys —— 拦截 Object.getOwnPropertyNames/ Symbols / keys() / for...in 的键枚举
  ownKeys(target) {
    console.log(`[ownKeys] 获取自身所有键`);
    return Reflect.ownKeys(target);
  },
  // 6. getOwnPropertyDescriptor —— 拦截 Object.getOwnPropertyDescriptor
  getOwnPropertyDescriptor(target, prop) {
    console.log(`[getOwnPropertyDescriptor] 获取属性描述符:${String(prop)}`);
    return Reflect.getOwnPropertyDescriptor(target, prop);
  },
  // 7. defineProperty —— 拦截 Object.defineProperty / defineProperties
  defineProperty(target, prop, descriptor) {
    console.log(`[defineProperty] 定义属性:${String(prop)}`, descriptor);
    return Reflect.defineProperty(target, prop, descriptor);
  },
  // 8. preventExtensions —— 拦截 Object.preventExtensions
  preventExtensions(target) {
    console.log(`[preventExtensions] 禁止扩展对象`);
    return Reflect.preventExtensions(target);
  },
  // 9. isExtensible —— 拦截 Object.isExtensible
  isExtensible(target) {
    console.log(`[isExtensible] 检查对象是否可扩展`);
    return Reflect.isExtensible(target);
  },
  // 10. getPrototypeOf —— 拦截 Object.getPrototypeOf / obj.__proto__
  getPrototypeOf(target) {
    console.log(`[getPrototypeOf] 获取原型`);
    return Reflect.getPrototypeOf(target);
  },
  // 11. setPrototypeOf —— 拦截 Object.setPrototypeOf
  setPrototypeOf(target, prototype) {
    console.log(`[setPrototypeOf] 设置原型`);
    return Reflect.setPrototypeOf(target, prototype);
  },
  // 12. apply —— 拦截函数调用 (仅当 target 是函数时有效)
  // 注意:本例 target 是普通对象,此方法不会被触发
  apply(target, thisArg, args) {
    console.log(`[apply] 调用函数`, { thisArg, args });
    return Reflect.apply(target, thisArg, args);
  },
  // 13. construct —— 拦截 new 操作 (仅当 target 是构造函数时有效)
  // 注意:本例 target 是普通对象,此方法不会被触发
  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 即可访问所有形状类。

聚合导出

// 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);
});

html 使用模块

<!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>

基本导入

// main.js
// 导入命名导出 - 必须使用大括号,且名称必须匹配
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); // 调用命名导出

基本导出

// mathUtils.js
// 命名导出 (Named Exports) - 可导出多个
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Calculator { ... }
// 默认导出 (Default Export) - 一个模块只能有一个
export default function multiply(a, b) { return a * b; }

旧规范(CommonJS)的本质区别

特性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。

Promise.reject(reason)

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(s) {
  console.log(s)
}); // 出错了

Promise.resolve()

Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo'))

Promise.any()

Promise.any([fetch('https://v8.dev/').then(() => 'home'), fetch('https://v8.dev/blog').then(() => 'blog'), fetch('https://v8.dev/docs').then(() => 'docs')]).then((first) => {
  // 只要有一个 fetch() 请求成功
  console.log(first);
}).catch((error) => {
  // 所有三个 fetch() 全部请求失败
  console.log(error);
});

Promise.allSettled()

const promises = [fetch('/api-1'), fetch('/api-2'), fetch('/api-3'),];
await Promise.allSettled(promises); // 三个请求都结束(无论成功还是失败)removeLoadingIndicator() 才会执行
removeLoadingIndicator();

Promise.race()

// 如果 5 秒之内 fetch 方法无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数。
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);

Promise.all()

const p = Promise.all([p1, p2, p3]);

Promise.prototype.finally()

promise
  .then(result => { ··· })
  .catch(error => { ··· })
  .finally(() => { ··· });

基本用法

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

// async 函数返回的是 Promise
async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}
// 50 毫秒以后,输出 hello world。
asyncPrint('hello world', 50);

workers

  • Worker 给了你在不同线程中运行某些任务的能力。
  • Workers 和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互,这意味着 workers 不能访问 DOM(窗口、文档、页面元素等等),也不能直接访问彼此的变量。
  • dedicated workers
  • shared workers
    • 可以由运行在不同窗口中的多个不同脚本共享。
    • 要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口)。
  • service workers
    • Service worker 的行为就像代理服务器,缓存资源以便于 web 应用程序可以在用户离线时工作。他们是渐进式 Web 应用的关键组件。

浏览器

渲染机制

  • 核心步骤:
    1. 第一步:处理 HTML 标记并构造 DOM 树。
      • 遇到
      • async:脚本异步下载,下载完立即执行(可能打断 HTML 解析)。
      • defer:脚本异步下载,等 HTML 解析完再按顺序执行。
    2. 第二步:处理 CSS 并构建 CSSOM 树。
      • CSSOM 是阻塞渲染的:在 CSSOM 构建完成前,页面不会渲染。
    3. 第三步:将 DOM 和 CSSOM 组合成渲染树。
      • 只包含需要显示的节点(不包括 display: none 的元素)。
    4. 第四步:计算布局。
      • 每个渲染树节点在窗口中的精确位置和大小,这个过程也叫回流。
      • DOM 结构变化、窗口大小改变、读取某些布局属性(如 offsetWidth)都会触发回流。
    5. 第五步:绘制。
      • 将渲染树转换为屏幕上的像素,包括颜色、边框、阴影等。
      • 拆分为多个图层(Layers),便于后续合成。
    6. 第六步:合成。
      • 将多个图层按正确顺序合成最终图像,利用 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">
<!-- 预加载关键 JS 模块 -->
<link rel="preload" as="script" href="critical.js">

DOM 事件模型

  1. 捕获阶段(Capturing Phase):事件从 window 开始,逐级向下传递到目标元素的父级。此阶段默认不触发监听器。
  2. 目标阶段(Target Phase):事件到达目标元素本身。
  3. 冒泡阶段(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) => {
  // 检查实际被点击的元素是否是我们关心的 <li>
  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)');
});
// 设置为 true:捕获阶段触发
parent.addEventListener('click', () => {
  console.log('Parent (Capture)');
}, true);
child.addEventListener('click', () => {
  console.log('Child (Target)');
});
--- 输出结果 ---
Parent (Capture) // 捕获阶段
Child (Target) // 目标阶段
Parent (Bubble) // 冒泡阶段

W3C 标准定义的三阶段事件流:

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)完成工作后,会将回调推入此队列。
  • 任务优先级和分类
    • 执行流程
      1. 从宏任务队列中取出第一个宏任务并执行。
      2. 执行过程中产生的所有微任务,都被推入微任务队列。
      3. 当前宏任务执行完毕后,立即清空微任务队列(全部执行完)。
      4. (浏览器环境)进行 UI 渲染(如果需要)。
      5. 开始下一次事件循环,回到步骤 1。
  • 避免性能陷阱:queueMicrotask
    • Window 接口的 queueMicrotask() 方法,可以向任务队列增加微任务。如果在微任务中不断添加新的微任务,会导致宏任务(如用户交互、UI 渲染)被无限期阻塞,造成页面卡死。
    • 输出结果:1 → 5 → 4 → 2 → 3
      1. 宏任务 1(script 整体)开始执行。
        • console.log('1') → 输出 1。
        • 遇到 setTimeout,将其回调(打印'2')交给 Web API,稍后推入宏任务队列。
        • 遇到 Promise.resolve().then(...),将 then 回调(打印'4')推入微任务队列。
        • console.log('5') → 输出 5。
      2. 宏任务 1 执行完毕。
      3. 清空微任务队列。
        • 执行微任务:console.log('4') → 输出 4。
      4. (假设此时需要渲染)
      5. 下一次事件循环开始,宏任务 2(setTimeout 回调)开始执行。
        • console.log('2') → 输出 2。
        • 执行过程中遇到新的 Promise.resolve().then(...),将其回调(打印'3')推入微任务队列。
      6. 宏任务 2 执行完毕。
      7. 清空微任务队列。
        • 执行微任务: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 提供了一个更强的机制观察垃圾回收。它让你注册对象以及对象被垃圾回收时得到通知。

参考资料

  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes
  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Async_JS
  • https://developer.mozilla.org/zh-CN/docs/Web/Performance/Guides/Critical_rendering_path
  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Core/Scripting/Events
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Execution_model
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Memory_management
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Closures
  • https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide
  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
  • https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Cookies
  • https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Storage_API
  • https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
  • https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API

目录

  1. 面向对象
  2. 原型链
  3. class
  4. 定义对象属性
  5. 作用域
  6. 闭包
  7. Proxy
  8. 模块系统
  9. 异步编程
  10. 回调函数
  11. Promise
  12. async 函数
  13. workers
  14. 浏览器
  15. 渲染机制
  16. DOM 事件模型
  17. 本地存储
  18. JS 执行模型
  19. 事件循环
  20. 内存管理
  21. 参考资料
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 前端视角 | 从零搭建并启动若依后端(环境配置)
  • 基于 Next.js 和 Wagmi 构建支持 TokenP 钱包登录的 DApp 前端
  • 前端框架选型指南:React、Vue 与 Angular 对比分析
  • C++ 核心知识点解析(九)
  • LLaMA-Factory 详细安装与配置指南
  • PX4+ROS 无人机 Offboard 控制:模式解析与实战
  • KingbaseES 内核级 SQL 防火墙:白名单机制与性能实测
  • Spring AI 1.1.2 集成 MCP(Model Context Protocol)实战:以 Tavily 搜索为例
  • 使用 Python 和 WinRM 远程控制 Windows 服务器
  • 人工智能:自然语言处理在法律领域的应用与实战
  • Python EXE 解包工具实战:py2exe 与 pyinstaller 逆向
  • 2025年12月电子学会青少年软件编程Python三级等级考试真题
  • Qwen3Guard-Gen-WEB 全球多语言内容合规部署实测
  • HTML input 类型全解析与实战避坑指南
  • GLM-4.7-Flash 本地 Copilot 工具构建实战教程
  • FPGA 原型验证平台中 Vivado 许可证的动态加载方法
  • 基于Python的轻量级上位机开发流程解析
  • 低空无人机 AI 算法全景:74 种行业场景应用解析
  • 使用 VibeThinker 解决动态规划典型题例
  • 基于 CSANMT 的实时中英对照翻译服务实战

相关免费在线工具

  • 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