DOM 中 closest 方法的作用是什么?举个例子
closest() 是 DOM 元素方法,从自身开始沿父链向上查找第一个匹配 CSS 选择器的元素。
- 查找方向:自身 → 父 → 祖父 →
<html>,找到即返回,否则返回null - 包含自身:当前元素就匹配时直接返回它,不是从父级才开始
- 接受任意选择器:标签、类、属性、伪类、复合选择器都支持
- 核心场景:事件委托、点击外部关闭、从子节点反查所属组件根
- 替代写法:等价于手写
while (el && !el.matches(sel)) el = el.parentElement
最佳实践:原生事件委托里只要涉及"从点击目标反查父级",优先用 closest(),别再手写 parentElement 循环。IE 不支持,老项目需 polyfill。
它解决的问题
写交互代码时经常遇到这种情况:列表里有很多卡片,卡片内部又有图标、文字、按钮。用户点的是图标,但你需要的是这张卡片的 id。
直接用 event.target 拿到的是图标本身,还得手动往上爬几层 DOM 才能找到卡片。closest() 就是把"往上爬到某个特征节点"封装成一次方法调用。
签名很简单:
element.closest(selectors: string): Element | null
传入 CSS 选择器,返回最近的匹配祖先(含自身),找不到返回 null。
一个真实的例子:删除待办
页面上一个待办列表,每项有删除按钮:
<ul class="todo-list">
<li class="todo-item" data-id="1">
<span class="todo-text">买菜</span>
<button class="todo-delete">
<svg class="icon"><!-- 垃圾桶图标 --></svg>
</button>
</li>
<li class="todo-item" data-id="2">
<span class="todo-text">写博客</span>
<button class="todo-delete">
<svg class="icon"></svg>
</button>
</li>
<!-- 更多 li ... -->
</ul>
不用 closest() —— 事件委托要手写两段循环:
document.querySelector('.todo-list')!.addEventListener('click', (e) => {
let el = e.target as HTMLElement | null;
// 一路往上找按钮
while (el && !el.classList.contains('todo-delete')) {
el = el.parentElement;
}
if (!el) return;
// 再一路往上找 li
let li = el as HTMLElement | null;
while (li && !li.classList.contains('todo-item')) {
li = li.parentElement;
}
if (!li) return;
const id = li.dataset.id;
deleteTodo(id);
});
用 closest():
document.querySelector('.todo-list')!.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
// 一行判断:点击是否落在删除按钮里
if (!target.closest('.todo-delete')) return;
// 一行拿到所属的 li
const li = target.closest<HTMLLIElement>('.todo-item');
if (!li) return;
deleteTodo(li.dataset.id);
});
两段代码做同一件事,下面这段更短、更不容易出 bug。点 <svg>、点 <button> 边缘、点 <li> 上的文字都能正确触发,因为 closest() 会从事件目标开始一路往上找,直到匹配为止。
包含自身这点很关键
很多人以为 closest() 是"找祖先",其实它是"从自己开始找最近的匹配元素"。这两个语义有差别:
const li = document.querySelector('.todo-item')!;
li.closest('.todo-item'); // 返回 li 自己,不是 null
li.closest('ul'); // 返回外层 <ul>
li.closest('body > div'); // 也支持复合选择器
实践中这点非常有用。比如"点击任意位置关闭弹窗,但点弹窗内部不关闭":
document.addEventListener('click', (e) => {
// 如果点击发生在 .modal 内部(含 .modal 本身),不关闭
if ((e.target as HTMLElement).closest('.modal')) return;
closeModal();
});
如果 closest() 不包含自身,就得写成 target.matches('.modal') || target.closest('.modal'),啰嗦且容易漏。
和其它 API 的区别
容易混淆的几个:
| API | 方向 | 包含自身 | 返回 |
|---|---|---|---|
closest(sel) | 自身 → 祖先 | 是 | 第一个匹配元素或 null |
matches(sel) | 只看自身 | — | 布尔值 |
querySelector(sel) | 自身 → 后代 | 否(只看后代) | 第一个匹配元素或 null |
parentElement | 直接父级 | 否 | 父元素或 null |
一句话记忆:querySelector 往下找,closest 往上找,matches 只问自己。
性能与边界
- 原生方法比手写
while略快,但差距可忽略,真正的价值是可读性 - 选择器越复杂匹配越慢,但通常都是几层 DOM 的事,毫秒级以下
- 对非元素节点要小心:
event.target在 SVG 内部、Text 节点上可能不是Element。React 通常没问题;原生事件里如果不确定,先做类型判断或先.parentElement提升一下 - 不支持 IE,需要 polyfill:
if (!Element.prototype.closest) {
Element.prototype.closest = function (s: string) {
let el: Element | null = this;
while (el && el.nodeType === 1) {
if (el.matches(s)) return el;
el = el.parentElement;
}
return null;
};
}
在 React/Vue 里还有用吗
有用,但场景变窄了。框架里大部分时候直接把 onClick 绑到元素上,不再需要事件委托。但这些场景仍然依赖 closest():
- 点击外部关闭(弹窗、下拉菜单、Popover),判断点击是否落在某个 ref 内
- 拖拽,从
mousedown的 target 反查"可拖动的那一层" - 富文本编辑器,从选区节点反查所在的块级元素(段落、列表项)
- 第三方库/原生集成,比如在 D3 渲染的 SVG 上做交互
记住它的本质:给你一个元素,帮你回答"它(或它的某个祖先)属于哪一类组件"。浏览器内置的一把上行扫描尺,遇到 DOM 反查的需求先想到它,能省下大量缠绕的 parentElement 链。