For a series of exercises that are recreating Underscore.js, I am trying to understand how the call() method is working under the hood.
I do understand how the call() method works in the example below.
let person = {
firstName:"John",
lastName: "Doe",
fullName: function() {
return this.firstName + " " + this.lastName;
}
}
let myObject = {
firstName:"Mary",
lastName: "Doe",
}
person.fullName.call(myObject); // Will return "Mary Doe"
However, I am having a hard time to understand the idea of context as in iteratee.call(context, collection[i], i, collection).
Here is the exercise I am working on:
// _.each(collection, iteratee, [context])
// Iterates over a collection of elements (i.e. array or object),
// yielding each in turn to an iteratee function, that is called with three arguments:
// (element, index|key, collection;), and bound to the context if one is passed.
// Returns the collection for chaining.
_.each = function (collection, iteratee, context) {
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
iteratee.call(context, collection[i], i, collection);
}
} else if (collection !== null) {
Object.entries(collection).map(([key, value]) => {
iteratee.call(context, value, key, collection);
});
}
return collection;
};
Thanks in advance for your help.
Many years ago it was common to use the word "context" to refer to the value that this should have during a function call. (It's not a good term and has fallen out of favor.) What that _.each definition is doing is accepting an optional parameter, context, and using that to set what this is when calling the iteratee function that was passed in so that this within the iteratee call is whatever context is. (If context isn't provided, it will be undefined, and this within iteratee will either be undefined [in strict mode] or the global object [in loose mode].)
For example, suppose you have an object with a method and you want to call that method for each entry in an array using _.each. You can do that like this:
const obj = {
id: "some ID",
method(value) {
console.log(`this.id = ${this.id}, value = ${value}`);
}
};
_.each([1, 2, 3], obj.method, obj);
Live Example:
"use strict";
const _ = {};
_.each = function (collection, iteratee, context) {
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
iteratee.call(context, collection[i], i, collection);
}
} else if (collection !== null) {
Object.entries(collection).map(([key, value]) => {
iteratee.call(context, value, key, collection);
});
}
return collection;
};
const obj = {
id: "some ID",
method(value) {
console.log(`this.id = ${this.id}, value = ${value}`);
}
};
_.each([1, 2, 3], obj.method, obj);
If you didn't provide obj as the third parameter, this.id in obj.method wouldn't work correctly because this wouldn't be obj, it would be either undefined (strict mode) or the global object. For instance (strict mode):
"use strict";
const _ = {};
_.each = function (collection, iteratee, context) {
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
iteratee.call(context, collection[i], i, collection);
}
} else if (collection !== null) {
Object.entries(collection).map(([key, value]) => {
iteratee.call(context, value, key, collection);
});
}
return collection;
};
const obj = {
id: "some ID",
method(value) {
console.log(`this.id = ${this.id}, value = ${value}`);
}
};
_.each([1, 2, 3], obj.method); // <== Note no third argument
The built-in array method forEach also has this parameter (called thisArg), as do many other built-in array methods.
But these days, it seems to me it's more common to just use a wrapper arrow function instead:
_.each([1, 2, 3], value => obj.method(value));
You can think of this as an implicit parameter of all functions. To illustrate, the following function will throw an error because there is no declared variable banana:
function fruit() {
console.log(banana);
}
fruit();
But the following function is fine because this is an implicitly declared parameter:
'use strict';
function fruit() {
console.log(this);
}
fruit();
You cannot pass this directly as an argument, because then you would have to redeclare the name:
function broken(this) {
console.log(this.name);
}
broken({name: 'Ann'});
The only valid ways to pass a this argument to a function are to either call it as a method, or use call or apply:
// the following lines are equivalent
anObject.aMethod(argument1, argument2);
aMethod.call(anObject, argument1, argument2);
aMethod.apply(anObject, [argument1, argument2]);
In all cases, aMethod will see anObject as its this argument. However, the first case only works if aMethod is a property of anObject, while the others always work. The only difference between call and apply is that the latter takes all arguments after this in a single array.
Now to get to the line that confused you:
iteratee.call(context, collection[i], i, collection)
iteratee is just any function, and context is just a variable that will be supplied as its implicit this argument. The context variable could have had any other name, such as thisArg or object.