Understand JavaScript better: Implementing array methods from scratch

Understand JavaScript better: Implementing array methods from scratch

ยท

9 min read

Introduction

Javascript contains several array methods which are baked within the language itself that go beyond your average for loop. You probably already used several of these methods but have you ever wondered how they are implemented?

The best way to learn how something works is by trying to reproduce it yourself with easy and concise code. In just a few seconds you will implement forEach, map, filter, reduce and sort with the help of this article.

In order to code along I recommend whether to use a coding editor such as Visual Studio Code and a live execution extension (e.g. Quokka.js) or CodePen.

Array methods explained

Before starting to implement these methods we should first understand how they work. These methods are also called higher-order functions which basically means that they take another function as a parameter. The function also named callback function will be called for each element of the array.

Let's take a closer look at the forEach method. Without using said method you would need to write the following code:

// Let's print to the console if each number is even or odd.
const arr = [1, 2, 3, 4];
for(let i = 0; i < arr.length; i++) {
    console.log(`${arr[i]} is ${arr[i] % 2 === 0 ? 'even' : 'odd'}`);
}

If you are confused about the content of the console.log you can read more about template literals.

With the help of forEach our code is cleaner and would look like this:

// We will print the same thing as the example above.
const arr = [1, 2, 3, 4];
arr.forEach(n => console.log(`${n} is ${n % 2 === 0 ? 'even' : 'odd'}`);

Now let's try to do our own implementations of these methods.

Implementing forEach

forEach header

forEach goes through every item of the array and applies the callback function for each of them.

Array methods are part of the Array prototype. In order to add new methods we need to assign them to Array.prototype.

WARNING! In a production environment it is not recommended to alter JavaScript global objects prototypes as this could produce unwanted side effects.

Let's first assign a new method to the Array.prototype and test if it works.

Array.prototype.myForEach = function(callback) {
    console.log("My forEach implementation works!");
}

const arr = [1, 2, 3, 4];

arr.myForEach();

If everything goes as planned the code above should display in the console:

// Output
"My forEach implementation works!"

Now let's modify our myForEach function so it iterates through the array and passes to the callback function the required arguments (forEach method provides the callback with the value, index and array, in this order)

Array.prototype.myForEach = function(callback) {
    console.log("My forEach implementation works!");
    // 'this' is the array our method is called upon
    for(let i = 0; i < this.length; i++) {
        // value, index, array
        callback(this[i], i, this);
    }
}

Let's now test our new method to see if it works:

const arr = [1, 2, 3, 4];

arr.myForEach(n => console.log(n));

You should receive the following output:

// Output
"My forEach implementation works!"
1
2
3
4

Implementing map

map header

map will pass through each element, modify it in some way defined in your callback function and return a new array that contains the modified items.

As we have already done previously let's create a new function and assign it to Array.prototype.

Array.prototype.myMap = function(callback) {
    console.log("My map implementation works!");

    for(let i = 0; i < this.length; i++){
        // do some kind of modification on the elements
    }
}

Now we need to create a new array and assign it the modified values of our original array:

Array.prototype.myMap = function(callback) {
    console.log("My map implementation works!");

    const newArr = [];
    for(let i = 0; i < this.length; i++){
        newArr[i] = callback(this[i], i, this);
    }
    return newArr;
}

Lastly, we return the new array. Let's try to execute our new method and check if it works.

const arr = [1, 2, 3, 4];

console.log(arr.myMap(n => n * 2));

As our myMap method returns a new array, if we don't pass it to console.log we wouldn't be able to see its output in the console.

Our output in this case will be:

// Output
"My map implementation works!"
[1, 4, 6, 8]

Implementing filter

filter header

filter will pass through each element of the array and return a new array with only those elements where the callback function returns true.

Let's define our new function on the Array.prototype:

Array.prototype.myFilter = function(callback) {
    console.log("My filter implementation works!");

    for(let i = 0; i < this.length; i++){
        // perform our filter operation here
    }
}

Now we need to create a new array and decide whether each value gets to be in the new array or not with the help of our callback function:

Array.prototype.myFilter = function(callback) {
    console.log("My filter implementation works!");

    const newArr = [];
    for(let i = 0; i < this.length; i++){
        // the callback function needs to return a boolean (true or false)
        if(callback(this[i], i, this)) {
            newArr.push(this[i]);
        }
    }
    return newArr;
}

Let's filter out all odd numbers from this array:

const arr = [1, 2, 3, 4];

console.log(arr.filter(n => n % 2 === 0));

The modulo % operator returns the remainder of the division.

If there is no remainder that means our number is even.

The above code will output:

// Output
"My filter implementation works!"
[2, 4]

Implementing reduce

reduce header

reduce can be a bit harder to grasp but at its core it takes an array of elements and reduces it to a single value. Commonly it is used to perform the sum of all elements from an array.

Let's create our new function which will iterate the array and assign it to Array.prototype:

Array.prototype.myReduce = function(callback){
    console.log("My reduce implementation works!");

    for(let i = 0; i < this.length; i++){
        // reduce our array to a single value
    }
}

In order for reduce to return a single value (aka accumulator) it needs an initial value. This value can be passed as a parameter or if omitted will be initialized with the first element of the array. Our callback function needs to return the new value for our accumulator.

So we have two cases:

  • accumulator parameter is omitted so it gets initialized with the first element of the array and the for loop will begin with the second element
  • accumulator is not omitted and it gets initialized with the parameter value and the for loop starts with the first element of the array

Let's apply this logic to our code:

Array.prototype.myReduce = function(callback, accumulator){
    console.log("My reduce implementation works!");

    let newAccumulator = accumulator ? accumulator : this[0]
    for(let i = accumulator ? 0 : 1; i < this.length; i++){
        // reduce our array to a single value
    }
    return newAccumulator;
}

Now, our callback function needs to return the new accumulator value so let's update that in our myReduce method:

Array.prototype.myReduce = function(callback, accumulator){
    console.log("My reduce implementation works!");

    let newAccumulator = accumulator ? accumulator : this[0]
    for(let i = accumulator ? 0 : 1; i < this.length; i++){
        // reduce callback function parameters (previousValue, currentValue, currentIndex, array)
        newAccumulator = callback(newAccumulator, this[i], i, this);
    }
    return newAccumulator;
}

Let's test our new method:

const arr = [1, 2, 3, 4];

console.log("Without accumulator:");
console.log(arr.myReduce((acc, curr) => acc + curr));

console.log("With accumulator:");
console.log(arr.myReduce((acc, curr) => acc + curr), 4);

Our output in this case will be:

// Output
"My reduce implementation works!"

"Without accumulator:"
10

"With accumulator:"
14

Bonus example

Let's also test the output for an array of objects:

const galaxies = [{stars: 2, planets: 3}, {stars: 3, planets: 7}];

console.log(galaxies.reduce((acc, curr) => {
    return {
        stars: acc.stars + curr.stars,
        planets: acc.planets + curr.planets
    }
});

The output will be:

// Output
"My reduce implementation works!"
{
  planets: 10,
  stars: 5
}

Implementing sort

sort header

sort will arrange the elements in your array in ascending order by default. This method actually changes the original array and also returns the sorted array. If not given a callback function it will convert the elements to strings and compare them. This may lead sometimes to unwanted behavior.

Let's create our function and assign it to the Array.prototype:

Array.prototype.mySort = function(callback) {
    console.log("My sort implementation works!");

    // we need a double for loop to compare elements to each other
    for (let i = 0; i < this.length; i++) {
        for (let j = 0; j < this.length - 1; j++) {
        }
    }
}

Now we have two cases to deal with:

  • callback is provided and based on the value from this function we decide whether to swap or not the elements of the array (if value > 0 we swap the values)
  • callback is not provided so we convert elements to strings and compare them

Let's apply this logic to our function:

Array.prototype.mySort = function(callback) {
    console.log("My sort implementation works!");

    // we need a double for loop to compare elements to each other
    for (let i = 0; i < this.length; i++) {
        for (let j = 0; j < this.length - 1; j++) {
            if (callback
            ? callback(this[j], this[j + 1]) > 0
            : this[j].toString() > this[j + 1].toString()) {
                // swap the elements i.e. j value takes the place of j + 1 value
            }
        }
    }
}

I used a ternary operator to make the code more concise

The only thing we need to do now is actually swap the elements of the original array with the help of a temporary variable:

Array.prototype.mySort = function(callback) {
    console.log("My sort implementation works!");

    // we need a double for loop to compare elements to each other
    for (let i = 0; i < this.length; i++) {
        for (let j = 0; j < this.length - 1; j++) {
            if (callback
            ? callback(this[j], this[j + 1]) > 0
            : this[j].toString() > this[j + 1].toString()) {
                const temp = this[j + 1];
                this[j + 1] = this[j];
                this[j] = temp;
            }
        }
    }

    // the sort method also returns the array
    return this;
}

Let's try the two cases we described above:

const arr = [1, 4, 33, 2];

console.log("Without callback function");
console.log(arr.mySort());
console.log(arr);

console.log("With callback function");
console.log(arr.mySort((prev, curr) => prev - curr));
console.log(arr);

The output will be:

"Without callback function"
"My sort implementation works!"
[1, 2, 33, 4]
[1, 2, 33, 4]
"With callback function"
"My sort implementation works!"
[1, 2, 4, 33]
[1, 2, 4, 33]

As you can see without a callback function the sorted result may not be what you wanted. In this case 33 is considered to be smaller than 4 because it starts with a 3. Also it is important to note that the original array has mutated.

Let's wrap things up

A thing that needs to be mentioned is that these implementations are not production-ready. One of the things we have not done is check if the callback is actually a function. You can practice and try to implement this check in the above methods.

Congratulations!๐ŸŽ‰ You should be proud of yourself as you have successfully implemented five array methods and hopefully understand them better now.

Did you find this article valuable?

Support Toader Daniel by becoming a sponsor. Any amount is appreciated!

ย