2

I jump between Java and Javascript a lot at work. I also write a lot of functional code, i.e. chains of lambdas.

One thing I like about Java is I can replace a lambda with a method reference. For example, I can replace this code

List<String> trimmedStrings = List.of("hi ", "bye ").stream()
  .map(original -> original.trim())
  .collect(toList());

with this:

List<String> trimmedStrings = List.of("hi ", "bye ").stream()
  .map(String::trim)
  .collect(toList());

I often make this replacement because I'm usually happier with the end state of how the code looks.

I have been wondering if I can do this with Javascript. I just tested this code in my browser console:

["hi ", "bye "].map(original => original.trim());

first I tried replacing it the simple way, which worked but doesn't accomplish my goal:

["hi ", "bye "].map(original => String.prototype.trim.apply(original))

So I figured the following would work, but it didn't:

["hi ", "bye "].map(String.prototype.trim.apply)

it gave me an error saying Uncaught TypeError: Can't call method on undefined (in Firefox).

So my questions are:

  1. Why doesn't this work?
  2. Is there another, better way to do what I want here?
Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
Devin Howard
  • 675
  • 1
  • 6
  • 17
  • also [Javascript Array.prototype.map function with instance methods](https://stackoverflow.com/questions/36228710/javascript-array-prototype-map-function-with-instance-methods) – pilchard Mar 14 '23 at 22:06
  • 1
    Also keep in mind the possible pitfalls of optional parameters as seen in [Why does parseInt yield NaN with Array#map?](https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap) – pilchard Mar 14 '23 at 22:07

2 Answers2

1

The issue here is that the this context needs to be set to the string. You can use Function#call to execute a function with a particular this value.

With just String.prototype.trim.call, the this context for Function#call is not set at the time is called, so you need to use bind to keep it.

let res = ["hi ", "bye "].map(Function.prototype.call.bind(String.prototype.trim));
console.log(res);
// or to make it a bit shorter:
console.log(["hi ", "bye "].map("".trim.call.bind("".trim)));
Unmitigated
  • 76,500
  • 11
  • 62
  • 80
1

String.prototype.trim is just a function. When you call "foo".trim() you call that function and set "foo" as the context. This is just how methods work in JavaScript. I might suggest a method helper to get around this -

const method = f => f.call.bind(f)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(method("".trim)))

Another option is to define trim in advance -

const method = f => f.call.bind(f)
   
const trim = method("".trim)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim))
[
  "alice",
  "bob",
  "charlie"
]

Some methods take additional arguments. method works with that too -

const method = f => f.call.bind(f)
   
const trim = method("".trim)
const replace = method("".replace)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim).map(v => replace(v, "e", "E")))
[
  "alicE",
  "bob",
  "charliE"
]

You can rewrite method to enable tacit programming (aka point-free style) -

const method = f => (...args) => data =>
  f.apply(data, args)
   
const trim = method("".trim)()
const upper = method("".toUpperCase)()
const replace = method("".replace)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim).map(replace(/[aeiou]/g, upper)))
[
  "AlIcE",
  "bOb",
  "chArlIE"
]

Finally we can make method smarter to analyze f.length to determine if more arguments should be supplied by the caller -

const method = f => 
  f.length == 0
    ? data => f.call(data)
    : (...args) => data => f.apply(data, args)
   
const trim = method("".trim)
const upper = method("".toUpperCase)
const replace = method("".replace)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim).map(replace(/[aeiou]/g, upper)))
[
  "AlIcE",
  "bOb",
  "chArlIE"
]
Mulan
  • 129,518
  • 31
  • 228
  • 259