【JS DOM】告别 jQuery:原生 JS DOM 操作神技(closest, matches, classList)

所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【CSS 优化】一行代码提升用户体验:你必须知道的几个 CSS 小属性
作者: 码力无边


引言:那一年,我们都曾是 “$” 的信徒

嘿,各位前端道友,我是码力无边。欢迎回到我们的修炼道场!

在过去的几篇文章里,我们一直在 CSS 的奇妙世界里遨游,用各种神兵利器打磨页面的“血肉”与“筋骨”。但一个真正有生命的网页,离不开 JavaScript 赋予它的“灵魂”——交互。而交互的核心,就是对 DOM (文档对象模型) 的操作。

请允许我带大家进行一次“考古”。在遥远的“上古时代”(大概是 10 年前),当我们要对 DOM 做点什么时,我们的第一反应是什么?

// 引入一个神奇的库
<script src="jquery.min.js"></script>

// 然后,我们开始念动咒语...
$('#myButton').on('click', function() {
  $(this).addClass('active').siblings().removeClass('active');
  var parentCard = $(this).closest('.card');
  parentCard.find('.card-title').text('我被点击了!');
  if (parentCard.is(':visible')) {
    // ... do something
  }
});

$,这个简洁而强大的符号,曾是无数前端开发者的信仰。jQuery 以其无与伦比的链式调用、简洁的 API 和强大的浏览器兼容性,统治了一个时代。它让我们这些“切图仔”也能轻松地写出复杂的交互逻辑。

然而,时代变了。IE 这座大山已经(基本)被移走,现代浏览器纷纷遵循 W3C 标准,原生 JavaScript 的能力得到了前所未有的增强。过去那些需要 jQuery 才能轻松实现的操作,现在用原生 JS 也能优雅地完成,而且无需加载任何外部库,性能更高,代码也更符合现代前端的开发范式。

今天,码力无边就要带大家“破除迷信”,学习几招原生 JS 的 DOM 操作“真传”,它们分别是:

  • element.closest():向上查找的“导航仪”。
  • element.matches():判断元素的“验身神器”。
  • element.classList:管理样式的“贴身管家”。

掌握它们,你就能在很多场景下自信地对 jQuery 说一声:“谢谢你,再见!”,迈入更轻量、更现代的原生 JS 开发新纪元。

一、element.closest(selector) —— 寻找祖先的“GPS”

这是我个人认为最能直接替代 jQuery 核心功能之一的 API。

1.1 jQuery 的经典场景:事件委托

事件委托是前端性能优化的一个重要技巧。我们不给每个子元素都绑定事件,而是把事件监听器绑定在它们的父元素上,然后通过判断事件源(event.target)来执行相应的逻辑。

在 jQuery 中,我们通常这样做:

$('.card-list').on('click', '.card-button', function(event) {
  // 这里的 `this` 已经是被匹配的 '.card-button' 元素
  var card = $(this).closest('.card');
  // ... 对 card 进行操作
});

这个 .closest('.card') 非常方便,它能从当前点击的按钮开始,向上查找第一个匹配 .card 选择器的祖先元素。

1.2 原生 JS 的优雅替代

在原生 JS 中,我们如何实现同样的效果?答案就是 element.closest()

<div class="card-list">
  <div class="card">
    <p>Card 1</p>
    <button class="card-button">Click Me</button>
  </div>
  <div class="card">
    <p>Card 2</p>
    <button class="card-button">Click Me</button>
  </div>
  <div class="card">
    <p>Card 3 <span>(I'm inside a span) <button class="card-button">Click Me</button></span></p>
  </div>
</div>
const cardList = document.querySelector('.card-list');

cardList.addEventListener('click', (event) => {
  // event.target 是用户实际点击的那个元素,可能是 button,也可能是 button 里的 span
  
  // 使用 closest() 来找到我们真正关心的那个按钮
  const button = event.target.closest('.card-button');
  
  // 如果点击的不是按钮或其子元素,button 会是 null
  if (!button) {
    return; 
  }
  
  // 找到了按钮,现在再用 closest() 去找它所在的卡片
  const card = button.closest('.card');
  
  if (card) {
    card.style.backgroundColor = 'lightblue';
    console.log(`你点击了 ${card.querySelector('p').innerText} 里的按钮`);
  }
});

closest() 的神奇之处:

  • 从自身开始查找:它会先检查当前元素(event.target)是否匹配选择器,如果匹配,就直接返回自身。
  • 一路向上:如果不匹配,它会像爬楼梯一样,一层一层地检查父元素、祖父元素…直到找到第一个匹配的祖先,或者到达文档的根节点(<html>)都没找到,此时它会返回 null

这种行为完美地解决了事件委托中的一个核心痛点:用户可能点击的是目标元素内部的子元素(比如按钮上的图标或文字 <span>)。通过 event.target.closest('.card-button'),我们总能准确地定位到我们想要操作的那个可交互的父级元素,而不用写一堆 if (event.target.nodeName === 'BUTTON' || event.target.parentElement.nodeName === 'BUTTON') 这样的丑陋代码。

二、element.matches(selector) —— “你是不是我要找的人?”

有时候,我们不想查找祖先,我们只想知道当前这个元素本身是否匹配某个 CSS 选择器。

2.1 jQuery 的 .is()

在 jQuery 中,.is() 方法非常方便:

if ($(this).is('.active')) {
  // ...
}
if ($(event.target).is('input[type="checkbox"]')) {
  // ...
}
2.2 原生 JS 的 matches()

原生 JS 提供了几乎完全一样的功能,它就是 element.matches()。它接受一个 CSS 选择器作为参数,如果当前元素匹配该选择器,就返回 true,否则返回 false

让我们重写上面的事件委托例子,用一种不同的逻辑:

cardList.addEventListener('click', (event) => {
  let target = event.target;
  
  // 如果当前点击的元素不是 .card,就一直向上找,直到找到 .card 或者到顶
  while (target && target !== cardList) {
    // 关键:判断当前遍历到的元素是不是我们要找的按钮
    if (target.matches('.card-button')) {
      const card = target.closest('.card');
      card.style.backgroundColor = 'lightgreen';
      console.log('通过 matches 找到了按钮!');
      
      // 找到并处理后,可以停止循环
      break; 
    }
    target = target.parentElement;
  }
});

虽然在这个特定场景下,closest() 更简洁,但 matches() 在其他地方大放异彩。比如,在一个复杂的表单里,你想根据点击的元素类型做不同的事:

form.addEventListener('click', (event) => {
  const target = event.target;
  
  if (target.matches('input[type="submit"]')) {
    console.log('提交按钮被点击了!');
  } else if (target.matches('.form-help-icon')) {
    console.log('帮助图标被点击了!');
    showHelpTooltip(target);
  } else if (target.matches('a.external-link')) {
    if (!confirm('你将要离开本站,确定吗?')) {
      event.preventDefault();
    }
  }
});

matches() 让你的 if/else 逻辑变得像写 CSS 选择器一样清晰和强大。

三、element.classList —— 优雅的 class 管理器

在前端开发中,我们 90% 的动态样式变化,都是通过添加、删除或切换 CSS 类名来实现的。

3.1 jQuery 的 class 操作

jQuery 的 addClass(), removeClass(), toggleClass(), hasClass() 简单好用,深入人心。

var $el = $('#myElement');
$el.addClass('active highlight');
$el.removeClass('old-class');
$el.toggleClass('is-visible');
if ($el.hasClass('some-class')) {
  // ...
}
3.2 原生 JS 的 classList 对象

原生 JS 为每个元素提供了一个 classList 属性。它是一个 DOMTokenList 对象,提供了比直接操作 className 字符串安全、方便得多的方法。

classList 就像一个 class 的“贴身管家”,它有以下常用方法:

  • add(...classNames): 添加一个或多个类名。
  • remove(...classNames): 删除一个或多个类名。
  • toggle(className, [force]): 切换一个类名。如果存在就删除,不存在就添加。如果提供了第二个参数 force (布尔值),true 表示强制添加,false 表示强制删除。
  • contains(className): 检查是否存在某个类名,返回 truefalse
  • replace(oldClass, newClass): 替换一个已有的类。

让我们用 classList 来实现一个经典的“手风琴”效果:

<div class="accordion">
  <div class="accordion-item">
    <div class="accordion-header">第一项</div>
    <div class="accordion-content">内容一...</div>
  </div>
  <div class="accordion-item active">
    <div class="accordion-header">第二项</div>
    <div class="accordion-content" style="display:block;">内容二...</div>
  </div>
</div>
const accordion = document.querySelector('.accordion');

accordion.addEventListener('click', (event) => {
  const header = event.target.closest('.accordion-header');
  if (!header) return;
  
  const currentItem = header.parentElement;
  
  // 切换当前点击项的 active 类
  currentItem.classList.toggle('active');
  
  // (可选) 实现“只打开一个”的逻辑
  const allItems = accordion.querySelectorAll('.accordion-item');
  allItems.forEach(item => {
    // 如果不是当前点击的项,就强制移除 active 类
    if (item !== currentItem) {
      item.classList.remove('active');
    }
  });
});

在这个例子里,classList.toggle()classList.remove() 让我们能够精确、安全地控制类名,而不用去拼接或切割 className 字符串,代码既清晰又健壮。

写在最后:拥抱原生,回归本质

jQuery 是一个伟大的先行者,它在那个混沌的年代为我们披荆斩棘,统一了江湖。我们应该感谢它,但不必再迷恋它。

随着浏览器标准的统一和原生 API 的日益强大,closest, matches, classList 只是冰山一角。还有 querySelector/querySelectorAll, forEach (NodeList), fetch API, Promises, async/await… 整个原生 JavaScript 生态已经变得异常强大和便捷。

转向原生 JS,不仅仅是为了那一点点性能提升,或减少一个 HTTP 请求。更重要的是,它让我们回归了 Web 开发的本质,让我们更深入地理解浏览器是如何工作的。它让我们写的代码更符合平台标准,更经得起时间的考验。

所以,道友们,从现在开始,在你下一个项目中,当你想用 $ 的时候,请先停下来想一想:这个功能,原生 JS 是不是已经有了更优雅的实现?

相信我,当你敲下 element.closest() 并看到它完美工作时,那种“原来我也行”的喜悦,会让你对前端开发有更深的理解和热爱。


专栏预告与互动:

DOM 操作的效率提升了,但 JS 的代码逻辑本身如何写得更“骚”?数组是我们在 JS 中处理最多的数据结构,你真的精通它的所有玩法吗?

下一篇,我们将深入 JS 数组的“瑞士军刀”——Array.reduce(),探索它除了求和之外,还有哪些能让你同事惊呼“卧槽”的神仙用法,比如数据转换、数组去重、甚至实现 pipe 函数!

觉得码力无边的“真传”对你有用?别忘了点赞、收藏、关注,为我注入下一篇的创作真气!

今日话题: 在你过去的开发经历中,有没有哪个 jQuery 的功能是你觉得最“难以割舍”的?分享出来,我们一起看看原生 JS 有没有完美的替代方案!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐