How to pass the method defined on prototype to Array.map as callback
Asked Answered
A

3

15

I have an array

var arr = [' A ', ' b ', 'c'];

and I want to trim the spaces from each of the element from array.

It can be done by using Array.map as

arr.map(function(el) {
    return el.trim();
});

I'm curious about passing the trim/toLowerCase function directly to the map as callback function, like arr.map(Math.max.apply.bind(Math.max, null)); to get the maximum element from each subarray or arr.map(Number); to cast each element to Number.

I've tried

arr.map(String.prototype.trim.apply);

but it is throwing error

Uncaught TypeError: Function.prototype.apply was called on undefined, which is a undefined and not a function

I expect that String.prototype.trim.apply should be called for each element in the array with the context set to the element from array(passed to apply);

I've also tried different combinations of apply, call and bind with no success.

  1. Why the function on prototype cannot be referenced when using map
  2. How function can be passed as parameter to map
Antipasto answered 8/10, 2015 at 3:41 Comment(3)
I'm confused as to why arr.map(function... works whereas arr.map(String... throws "arr is not defined."Aflame
In Firefox it says “TypeError: Function.prototype.apply called on incompatible undefined”.Towrey
var f = String.prototype.trim.call; f(' 3 '); throws error, even though String.prototype.trim.call(' 3' ) works and typeof f === 'function'. No idea why.Aflame
E
13
arr.map(String.prototype.trim.call.bind(String.prototype.trim));

call uses this internally, which must point to the trim function to work properly in this case. Simply passing String.prototype.trim.call would leave call unbound to any method, resulting in the this value pointing to window instead.

It works, but when used apply instead of call it throws error, arr.map(String.prototype.trim.apply.bind(String.prototype.trim));

The problem is that map will pass 2 arguments, the item and the index. Therefore it ends up calling something like 'String.prototype.trim.apply('test', 0) which fails since the second argument must be an array.

one more thing [' A ', ' B ', 'c'].map(String.prototype.trim.call.bind(String.prototype.toLowerCase));, in this, I've used trim.call and passed toLowerCase as context then why we need trim here, why trim is not called

When using call.bind the path that you chose to access the call function reference becomes irrelevant. The function that will get called is the one that is bound.

If you want to compose functions together you will need a different approach:

var call = Function.prototype.call,
    trim = call.bind(String.prototype.trim),
    toLowerCase = call.bind(String.prototype.toLowerCase),
    trimAndLowerCase = pipelineFrom(trim, toLowerCase);

[' TeST '].map(trimAndLowerCase);

function pipelineFrom(fn1, fn2) {
    return function (val) {
        return fn2(fn1(val));
    };
}

However at this point you're better off with:

arr.map(function (val) {
    return val.trim().toLowerCase();
});
Enmity answered 8/10, 2015 at 3:55 Comment(3)
It works, but when used apply instead of call it throws error, arr.map(String.prototype.trim.apply.bind(String.prototype.trim));Antipasto
one more thing [' A ', ' B ', 'c'].map(String.prototype.trim.call.bind(String.prototype.toLowerCase));, in this, I've used trim.call and passed toLowerCase as context then why we need trim here, why trim is not calledAntipasto
@Antipasto trim will not get called because you only used it as a shortcut to get a reference to the call function. The function that will be called is toLowerCase. For instance, you could have done Function.prototype.call.bind(String.prototype.toLowerCase) and it would also work. How you get a reference to the call function becomes irrelevant.Enmity
A
4

This works, it sure is long-winded though:

var t = String.prototype.trim.call.bind(String.prototype.trim);
arr.map(t);

Because it's longwinded there are blog posts and modules devoted to uncurrying, which is what you are trying to do here.

I did ask about this here once...

Aflame answered 8/10, 2015 at 3:55 Comment(2)
It works, but when used apply instead of call it throws error, arr.map(String.prototype.trim.apply.bind(String.prototype.trim));Antipasto
one more thing [' A ', ' B ', 'c'].map(String.prototype.trim.call.bind(String.prototype.toLowerCase));, in this, I've used trim.call and passed toLowerCase as context then why we need trim here, why trim is not calledAntipasto
M
1

I use a small method function for this kind of thing -

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"
]
Mchugh answered 15/3, 2023 at 3:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.