◆

JavaScript Element 元素 API:closest、getAttribute

2023-02-02 01:370

Element 是所有 HTML 元素节点的公共基类,我们平时通过 document.getElementById、querySelector 拿到的东西,本质上都是它的实例。日常业务里我们用得最多的其实只有 classList、innerHTML 这几个属性,但 Element 上还挂着一批很实用的方法:从某个深层节点向上查找祖先、读取元素上的任意属性、拿到元素相对视口的真实坐标。这些需求如果不知道有原生 API,很容易写成手动 while (el.parentNode) 的循环,或者去读 offsetTop 逐层累加,代码又长又容易在 position 上下文变化时出错。

这篇整理三个我用得最频繁的方法:closest()、getAttribute() 和 getBoundingClientRect()。为了让每个方法的返回值都能对照着看,下面先准备一段固定的 HTML 结构,后面所有示例都基于它。

示例代码

这段结构里刻意放了几种典型情况:多层嵌套的 div、带多个 class 的元素、行内 style、display: none 的节点,以及混在元素之间的 script 和 style 标签。后面查找祖先、读属性、算尺寸时,这些差异都会体现在结果上。

<p id="p1">1</p>
<div id="test">
  <div id="son_one">son_one</div>
  <div style="display: block" id="son_two" class="test_one test_two">
    <p id="son_two_p">son_two</p>
  </div>
  <div style="color: blue!important;">son_three</div>
  <div style="display: none" id="son_four"><p id="son_four_p">son_four</p></div>
  <script>
    var test = '这是测试';
  </script>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
  </style>
</div>

closest()

  • Element.closest() 方法用来获取:匹配特定选择器且离当前元素最近的祖先元素(也可以是当前元素本身)。如果匹配不到,则返回 null。

它的查找方向和 querySelector 正好相反:querySelector 是从当前元素往下找后代,closest 是从当前元素自己开始,沿着 parentNode 一路往上匹配,命中第一个满足选择器的就返回。注意「包含自身」这一点很关键,el.closest('div') 如果 el 本身就是 div,返回的就是 el 自己。

下面从最内层的 son_two_p 出发,用 body>div 这个带层级关系的选择器往上找:

var son_two_p = document.getElementById('son_two_p');
console.log(son_two_p.closest('body>div'));

结果拿到的是 #test,因为它是从 son_two_p 往上数第一个「父元素为 body 的 div」。选择器可以写任意复杂度,closest('.list-item[data-id]')、closest('form') 都合法。

这个方法在事件委托里特别好用:给容器绑一次 click,回调里用 e.target.closest('.item') 就能直接定位到真正被点击的那一行,不用担心用户点到的是 .item 内部的图标还是文字。

getAttribute

  • 返回元素上一个指定的属性值。如果指定的属性不存在,则返回 null 或 "" (空字符串)
console.log('getAttribute:', son_two_p.getAttribute('id'));

这里要区分「属性(attribute)」和「特性(property)」:getAttribute 读的是 HTML 标签上字面写着的那个字符串,而 el.id、el.value 这类点号访问读的是 DOM 对象上的属性。两者大多数时候一致,但也有对不上的场合,比如 input 输入框,用户敲字之后 el.value 是最新值,el.getAttribute('value') 拿到的还是 HTML 里写死的初始值。

getAttribute 的返回值永远是字符串,所以读数字要自己转换。配套的还有 setAttribute(name, value) 写入、hasAttribute(name) 判断存在、removeAttribute(name) 移除。如果只是读写 data-* 自定义属性,用 el.dataset.foo 会更顺手,它会自动把 data-user-id 转成 dataset.userId 这样的驼峰形式。

getBoundingClientRect

  • 返回元素的大小及其相对于视口的位置。
  • 如果是标准盒子模型,元素的尺寸等于 width/height + padding + border-width 的总和。
  • 如果 box-sizing: border-box,元素的的尺寸等于 width/height。

调用后返回一个 DOMRect 对象,包含 x、y、width、height、top、right、bottom、left 八个字段。有几个容易踩的点值得记住:

第一,坐标是相对视口的,不是相对文档。页面往下滚动之后,同一个元素的 top 会变小甚至变成负数。要换算成文档坐标得自己加上滚动距离,rect.top + window.scrollY。

第二,返回的是元素经过 CSS transform 之后的视觉尺寸。如果元素上有 transform: scale(0.5),rect.width 拿到的是缩放后的一半宽度,而 offsetWidth 仍然是布局宽度。想要哪个,取决于你是要做碰撞检测还是要读布局值。

第三,这是一个会触发强制同步布局(reflow)的方法。浏览器为了给出准确坐标,必须把之前排队的样式改动全部计算完。所以千万别写成「改样式 → 读 rect → 改样式 → 读 rect」的交替循环,那会让每次读取都触发一次重排,列表一长就明显卡顿。正确做法是先把所有读操作集中做完,再统一做写操作。

var son_two = document.getElementById('son_two');
var rect = son_two.getBoundingClientRect();
console.log(rect.width, rect.height, rect.top, rect.left);
// 换算为相对文档的位置
console.log(rect.top + window.scrollY, rect.left + window.scrollX);

还有一点:display: none 的元素(比如示例结构里的 #son_four)不参与布局,它的 DOMRect 各项全是 0。所以要测量一个隐藏元素的真实尺寸,得先让它可见,或者改用 visibility: hidden 配合绝对定位,这样元素仍然占位、尺寸可读。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEJavaScript Element 元素 API:closest、getAttribute加载中