27

This, of course, returns what you would expect:

["A","B","C"].map(function (x) {
    return x.toLowerCase();
});
// --> ["a", "b", "c"]

So does using String.prototype.toLowerCase.call:

["A","B","C"].map(function (x) {
    return String.prototype.toLowerCase.call(x);
});
// --> ["a", "b", "c"]

It also works if you pass the extra arguments given by map, as it throws away the arguments:

["A","B","C"].map(function (x, index, arr) {
    return String.prototype.toLowerCase.call(x, index, arr);
});
// --> ["a", "b", "c"]

But, this does not work:

["A","B","C"].map(String.prototype.toLowerCase.call);
// --> TypeError: undefined is not a function

The following doesn't work either, because arguments has the Object prototype instead of the Array prototype, so slice is undefined on it. Is the reason for the above behavior perhaps because of something like this-- where slice or some other similar Array function is used internally?

["A","B","C"].map(function (x) {
    return String.prototype.toLowerCase.apply(x, arguments.slice(1));
});
// --> TypeError: undefined is not a function
Greg Smith
  • 299
  • 3
  • 9
  • 5
    You're passing the `call` function to `map`, it's identical to `['A','B','C'].map(Function.prototype.call)`, you want to pass the `String.prototype.toLowerCase` function in a way that it is called on each element of the array. – zzzzBov Apr 24 '14 at 22:30
  • 2
    The proper way to do this is the first example in your code. – adeneo Apr 24 '14 at 22:32

4 Answers4

27

Similar Question: Why won't passing `''.trim()` straight to `[].map()`'s callback work?

Map has a optional thisArg which can be used like so:

['A', 'B', 'C'].map(Function.prototype.call, String.prototype.toLowerCase);  
// gives ["a", "b", "c"]
Community
  • 1
  • 1
Declan Cook
  • 6,066
  • 2
  • 35
  • 52
15

This is a special behavior of JavaScript's dot-notation.

toLowerCase.call(x) is working because JavaScript uses toLowerCase as this while executing call. This is how call (which is the same Function.prototype.call you find on every function) knows you want it to execute toLowerCase.

Passing call into another function loses that reference, so this no longer refers to toLowerCase.

Keen
  • 954
  • 12
  • 19
  • The JavaScript engine executes `call` and within `call` it executes the `this` value, which happens to be `toLowerCase` in the correct code. What you have said and what I have said are facts consistent with each other. – Keen Apr 24 '14 at 22:36
  • This doesn't really explain the behaviour! Trick question, why does `[1,2,3].map(Math.sqrt)` work, but not `["A","B","C"].map(String.toLowerCase)` ? – adeneo Apr 24 '14 at 22:36
  • `Math.sqrt` can be separated from `Math` safely due what is essentially an implementation detail you need to memorize before it can be trusted. `Math.sqrt` does not use its `this` value, which is only typically `Math`. – Keen Apr 24 '14 at 22:37
  • Ahh, I see! It didn't occur to me that there is only a single `call()` in Javascript, and that `this` is used by it to get the function in front of it just like the rest of the language works. – Greg Smith Apr 24 '14 at 22:38
  • @Cory - Now you are on to something, it's how `String`'s methods work that make them unsuitable to pass directly to methods like `map()`, and why the first example in the OP's question should generally be used, and why `Math`'s methods can safely be passed – adeneo Apr 24 '14 at 22:42
8

The problem is that String.prototype.toLowerCase.call == Function.prototype.call. If you want to get a function that converts the argument to lower case, you could bind the toLowerCase function to the call function like that:

var toLowerCase = String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase);
["A","B","C"].map(toLowerCase);
Yogu
  • 9,165
  • 5
  • 37
  • 58
2

But, this does not work:

["A","B","C"].map(String.prototype.toLowerCase.call);

The first argument passed to map is supposed to be a function that will be passed the value of members of the array. The above passes a direct reference to Function.prototype.call, so the function will attempt:

call(x);

So call has been passed without setting its this, so it will be undefined on entering the call function.

RobG
  • 142,382
  • 31
  • 172
  • 209