Skip to main content

DOM 中 closest 方法的作用是什么?举个例子

· 5 min read

closest() 是 DOM 元素方法,从自身开始沿父链向上查找第一个匹配 CSS 选择器的元素。

  1. 查找方向:自身 → 父 → 祖父 → <html>,找到即返回,否则返回 null
  2. 包含自身:当前元素就匹配时直接返回它,不是从父级才开始
  3. 接受任意选择器:标签、类、属性、伪类、复合选择器都支持
  4. 核心场景:事件委托、点击外部关闭、从子节点反查所属组件根
  5. 替代写法:等价于手写 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 链。