Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs

Dashboard
Notifications
Mark all as read
Q&A

Function.prototype.call()

+2
−1

I wanted to print to browser console the number of li list items of a given ul list.

This didn't work (console output was undefined):

console.log(document.querySelectorAll(".example").length);

This worked (console output was a plausible number):

[].filter.call(
    document.getElementsByClassName('example')[0].children, 
    function (el) {
        return el.nodeName === 'LI';
    }
).length

My question

It was written in the MDN page for Function.prototype.call():

The call() method calls a function with a given this value and arguments provided individually.

But in the working code there was no "this".

What is the meaning of "call" in the second code?

Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

Can you provide an example of the HTML? I'm assuming that `.example` refers to the `ul` element, but ... (7 comments)

1 answer

+4
−0

TL;DR

To count the number of li items, just do:

console.log(document.querySelectorAll('ul.example > li').length);

That's all, no need to complicate with filter.call (but I'll explain that too, hang on).


Adapt that to your HTML

Of course you could change the selector to be more (or less) specific. Ex: if you want any ul, you could use document.querySelectorAll('ul > li'), or if you want ul with specific id, use document.querySelectorAll('ul#specificId > li').

Just reminding that > is a Child Combinator, which means it gets only the li tags that are direct descendants of the ul. Example, if you have this HTML:

<ul class="example">
 <li>item 1</li>
 <li>item 2</li>
 <li>
   Item 3:
   <ul>
     <li>sub item 3.1</li>
     <li>sub item 3.2</li>
   </ul>
 </li>
</ul>

Then document.querySelectorAll('ul.example > li').length will return 3, because the selector gets only the li's containing "Item 1", "Item 2" and "Item 3". The tags with "sub item 3.x" won't be returned, because they're not direct descendants of ul.example.

On the other hand, if I do document.querySelectorAll('ul.example li').length, it'll return 5, because now it gets all li tags inside ul.example (no matter how many depth levels they're in).

Which one to use will depend on what you mean by "li list items of a given ul" (based on your second code with filter, I guess you need ul.example > li).


Regarding undefined

According to MDN, querySelectorAll returns a NodeList (even if nothing is found, it still returns an empty one). And its length property returns the number of items (zero if it's empty). So there's no way to have document.querySelectorAll('whatever').length equals undefined.

What is probably happening: when you call console.log(whatever) in browser's console (or in Node's console - when you run just node in the command line), it prints the value of whatever and it also shows the return of console.log function (which is undefined). If that's the case (you're testing in a console), try typing just document.querySelectorAll(etc... (without console.log) and see that it won't show undefined anymore.


Explaining filter

Using filter, IMO, is not necessary in this case, as just using querySelectorAll (with the correct selector) is enough. But let's analyze your code.

First, according to the documentation, getElementsByClassName "returns an array-like object" (actually a HTMLCollection), which means it can return more than one element (just as querySelectorAll).

Then you get the first element of this HTMLCollection (when you do [0] on it). As example is the ul's class, the search is returning this ul inside the collection, and by using [0] you got that ul element.

After that, you get the children property of this ul, which may contain the li elements, but it also can have other elements. So we need to filter those elements.

One alternative would be to use Array.prototype.filter, but the children property is not an array and it doesn't have this method:

// get ul's children
var children = document.getElementsByClassName('example')[0].children;
// children is not an array
console.log(Array.isArray(children)); // false
// and it doesn't have the filter method
console.log(children.filter); // undefined

// just to compare with arrays, they have the filter method
console.log([].filter); // function filter() { ... } 

So one "trick" is to use call, which is a way to change the this value that a function "sees".

Just to give a brief explanation, every function has a this binding (see here for some examples), but that can be changed with call:

function someFunc() {
    return this;
}

// in a browser, "this" is the same as "window"
console.log(someFunc() == window); // true

// passing some object to call, it becomes "this"
let someObject = { id: 1, name: 'Some Object' };
console.log(someFunc.call(someObject)); // { id: 1, name: 'Some Object' }

By using someFunc.call(someObject), I'm saying "call the someFunc function, but using someObject as the this value".

And that's what is happening in your code: it calls Array.prototype.filter, but instead of calling in in the array ([], an empty array), it's calling it on the children property (which is a HTMLCollection containing all the child nodes of the ul element).

In other words, when you call filter in an array:

var array = [1, 2, 3];
array.filter(function(etc...))

Inside the filter method, this refers to the array itself. But if I use call:

var array = [1, 2, 3];
array.filter.call(someOtherObject, function(etc...))

Then I'm saying that filter should consider someOtherObject as the this value. In your code, the array is [] (an empty array), and someOtherObject is the children nodes of the ul element. Hence, filter is checking which children nodes of ul are li tags.

this is a very complicate thing in JavaScript, I recommend reading here for more info.

But anyway, I wouldn't use filter in this case. IMO, it makes the code more confusing (although many might disagree with me), not to mention that it returns another array (and creating another array just to get its length is a bit overkill IMO). I would do it like this:

// get ul's children
var children = document.getElementsByClassName('example')[0].children;
// count how many children are "li"
var count = 0;
for (var element of children)
    if (element.nodeName === 'LI')
        count++;
console.log(count);

Of course, if you "know" that the ul contains only li tags and nothing else, you could simply use children.length.


But there's a difference...

One important difference is that a HTMLCollection is a live collection, so it'll change if the DOM is updated. On the other hand, the NodeList returned by querySelectorAll doesn't change.

Let's consider this HTML:

<ul class="example">
 <li>item 1</li>
 <li>item 2</li>
</ul>

Now let's see what happens when we count ul's children and then update the DOM:

var list = document.querySelectorAll('ul.example > li');
var children = document.getElementsByClassName('example')[0].children;

console.log(list.length); // 2
console.log(children.length); // 2

// update the DOM, add another li
var ul = document.querySelector('ul.example');
ul.appendChild(document.createElement('li'));

console.log(list.length); // 2
console.log(children.length); // 3  <--- it's updated!

After adding another li in the ul, the NodeList returned by querySelectorAll remains unchanged, while the HTMLCollection is updated. Depending on what you do with the DOM while counting, choosing one of them might lead to different results.

Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »