普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月18日首页

面试官 : “请你讲一下 JS 的 《垃圾回收机制》 ? ”

作者 千寻girling
2026年1月18日 16:37

1. 垃圾回收到底是什么?

JavaScript 是自动内存管理的语言,你不用手动申请 / 释放内存(比如 C/C++ 需要 malloc/free),垃圾回收就是 JS 引擎(如 V8)自动做的两件事:

  • 找 “垃圾” :识别出程序中不再使用的变量 / 对象(占用的内存就是 “垃圾内存”);
  • 清垃圾:释放这些 “垃圾” 占用的内存,避免内存泄漏、提升性能。

举个简单例子:

function fn() {
  let num = 10; // 函数执行时,num 占用内存
}
fn(); // 函数执行完后,num 再也访问不到了 → 变成“垃圾”,GC 会回收它的内存

2. JS 怎么判断 “哪些是垃圾”?

GC 不是瞎回收的,核心判断标准是:一个对象 / 变量是否还能被 “访问到”(是否有引用指向它)

  • 能访问到 → 存活(不回收);
  • 访问不到 → 垃圾(会被回收)。

3. JS 垃圾回收的核心算法(V8 引擎为主)

不同 JS 引擎的 GC 算法略有差异,但核心是两种:标记 - 清除(主流)和引用计数(辅助 / 历史)。

算法 1:标记 - 清除(Mark-and-Sweep,现代引擎主流)

这是 V8 最核心的 GC 算法,分为 “标记” 和 “清除” 两步,逻辑很直观:

GC 启动

标记阶段:从根对象(如 window/global)出发,遍历所有可访问的对象,打上“存活”标记

清除阶段:遍历堆内存,清除所有没有“存活”标记的对象,释放内存

内存整理可选):将空闲内存碎片合并,方便后续分配

举个例子理解

// 根对象:window(浏览器环境)
let obj1 = { name: "John" }; // obj1 被 window 引用 → 标记为存活
let obj2 = obj1; // obj2 也引用 obj1 → 还是存活
obj1 = null; // 解除 obj1 的引用,但 obj2 还指向 → 仍存活
obj2 = null; // 所有引用都解除 → obj1 无法访问 → 标记为垃圾,下次 GC 清除

优点

  • 解决了引用计数的 “循环引用” 问题(下面会说);
  • 逻辑简单,效率高。

缺点

  • 清除后会产生内存碎片(比如内存里零散的空闲空间),但 V8 会通过 “内存整理” 优化。

算法 2:引用计数(Reference Counting,历史算法,已淘汰核心场景)

早期(如 IE8 之前)的算法,逻辑是:给每个对象记录 “被引用的次数”,次数为 0 就回收

  • 当对象被引用 → 计数 + 1;
  • 当引用解除 → 计数 - 1;
  • 计数 = 0 → 立即回收。

例子

let obj = { a: 1 }; // 引用计数 = 1
let obj2 = obj;     // 引用计数 = 2
obj = null;         // 引用计数 = 1(还不能回收)
obj2 = null;        // 引用计数 = 0 → 变成垃圾,被回收

致命缺点:无法处理循环引用(这也是它被标记 - 清除取代的核心原因):

// 循环引用:obj1 和 obj2 互相引用,引用计数都为 1,永远不会为 0
let obj1 = {};
let obj2 = {};
obj1.fn = obj2;
obj2.fn = obj1;

// 即使解除外部引用,计数仍为 1 → 引用计数算法不会回收,造成内存泄漏
obj1 = null;
obj2 = null;

👉 而标记 - 清除算法能解决这个问题:因为 obj1 / obj2 都无法从根对象访问到,会被标记为垃圾,最终回收。

4. V8 引擎的 GC 优化(进阶,面试高频)

V8 为了提升 GC 效率,还做了针对性优化,核心是 “分代回收”:

  • 将内存分为 新生代(Young Generation)老生代(Old Generation)

    • 新生代:存储短期存活的对象(如函数内部的临时变量),GC 频率高、速度快(用 “Scavenge 算法”,复制 - 清除);
    • 老生代:存储长期存活的对象(如全局变量),GC 频率低,用 “标记 - 清除 + 标记 - 整理” 算法。
  • 优点:避免对整个内存做全量 GC,减少卡顿(JS 是单线程,GC 时会暂停代码执行,分代回收能缩短暂停时间)。

5. 常见的内存泄漏场景(GC 没回收的 “伪垃圾”)

垃圾回收不是万能的,如果代码写得不好,会导致 “本该回收的对象没被回收”,也就是内存泄漏,常见场景:

  1. 意外的全局变量(最常见):

    function fn() {
      num = 10; // 没写 let/var/const → 自动挂载到 window → 全局变量,永远不回收
    }
    
  2. 未清除的定时器 / 事件监听

    // 定时器引用了 obj,即使页面关闭前不清除定时器,obj 永远存活
    let obj = { data: "xxx" };
    setInterval(() => { console.log(obj); }, 1000);
    // 解决:不用时 clearInterval(timer)
    
  3. 闭包滥用

    function outer() {
      let bigData = new Array(1000000); // 大数组
      return function() { // 闭包引用 bigData,outer 执行完后 bigData 也不回收
        console.log(bigData);
      };
    }
    let fn = outer();
    // 解决:不用时 fn = null,解除引用
    

最后总结 🤔

  1. 核心本质:JS 垃圾回收是引擎自动回收 “不可访问” 对象的内存,避免手动管理内存的繁琐和错误。
  2. 核心算法:现代引擎以标记 - 清除为主(解决循环引用),引用计数已淘汰核心场景。
  3. V8 优化:分代回收(新生代 + 老生代)减少 GC 卡顿,提升性能。
  4. 避坑重点:避免意外全局变量、未清除的定时器 / 监听、滥用闭包,防止内存泄漏。

| ES6 | 异步 | 闭包 | 原型链 | DOM操作 | 事件处理 |

作者 千寻girling
2026年1月17日 22:07

一、ES6+ 新特性

ES6(ECMAScript 2015)及后续的 ES7-ES14 被统称为 ES6+,是 JavaScript 语言的重大升级,解决了 ES5 时代的语法冗余、作用域混乱、功能缺失等问题,大幅提升了代码的可读性、可维护性和开发效率。

1. 块级作用域与变量声明

ES5 中只有全局作用域和函数作用域,var 声明的变量存在 “变量提升” 和 “作用域穿透” 问题,极易引发 bug。ES6 新增 letconst 关键字,引入块级作用域({} 包裹的区域):

  • let:声明可变变量,仅在当前块级作用域有效,无变量提升,不允许重复声明;
  • const:声明常量,一旦赋值不可修改(引用类型仅保证地址不变),同样遵循块级作用域规则。示例:
// ES5 问题:变量提升+作用域穿透
if (true) {
  var a = 10;
}
console.log(a); // 10(全局作用域可访问)

// ES6 解决
if (true) {
  let b = 20;
  const c = 30;
}
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined

2. 箭头函数

简化函数声明语法,核心特性:

  • 语法简洁:单参数可省略括号,单返回语句可省略大括号和 return
  • 无独立 this:箭头函数的 this 继承自外层作用域,解决了 ES5 中 this 指向混乱的问题(如回调函数中 this 丢失);
  • 不能作为构造函数:无法使用 new 调用,无 arguments 对象(可改用剩余参数)。示例:
// ES5 函数
const add = function(a, b) {
  return a + b;
};

// ES6 箭头函数
const add = (a, b) => a + b;

// this 指向示例
const obj = {
  name: "张三",
  fn1: function() {
    setTimeout(function() {
      console.log(this.name); // undefined(this 指向全局)
    }, 100);
  },
  fn2: function() {
    setTimeout(() => {
      console.log(this.name); // 张三(this 继承自 fn2 的作用域)
    }, 100);
  }
};
obj.fn1();
obj.fn2();

3. 解构赋值

允许从数组 / 对象中提取值,赋值给变量,简化数据提取逻辑:

  • 数组解构:按索引匹配,支持默认值;
  • 对象解构:按属性名匹配,支持重命名和默认值。示例:
// 数组解构
const [a, b, c = 30] = [10, 20];
console.log(a, b, c); // 10 20 30

// 对象解构
const { name: userName, age = 18 } = { name: "李四" };
console.log(userName, age); // 李四 18

4. 扩展运算符与剩余参数

  • 扩展运算符(...):将数组 / 对象展开为单个元素,用于合并数据、传递参数;
  • 剩余参数(...):收集剩余的参数,转为数组,替代 arguments。示例:
// 扩展运算符
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2]; // [1,2,3,4,5,6]

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { ...obj1, ...obj2 }; // {a:1, b:2}

// 剩余参数
const sum = (...args) => args.reduce((total, cur) => total + cur, 0);
console.log(sum(1,2,3)); // 6

5. 模板字符串

用反引号()包裹字符串,支持换行和变量插值(${变量}`),解决 ES5 字符串拼接繁琐的问题:

const name = "王五";
const age = 20;
// ES5 拼接
const str1 = "姓名:" + name + ",年龄:" + age + "岁";
// ES6 模板字符串
const str2 = `姓名:${name},年龄:${age}岁`;

6. 其他核心特性

  • Set/Map 数据结构:Set 用于存储唯一值(数组去重),Map 键值对集合(键可为任意类型,替代对象);
  • Class 类:语法糖,简化原型链继承,提供 constructorextendssuper 等关键字;
  • 模块化(import/export):替代 CommonJS/AMD,实现按需加载,提升代码模块化程度;
  • 可选链(?.)、空值合并(??):ES2020 特性,简化空值判断,避免 Cannot read property 'xxx' of undefined 错误。

ES6+ 新特性的核心价值在于 “语法简化” 和 “功能补全”,让 JavaScript 从 “脚本语言” 向 “工程化语言” 迈进,是现代前端开发(React/Vue/TypeScript)的基础。

二、异步(Promise, async/await)

JavaScript 是单线程语言,默认同步执行代码,但网络请求、定时器、文件操作等场景需要异步处理,否则会阻塞主线程。异步编程经历了 “回调函数 → Promise → async/await” 的演进,核心目标是解决 “回调地狱”,让异步代码更易读、易维护。

1. 异步编程的核心问题:回调地狱

ES5 中异步操作依赖回调函数,多个异步嵌套时会出现 “回调地狱”(代码层级深、可读性差、错误处理繁琐):

// 回调地狱:获取用户信息 → 获取用户订单 → 获取订单详情
$.get("/api/user", (user) => {
  $.get(`/api/order?userId=${user.id}`, (order) => {
    $.get(`/api/orderDetail?orderId=${order.id}`, (detail) => {
      console.log(detail);
    }, (err) => {
      console.error("获取订单详情失败", err);
    });
  }, (err) => {
    console.error("获取订单失败", err);
  });
}, (err) => {
  console.error("获取用户失败", err);
});

问题:层级嵌套过深,错误处理分散,代码难以调试和维护。

2. Promise:异步操作的标准化封装

Promise 是 ES6 引入的异步编程解决方案,本质是一个对象,代表异步操作的 “未完成 / 成功 / 失败” 状态,核心特性:

  • 三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败),状态一旦改变不可逆转;
  • 两个回调:then() 处理成功结果,catch() 处理失败结果,支持链式调用;
  • 解决回调地狱:通过链式调用替代嵌套,错误可统一捕获。

(1)Promise 基本用法

// 创建 Promise 对象
const getPromise = (url) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText)); // 成功:调用 resolve
      } else {
        reject(new Error(xhr.statusText)); // 失败:调用 reject
      }
    };
    xhr.onerror = () => {
      reject(new Error("网络请求失败"));
    };
    xhr.send();
  });
};

// 链式调用:解决回调地狱
getPromise("/api/user")
  .then((user) => getPromise(`/api/order?userId=${user.id}`))
  .then((order) => getPromise(`/api/orderDetail?orderId=${order.id}`))
  .then((detail) => console.log(detail))
  .catch((err) => console.error("请求失败", err)); // 统一捕获所有错误

(2)Promise 常用方法

  • Promise.all():接收多个 Promise 数组,全部成功才返回结果数组,一个失败则立即失败;
  • Promise.race():接收多个 Promise 数组,返回第一个完成的 Promise 结果(无论成功 / 失败);
  • Promise.resolve()/Promise.reject():快速创建成功 / 失败的 Promise 对象;
  • Promise.allSettled():等待所有 Promise 完成(无论成功 / 失败),返回所有结果(包含状态和值)。

示例(Promise.all):

// 同时请求多个接口,全部完成后处理
const promise1 = getPromise("/api/user");
const promise2 = getPromise("/api/goods");
Promise.all([promise1, promise2])
  .then(([user, goods]) => {
    console.log("用户信息", user);
    console.log("商品信息", goods);
  })
  .catch((err) => console.error("某个请求失败", err));

3. async/await:异步代码同步化

ES2017 引入的 async/await 是 Promise 的语法糖,允许用 “同步代码的写法” 处理异步操作,核心规则:

  • async 修饰函数:使函数返回一个 Promise 对象;
  • await 修饰 Promise:暂停函数执行,直到 Promise 状态变为成功,返回结果;若 Promise 失败,需用 try/catch 捕获错误。

(1)基本用法(解决回调地狱的终极方案)

// 封装异步请求函数(返回 Promise)
const getUser = () => getPromise("/api/user");
const getOrder = (userId) => getPromise(`/api/order?userId=${userId}`);
const getOrderDetail = (orderId) => getPromise(`/api/orderDetail?orderId=${orderId}`);

// async/await 写法:同步风格的异步代码
const getOrderInfo = async () => {
  try {
    const user = await getUser(); // 等待 getUser 完成
    const order = await getOrder(user.id); // 等待 getOrder 完成
    const detail = await getOrderDetail(order.id); // 等待 getOrderDetail 完成
    console.log(detail);
  } catch (err) {
    console.error("请求失败", err); // 统一捕获所有错误
  }
};

getOrderInfo();

(2)async/await 优势

  • 代码扁平化:无嵌套,可读性接近同步代码;
  • 错误处理统一:通过 try/catch 捕获所有异步错误,替代 Promise 的 catch()
  • 调试友好:可在 await 处打断点,调试流程与同步代码一致。

4. 异步编程的核心原则

  • 避免同步阻塞:异步操作始终不阻塞主线程(如定时器、网络请求由浏览器内核的线程处理);
  • 错误处理全覆盖:Promise 需加 catch(),async/await 需包 try/catch,避免未捕获的异步错误;
  • 并行处理优化:多个无依赖的异步操作,用 Promise.all() 替代串行 await,提升执行效率。

异步编程是前端开发的核心难点,Promise 解决了 “回调地狱” 的结构问题,async/await 则让异步代码的可读性达到了同步代码的水平,是现代前端处理网络请求、异步数据加载的标配。

三、闭包和原型链

闭包和原型链是 JavaScript 的两大核心特性,也是面试高频考点。闭包关乎作用域和变量生命周期,原型链则是 JavaScript 实现继承的底层机制,理解这两个概念能帮你突破 “语法使用” 到 “原理理解” 的瓶颈。

1. 闭包(Closure)

(1)闭包的定义

闭包是指 “有权访问另一个函数作用域中变量的函数”,本质是函数作用域链的保留:当内部函数被外部引用时,其所在的作用域不会被垃圾回收机制销毁,从而可以持续访问外层函数的变量。

(2)闭包的形成条件

  1. 存在嵌套函数(内部函数 + 外部函数);
  2. 内部函数引用外部函数的变量 / 参数;
  3. 外部函数执行后,内部函数被外部环境引用(如返回、赋值给全局变量)。

(3)基本用法与示例

// 基础闭包:外部函数执行后,内部函数仍能访问其变量
function outer() {
  const num = 10; // 外部函数的变量
  // 内部函数引用外部变量
  function inner() {
    console.log(num);
  }
  return inner; // 返回内部函数,使其被外部引用
}

const fn = outer(); // outer 执行完毕,但其作用域未被销毁
fn(); // 10(inner 仍能访问 num)

(4)闭包的核心应用场景

  • 封装私有变量:模拟 “私有属性 / 方法”,避免全局变量污染;

    // 封装计数器:count 是私有变量,只能通过方法修改
    function createCounter() {
      let count = 0;
      return {
        increment: () => count++,
        decrement: () => count--,
        getCount: () => count
      };
    }
    
    const counter = createCounter();
    counter.increment();
    counter.increment();
    console.log(counter.getCount()); // 2
    console.log(counter.count); // undefined(无法直接访问)
    
  • 防抖 / 节流函数:利用闭包保存定时器 ID、上次执行时间等状态;

    // 防抖函数(闭包保存 timer 变量)
    function debounce(fn, delay) {
      let timer = null; // 闭包保存 timer,多次调用共享同一个 timer
      return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, args);
        }, delay);
      };
    }
    
  • 柯里化函数:将多参数函数转为单参数函数,利用闭包缓存已传入的参数。

(5)闭包的注意事项

  • 内存泄漏风险:闭包会保留外层作用域,若长期引用未释放(如赋值给全局变量),会导致变量无法被垃圾回收,占用内存;
  • 解决:使用完闭包后,手动解除引用(如 fn = null),让作用域可以被回收。

2. 原型链(Prototype Chain)

JavaScript 是 “基于原型的面向对象语言”,没有类(ES6 Class 是语法糖),所有对象都通过 “原型” 实现属性和方法的继承,原型链是实现继承的核心机制。

(1)核心概念

  • 原型(prototype):函数特有的属性,指向一个对象,该对象是当前函数创建的所有实例的原型;
  • 隐式原型(__proto__):所有对象(包括函数)都有的属性,指向其构造函数的 prototype
  • 原型链:当访问对象的属性 / 方法时,先在自身查找,找不到则通过 __proto__ 向上查找,直到 Object.prototype,这个查找链条就是原型链。

(2)原型链的基本结构

// 构造函数
function Person(name) {
  this.name = name;
}
// 给原型添加方法
Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};

// 创建实例
const p1 = new Person("张三");

// 原型链查找:p1 → Person.prototype → Object.prototype → null
console.log(p1.name); // 自身属性,直接返回
p1.sayHello(); // p1 自身无 sayHello,查找 p1.__proto__(Person.prototype)找到
console.log(p1.toString()); // p1 和 Person.prototype 无 toString,查找 Object.prototype 找到
console.log(p1.xxx); // 原型链末端为 null,返回 undefined

(3)原型链的核心应用:继承

ES5 中通过修改原型链实现继承(ES6 Class 的 extends 底层仍是原型链):

// 父类
function Parent(name) {
  this.name = name;
}
Parent.prototype.eat = function() {
  console.log(`${this.name} 吃饭`);
};

// 子类
function Child(name, age) {
  Parent.call(this, name); // 继承父类实例属性
  this.age = age;
}
// 继承父类原型方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修正构造函数指向

// 子类添加自有方法
Child.prototype.run = function() {
  console.log(`${this.name} 跑步,年龄 ${this.age}`);
};

const child = new Child("李四", 10);
child.eat(); // 继承父类方法
child.run(); // 子类自有方法

(4)原型链的关键规则

  • 所有对象的最终原型是 Object.prototype,其 __proto__null
  • 函数的 prototype 是普通对象,Function.prototype 是函数(特殊);
  • 修改原型会影响所有实例(原型共享特性)。

3. 闭包与原型链的关联

闭包关注 “作用域和变量保留”,原型链关注 “对象属性继承”,二者共同构成 JavaScript 的核心底层逻辑:闭包让函数可以突破作用域限制访问变量,原型链让对象可以突破自身结构继承方法,是理解 JavaScript 设计思想的关键。

四、DOM 操作和事件处理

DOM(文档对象模型)是浏览器将 HTML 文档解析成的树形结构,前端开发的核心是通过 JavaScript 操作 DOM 实现页面交互,事件处理则是响应用户操作(点击、输入、滚动等)的核心机制。

1. DOM 操作

DOM 操作分为 “查找节点”“创建 / 插入节点”“修改节点”“删除节点” 四类,核心是操作 DOM 树的节点(元素节点、文本节点、属性节点)。

(1)查找 DOM 节点(核心)

查找是 DOM 操作的第一步,常用方法:

  • 按 ID 查找:document.getElementById("id") → 返回单个元素(效率最高);
  • 按类名查找:document.getElementsByClassName("className") → 返回 HTMLCollection(动态集合);
  • 按标签名查找:document.getElementsByTagName("tagName") → 返回 HTMLCollection;
  • 按选择器查找:document.querySelector("selector")(返回第一个匹配元素)、document.querySelectorAll("selector")(返回 NodeList,静态集合)→ 最灵活,支持 CSS 选择器。

示例:

// 按 ID 查找
const box = document.getElementById("box");

// 按选择器查找
const item = document.querySelector(".list .item");
const items = document.querySelectorAll(".list .item"); // NodeList 可通过 forEach 遍历

(2)创建与插入节点

动态生成页面内容的核心,常用方法:

  • 创建元素:document.createElement("tagName")

  • 创建文本节点:document.createTextNode("text")

  • 插入节点:

    • parent.appendChild(child):将子节点插入父节点末尾;
    • parent.insertBefore(newNode, referenceNode):将新节点插入参考节点之前;
    • element.innerHTML:直接通过 HTML 字符串插入节点(简洁但有 XSS 风险)。

示例:

// 创建元素并插入
const ul = document.querySelector("ul");
const li = document.createElement("li");
li.textContent = "新列表项"; // 设置文本内容(安全,无 XSS)
ul.appendChild(li);

// innerHTML 方式(慎用,避免用户输入内容)
ul.innerHTML += "<li>新列表项</li>";

(3)修改 DOM 节点

  • 修改属性:element.setAttribute("attr", "value")(设置属性)、element.getAttribute("attr")(获取属性)、element.removeAttribute("attr")(移除属性);

    const img = document.querySelector("img");
    img.setAttribute("src", "new.jpg");
    console.log(img.getAttribute("src")); // new.jpg
    
  • 修改样式:

    • 行内样式:element.style.cssProperty = "value"(驼峰命名,如 backgroundColor);
    • 类名样式:element.classList.add("className")element.classList.remove("className")element.classList.toggle("className")(推荐,分离样式和逻辑)。
    const div = document.querySelector(".box");
    div.style.width = "200px";
    div.classList.add("active"); // 添加类名
    div.classList.toggle("show"); // 切换类名
    
  • 修改文本 / HTML:element.textContent(纯文本,安全)、element.innerHTML(HTML 字符串,有 XSS 风险)。

(4)删除 DOM 节点

  • parent.removeChild(child):父节点移除子节点;
  • element.remove():元素自身移除(ES6+ 方法,更简洁)。

示例:

const li = document.querySelector("li");
li.parentElement.removeChild(li); // 传统方式
// 或
li.remove(); // 简洁方式

(5)DOM 操作的性能优化

DOM 操作是 “重操作”,频繁修改会触发浏览器重排(Reflow)/ 重绘(Repaint),导致页面卡顿,优化手段:

  • 批量操作:先将节点脱离文档流(如隐藏父节点),操作完成后再恢复;

  • 使用文档碎片:document.createDocumentFragment(),批量插入节点仅触发一次重排;

    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 1000; i++) {
      const li = document.createElement("li");
      li.textContent = `项 ${i}`;
      fragment.appendChild(li); // 先插入碎片,无重排
    }
    document.querySelector("ul").appendChild(fragment); // 仅一次重排
    
  • 避免频繁查询 DOM:将查询结果缓存到变量,减少 DOM 遍历。

2. 事件处理

事件是浏览器触发的 “信号”(如点击、输入、加载),事件处理是 JavaScript 响应用户操作的核心,分为 “事件绑定”“事件流”“事件对象”“事件优化” 四部分。

(1)事件绑定方式

  • 行内绑定(不推荐):<button onclick="handleClick()">点击</button> → 耦合度高,不利于维护;

  • DOM0 级绑定:element.onclick = function() {} → 简单,但一个事件只能绑定一个处理函数;

  • DOM2 级绑定:element.addEventListener("eventName", handler, useCapture) → 推荐,支持绑定多个处理函数,可控制事件阶段;

  • DOM0 级:浏览器原生支持,无官方规范 → element.onclick = function() {}

  • DOM1 级:仅规范 DOM 结构,未新增事件绑定方式 → 无事件相关内容

  • DOM2 级:W3C 发布标准,新增 addEventListener → 支持多绑定、事件阶段

  • DOM3 级:在 DOM2 基础上新增了更多事件类型(如键盘、鼠标滚轮事件)

const btn = document.querySelector("button");
// DOM0 级
btn.onclick = function() {
  console.log("点击1");
};
btn.onclick = function() {
  console.log("点击2"); // 覆盖上一个处理函数
};

// DOM2 级
const handleClick = () => console.log("点击1");
btn.addEventListener("click", handleClick);
btn.addEventListener("click", () => console.log("点击2")); // 可绑定多个
btn.removeEventListener("click", handleClick); // 可移除

(2)事件流(事件传播机制)

事件流分为三个阶段:

  1. 捕获阶段:事件从 document 向下传播到目标元素;
  2. 目标阶段:事件到达目标元素;
  3. 冒泡阶段:事件从目标元素向上传播到 document

addEventListener 的第三个参数 useCapturetrue 表示在捕获阶段触发,false(默认)表示在冒泡阶段触发。

(3)事件对象(Event)

事件处理函数的第一个参数是事件对象,包含事件的核心信息:

  • event.target:触发事件的原始元素(事件源);
  • event.currentTarget:绑定事件的元素;
  • event.preventDefault():阻止默认行为(如表单提交、链接跳转);
  • event.stopPropagation():阻止事件传播(冒泡 / 捕获);
  • event.stopImmediatePropagation():阻止事件传播,且阻止当前元素后续的事件处理函数执行。

示例:

// 阻止链接跳转
const a = document.querySelector("a");
a.addEventListener("click", (e) => {
  e.preventDefault(); // 阻止默认跳转
  console.log("点击链接,不跳转");
});

// 事件委托(利用事件冒泡)
const ul = document.querySelector("ul");
ul.addEventListener("click", (e) => {
  if (e.target.tagName === "LI") { // 判断点击的是 li 元素
    console.log("点击了列表项", e.target.textContent);
  }
});

(4)核心优化:事件委托

利用事件冒泡,将子元素的事件绑定到父元素,减少事件绑定数量,优化性能(尤其适合动态生成的元素):

// 动态生成的 li 无需单独绑定事件,父元素 ul 委托处理
const ul = document.querySelector("ul");
ul.addEventListener("click", (e) => {
  if (e.target.classList.contains("item")) {
    console.log("点击了动态生成的列表项");
  }
});

// 动态添加 li
const li = document.createElement("li");
li.classList.add("item");
li.textContent = "动态项";
ul.appendChild(li);

(5)常见事件类型

  • 鼠标事件:clickdblclickmouseovermouseoutmousedownmouseup
  • 键盘事件:keydownkeyupkeypress
  • 表单事件:inputchangesubmitfocusblur
  • 页面事件:loadDOMContentLoaded(DOM 解析完成)、scrollresize

DOM 操作和事件处理是前端交互的基础,核心原则是 “减少 DOM 操作次数”“合理利用事件机制”,既保证交互的流畅性,又避免性能问题。

总结

  1. ES6+ 新特性核心是简化语法、补全功能,是现代前端开发的基础,重点掌握块级作用域、箭头函数、解构、async/await 等高频用法;
  2. 异步编程从回调地狱演进到 Promise/async/await,核心是让异步代码更易读、易维护,async/await 是当前最优写法;
  3. 闭包是作用域链的保留,用于封装私有变量、实现防抖节流,需注意内存泄漏;原型链是 JS 继承的底层机制,所有对象通过 __proto__ 形成继承链条;
  4. DOM 操作需注重性能(批量操作、文档碎片),事件处理核心是事件委托,利用冒泡减少绑定数量,提升页面性能。
❌
❌