Array-vengers: Assembling Polyfills to Save Old Browsers

Array-vengers: Assembling Polyfills to Save Old Browsers

In a world where outdated browsers threaten to break your beautifully written JS code, a team of hero rises to restore balance and we know them as Polyfills.

Just like the Avengers assemble to protect the universe, polyfills step in to bring modern JavaScript array methods to older browsers that lack support. Ever tried using .map(), .filter(), or .reduce() only to have your code crash in an ancient version of Internet Explorer? Fear not! With the power of polyfills, we can manually implement these missing methods, ensuring compatibility across all environments.

Suit up, coders — It’s time to assemble some polyfills!

What are Polyfills?

A polyfill is a piece of code that mimics the behavior of a newer feature in older browsers that don’t support it. Think of it as a Patch that fills in the missing functionality.

For e.g., If a browser doesn’t support .map() on arrays, we can manually define a function that works the same way so that our code doesn’t crash when running on the old browsers.

Now that we understand what polyfills are, the next step is learning how to create them. To do this, we first need to understand how a function behaves internally. Once we grasp its logic, we can replicate it using JavaScript.

How to create polyfills?

✅ Understand the Function You Need to Polyfill

✅ Understand the Function Signature

✅ Know the return type

✅ Now create it’s polyfill

Polyfill for slice() method

The slice() method of array is use to create a subarray without modifying the original array.

Syntax:

arr.slice([start], [end]);

Analysis of the slice() method:

  • It accept two argument start & end, both are optional.
  • It returns a new array copying all items from index start to end (not including end).

  • Default value of start is 0 and end is length of array.

  • Both start and end can be negative, in that case position from array end is assumed.

  • If end is less than or equal to start, empty array is returned.

For instance:

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

console.log(arr.slice(1, 3)); // [ 2, 3 ] (copy from 1 to 3(not included))
console.log(arr.slice(-2)); // [ 4, 5 ] (copy from -2 till the end)
console.log(arr.slice(2, 1)); // [] (start > end)

So it’s polyfill will be:

if (!Array.prototype.mySlice) {
  Array.prototype.mySlice = function (start = 0, end = this.length) {
    const newArr = []; // Array to store the extracted elements

    // Handle negative indices: Convert them to positive indices from the end
    start = start < 0 ? this.length + start : start;
    end = end < 0 ? this.length + end : end;

    // If start is less than end, extract elements
    if (start < end) {
      for (let i = start; i < end; i++) {
        newArr.push(this[i]);
      }
    }

    return newArr; // Return the new subarray without modifying the original array
  };
}

Testing the polyfill:

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

console.log(arr.mySlice(-3, -1)); // [ 3, 4 ]
console.log(arr.mySlice(1, 3)); // [ 2, 3 ]
console.log(arr.mySlice(3, 1)); // []

Polyfill for splice() method

The .splice() method is used to modify an array by adding, removing, or replacing elements at a specified index.

Syntax:

arr.splice(start, deleteCount, item1, item2, ...);
  • start (Required), from here operation start.

  • deleteCount (Optional), number of element to remove. Default value is 0.

  • item1, item2, ... (Optional), elements to insert at start index.

Analysis of the splice() method:

  • It modifies the original array.

  • Return array containing the removed element.

  • If deleteCount exceeds available elements, it removes all elements from start onward.

  • start can be negative, in that case position from array end is assumed.

For instance:

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

// Removing Element
const rem = arr.splice(1, 2); // Removes 2 elements starting from index 1
console.log(arr);  // [ 1, 4, 5 ]
console.log(rem);  // [ 2, 3 ]

// Inserting Element
arr.splice(2, 0, "a", "b"); // Inserts 'a', 'b' at index 2
console.log(arr); // [ 1, 4, 'a', 'b', 5 ]

// Replacing Element
arr.splice(1, 2, "x", "y"); // Replaces 2 elements at index 1 with 'x', 'y'
console.log(arr); // [ 1, 'x', 'y', 'b', 5 ]

// Negative start index
arr.splice(-2, 1); // Removes element at index -2
console.log(arr); // [ 1, 'x', 'y', 5 ]

// deleteCount exceeds remaining element
const removed = arr.splice(2, 10);
console.log(arr); // [ 1, 'x' ]
console.log(removed); // [ 'y', 5 ]

So it’s polyfill will be:

if (!Array.prototype.mySplice) {
  Array.prototype.mySplice = function (start, deleteCount, ...rest) {
    const deletedItem = []; // Array to store the deleted elements

    // Handle negative start index    
    start = start < 0 ? this.length + start : start;

    // Finding the end, till where element will be removed (exclusive)
    const end = deleteCount ? start + deleteCount : this.length;

    // Collect the elements to be deleted
    for (let i = start; i < end; i++) {
      deletedItem.push(this[i]);
    }

    /**
     * Create a new array with:
     * - Elements before 'start'
     * - Elements to be inserted
     * - Elements after 'end'
    */
    const newArray = [...this.slice(0, start), ...rest, ...this.slice(end)];

    // Update the original array length to match the new array
    this.length = newArray.length;

    // Copy modified elements back into the original array
    for (let i = 0; i < newArray.length; i++) {
      this[i] = newArray[i];
    }

    return deletedItem; // Return the deleted elements
  };
}

Testing the polyfill:

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

// Removing elements
const rem1 = arr.mySplice(1, 2); 
console.log(rem1); // [ 2, 3 ]
console.log(arr); // [ 1, 4, 5 ]

// Inserting elements
const rem2 = arr.mySplice(1, 0, "a", "b"); 
console.log(rem2); // [] 
console.log(arr); // [ 1, 'a', 'b', 4, 5 ]

// Replacing elements
const rem3 = arr.mySplice(2, 2, "x", "y");
console.log(rem3); // [ 'b', 4 ]
console.log(arr); // [ 1, 'a', 'x', 'y', 5 ]

// Negative start index
const rem4 = arr.mySplice(-2, 1); 
console.log(rem4); // [ 'y' ]
console.log(arr); // [ 1, 'a', 'x', 5 ]

Polyfill for concat() method

The concat() method of an array is used to merge two or more arrays without modifying the original arrays.

Syntax:

arr.concat(value1, value2, ..., valueN)

Analysis of the concat() method:

  • It accepts multiple arguments, which can be individual values or arrays.

  • It returns a new array that consists of elements from the original array followed by the provided values or arrays.

  • If an argument is an array, its elements are added individually.

  • If the argument is an nested array, it only open the first level

For instance:

const arr = [1,2];

console.log(arr.concat([3,4])); // [ 1, 2, 3, 4 ]
console.log(arr.concat([3, 4], [5, 6])); // [ 1, 2, 3, 4, 5, 6 ]
console.log(arr.concat([3, 4], 5, 6)); // [ 1, 2, 3, 4, 5, 6 ]
console.log(arr.concat([3, 4], [[5, 6], 7, 8])); // [ 1, 2, 3, 4, [ 5, 6 ], 7, 8 ]

So it’s polyfill will be:

if (!Array.prototype.myConcat) {
  Array.prototype.myConcat = function (...rest) {
    // Create a new array starting with all elements of the original array
    const newArr = [...this];

    // Iterate through the arguments passed to the function
    for (let i = 0; i < rest.length; i++) {
      // If the argument is an array, spread its elements into newArr
      if (Array.isArray(rest[i])) {
        newArr.push(...rest[i]);
      } else {
        // Otherwise, push the value as a single element
        newArr.push(rest[i]);
      }
    }
    return newArr; // Return the newly formed concatenated array
  };
}

Testing the polyfill:

const arr = [1, 2];

console.log(arr.myConcat([3, 4])); // [ 1, 2, 3, 4 ]
console.log(arr.myConcat([3, 4], 5, 6)); // [ 1, 2, 3, 4, 5, 6 ]
console.log(arr.myConcat([3, 4], [[5, 6], 7, 8])); // [ 1, 2, 3, 4, [ 5, 6 ], 7, 8 ]
console.log(arr.myConcat([])); // [ 1, 2 ]

Polyfill for forEach() Method

The forEach() method of an array is used to execute a provided function once for each array element.

Syntax:

arr.forEach(callback(element, index, array));

Analysis of the forEach() Method:

  • It accepts a callback function that gets executed on each element of the array.

  • The callback function takes three arguments:

    1. element - The current element being processed.

    2. index (optional) - The index of the current element.

    3. array (optional) - The array on which forEach was called.

  • It does not return a new array.

  • It does not modify the original array but allows operations on its elements.

For instance:

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

arr.forEach((element) => {  // Output: 1 2 3 4 5
  console.log(element); 
});

So it’s polyfill will be:

if (!Array.prototype.myForEach) {
  Array.prototype.myForEach = function (cb) {
    // Loop through each element of the array
    for (let i = 0; i < this.length; i++) {
      /**
       * Call the provided callback function with:
       * - the current element (this[i])
       * - the index of the element (i)
       * - the entire array (this)
      */
      cb(this[i], i, this);
    }
  };
}

Testing the polyfill:

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

arr.myForEach((val) => console.log(val)); 
// 1
// 2
// 3
// 4
// 5

let sum = 0;
arr.myForEach((val) => (sum += val));
console.log(sum); // 15

Polyfill for .map() Method

The .map() method of an array is used to create a new array by applying a provided function to each element of the original array.

Syntax:

arr.map(callback(element, index, array));

Analysis of the .map() Method:

  • It accepts a callback function that gets executed on each element of the array.

  • The callback function takes three arguments:

    1. element - The current element being processed.

    2. index (optional) - The index of the current element.

    3. array (optional) - The array on which .map() was called.

  • It returns a new array with transformed elements.

  • It does not modify the original array but applies the callback function to each element.

For instance:

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

const squared = arr.map((element) => element * element);
console.log(squared); // [1, 4, 9, 16, 25]

So it’s polyfill will be:

if (!Array.prototype.myMap) {
  Array.prototype.myMap = function (cb) {
    const newArr = []; // Array to store the transformed elements

    // Iterate over each element in the array
    for (let i = 0; i < this.length; i++) {
      const val = cb(this[i], i, this); // Apply the callback function to the current element
      newArr.push(val); // Store the transformed value in the new array
    }
    return newArr; // return the new array
  };
}

Testing the polyfill:

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

console.log(arr.myMap((num) => num * 2)); // [2, 4, 6, 8, 10]
console.log(arr.myMap((num, index) => num + index)); // [1, 3, 5, 7, 9]
console.log(arr.myMap((num) => num.toString())); // ["1", "2", "3", "4", "5"]

Polyfill for .filter() Method

The .filter() method of an array is used to create a new array containing only the elements that satisfy a given condition specified in a callback function.

Syntax:

arr.filter(callback(element, index, array));

Analysis of the .filter() Method:

  • It accepts a callback function that gets executed on each element of the array.

  • The callback function takes three arguments:

    1. element - The current element being processed.

    2. index (optional) - The index of the current element.

    3. array (optional) - The array on which .filter() was called.

  • It does not modify the original array.

  • If no elements match the condition, an empty array is returned.

For instance:

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

console.log(arr.filter((num) => num % 2 === 0)); // [ 2, 4, 6, 8, 10 ]
console.log(arr.filter((num) => num > 5)); // [ 6, 7, 8, 9, 10 ]

So it’s polyfill will be:

if (!Array.prototype.myFilter) {
  Array.prototype.myFilter = function (cb) {
    const newArr = []; // Array to store elements that satisfy the condition

    // Iterate through each element of the array
    for (let i = 0; i < this.length; i++) {
      if (cb(this[i], i, this)) {
         // Call the callback function with current element, index, and the entire array
        // If callback returns true, add the element to newArr
        newArr.push(this[i]);
      }
    }
    return newArr; // Return the new array
  };
}

Testing the polyfill:

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

console.log(arr.myFilter((num) => num % 2 === 0)); // [ 2, 4, 6 ]
console.log(arr.myFilter((num) => num <= 2)); // [ 1, 2 ]
console.log(arr.myFilter((num) => num > 10)); // []

Polyfill for .reduce() Method

The .reduce() method of an array is used to apply a function to each element of the array and reduce it to a single value.

Syntax:

arr.reduce(callback(accumulator, element, index, array), initialValue);

Analysis of the .reduce() Method:

  • It accepts a callback function that gets executed on each element of the array.

  • The callback function takes four arguments:

    1. accumulator - The accumulated result of previous iterations.

    2. element - The current element being processed.

    3. index (optional) - The index of the current element.

    4. array (optional) - The array on which .reduce() was called.

  • It also takes an optional initial value.

  • The accumulator starts with the initial value (if provided), otherwise, the first element of the array is used as the initial value.

  • It does not modify the original array.

For instance:

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

console.log(arr.reduce((acc, num) => acc + num, 0)); // 15
console.log(arr.reduce((max, num) => (num > max ? num : max), arr[0]));

So it’s polyfill will be:

if (!Array.prototype.myReduce) {
  Array.prototype.myReduce = function (cb, initial) {
    let acc = initial; // Initialize the accumulator with the provided initial value
    for (let i = 0; i < this.length; i++) {
      // If initial value is provided, apply the callback function
      // Otherwise, use the first element of the array as the initial accumulator value
      acc = acc ? cb(acc, this[i], i, this) : this[i];
    }
    return acc; // Return the accumulated result
  };
}

Testing the polyfill:

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

console.log(arr.myReduce((acc, num) => acc + num, 0)); // 15

// Count occurrences of elements
const chars = ['a', 'b', 'a', 'c', 'b', 'a'];
console.log(chars.myReduce((acc, char) => {
  acc[char] = (acc[char] || 0) + 1;
  return acc;
}, {}));
// Output is : { a: 3, b: 2, c: 1 }

Conclusion

And there you have it! Just like superheroes step in to save the day, polyfills come to the rescue when older browsers struggle with modern JavaScript methods. By understanding how these functions work internally and implementing their polyfills, we ensure that our code remains robust, future-proof, and accessible to a wider audience.

Mastering polyfills not only enhances your JavaScript skills but also deepens your understanding of how these essential array methods function under the hood. So, the next time you face compatibility issues, don’t panic—just suit up and write your own polyfill!

If you enjoyed this article, don’t forget to like and subscribe to our newsletter for more updates! 🚀