-1

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?


* Some time after posting this question, Mozilla Contributors added a clause to the paragraph quoted above:

..., and mapFn only receives two arguments (element, index) without the whole array, because the array is still under construction.

But this "because" seems to mix up the original data with the result of the mapping. The array that is being constructed, is the result array, while the third argument that the Array#map callback takes is the source object. Also there a result array is still under construction... That is a red herring in my opinion. It would well be possible to pass as third argument the source object that had been passed as first argument to Array.from.

4
  • Umm, the thisArg is supposed to be literally the this value passed to the callback. Not the third parameter of the map function. These are two different things. Commented Mar 22, 2021 at 10:39
  • In other words, Array.from does not invoke map as MDN confusingly states, it is map. Commented Mar 22, 2021 at 10:44
  • @pilchard and there might not be. An array-like (as shown) is at least present but if an iterator is passed in, there is no way to pass a fully materialised view of this iterator. Commented Mar 22, 2021 at 10:45
  • @georg I suppose the wiki meant something like "allows you to do a mapping operation" but it sort of got lost in translation into "allows you to execute .map()" Commented Mar 22, 2021 at 10:46

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

5 Comments

But still, if the first argument given to Array.from would have been passed as third argument to the callback -- and it concerns an iterator -- the callback could still have used that reference to do other things than access values. For instance, it could decide to call iter.return(), which actually might be a handy possibility in some circumstances.
@trincot but it leads to inconsistencies. Now you cannot create one mapFn because the last argument might be an array, might be an array-like, might be a generator object (OK, I cheated a bit in the answer), it might be an iterator. The last one might not even have a .return() as it's an optional method, so you might not even be able to call it. And an array-like might have a .return() which doesn't conform to the iterator protocol. It's just a mess. Besides, it's expected the mapping to not alter whatever it's being mapped.
True, but that argument can already be made when performing a standard .map call on an array: I might have decided to have my own properties on that array, or use an object that is an instance of a class that extends Array, and yet I will still be able to get that not-so-standard array as third argument in my callback, giving me access to non standard methods. Moreover, if at design time we know the "type" of what we map on, why would it be inconsistent to make use of that knowledge in that particular scenario?
*shrug* you can make that argument for very many things. "But what if <something that is outside the spec>". It's how it's been designed. Apparently, doesn't really cater to you. I personally lament the lack of the binding operator isn't in. But the language is not for you or me. Concessions have to be made what to go in and how it's to be used. If the vast majority of mappings don't use the third argument, then shipping Array.from without it seems like a sensible tradeoff vs complexity.
@trincot out of interest, I checked the proposal for Array.from to see it has a rationale for omitting a third argument. It doesn't contain one in itself - it does link to a mailing list thread discussing it (alongside Array.of) but I didn't go through the email chain. The title of the thread is "Pure win: Array.from and Array.of" so it suggests there were no plans for a third argument from the very start.

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.