使用原生DOM查找最接近的祖先匹配选择器?

17

是否有人在DOM API中开发与jQuery.closest()相当的功能?

看起来Selectors Level 2草案添加了matches()相当于jQuery.is(),因此原生的closest函数应该更容易编写。是否已经考虑将closest()添加到Selectors中呢?


只有当所有浏览器都实现该方法时,它才会“变得更容易”——但事实并非如此。 :-( 因此,无论如何都必须编写一种长手方法,我猜测需要使用matches分支的特征检测。这并不难,但可能会有点慢。 - RobG
是的,modernizer有一个例子可以回退到供应商前缀的matchesSelector - hurrymaplelad
6个回答

36

继Alnitak的答案之后,这里是使用matchesSelector的当前实现版本,该方法在DOM规范中已更名为matches

// get nearest parent element matching selector
function closest(el, selector) {
    var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    while (el) {
        if (matchesSelector.call(el, selector)) {
            break;
        }
        el = el.parentElement;
    }
    return el;
}

浏览器支持很好:http://caniuse.com/matchesselector


我很矛盾。感谢您的实现,但这是否意味着本地最近的功能不会出现? - hurrymaplelad
1
为什么使用.bind(ctx)(arg)而不是.call(ctx, arg)?前者创建一个新的执行上下文,而后者则不会。 - Alnitak
1
call(elem, selector) 的性能比 bind(elem)(selector) 更优,基准测试请看 http://jsperf.com/native-vs-jquery-closest。然而简单的 .parentNode.parentNode 方法速度更快。 - Binyamin
1
为什么要返回false,而不是null? - HeyHeyJC
好的,谢谢你们! - Paul Irish

14

2
"element.closest" 已经在 Chrome 41 中推出,而不是 40(请参见 https://crbug.com/422731#c7)。它也已经在 Firefox 35 中推出。 - Rob W
您还可以使用 polyfill 在旧版浏览器上使用此函数,例如 https://github.com/jonathantneal/closest - user1613797

7

参见 element.closest() 文档

使用 Element.matches() 来实现这样的函数在性能上似乎不太优秀,因为 matches() 显然会在每次测试父元素时调用 querySelectorAll(),而仅需一次调用即可完成工作。

以下是 MDN 上关于 closest() 的 polyfill。请注意只有一次对 querySelectorAll() 的调用。

if (window.Element && !Element.prototype.closest) {
  Element.prototype.closest = 
  function(s) {
      var matches = (this.document || this.ownerDocument).querySelectorAll(s),
          i,
          el = this;
      do {
          i = matches.length;
          while (--i >= 0 && matches.item(i) !== el) {};
      } while ((i < 0) && (el = el.parentElement)); 
      return el;
  };
}

但请记住,像这样实现的函数将无法在未连接到document.documentElement根的树上正常工作。
//Element.prototype.closestTest = function(s){...as seen above...};

var detachedRoot = document.createElement("footer");
var child = detachedRoot.appendChild(document.createElement("div"));
detachedRoot.parentElement; //null

child.closestTest("footer"); //null

document.documentElement.append(detachedRoot);
child.closestTest("footer"); //<footer>   

虽然Firefox 51.0.1中实现的closest()函数似乎可以很好地处理分离的树

document.documentElement.removeChild(detachedRoot);
child.closestTest("footer"); //null
child.closest("footer"); //<footer>

根据MDN的说法,似乎不再是“实验性”的了:https://developer.mozilla.org/en-US/docs/Web/API/Element/closest - Xenos

3

这听起来应该很容易,因为有 matches 函数,虽然目前它还没有被广泛支持:

function closest(elem, selector) {
    while (elem) {
        if (elem.matches(selector)) {
            return elem;
        } else {
            elem = elem.parentElement;
        }
    }
    return null;
}

问题在于,matches函数得不到良好的支持。由于它是一个相对较新的API,在Chrome和Safari中可以使用webkitMatchesSelector,在Firefox中可以使用mozMatchesSelector


没错,使用 *MatchesSelector,只需使用本机 DOM 就可以轻松创建最接近的元素。我更好奇是否有任何动力将 element.closest(selector) 添加为元素的本机方法。 - hurrymaplelad
@hurrymaplelad 你可能需要加入 W3C 并提出建议。但我怀疑他们不会介意,因为实现起来非常简单(参考上面的代码)。 - Alnitak

1
使用element.closest()方法我们可以找到最接近的符合选择器的祖先元素。该方法需要一个选择器列表作为参数,并返回最接近的祖先元素。根据Rob的评论,该API将在Chrome 41和FF 35中可用。
正如whatwg规范https://dom.spec.whatwg.org/#dom-element-closest中所解释的那样。
示例:下面的HTML将显示警报消息“true”。
<html>
    <body>
        <foo>
            <bar>
                <a id="a">
                    <b id="b">
                        <c id="c"></c>
                    </b>
                </a>
            </bar>
         </foo>
    <script>
        var a = document.getElementById('a');
        var b = document.getElementById('b');
        var c = document.getElementById('c');
        alert(c.closest("a, b")==b);
    </script>
    </body>
</html>

1
一点递归就可以解决问题。
// get nearest parent element matching selector
var closest = (function() {
    var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    return function closest(el, selector) {
        return !el ? null :
        matchesSelector.call(el, selector) ? el : closest(el.parentElement, selector);
    };
})();

或者只需返回 element.tagName ==='HTML'? null:element.matches(selector)? element:closest(element.parentNode,selector); ;) - yckart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接