Allow me to expand on my comment a little bit. All of this is based on your second simplified example and jQuery 1.6.4. This is a little long winded perhaps but we need to walk through the jQuery code to find out what it is doing.
We do have the jQuery source available so let us go a wandering
through it and see what wonders there are to behold therein.
The guts of siblings
looks like this:
siblings: function( elem ) {
return jQuery.sibling( elem.parentNode.firstChild, elem );
}
wrapped up 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
is this:
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's first child,
and continue sideways to get all of the parent's children (except
the node we started at!) as an array of DOM elements.
That leaves us with all of our sibling DOM elements in ret
and
now to look at the filtering:
ret = jQuery.filter( selector, ret );
So what is filter
all about? filter
is all about this:
filter: function( expr, elems, not ) {
//...
return elems.length === 1 ?
jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
jQuery.find.matches(expr, elems);
}
In your case, elems
will have have exactly one element (as #d1
has one sibling) so we're off 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's 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 bit of experimentation indicates that neither the Gecko nor WebKit
versions of matchesSelector
can handle div span:first
so we end
up in the final Sizzle()
call; note that both the Gecko and WebKit
matchesSelector
variants can handle div span
and your
jsfiddles work as expected in the div span
case.
What does Sizzle(expr, null, null, [node])
do? Why it returns an array
containing the <span>
inside your <div>
of course. We'll have
this in expr
:
'div span:last'
and this in node
:
<div id="d2">
<span id="s1"></span>
</div>
So the <span id="s1">
inside node
nicely matches the selector
in expr
and the Sizzle()
call returns an array containing the
<span>
and since that array has a non-zero length, the matchesSelector
call returns true and everything falls apart in a pile of nonsense.
The problem is that jQuery isn't interfacing with Sizzle properly in this case. Congratulations, you are the proud father of a bouncing baby bug.
Here's a (massive) jsfiddle with an inlined version of jQuery with a couple console.log
calls to support what I'm talking about above:
http://jsfiddle.net/ambiguous/TxGXv/
A few things to note:
- You will get sensible results with
div span
and div span:nth-child(1)
; both of these use the native Gecko and WebKit selector engine.
- You will get the same broken results with
div span:first
, div span:last
, and even div span:eq(0)
; all three of these go through Sizzle.
- The four argument version of the
Sizzle()
call that is being used not documented (see Public API) so we don't know if jQuery or Sizzle is at fault here.
parent
method takes you from thea
to thediv
.div span
anddiv span:nth-child(1)
behave sensible in your second fiddle butdiv span:first
anddiv span:eq(0)
do not; the first two selectors are matched using the browser's native API, the second two are handled by Sizzle in JavaScript.