One way to understand JavaScript methods is to roll your own version. Today, let’s write Array.map
!
It turns out, Array.map
takes two arguments:
thisArg
, which will be a reference to an object that will be the this
context in the provided function. In my experience, I have not really used the second argument, but we’ll want to be sure to include it.
Since I’m not interested in extending the Array
prototype, I’ll just create a separate map
function. Therefore, I’ll actually pass the array as an argument, meaning we’ll have three total arguments:
function map(arr, fn, thisArg) { // Magic goes here }
The fn
we provide must be applied to each element of the array. Let’s make that happen.
function map(arr, fn, thisArg) { const len = arr.length; const result = new Array(len); for (let i = 0; i < len; i++) { if (i in arr) { result[i] = fn(arr[i], i, arr); } } return result; }
Importantly, we pass three arguments to fn
: The current array element, the index of the current array element, and the original input array. Let’s see this in action:
const mapped = map([1, 2, 3], el => el * 2); console.log(mapped); // [2, 4, 6]
Great, looks like the basics are working! This example doesn’t include use of the i
or arr
passed to our fn
, but you can test that on your own.
Let’s not forget the thisArg
! If thisArg
is provided, we want to make sure we bind
our provided function to the thisArg
. Here’s the amended code to make it work:
function map(arr, fn, thisArg) { fn = thisArg === undefined ? fn : fn.bind(thisArg); const len = arr.length; const result = new Array(len); for (let i = 0; i < len; i++) { if (i in arr) { result[i] = fn(arr[i], i, arr); } } return result; }
And here it is in action. (Note that my provided function can’t be an arrow function since you cannot rebind an arrow function’s this
reference.)
const obj = { num: 10, }; const mapped = map( [1, 2, 3], function (el) { return el + this.num; }, obj ); console.log(mapped); // [11, 12, 13]
And we can see this
refers to obj
!
I wrote this map
function using Test-Driven Development (TDD)! I laid out tests for all the scenarios that needed to pass for Array.map
and then, one-by-one, reworked the code to make them pass. Consider using this method if you try this with other built-in JS methods.
Here are the tests I used for the map
function:
describe("array", () => { describe("map", () => { it("maps a simple array", () => { const arr = [1, 2, 3]; const fn = el => el * 10; const answer = arr.map(fn); expect(map(arr, fn)).toEqual(answer); }); it("maps an empty array", () => { const arr = []; const fn = el => el * 10; const answer = arr.map(fn); expect(map(arr, fn)).toEqual(answer); }); it("maps an array with holes", () => { const arr = [1, 2, , , 3]; const fn = el => el * 10; const answer = arr.map(fn); expect(map(arr, fn)).toEqual(answer); }); it("uses thisArg", () => { const obj = { 0: "foo", 1: "bar", 2: "baz" }; const arr = [1, 2, 3]; const fn = function(el, i) { return this[i] + el; }; const answer = arr.map(fn, obj); expect(map(arr, fn, obj)).toEqual(answer); }); it("uses the idx and arr parameters", () => { const arr = [1, 2, 3]; const fn = (el, idx, arr) => JSON.stringify([el, idx, arr]); const answer = arr.map(fn); expect(map(arr, fn)).toEqual(answer); }); });
I hope you enjoyed this! Let me know if you end up rolling your own versions of other built-in methods.