JavaScript's prototype is a powerful tool to help us create scripts that manipulates data through a chain of prototypes calls. It is more flexible and easier to use than classic object oriented inheritance. And as most of the data types in JavaScript are objects, it is just easy and pleasant to apply different methods to them.
"use strict";
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const sumDoubleOdds = numbers.filter(function(number) {
return number % 2 === 0;
}).map(function(number) {
return number * 2;
}).reduce(function(sum, number) {
return sum + number
}, 0);
console.log(sumDoubleOdds); // 40
If we use some arrow functions, we could even shorten this algorithm a bit.
"use strict";
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const sumDoubleOdds = numbers
.filter(n => n % 2 === 0)
.map(n => number * 2)
.reduce((s, n) => s + n, 0);
console.log(sumDoubleOdds); // 40
And that would totally work! But sometimes, we cannot use prototypes because of the nature of the data structure we are dealing with. Let's take a concrete example with a function that has to return the inner type of anything.
As we said, almost everything in JavaScript is an object. But this will not help us much, especially when our function should only work for a specific type. So you might know that you can use the typeof
operator to guess the type of something.
"use strict";
console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof []); // object, wait what???
That's odd! Well, not really since we said that almost everything is an object. But the typeof
is really not something we can rely on when we have to deal with object
s or array
s or even promise
s. These will all fall into that rule if we use the typeof
operator.
"use strict";
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof (new Promise(() => {}))); // object
So how could we do that in JavaScript? We could say that we would create a function called type
that will try to guess these types with something more explicit than object
. And we could try to cover all these edge cases.
"use strict";
function type(something) {
return typeof something;
}
console.log(type(1)); // number
console.log(type(true)); // boolean
console.log(type("")); // string
console.log(type({})); // object
console.log(type([])); // object
console.log(type(new Promise(() => {}))); // object
For now, we only used the typeof
operator. But there is something else we can use to get a better representation of something. As we said, again, almost everything is an object. So we could try to use the Object.prototype.toString
method which is implemented for native objects in JavaScript and has the representation of that said object (or something).
"use strict";
function type(something) {
return something.toString();
}
console.log(type(1)); // 1
console.log(type(true)); // true
console.log(type("")); //
console.log(type({})); // [object Object]
console.log(type([])); //
console.log(type(new Promise(() => {}))); // [object Promise]
So, this is a fail, but let's take a look at the win cases. For the object and the promise, it did work somehow. It is still better than our typeof
operator, though it added some unecessary garbage string. But for the others data types, it failed hard. And even worse, it will throw an exception for some others data types like undefined
or null
.
"use strict";
function type(something) {
return something.toString();
}
console.log(type(undefined)); // Cannot read property 'toString' of undefined
console.log(type(null));
I'm sure this error message is something we all got someday in our JavaScript experience. Ah, memories... But we could try something else, like the Function.prototype.call
method which allows us to call a method from a prototype even for data that aren't of the same type as our prototype. This means, for instance, that we can use the Array.prototype.map
not only on arrays, but also on strings, even if this is not an array. So this will fail:
"use strict";
[1, 2, 3].map(x => x + 1);
"123".map(x => x + 1);
// TypeError: "123".map is not a function
But this won't:
"use strict";
Array.prototype.map.call([1, 2, 3], x => x + 1); // [2, 3, 4]
Array.prototype.map.call("123", x => x + 1); // ["11", "21", "31"]
Cool, huh? Well, this example might not be the best because there is already some tools that can help us transform iterable data structures into an arrayish data structure that can be mapped on. But let's go back to our type
function and use this little trick to try and solve our problem.
"use strict";
function type(something) {
return Object.prototype.toString.call(something);
}
console.log(type(1)); // [object Number]
console.log(type(true)); // [object Boolean]
console.log(type("")); // [object String]
console.log(type({})); // [object Object]
console.log(type([])); // [object Array]
console.log(type(new Promise(() => {}))); // [object Promise]
Ah! That's better. Way better! Even if this little [object ...]
is still there, we have an accurate representaton of what our something is. This even works for undefined
and null
.
"use strict";
function type(something) {
return Object.prototype.toString.call(something);
}
console.log(type(undefined)); // [object Undefined]
console.log(type(null)); // [object Null]
console.log(type(1)); // [object Number]
console.log(type(true)); // [object Boolean]
console.log(type("")); // [object String]
console.log(type({})); // [object Object]
console.log(type([])); // [object Array]
console.log(type(new Promise(() => {}))); // [object Promise]
We could add some more code to make it operate exactly like the typeof
operator by removing every [object ...]
occurrences but that will be out of the scope of this article. What I'm trying to show you is that this call
method is really powerful and can operate on multiple data types. But it can be sometimes painful to write. Imagine we have to do this for the map
, filter
, reduce
methods. This will require us to write the same thing over and over. Even if we use arrow functions.
"use strict";
const type = x => Object.prototype.toString.call(x);
const map = (...x) => Array.prototype.map.call(...x);
const filter = (...x) => Array.prototype.filter.call(...x);
const reduce = (...x) => Array.prototype.reduce.call(...x);
And there are some more useful array methods I didn't cover here like find
, findIndex
, some
, every
, ...
So the final solution would be to write our own function. It will take a prototype and convert this prototype into a reusable function.
"use strict";
function prototypeToFunction(prototype) {
return function(...parameters) {
return prototype.call(...parameters);
};
}
const map = prototypeToFunction(Array.prototype.map);
const filter = prototypeToFunction(Array.prototype.filter);
const reduce = prototypeToFunction(Array.prototype.reduce);
First, we take the prototype to convert as our only parameter. We return a function since our own function is like a function factory. map
needs to be called with some arguments so it is necessary to return here a function. Then, we use the spread operator for our returned function. This is useful because we don't know for sure how many parameters the prototype method is awaiting. And finally, when the returned function is called, we just call the call
method on that prototype with the given parameters. Without modifying it in any way. So this means that we can now use our map
on almost everything that is iterable.
"use strict";
function prototypeToFunction(prototype) {
return function(...parameters) {
return prototype.call(...parameters);
};
}
const map = prototypeToFunction(Array.prototype.map);
map([1, 2, 3], x => x + 1); // [2, 3, 4]
map("123", x => x + 1); // ["11", "21", "31"]
And you can even use all the power of JavaScript's prototypes to help you build larger and more complex algorithms.
"use strict";
function prototypeToFunction(prototype) {
return function(...parameters) {
return prototype.call(...parameters);
};
}
const map = prototypeToFunction(Array.prototype.map);
map("123", x => x + 1).reduce((s, x) => s + x, ""); // "112131"
If you know JavaScript enough, you might know that there is a language construct, the destructuring operator that might do the job instead of all this hassle. The point of this article is not to change the language or people's habit but rather to open minds on what the language is capable to do. It is also something I discoverd and use now a lot, especially for the type
function. And if you want the full definition of what I'm using, here it is:
"use strict";
function prototypeToFunction(prototype) {
return function(...parameters) {
return prototype.call(...parameters);
};
}
const $type = prototypeToFunction(Object.prototype.toString);
const type = x => $type(x).replace(/\[\object\s(.*)]/, "$1").toLowerCase();
console.log(type(undefined)); // "undefined"
console.log(type(null)); // "null"
console.log(type({})); // "object"
console.log(type([])); // "array"
console.log(type(new Promise(() => {}))); // "promise"
Some more reading:
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Top comments (0)