So this might be helpful, I typed it in a rush so please ask for clarification on anything.
Say you want to create a new type of object (all things in JS are objects, it’s how it works).
We’ll use a function to do that, and the new
keyword, because this is what kinda glues it all together. The function is, by convention, called a constructor. And if I use the new
keyword when I call that function, it will create an object out of thin air. Inside that function, this
is the object itself (there are wierdnesses surrounding this
, but for now, that explanation will do):
function MyStupidArray(length) {
this.length = length;
}
If I do new MyStupidArray(4)
, I get an object of the type MyStupidArray
with a property length
, which I’ve set to 4. this
is the object I’ve just created, so this.length
is a property called length
.
let myArr = new MyStupidArray(4);
// myArr is: { length: 4 }
That isn’t really like an array though! So lets say that whatever it’s length
is, it should have that many items:
function MyStupidArray(length) {
this.length = length;
for (let i = 0; i < length; i++) {
this[i] = undefined;
}
}
Now when I do new MyStupidArray(4)
, this is what I get:
{ 0: undefined, 1: undefined, 2: undefined, 3: undefined, length: 4 }
That’s sorta like an array. I can do this:
let myArr = new MyStupidArray(2);
// { 0: undefined, 1: undefined, length: 2}
myArr[0] = 10
myArr[1] = 20
// { 0: 10, 1: 20, length: 2}
My array has a number of items, each with an index (in order, starting at 0), and a length which tells me the total number of items. This isn’t very useful at all yet though. It’s very clunky.
This is the point where API design becomes important. What do I need to do when I create the object? How do I modify the object? How do I prevent users of my object doing stupid things? For example, I can do
myArr[475] = "hiya"
But the length will still be 2. Ugh. This is an dreadful implentation, but I’ll plow on.
Every function has a property called prototype
. Every object created by calling a function using new
gets access to that function’s prototype
property. It does a few magic things under the hood, but basically it means you can attach functions to your object, and they will apply to all objects of that type that you create.
(Importantly for this challenge, you can modify the functions attached to a prototype whenever and however you want. I’ll come back to that: it is just a case of redefining what they do)
function MyStupidArray(length = 0) {
this.length = length;
for (let i = 0; i < length; i++) {
this[i] = undefined;
}
}
MyStupidArray.prototype.push = function (value) {
// remember `this` is going to be the object you create.
// So `this` is going to be an instance of `MyStupidArray`
// Assign the value you want to push to the end of the array
this[this.length] = value;
// Increase the length by one
this.length++;
// return the new length
return this.length;
}
So if I do:
let myArr = new MyStupidArray()
// myArr is just { length: 0 }
myArr.push(10)
// myArray is now { 0: 10, length: 1 }
myArr.push(20)
// myArr is now { 0: 10, 1: 20, length: 2 }
Let’s add a pop
method:
MyStupidArray.prototype.pop = function () {
// Pop takes the last value in the array, and "pop"s it off
// Keep a hold of the last value:
let poppedValue = this[this.length - 1];
// Delete it from the object:
delete this[this.length - 1];
// Drop the length by one
this.length--;
// return the popped value
return poppedValue;
}
And to use it:
let myArr = new MyStupidArray()
// myArr is just { length: 0 }
myArr.push(10)
// myArray is now { 0: 10, length: 1 }
// returns `1`
myArr.push(20)
// myArr is now { 0: 10, 1: 20, length: 2 }
// returns `2`
myArr.pop()
// myArr is now { 0: 10, length: 1}
// returns `20`
This is still really horrible, but it kinda does useful stuff now. Lets add a method that lets me transform every item in the “array”. What it needs is to run a function on every member in the array, but not change the length, just the values:
MyStupidArray.prototype.map = function(callback) {
for (let i = 0; i < this.length; i++) {
this[i] = callback(this[i]);
}
// Just return the mutated version of my stupid array,
// so I can see what happened:
return this;
}
Let’s give it a go:
let myArr = new MyStupidArray()
// myArr is just { length: 0 }
myArr.push(10)
// myArray is now { 0: 10, length: 1 }
// returns `1`
myArr.push(20)
// myArr is now { 0: 10, 1: 20, length: 2 }
// returns `2`
I want to multiply every value by 10. So if I define a function to do that:
function times10 (number) {
return number * 10;
}
I can give that function to map
and it will run once for every value in the “array”
myArr.map(times10)
// myArr is now { 0: 100, 1: 200, length: 2 }
// returns { 0: 100, 1: 200, length: 2 }
One of the tenets of functional programming is immutability. I don’t want map
to mutate the array, I want it to give me a whole new array with the modified values.
MyStupidArray.prototype.map = function(callback) {
// So I create a new stupid array, then copy the properties from
// the current stupid array into it:
let myArrayCopy = Object.assign(new MyStupidArray(), this);
// Then do the same thing:
for (let i = 0; i < myArrayCopy.length; i++) {
myArrayCopy[i] = callback(myArrayCopy[i]);
}
// Just return the new version of my stupid array,
// otherwise all this was for naught:
return myArrayCopy;
}
This now:
myArr.map(times10)
// myArr is still { 0: 10, 1: 20, length: 2 }
// but this returns { 0: 100, 1: 200, length: 2 }
(NOTE this is why chaining works, for example:
[1,2,3,4,5,6].slice(2) // get a slice of the array from index 2 to the end
.map(v => v * 10) // multiply all the values by 10
.filter(v => v > 40) // only keep those > 40
Because slice
returns this
, and this
is an array, the value of myArray.slice()
is an array. So I can map
that. map
returns this
, which is an array, so the value of mySlicedArray.map()
is an array. So I can filter
that. And mySlicedAndMappedArray.filter
returns this
, which is an array, so I can apply another method to that, and so on and so forth)
Thankfully you don’t have to think about how to implement this for basic stuff like arrays, because JS provides an Array object that does all this stuff out of the box. Close to what I’ve described above (note that you don’t need the new
for builtin object types, but that it is using it under-the-hood):
let myNormalArray = Array(2)
// {0: undefined, 1: undefined, length: 2 }
myNormalArray[0] = 10;
myNormalArray[1] = 20;
myNormalArray.push(30);
myNormalArray.map(function (v) { return v * 10 });
// etc etc
Better still, JS has built-in syntactic sugar so it ain’t as clunky. []
is the same as Array()
which is the same as new Array(0)
:
let myNormalArray = [10, 20, 30].map(v => v * 10);
So. For the challenge, you’re being asked to make your own version of the filter
function. Remember:
- Objects of a specific type can have functions attached to the
prototype
property of the function that creates the object. For arrays, that function is called, unsurprisingly, Array
.
- You can add more prototype functions to Array or write new definitions for the ones already present.
- Inside your prototype function,
this
is going to refer to the array that your filter
function is going to operate on.
- Arrays always have a
length
property, so you can access that inside the function using this.length
.
- You can add more prototype functions to Array, or modify the ones already there.
Note that this is a powerful feature and should be avoided as a rule, as it can very easily break things. For example:
Array.prototype = {
map() { return "I don't want to map your array." },
filter() { return "I don't want to filter your array." },
reduce() { return "I don't want to reduce your array." },
reverse() { return "I don't want to reverse your array." },
join() { return "join! join!" },
};
Now nothing that tries to use those methods in your program will work! Shame! This can be an attack vector (less of an issue nowadays as there are protections against it, but still): you could, inside a malicious program, redefine how a prototype method used on an object like Window
(used in browser APIs) works and get it to do something nasty like log input or whatever 