Let me expand my comment a bit. All of this is based on your second simplified example and jQuery 1.6.4. It may be a little longer, but we need to go through jQuery code to find out what it does.
We have a jQuery source, so let's take a walk through it and see what miracles are there.
The feelings of siblings
are as follows:
siblings: function( elem ) { return jQuery.sibling( elem.parentNode.firstChild, elem ); }
completed in this:
// `name` is "siblings", `fn` is the function above. jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ) //... if ( selector && typeof selector === "string" ) { ret = jQuery.filter( selector, ret ); } //... };
And then jQuery.sibling
:
sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { r.push( n ); } } return r; }
So, we go up one step in the DOM, go to the parent first child, and continue sideways to get all the parent children (except node we started with!) As an array of DOM elements.
This leaves us with all of our Sibling DOM elements in ret
and now let's look at the filtering:
ret = jQuery.filter( selector, ret );
So what is a filter
? filter
is everything:
filter: function( expr, elems, not ) {
In your case, elems
will have exactly one element (as #d1
has one brother), so we will go to jQuery.find.matchesSelector
, which is actually Sizzle.matchesSelector
:
var html = document.documentElement, matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; //... Sizzle.matchesSelector = function( node, expr ) { // Make sure that attribute selectors are quoted expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); if ( !Sizzle.isXML( node ) ) { try { if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { var ret = matches.call( node, expr ); // IE 9 matchesSelector returns false on disconnected nodes if ( ret || !disconnectedMatch || // As well, disconnected nodes are said to be in a document // fragment in IE 9, so check for that node.document && node.document.nodeType !== 11 ) { return ret; } } } catch(e) {} } return Sizzle(expr, null, null, [node]).length > 0; };
A little experimentation shows that neither Gecko nor the WebKit version of matchesSelector
can handle div span:first
, so we end up in the final Sizzle()
call; note that both Gecko and WebKit matchesSelector
options can handle div span
and your jsfiddles work as expected in the case of div span
.
What does Sizzle(expr, null, null, [node])
do Sizzle(expr, null, null, [node])
? Why does it return an array containing a <span>
inside your <div>
, of course. We will have this in expr
:
'div span:last'
and this is in node
:
<div id="d2"> <span id="s1"></span> </div>
So, <span id="s1">
inside node
matches the selector in expr
nicely, and calling Sizzle()
returns an array containing <span>
, and since this array is non-zero length, matchesSelector
call returns true, and everything falls apart in a bunch of rubbish .
The problem is that jQuery does not interact with Sizzle in this case. Congratulations, you are the proud father of a bouncing baby.
Here's a (massive) jsfiddle with a built-in version of jQuery with the password console.log
to support what I'm talking about above:
http://jsfiddle.net/ambiguous/TxGXv/
A few notes:
- You will get reasonable results with
div span
and div span:nth-child(1)
; both of them use their own Gecko and WebKit selection engine. - You will get the same broken results with
div span:first
, div span:last
and even div span:eq(0)
; all three of them go through Sizzle. - The four argument version of the call to
Sizzle()
, which is not documented (see the Open API ), so we don’t know if jQuery or Sizzle is to blame.