Reverse Engineering: Array.map
Create My Own JavaScript Array.map
Years ago, I applied to a financial company for a frontend developer role. Back to the day, I’ve felt like I’m capable to do most of the tasks that a SAAS product centric frontend developer can do. Not saying I’m fully confident, but I believe a lot of us will eventually come to a point and think…
“Com’on, all I do is creating UI with JavaScript. It’s not that hard.”
The job description looks just like another React frontend developer role. And the interviewer did briefly ask some React related questions, which are not so difficult for me. After the chit-chat session, they told me…
“Although this is a frontend developer role, but we also make plugins and libraries. Which heavily relies on your vanilla JavaScript ability. And we don’t actually care about if you know React or not, because it shouldn’t be a problem if you already have a good understanding of JavaScript.”
Then they started to ask me some questions about how JavaScript works behind the scenes. I can barely answer some of them. At that moment I’m sure I’m visibly struggling.
About half hour in, they decide to put me to the real test. And asked…
“Can you implement a map
right now?”
And I can’t. I know how to use it… But I don’t know I can implement my own map
. What do you mean “implement a map
right now”? It’s just there, why would anyone want to do that? How is it possible to make my own map
?
Of course, I failed the interview. But it does show me the next step I can take after building UIs for years. It’s to be the one who does not only know how to utilize the tool, but also knows how the tool actually works. And maybe able to invent a new tool based on the knowledge of programming basics. To see an abstraction of something, and able to pick up the suitable data structure for it. To dissect the abstraction and know what I’m looking for. And implement it with the information I gathered from it.
Anyway…
Speaking of reverse engineering, first we need to know how to use it. So, let’s take a look of the minimal use case of map
.
[1, 2, 3, 4, 5].map((val, index) => val + index);
// [1, 3, 5, 7, 9]
Here’s what we can see from this minimal example…
map
is anArray
method (duh). So, to implement our own version, we’ll need to extendArray
to be able to use it.map
essentially just a loop, it loops through all the element in theArray
.map
takes a callback function as its argument.- The callback function has two arguments, the first one is the element itself and the second one is the element’s index.
- It processes each element with the callback function.
- Instead mutate the
Array
,map
will return a newArray
. Well, we’re not able to confirm this in the example. But we knew it by the spec…
And our end goal will be making an array method called transform
with the exact API and behaviour.
[1, 2, 3, 4, 5].transform((val, index) => val + index);
// [1, 3, 5, 7, 9]
To tackle this question, we’ll need to know two JavaScript’s concept. prototype
and the infamous this
.
Prototype
ECMAScript does not use classes such as those in C++, Smalltalk, or Java. Instead objects may be created in various ways including via a literal notation or via constructors which create objects and then execute code that initialises all or part of them by assigning initial values to their properties. Each constructor is a function that has a property named “
prototype
” that is used to implement prototype-based inheritance and shared properties. Objects are created by using constructors in new expressions; for example,new Date(2009,11)
creates a new Date object. Invoking a constructor without using new has consequences that depend on the constructor. For example,Date()
produces a string representation of the current date and time rather than an object.Every object created by a constructor has an implicit reference (called the object’s prototype) to the value of its constructor’s “
prototype
” property. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain. When a reference is made to a property in an object, that reference is to the property of that name in the first object in the prototype chain that contains a property of that name. In other words, first the object mentioned directly is examined for such a property; if that object contains the named property, that is the property to which the reference refers; if that object does not contain the named property, the prototype for that object is examined next; and so on.
As the specification says, JavaScript achieve inheritance through prototype
. And whenever you try to access a property of any object, JavaScript will try to trace it back to its root until it finds the specific property you are looking for.
const arr = [1, 2, 3, 4, 5];
Let’s say we have an array arr
. As you probably heard that almost everything in JavaScript is Object
. And since JavaScript is a dynamic language, we can assign whatever property to our arr
like so…
arr.transform = () => console.log('hello');
// [1, 2, 3, 4, 5, transform: ƒ]
This is basically how we extend our Object
. But this only works for the arr
object. What we want is all the Array
‘s can access the transform
method. Remember that whenever you try to access a property of any object, JavaScript will try to trace it back to its root until it finds the specific property you are looking for? So, instead attach the transform
method to an instance, we need to do it with Array
‘s prototype
.
Array.prototype.transform = () => console.log('hello');
If we try to take a look to the Array
‘s prototype
now, we should be able to see our new method there.
Now, we can call our transform
method on any Array
, all thanks to the prototype
.
[1, 2, 3, 4, 5].transform()
// hello
[2, 3, 4, 5, 6].transform()
// hello
This
The second part is try to understand how this
works.
this
in JavaScript acts kind of funky. I highly suggest anyone who want to learn JavaScript itself have at least try to walk through the MDN documentation once. I’m not going to explain it here, since it’s a quite over-saturated topic on the internet.
The value of
this
is not the object that has the function as an own property, but the object that is used to call the function. You can prove this by calling a method of an object up in the prototype chain.
In the Function Context section, we can find the explanation, which is exactly what we need. We need our transform
to access the object that calls it.
We can do a small experiment to prove it by modify our transform
again…
Array.prototype.transform = function() {
console.log(this);
}
[1, 2, 3].transform();
// [1, 2, 3]
First, we need to convert the arrow function to a function expression. Because we need the Function Context we just talked about. Arrow function does not bind its own this
. It inherits its parent scope. In this case it will point to the global/window object.
Once we are able to figure out these concepts, the actual implementation of our transform
function should be the simplest thing in the world.
Implementation of transform method
Array.prototype.transform = function(callback) {
var newArr = [];
var len = this.length;
for(var i = 0; i < len; i++) {
newArr.push(callback(this[i], i));
}
return newArr;
}
[1, 2, 3, 4, 5].map((elm, idx) => elm + idx);
// [1, 3, 5, 7, 9]
[1, 2, 3, 4, 5].transform((elm, idx) => elm + idx);
// [1, 3, 5, 7, 9]
Just like our game plan at beginning. Our transform
method takes one callback
argument. And the callback
function takes two arguments, the element itself and the index. And we push the value processed by the callback
function into a new array. And then just return the new array at the end of it.
I intentionally use older syntax like var
and for
. Because whenever we are doing this kind of monkey patching things, we tend to maximize the compatibility. There’s no obvious reason to create our own transform
if map
is available in our runtime. This is more likely to be happened when we try to make our own map
-like function in Internet Explorer or something which doesn’t have const
or forEach
etc… So, it’s better to keep it as simple as possible without any fancy new syntax. Although we’ll probably use babel for compatibility reason, but it’s a good addition to let the interviewer know what I’m doing.
With the knowledge above, we can implement all the Array
method by ourselves.
For example, we can create a forEach
like…
Array.prototype.forEachOne = function(callback) {
var len = this.length;
for(var i = 0; i < len; i++) {
// just do whatever the callback want to do
callback(this[i], i);
}
}
Or filter
…
Array.prototype.funnel = function(callback) {
var newArr = [];
var len = this.length;
for(var i = 0; i < len; i++) {
// only push the one that returns `true`
if(callback(this[i], i)) newArr.push(this[i]);
}
return newArr;
}
Everything!
The only tricky one is the notorious sort
… Which I found it’s actually worth some time to implement a JavaScript flavoured sort
. Maybe next time…