Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Function.prototype.call()
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?
1 answer
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.
1 comment thread