An Intro To JS Higher Order Array Functions
Mike Cronin
Posted on December 1, 2021
One of the most crucial skills in JS is understanding how higher order and callback functions work. Simply put, a higher order function is a function that: 1) takes a different function as an argument and/or 2) returns a new function. That's it. A callback function is just the function that gets passed in. These are comp-sci words that hides simple concepts. For example, this is basically all forEach
does:
const fakeForEach = (arr, callbackFunction) => {
for (let i = 0; i < arr.length; i++) {
const value = arr[i]
const index = i;
const givenArr = arr;
callbackFunction(value, index, givenArr)
}
}
const myArr = ['a', 'b', 'c']
const myCallback = (val, idx, arr) => {
console.log('Value at index:', val);
console.log('Current index:', idx);
console.log('Original array:', arr);
};
// these will log the same things!
fakeForEach(myArr, myCallback);
myArr.forEach(myCallback);
By passing in a function but not calling it we allow a higher order function, in this case fakeForEach
or .forEach
to invoke it with each iteration of the loop. Now lets break down some the major higher order array functions that come built in with JS.
Also, you can of course define the callback functions inline, but for the following examples, I'm explicitly creating a variable just so it's perfectly clear what the callback refers to.
// inline
arr.forEach((val) => {
console.log(val)
});
// variable
const callback = (val) => {
console.log(val)
});
arr.forEach(callback);
// both are fine!
.forEach
Function Description
.forEach
iterates through an array without caring about return values. If you essentially want a basic loop or mutate an existing object, this is your method.
Callback Description
The callback for forEach
takes in the value, the index, and the original array during each iteration. The return value of the provided callback is ignored.
Example
const letters = ['a', 'b', 'c'];
const callback = (val, idx, arr) => {
console.log('Value at index:', val);
console.log('Current index:', idx);
console.log('Original array:', arr);
};
letters.forEach(callback);
// Value at index: a
// Current index: 0
// Original array: [ 'a', 'b', 'c' ]
// Value at index: b
// Current index: 1
// Original array: [ 'a', 'b', 'c' ]
// Value at index: c
// Current index: 2
// Original array: [ 'a', 'b', 'c' ]
.map
Function Description
.map
is a lot like forEach
, except that it builds up and returns a new array.
Callback Description
Like forEach
, the provided callback gives you access to the value, the index, and the original array. Each individual return value from the callback is what gets saved to the new array.
Example
const numbers = [10, 20, 30];
const callback = (val, idx, arr) => {
console.log('Value at index:', val);
console.log('Current index:', idx);
console.log('Original array:', arr);
return val * 100;
};
const bigNumbers = numbers.map(callback);
console.log('bigNumbers: ', bigNumbers);
// bigNumbers: [ 1000, 2000, 3000 ]
.filter
Function Description
filter
is used to return a new array based off of values that pass a condition.
Callback Description
The callback has the value, index, and array, however it is the return value that's interesting. If an iteration has a truthy return value, the value in the array at that iteration gets saved to the new array.
const names = ['tom', 'ezekiel', 'robert'];
const callback = (val, idx, arr) => {
return val.length > 3;
};
const longNames = names.filter(callback);
console.log('longNames: ', longNames);
// longNames: [ 'ezekiel', 'robert' ]
.some
Function Description
Some returns a boolean if at least one of the elements in the array meets the given condition.
Callback Description
It's a standard value, index, arr situation. However, unlike the other methods so far, once the callback returns true
, some
will stop iterating through the array. That's becuase theres no need to keep going. Remember, some
only cares if there's at least one value, if you want the exact number of truthy values, you should use either forEach
and keep a count
variable, or filter
and then just use the length of the new array. The only way some
will iterate through the entire array is if it never finds a value that returns a truthy value. At which point some
will return false
.
Example
const numbers = [1, 4, 9001, 7, 12];
const callback = num => {
console.log('num: ', num);
return num > 9000;
};
const isOver9000 = numbers.some(callback);
// num: 1
// num: 4
// num: 9001
console.log('isOver9000: ', isOver9000);
// isOver9000: true
.every
Function Description
every
returns a boolean, true
if every value in the array passes the callback's condition, and false
otherwise.
Callback description
The callback has the value, index, and array, we've come to know and love. It works exactly like some
, where it evaluates the return values as truthy/falsy. However, it abandons iteration if a single value returns falsy, which is the opposite of some
. It's kind of like ||
vs &&
short circuiting.
Example
const numbers = [9001, 9002, 7, 12];
const callback = (num) => {
console.log('num: ', num);
return num > 9000;
}
const areAllOver9000 = numbers.every(callback)
// num: 9001
// num: 9002
console.log('areAllOver9000: ', areAllOver9000);
// areAllOver9000: false
The more complicated iterators
The next methods depart somewhat from the val, idx, arr
pattern of callbacks and are a little more complicated. As such, let's explain them a little more in depth.
.reduce (basic use case)
This method reduces an array of values into a single one. The provided callback's first argument is the accumulator
. The second argument is the current value
. The main trick with reduce
is that whatever the iterator returns from one iteration becomes the accumulator
for the next. The final return value of reduce
is whatever the accumulator
has been built up to by the final iteration.
What about the first iteration?
reduce
has an optional, but highly recommended, second argument that sets the initial value
for the accumulator
. If no initial value is provided, reduce
will essentially take the first value of the array, treat that as the initial value
and the second value in the array as the current value
. In general, you should just always provide an initial value
, as it leads to fewer bugs.
const numbers = [12,8,23,5];
const startingVal = 0;
const callbackFn = (accumulator, currentVal) => {
console.log('Accumulator', accumulator);
console.log('Value at index:', currentVal);
// console.log('Current index:', idx);
// console.log('Original array:', arr);
return accumulator + currentVal;
}
const total = numbers.reduce(callbackFn, startingVal);
// Accumulator 0
// Value at index: 12
// Accumulator 12
// Value at index: 8
// Accumulator 20
// Value at index: 23
// Accumulator 43
// Value at index: 5
console.log('total', total);
// total: 48
.reduce (advanced use case)
At the end of the day, reduce
just adds things up into an accumulator. But no one said the accumulator couldn't be...an object?? Look how you can use reduce
to build up an object. For comparison, we do the exact same thing but using .forEach
. The crucial thing to remember is now the initial value must be explicitly set an object. Also, we don't need them in this case, but the idx
and arr
parameters are still available.
const arr = ['x', 'y', 'z', 'z', 'x', 'z'];
const countForEach = (arr) => {
const result = {};
arr.forEach((letter) => {
result[letter] = (result[letter]) ? result[letter] + 1 : 1;
});
return result;
};
const countReduce = (arr) => arr.reduce((acc, letter) => {
acc[letter] = acc[letter] ? acc[letter] + 1 : 1;
return acc;
}, {});
console.log(countForEach(arr));
// { x: 2, y: 1, z: 3 }
console.log(countReduce(arr));
// { x: 2, y: 1, z: 3 }
.sort
The default sort()
method sorts things alphabetically. Which means [1, 3, 2, 11]
would get sorted into [1, 11, 2, 3]
.` This is not ideal. To sort numbers correctly, you need to pass in a compare callback function. The compare function needs to return a positive number, a negative number, or 0. JS will then use these numbers to determine if the values are in the right order. So kind of like this:
js
const compare = (a, b) => {
if (a is less than b by some ordering criterion) {
return a negative number;
}
if (a is greater than b by the ordering criterion) {
return a positive number;
}
// a must be equal to b
return 0;
}
That's a very manual setup, which may be useful for sorting non-numeric values. However, if you're comparing numeric values, you can get away with a drastically simpler callback that still does the same thing:
js
// sorts smallest to biggest (ascending)
let compare = (a, b) => a - b;
// sorts biggest to smallest (descending)
compare = (a, b) => b - a;
Used in context, sort
looks like so.
js
const numbers = [4, 2, 5, 1, 3];
numbers.sort((a, b) => a - b);
console.log('numbers:', numbers);
// [ 1, 2, 3, 4, 5 ]
The compare function can easily deal with objects as well, simply access whatever property to need.
js
const houses = [
{color: 'blue', price: 350000},
{color: 'red', price: 470000},
{color: 'pink', price: 280000},
];
houses.sort((a,b) => a.price - b.price)
console.log('houses:', houses);
// houses [
// { color: 'pink', price: 280000 },
// { color: 'blue', price: 350000 },
// { color: 'red', price: 470000 }
// ]
Something important to note here is that unlike the other iterator functions on this list, sort is not pure; it will mutate the original array instead of making a new one.
More higher order functions await!
This is just the base of the higher order mountain, there is so much more to explore about this concept. But, you should now have a pretty good grasp on the basics, and I encourage you to open up a console and play around with the values until it just feels second nature.
happy coding everyone,
mike
Posted on December 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 5, 2021