The array methods map() and filter() are best understood by looking at how they could be implemented if we were to write them ourselves. In the next few sections we will present simplified versions of the native implementations. We have prefixed the method names with my
to distinguish them from the built-in versions.
Each of the three methods use a for
loop internally. You will notice that once you start using these methods the need for for
loops in your own code is greatly reduced (hurray!).
The map method returns a new array where each element of the subject array is transformed by a user-supplied transformation (= mapping) function.
Array.prototype.myMap = function (mapFn) {
const arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(mapFn(this[i], i, this));
}
return arr;
};
* Array#map is a short-hand notation for Array.prototype.map.
Because the map() method is called on an array (using dot-notation), the value of this
refers to that array itself (in this review called the subject array).
Internally, the map() method initializes a new, empty array to which it will push transformed elements, one by one, as it iterates through the subject array, calling the mapFn
function for each individual element. When the loop has been completed, the new array is returned. Note that the subject array itself remains unmodified.
this[i]
refers to an element of the subject array at loop index 'i' (because this
is a reference to the subject array).
As you can see, the mapFn
function is called with three arguments:
- the current array element to be transformed
- the index of the element (starting with
0
) - the subject array itself
In the example below we will use the Array#map method to create a new array that holds the squares of a subject array of numbers. The mapping function is represented by an ES6 fat arrow function:num => num * num
const numbers = [3, 5, 2, 7];
const squares = numbers.map(num => num * num);
console.log(squares); // -> [9, 25, 4, 49]
For illustrative purposes we can add a console.log
† statement to our mapping function and see what we get passed as second and third argument:
const numbers = [3, 5, 2, 7];
const mapFn = (num, index, arr) => {
console.log(num, index, arr);
return num * num;
}
const squares = numbers.map(mapFn);
console.log('squares', squares)
Output:
3 0 [ 3, 5, 2, 7 ]
5 1 [ 3, 5, 2, 7 ]
2 2 [ 3, 5, 2, 7 ]
7 3 [ 3, 5, 2, 7 ]
squares [ 9, 25, 4, 49 ]
For each of the first four lines in the output (from the console.log
inside the for
loop) the first number is the value of the current element, the second number is the current loop index value and the array value is the original subject array.
As is usual in JavaScript you do not necessarily have to use all the parameters that were passed to the mapFn
function. In fact, in many cases you will only need the first argument (the current array element) as we saw in the first example.
†You should normally not use console.log
inside the function you pass to the map
(or filter
) method. These functions should also not modify any variables outside their own function scope, although they can reference these variables for read-only purposes. In computer science terms, these functions should be pure with no 'side-effects'.
The filter() method returns a new array with all elements that pass the test implemented by a user-supplied (predicate*) function.
Array.prototype.myFilter = function (predicateFn) {
const arr = [];
for (let i = 0; i < this.length; i++) {
if (predicateFn(this[i], i, this)) {
arr.push(this[i]);
}
}
return arr;
};
*A predicate is a function that returns a boolean, whose value depends on its supplied arguments.
This method works in a similar fashion as the map() method, but now elements are only pushed to the new array if the predicate function returns true
.
In the example below the predicate function test whether the current element is even by checking whether its value divided by two has a remainder of zero. The result of this comparison (true
or false
) is the return value of the predicate and determines whether the current element gets added to the new array or not.
const numbers= [6, 3 , 10, 1];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // -> [6, 10]
Of the three methods map, filter and reduce, the reduce method presents the most difficulty for new learners. At present the reduce method is not covered in the HackYourFuture curriculum, nor required in the homework. It is nevertheless included in here for completeness.
The Mozilla Developer Network (MDN) web site gives the following definition:
The reduce() method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value**.
**Although reference is made to a 'single value', this single value may well be an array or an object, as you will see later in the examples below.
Array.prototype.myReduce = function (reducerFn, initialValue) {
let accumulator = initialValue;
for (let i = 0; i < this.length; i++) {
accumulator = reducerFn(accumulator, this[i], i, this);
}
return accumulator;
};
The key to understanding the reduce() method is in the line:
accumulator = reducerFn(accumulator, this[i], i, this);
In the case we don't need the current loop index and the subject array in the reducer function (which is often the case), we can simplify this to:
accumulator = reducerFn(accumulator, this[i]);
From this line we can define the reducer function as a function that takes an accumulator value and the current array element and returns a new accumulator value.
The reduce() method is the most flexible of the map/filter/reduce triplet. In fact, it is possible to rewrite map() and filter using reduce().
const arr = [6, 3, 10, 1];
const evenNumbers = arr.reduce((acc, elem) => {
if (elem % 2 === 0) {
acc.push(elem);
}
return acc;
}, []);
console.log(evenNumbers); // -> [6, 10]
In this example our accumulator is an (initially empty) array. We put elements (in this case integer numbers) in the accumulator only when they are divisible by 2.
In this example an array of integer numbers is mapped to an array of their squares.
const arr = [6, 3, 10, 1];
const squares = arr.reduce((acc, elem) => {
acc.push(elem * elem);
return acc;
}, []);
console.log(squares); // -> [36, 9, 100, 1]
In this example our accumulator is not an array, but an (initially empty) object. It groups the array elements by gender.
const arr = [
{ gender: 'F', name: 'Joyce'},
{ gender: 'M', name: 'Jim' },
{ gender: 'F', name: 'Lucy' },
{ gender: 'M', name: 'Ferdinand' }
];
const groupedNames = arr.reduce((acc, elem) => {
if (acc[elem.gender]) {
acc[elem.gender].push(elem);
} else {
acc[elem.gender] = [elem];
}
return acc;
}, {});
console.log(groupedNames);
Result:
{
F: [
{ gender: 'F', name: 'Joyce' },
{ gender: 'F', name: 'Lucy' }
],
M: [
{ gender: 'M', name: 'Jim' },
{ gender: 'M', name: 'Ferdinand' }
]
}
The methods map() and filter() each return a new array. This makes it possible to chain these methods and create a 'pipeline' of operations, to be applied in sequence. The reduce method can return anything, including an array. If a reduce method returns something other than an array it can only be included at the end of an array method chain.
Let's take the last example, but now filtering out only those array elements for which the name starts with a 'J':
const arr = [
{ gender: 'F', name: 'Joyce' },
{ gender: 'M', name: 'Jim' },
{ gender: 'F', name: 'Lucy' },
{ gender: 'M', name: 'Ferdinand' }
];
const groupedNames = arr
.filter(elem => elem.name.startsWith('J'))
.reduce((acc, elem) => {
if (acc[elem.gender]) {
acc[elem.gender].push(elem);
} else {
acc[elem.gender] = [elem];
}
return acc;
}, {});
console.log(groupedNames);
Result:
{
F: [{ gender: 'F', name: 'Joyce' }],
M: [{ gender: 'M', name: 'Jim' }]
}
Credit: http://www.globalnerdy.com/2016/06/23/map-filter-and-reduce-explained-using-emoji/