Why doesn’t [“a”].map(String.prototype.trim.call) work?

(This one twisted my head in circles even after I read the explanation twice, so I’m writing it all out.)

In JavaScript, you can write this:

1
2
3
["a"].map(function(str) {
return str.trim();
});
["a"].map(function(str) {
return str.trim();
});

But can you do this?

1
["a"].map(String.prototype.trim);
["a"].map(String.prototype.trim);

The answer is no. This would run trim(“a”), instead of “a”.trim(). String.prototype.trim doesn’t take the string as the first argument; it takes it as the context (You can see the context in a debugger by looking at the value of this).

JavaScript has a way of changing this though, .call():

1
2
3
4
5
function a() {
return this.name;
}
var b = { "name": "x" };
a.call(b); //returns b.name
function a() {
return this.name;
}
var b = { "name": "x" };
a.call(b); //returns b.name

So can you do this?

1
["a"].map(String.prototype.trim.call);
["a"].map(String.prototype.trim.call);

The answer is no. The confusing way to explain this: this is wrong, but a different this.

To explain more clearly, let’s ask: How does .call() work?

Let’s make up a fake variant of JavaScript, that’s almost the same as regular JS. In our version though, we write out this as the first argument (exactly like Python):

1
2
3
function a(this) {
return this.name;
}
function a(this) {
return this.name;
}

What’s the difference between these three?

1
2
3
4
5
6
7
8
9
10
//Way 1
a();
 
//Way 2
var b = {};
b.getName = a;
b.getName();
 
//Way 3
a.call(b);
//Way 1
a();

//Way 2
var b = {};
b.getName = a;
b.getName();

//Way 3
a.call(b);
  • Way 1: If we execute a() without specifying this, this is the global context (window if you’re in a browser).
    Result: a(this = window)
  • Way 2: If we run the function as a dot function under b, this will be set to b. b.getName() will return b.name;
    Result: a(this = b)
  • Way 3 is exactly the same as Way 2. By using call(), this will be set to b. a.call(b) will return b.name.
    Result: a(this = b)

So far, so good.

Now, how about [“a”].map(String.prototype.trim.call)?

On the surface, if we follow our rules, map will run String.prototype.trim.call() on “a”. This ought to be String.prototype.trim(this = “a”).

It’s not, though, and therein lies the trick. Remember the difference between Way 1 and Way 2; a function’s context changes based on how we call it. Calling a() on its own gives us a different this from calling a() as b.getName(). In this case, this matters because call() itself is a function.

If we use our variant of JavaScript where we explicitly write this as the first argument (here we’ll call it func), call() would look like:

1
2
3
Function.prototype.call = function(func, newContext, moreArguments) {
//func(moreArguments), where the "this" inside func is set to newContext
};
Function.prototype.call = function(func, newContext, moreArguments) {
//func(moreArguments), where the "this" inside func is set to newContext
};

call() only works if that implicit argument — the context — is correct. If I write:

1
2
3
4
5
6
//Version 1
String.prototype.trim.call("a")
 
//Version 2
var calltrim = String.prototype.trim.call;
calltrim("a");
//Version 1
String.prototype.trim.call("a")

//Version 2
var calltrim = String.prototype.trim.call;
calltrim("a");
  • Version 1 will return a trimmed version of “a”.
    Result: Function.prototype.call(func = String.prototype.trim, “a”)
  • Version 2: Here, calltrim’s “func” is no longer String.prototype.trim. Instead, it’s the global context.
    Result: Function.prototype.call(func = window, “a”)

That’s probably the best I can explain it for now. Leave a note if you found this helpful.

Leave a Reply

Your email address will not be published. Required fields are marked *