Seva Zaikov
2 Mar 2018
•
3 min read
Recently my friend sent me a very interesting JS snippet, which he found in one open-source library:
addressParts.map(Function.prototype.call, String.prototype.trim);
At first, I laughed and thought “nice try”. Second thought was that map
accepts only one argument, and I went to MDN docs, where I realized that you can pass context as a second argument. At this point I was really puzzled, and after running my confusion increased to the limit – it worked as expected!
I’ve spent at least half an hour playing with it, and it was an interesting example how magical JavaScript can be, even after years spent writing it. Feel free to figure it out by yourself, and if you want to check out my understanding, keep reading!
So, how does it work? Let’s start with super naive implementation (which is actually shorter and easier to read :)):
addressParts.map(str => str.trim());
But as it is clear, we strive here to avoid creating new functions, so let’s move on. Function.prototype.call is a function in prototype of all JavaScript functions, and it invokes function, using first argument as this
argument, passing rest arguments as parameters of function invocation. To illustrate:
// this function has `Function` in prototype chain
// so `call` is available
function multiply(x, y) {
return x * y;
}
multiply.call(null, 3, 5); // 15
multiply(3, 5); // same, 15
Typical usage of the second argument can be the following – imagine you have a class-based react component, and you want to render list of buttons:
class ExampleComponent extends Component {
renderButton({ title, name }) {
// without proper `this` it will fail
const { isActive } = this.props;
return (
<Button key={title} title={title}>
{name}
</Button>
);
}
render() {
const { buttons } = this.props;
// without second param our function won't be able
// to access `this` inside
const buttonsMarkup = buttons.map(this.renderButton, this);
}
}
However, from my experience, it is not that common to use this second argument, usually class properties or decorators are used to avoid binding all the time.
There is one similar method – Function.prototype.apply, which works the same, except that the second argument should be an array, which will be transformed to a normal list of arguments, separated by comma. So, let’s see how we can use it to calculate maximum:
Math.max(1, 2, 3); // if we know all numbers upfront
// we can call it like that
Math.max([1, 2, 3]); // won't work!
Math.max.apply(null, [1, 2, 3]); // will work!
// however, ES2015 array destructuring works as well:
Math.max(...[1, 2, 3]);
Now, let’s try to recreate a call which will solve our problem. We want to trim the string, and it is a method in String.prototype
, so we call it using .
notation (however, strings are primitives, but when we call methods, they are converted to objects internally). Let’s go back to our console:
// let's try to imagine how trim method is implemented
// on String.prototype
String.prototype.trim = function() {
// string itself is contained inside `this`!
const str = this;
// this is a very naive implementation
return str.replace(/(^\s+)|(\s+$)/g, '');
};
// let's try to use `.call` method to invoke `trim`
" aa ".trim.call(thisArg);
// but `this` is our string itself!
// so, next two calls are equivalent:
" aa ".trim.call(" aa ");
String.prototype.trim.call(" aa ");
We are now one step closer, but still not yet there to understand our initial snippet:
addressParts.map(Function.prototype.call, String.prototype.trim);
Let’s try to implement Function.prototype.call
itself:
Function.prototype.call = function(thisArg, ...args) {
// `this` in our case is actually our function!
const fn = this;
// also, pretty naive implementation
return fn.bind(thisArg)(...args);
};
So, now we can put all pieces together. When we declare function inside the .map
, we pass Function.prototype.call
with bound <String.prototype.trim>
as this
context, and then we invoke this function on each element in the collection, passing each string as thisArg
to the call
. It means that String.prototype.trim
will be invoked using string as this
context! We already found that it will work, using our example:
String.prototype.trim.call(" aa "); // "aa"
Problem solved! However, I think it is not the best example how one should write such a code in JavaScript, and instead just pass a simple anonymous function:
addressParts.map(str => str.trim()); // same effect
If you’re passionate about Front End development, check out the JavaScript Works job-board here!
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!