1

I've been working through Eloquent JavaScript's exercises and found out something I think is odd. I wrote a trivial array-flattening piece of code:

var arrays = [[1, 2, 3], [4, 5], [6]];
var out = arrays.reduce(function(acc, next){ return acc.concat(next); });
console.log(out);
// → [1, 2, 3, 4, 5, 6]

So far so good. But that didn't seem pretty to me, so I rewrote it as:

var arrays = [[1, 2, 3], [4, 5], [6]];
var my_concat = function(acc, next){ return acc.concat(next); }
var out = arrays.reduce(my_concat);
console.log(out);
// → [1, 2, 3, 4, 5, 6]

It was better, but do we really need to introduce a function, be it anonymous or named, to do such a basic thing? Array.prototype.concat.call's call signature is exactly what we need! Feeling smart, I rewrote the code again:

var arrays = [[1, 2, 3], [4, 5], [6]];
var out = arrays.reduce([].concat.call);
// → TypeError: arrays.reduce is not a function (line 2)

Well, that haven't turned out as I expected. The error message seemed cryptic to me.

I decided to investigate. This works:

var arrays = [[1, 2, 3], [4, 5], [6]];
var my_concat = function(acc, next){ return [].concat.call(acc,next); }
var out = arrays.reduce(my_concat);
console.log(out);
// → [1, 2, 3, 4, 5, 6]

And this also works:

var arrays = [[1, 2, 3], [4, 5], [6]];
arrays.my_concat = function(acc, next) { return [].concat.call(acc, next); }
var out = arrays.reduce(arrays.my_concat);
console.log(out);
// → [1, 2, 3, 4, 5, 6]

More tinkering in the console:

[].concat.call
// → call() { [native code] }
typeof [].concat.call
// → "function"
[].concat.call([1, 2, 3], [4, 5])
// → [1, 2, 3, 4, 5]
var cc = [].concat.call
cc
// → call() { [native code] }
typeof cc
// → "function"
cc([1, 2, 3], [4, 5])
// → Uncaught TypeError: cc is not a function(…)

And even this works:

Array.prototype.my_concat = function(acc, next) { return [].concat.call(acc, next); }
// → function (acc, next) { return [].concat.call(acc, next); }
[[1, 2, 3], [4, 5], [6]].reduce([].my_concat)
// → [1, 2, 3, 4, 5, 6]
[[1, 2, 3], [4, 5], [6]].reduce([].concat.call)
// → Uncaught TypeError: [[1,2,3],[4,5],[6]].reduce is not a function(…)

Is there something special about built-in functions like .call?

6
  • no. a bare [].concat.call would append all the arguments to the output, and [].reduce doesn't just pass the 2 arguments you use, it passes the index and the whole input array as well, so the output would be cluttered. Commented Feb 2, 2016 at 22:24
  • arrow function is the next best thing: [[1, 2, 3], [4, 5], [6]].reduce( (a,b)=>a.concat(b) )... in theory, i would like to use natives as well, but if you have to bind(), re-apply(), or mangle prototypes, then i think a simple anon callback is more semantic, even if it's not as hard-core... Commented Feb 2, 2016 at 22:34
  • @dandavis Yeah, but aren't they mostly unsupported in the browsers yet? Commented Feb 2, 2016 at 22:37
  • works for me in chrome and firefox, not sure about others... (prod FF+ch btw, egads; the future is here) Commented Feb 2, 2016 at 22:38
  • @dandavis Arrow functions are not supported by mobile browsers, like, at all :-( I don't think it's ok to use in production something that is not supported by iOS/Safari — it's something like 50% of all traffic. Commented Feb 3, 2016 at 18:53

1 Answer 1

2

call is just a method that most functions inherit from Function.prototype. That is,

arrays.reduce.call === Function.prototype.call

The call method knows which function you want to call because that function is passed as the this value.

When you pass call as the callback, it will be called passing undefined as the this value. Since undefined is not a function, it throws. On Firefox I get this error:

TypeError: Function.prototype.call called on incompatible undefined

Instead, you could try one of these callbacks

Function.call.bind([].concat);
[].concat.bind([]);

However, the problem is that this won't work properly, because the callback is called with 4 arguments, not 2:

  • previousValue
  • currentValue
  • currentIndex
  • array

You want to get rid of the last two, so you need a custom function anyways.

However, these are not good approaches. Each time you call concat, it creates a new array. Therefore, if you want to flatten an array, you should call concat only once instead of per each item in the array:

[].concat.apply([], arrays); // this works
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! It's not obvious to me, though, why passing [].my_concat (the last code example in my question) works then :-(
I think I got this. That's because I don't rely on this inside of my_concat?
@DanilaSentyabov [].my_concat works because it ignores undesired arguments and calls call on a function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.