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:
- The function that will be applied as you loop over the array
- A
thisArg
, which will be a reference to an object that will be thethis
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.
Write the Interface
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
}
Applying the Function to Each Element
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.
Finally, the thisArg
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
!
Bonus: Write Your Own with Test-Driven Development
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.
Top comments (1)
TIL map has a thisArg!