Search code examples
jquerysizzle

jQuery: subtle difference between .has() and :has()


When used with the child selector >, the two variants of jQuery's "has" behave differently.

Take this HTML:

<div>
  <span>Text</span>
</div>

Now:

$("div:has(>span)");

would return it, while:

$("div").has(">span");

would not. Is it a bug or a feature? Compare here: http://jsfiddle.net/aC9dP/


EDIT: This may be a bug or at least undocumented inconsistent behavior.

Anyway, I think it would be beneficial to have the child selector consistently work as an unary operator. It enables you to do something that otherwise would require a custom filter function — it lets you directly select elements that have certain children:

$("ul:has(>li.active)").show();     // works
$("ul").has(">li.active)").show();  // doesn't work, but IMHO it should

as opposed to:

$("ul").filter(function () {
  return $(this).children("li.active").length > 0;
}).show();

I've opened a jQuery ticket (7205) for this.


Solution

  • This happens because the sizzle selector is looking at all Div's that have span children in the :has example. But in the .has example, it's passing all DIV's to the .has(), which then looks for something that shouldn't be a stand-alone selection. ("Has children of nothing").

    Basically, :has() is part of the selection, but .has() gets passed those divs and then re-selects from them.

    Ideally, you don't use selectors like this. The > being in the selector is probably a bug, as it's semantically awkward. Note: the child operator isn't meant to be stand-alone.

    Sizzle vs target.sizzle:

    I'm always talking about v1.4.2 of jquery development release.

    .has (line 3748 of jQuery)

    Description: Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element.

    Code:

        var targets = jQuery( target );
        return this.filter(function() {
            for ( var i = 0, l = targets.length; i < l; i++ ) {
                if ( jQuery.contains( this, targets[i] ) ) { //Calls line 3642
                    return true;
                }
            }
        });
    

    Line 3642 relates to a 2008 plugin compareDocumentPosition, but the important bit here is that we're now basically just running two jquery queries here, where the first one selects $("DIV") and the next one selects $(">span") (which returns null), then we check for children.

    :has (line 3129 of jQuery)

    Description: Selects elements which contain at least one element that matches the specified selector.

    Code:

    return !!Sizzle( match[3], elem ).length;

    They are two differnt tools, the :has uses sizzle 100%, and .has uses targets passed to it.

    Note: if you think this is a bug, go fill out the bug ticket.