I'm a little bit confused by the behaviour when using children()
and find()
with :first
.
Consider the following markup:
<div class="parent">
<div>1</div>
<div class="child">2</div>
<div class="child">3</div>
</div>
<div class="parent">
<div>1</div>
<div class="child">2</div>
<div class="child">3</div>
</div>
From what I understand, the following should return the same collection of elements, as there are no grandchildren (which is the only difference stated between find()
and children()
in the documentation).
However, if I add a class to the first .child
, the results are different:
$('.parent').find('.child:first').addClass('active');
Results in the following markup:
<div class="parent">
<div>1</div>
<div class="child active">2</div>
<div class="child">3</div>
</div>
<div class="parent">
<div>1</div>
<div class="child active">2</div>
<div class="child">3</div>
</div>
When doing the same thing using the children()
method, I get:
<div class="parent">
<div>1</div>
<div class="child active">2</div>
<div class="child">3</div>
</div>
<div class="parent">
<div>1</div>
<div class="child">2</div>
<div class="child">3</div>
</div>
Why?
Nice question!
The difference is that find
searches individually starting from each wrapped element and then aggregates the results, while children
gets a single aggregate pool of candidate results which are then filtered based on the specified selector. This results in selectors like :first
giving a different result if the original jQuery object wraps more than one element.
The reason for this is understandable (children
knows from the get-go that all of its possible matches share a very important property in the DOM, so it makes sense to narrow down the candidate list up front for performance reasons), but IMO you can't call the result anything else than a bug in the current implementation.
$.fn.find
find
produces two matches like that because it runs a search on each of the wrapped elements of the current jQuery object and then aggregates the results. So for each .parent
we match the first descendant in document order that is a .child
.
Here's the source:
function (selector) {
var i, ret = [],
self = this,
len = self.length;
if (typeof selector !== "string") {
return this.pushStack(jQuery(selector).filter(function () {
for (i = 0; i < len; i++) {
if (jQuery.contains(self[i], this)) {
return true;
}
}
}));
}
for (i = 0; i < len; i++) {
jQuery.find(selector, self[i], ret); // ** IMPORTANT **
}
// Needed because $( selector, context ) becomes $( context ).find(selector)
ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret);
ret.selector = this.selector ? this.selector + " " + selector : selector;
return ret;
}
Everything happens in the line marked "IMPORTANT": jQuery.find
is an alias to Sizzle
, which appends its results to ret
each time. Obviously if you do .find(":first")
on a jQuery object that wraps N elements with each element having at least one descendant, you will get back exactly N results.
$.fn.children
children
takes another route: for each wrapped element it traverses the DOM to get hold of its children, and then filters the results as a whole based on the selector. Obviously in this case this would leave at most one element as the final result.
Here's how it happens:
function (until, selector) {
var ret = jQuery.map(this, fn, until); // ** IMPORTANT 1 **
if (name.slice(-5) !== "Until") {
selector = until;
}
if (selector && typeof selector === "string") {
ret = jQuery.filter(selector, ret); // ** IMPORTANT 2 **
}
if (this.length > 1) {
// Remove duplicates
if (!guaranteedUnique[name]) {
ret = jQuery.unique(ret);
}
// Reverse order for parents* and prev-derivatives
if (rparentsprev.test(name)) {
ret = ret.reverse();
}
}
return this.pushStack(ret);
}
This is not as self-explanatory because the code is shared with a bunch of methods that do structural traversal of the DOM (parent
, next
, prev
, siblings
, etc), but again the relevant part of the code is very simple: The line "IMPORTANT 1" collects the results of the structural traversal ("get the children") inside ret
, and those results as a whole are filtered based on the selector (".child:first"
). This will finally leave at most one result.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With