Why does Array.from not call the callback in the same way as Array.prototype.map?

Viewed 215

At the time of writing MDN's description of the Array.from function states:

Array.from() has an optional parameter mapFn, which allows you to execute a map() function on each element of the array being created.

More clearly, Array.from(obj, mapFn, thisArg)
has the same result as Array.from(obj).map(mapFn, thisArg),
except that it does not create an intermediate array.

However, it does not have exactly the same behaviour, as demonstrated here:

function mapFn(...args) {
    console.log(...args);
}

let obj = { 0: "value", length: 1 };
let thisArg = { };

Array.from(obj, mapFn, thisArg);
Array.from(obj).map(mapFn, thisArg);

Array.from does not pass on a third argument to the callback.

This is in line with the ECMAScript 2020 specification on Array.from:

Call(mapfn, thisArg, « nextValue, k »).

While for Array.prototype.map the specification has:

Call(callbackfn, thisArg, « kValue, k, O »).

Evidently, the above quote from MDN is not entirely correct.

Question

Why this difference? Would it not have made more sense to actually have Array.from work like a map() function as stated in the above quote from MDN?

1 Answers

The quote from MDN is almost exactly correct. But it's also incomplete.

Barely incorrect:

which allows you to execute a map() function

It doesn't execute the native .map() method. It performs a mapping operation which is almost the same.

Incomplete

More clearly, Array.from(obj, mapFn, thisArg)
has the same result as Array.from(obj).map(mapFn, thisArg),
except that it does not create an intermediate array.

While true should also include "As long as the third argument of .map() is not used." to match absolutely 100% with how things work.

Why this difference? Would it not have made more sense to actually have Array.from work like a map() function as stated in the above quote from MDN?

Interesting question but the answer is actually boring. It's because you can use Array.from() on non-arrays.

Why does that matter? You showed an array-like being mapped over. The array-like exists in its entirety and it could be passed around as a third argument. In fact it is by the implementation of .map()

function mapFn(...args) {
    console.log(this, ...args);
//              ^^^^^ also log this for clarity
}

let obj = { 0: "value", length: 1 };
let thisArg = { this: "arg" };

Array.prototype.map.call(obj, mapFn, thisArg);

However, that is not the case, if the entire value doesn't exist, like if you're using an iterator:

function* example() {
  yield 1;
  yield 2;
  yield 3;
  if (Math.random() < 0.5) //make the iterator result uncertain
    yield 4;
}

function mapFn(...args) {
    console.log(...args);
}

Array.from(example(), mapFn)

Each time the mapping callback is called, it only knows about the current existing values. The iterator is not materialised yet, so not only can you not get a full view of it, it's Array.from() that converts it into a materialised view. It's a chicken and egg problem.

Related