1

I can't help but notice that it's impossible to do something like

["cat", "dog"].map(String.prototype.toUpperCase);

which throws

Uncaught TypeError: String.prototype.toUpperCase called on null or undefined

The following code works (in es6 arrow notation for my typing convenience), but seems weirdly indirect:

["cat", "dog"].map(s=>s.toUpperCase())

Weirdly indirect because it creates an extra anonymous function just to wrap a perfectly good function that already exists. And maybe this is something that one must live with, but it doesn't taste right.

So I have two questions:

  1. Is there a direct way to map a string method over an array of strings without wrapping it in an anonymous function?

  2. What's the technical explanation for why the code I tried first doesn't work? I have two guesses, but don't know which is right.

Guess (a): this is because of boxing, i.e., string literals aren't the same as string objects, and for some reason mapping the method over them doesn't do the same kind of quiet coercion that just calling the method on the string does.

Guess (b): this is because while functions are first class objects, methods aren't in the same way, and can't ever be mapped?

Guess (c): there's some other syntax I should be using?! ("".prototype.toUpperCase??) Because of JavaScript gremlins? Because null both is and is not an object? Fixed in ES2025? Just use lodash and all will be cured?

Thanks!

Paul Gowder
  • 2,409
  • 1
  • 21
  • 36
  • 1
    Have a look at [Apply trim function to each string in an array](http://stackoverflow.com/q/19293997/1048572) – Bergi Apr 27 '17 at 00:21

4 Answers4

5

1. Is there a direct way to map a string method over an array of strings without wrapping it in an anonymous function?

No (assuming that by "without wrapping it in an anonymous function" you more generally mean "without creating an additional function").

2. What's the technical explanation for why the code I tried first doesn't work? I have two guesses, but don't know which is right.

The "problem" is that toUpperCase is essentially an instance method. It expects this to refer to the value that should be changed. .map however will pass the value as argument to the function. Your first example is basically doing

String.prototype.toUpperCase("dog")

and that's simply not how that function works. The following would work

 String.prototype.toUpperCase.call("dog")

but that in turn is not how .map works.

No to guess a and b. Regarding c, the syntax you should be using is your second solution. Of course there are other ways. You could write a helper function:

function toUpper(s) {
  return s.toUpperCase();
}

but that's not much different from using the arrow function.

Since toUpperCase doesn't accept arguments, you could even go crazy and to something like

["cat", "dog"].map(String.prototype.toUpperCase.call.bind(String.prototype.toUpperCase));

but that

  • also creates a new function
  • is probably less understandable
  • is not generally applicable
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • Actually [this approach](http://stackoverflow.com/a/19294490/1048572) works without creating an additional function (though I wouldn't recommend it over the clear and concise arrow function) – Bergi Apr 27 '17 at 00:23
  • Aah, thank you. That clears things up. (I mostly write functional code, so I'm not used to thinking about `this` or about methods as anything other than functions with weird syntax.) – Paul Gowder Apr 27 '17 at 01:08
  • @Bergi: Ah, clever :) – Felix Kling Apr 27 '17 at 01:50
2

Object methods (and dealing with this) can be a real pain in the neck when you're trying to write elegant functional programs. No worries tho, just abstract the problematic syntax away into its own function

const send = (method, ...args) => obj =>
  obj[method](...args)

let x = ["cat", "dog"].map(send('toUpperCase'))
console.log(x) // ['CAT', 'DOG']

let y = ['cat', 'dog'].map(send('substr', 0, 1))
console.log(y) // ['c', 'd']

Here's another way that might be nice to write it

const call = (f, ...args) => obj =>
  f.apply(obj, args)

let x = ["cat", "dog"].map(call(String.prototype.toUpperCase))
console.log(x) // ['CAT', 'DOG']

let y = ['cat', 'dog'].map(call(String.prototype.substr, 0, 1))
console.log(y) // ['c', 'd']

And another way by defining your own functions

const map = f => xs => xs.map(x => f(x))
const toUpperCase = x => x.toUpperCase()
const substr = (x,y) => z => z.substr(x,y)

let x = map (toUpperCase) (["cat", "dog"])
console.log(x) // ['CAT', 'DOG']

let y = map (substr(0,1)) (['cat', 'dog'])
console.log(y) // ['c', 'd']
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • That send function is pleasingly elegant. Part of me wants to just use that to stomp all over the global map method and write something like `Array.prototype.map(f) = try {this.map(f)} catch {this.map(send(f))}` – Paul Gowder Apr 27 '17 at 12:22
  • You definitely don't want to use try/catch for control flow. Please, please don't do that – Mulan Apr 27 '17 at 18:10
  • heh. is there some other good way to detect whether the function passed in is a real function or a method? – Paul Gowder Apr 28 '17 at 02:23
  • There's no such thing as a method. We just use that word to describe a function that has its context dynamically bound to an object. I added another technique you can use in an update to my answer. – Mulan Apr 28 '17 at 17:35
0

To answer both your questions, No you can't just map a string method to an array of strings without using a callback function.

And the first code you wrote:

["cat", "dog"].map(String.prototype.toUpperCase);

Doesn't work because .map() method expects a callback function that process every iterated element and return it, but you were just trying to call String.prototype.toUpperCase function on undefined because you haven't binded it to the current element.

Solution:

In order to get it to work and to be equivalent to ES6 provided code:

["cat", "dog"].map(s=>s.toUpperCase())

You need to amend your code to accept a callback function that calls String.prototype.toUpperCase on the iterated element which is passed by .map() method:

["cat", "dog"].map(function(s){return s.toUpperCase()});
cнŝdk
  • 31,391
  • 7
  • 56
  • 78
0

var a = ["cat", "dog"];

var res = a.map(String.prototype.toUpperCase.call, String.prototype.toUpperCase);

console.log(res);
Debug Diva
  • 26,058
  • 13
  • 70
  • 123