What is Array.apply actually doing

After reading this SO Question, I'm still a little confused as to what Array.apply is actually doing. Consider the following snippet:

new Array(5).map(function(){
  return new Array(5);
});

I expect this to init an array with 5 undefined entries, then map over them creating a two dimensional array of 5x5);

Instead I just get the array as if it was never mapped over:

[undefined, undefined, undefined, undefined, undefined]

When I wrap the constructor call to array in an Array.apply call, then map over that, it works as expected:

Array.apply(null, new Array(5)).map(function(){
  return new Array(5);
});

resulting in;

[[undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined]];

What's the deal? Is Array.apply just another way of calling new Array(), or Array.prototype.constructor? Are there any other situations where this would be advantageous? Also, why didn't my first approach pick up on the map I was sending it through?

Thanks! -Neil

Answers:

Answer

Good question!

The Array constructor function (with can be used without new), when passed more than 1 argument, creates an array containing the arguments passed in as its elements. So you can do this:

Array(1, 2, 3); // => [1, 2, 3]

As you probably know, Function.prototype.apply allows you to provide arguments to a function in the form of an array. So calling Array.apply (which just inherits its .apply() method from Function's prototype; any function that is an instance of Function would allows you to do this), which will be functionality equivalent to the code above:

Array.apply(null, [1, 2, 3]); // => [1, 2, 3]

Now, here's why things are a bit confusing. The Array.prototype.map method is spec'd in such a way that it deals with sparse arrays specially. A sparse array is one that has a "length" that is greater than the number of elements that have actually been inserted. For example:

var arr = [];
arr[0] = 'foo';
arr[5] = 'bar';

The array constructed above will have a length property of 6, because it has an element at index 0 and one at index 5. However, since no elements were ever inserted between those indices, if you call map on it you'll see that the mapping function does not get applied to the missing elements:

// This should throw, right? Since elements 1 through 4 are undefined?
var lengths = arr.map(function(s) { return s.length; });

// Nope!
lengths; // => [3, , , , , 3]

And why are we seeing this behavior in your example with new Array(5)? You guessed it: because the array constructor, when given a single argument, creates a sparse array with the specified length.

So the issue here is that while map (and other methods on Array.prototype, such as forEach) behaves specially with respect to sparse arrays by skipping over the missing values, the Function.prototype.apply method does not have any such special behavior.

Answer

It is very intresting example. And pretty good answer by Dan Tao. But I think that I can give a little additional explanation.

In the first case

new Array(5)

creates an empty object, then it passes throw function and gived length of 5. Because of missing any other arguments this object will get no allocated entries.

// Array(5) [ <5 empty slots> ]

And when you trying to "map" those entries nothing actualy happened because of missing real entries.

However, if you try after this step "array[0]" for example, it returns "undefined"...

In the next case you are using "Call" method of Array() function after first "new Array(5)" (but actualy it has no differense "Call" or "Construct" method of call is used with Array function).

Array.apply(null, new Array(5))

So "new Array(5)" already gived as result Array(5) [ <5 empty slots> ] and "Function.prototype.apply()" decomposes this array to the five parameters that Array() function gets in. And in the current step we get:

// Array(5) [ undefined, undefined, undefined, undefined, undefined ]

These are five real entries. And we can do "map()" with them. But there is a litle mistake in your result, because we currently get after

Array.apply(null, new Array(5)).map(function() { return new Array(5); });

a little bit different result

/*
[…]    
0: Array(5) [ <5 empty slots> ]    
1: Array(5) [ <5 empty slots> ]  ?  
2: Array(5) [ <5 empty slots> ] ?    
3: Array(5) [ <5 empty slots> ] ?    
4: Array(5) [ <5 empty slots> ]
*/

and to get more precise, to get "five on five, undefined" result we need to little upgrade your code

Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); });

this will return "five on five, undefined" array.

/*
[…] 
0: Array(5) [ undefined, undefined, undefined, … ] 
1: Array(5) [ undefined, undefined, undefined, … ] 
2: Array(5) [ undefined, undefined, undefined, … ] 
3: Array(5) [ undefined, undefined, undefined, … ] 
4: Array(5) [ undefined, undefined, undefined, … ]
*/

But what I'm talking about is that not only "Function.prototype.apply()" has current behavior to decompose array without real entries. I'll give you an example:

Array(...new Array(5)).map(() => Array(...new Array(5)));

this will actualy gived to us exactly the same result - five on five undefined.

If we took a closer look:

  1. In the first action new Array(5) Array() function returns an empty array but with length property value of '5' because it runs in "Construct" mode, and has one parameter (5).
  2. The second and third action Array.apply() | Array(...) first of all spreads Array without elements to 5 parameters and then pass them to an Array().

It's because of "apply()" or "..." behavior of decomposing arrays. When it gets length of an array it auto transform "empty slots" into undefined values.

Referense from Ecma-262/6.0

(http://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.apply)

19.2.3.1 Function.prototype.apply
    1. If IsCallable(func) is false, throw a TypeError exception.
    2. If argArray is null or undefined, then Return Call(func, thisArg).
    3. Let argList be CreateListFromArrayLike(argArray).

    7.3.17 CreateListFromArrayLike (obj [, elementTypes] )     
        1. ReturnIfAbrupt(obj).
        2. If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object).
        3. If Type(obj) is not Object, throw a TypeError exception.
        4. Let len be ToLength(Get(obj, "length")).
        5. ReturnIfAbrupt(len).
        6. Let list be an empty List.
        7. Let index be 0.
        8. Repeat while index < len
            a. Let indexName be ToString(index).
            b. Let next be Get(obj, indexName).
            c. ReturnIfAbrupt(next).
            d. If Type(next) is not an element of elementTypes, throw a TypeError exception.
            e. Append next as the last element of list.
            f. Set index to index + 1.
        9. Return list.
  1. Here in '8a' clause we get "0", "1"... indexNames to pass them in '8b' as argument names of arraylike object (in our case just array, without any 'like') - array["0"], array["1"]...
  2. Each of requested element values returns with "undefined" and then in 8'e' they consecutively appends to the arguments list.
  3. "CreateListFromArrayLike" returns with five arguments of undefined in list representation in '9'clause which is passed to Function.prototype.apply() and will be applied to an Array() (it will be looking like

new Array(undefined,undefined,undefined,undefined,undefined)).

Spread operator uses iteration protokol instead, but in practice they act simulary on this case.

Answer

The Array constructor , when passed with a single numeric value (Let's say n) , creates a sparse Array of length n and has zero elements in it... so .map() wont work on it as explained by Dan Tao...

let ar = Array(5); // Array(5) [ <5 empty slots> ]

So , you can use .fill() method , that changes all elements in an array to given value, from a start index (default 0) to an end index (default array.length). It returns the modified array... So we can fill the empty (sparse) array with "undefined" value... It won't be sparse anymore...And finally, you can use .map() on returned array...

let ar = Array(5).fill(undefined) ; // [undefined,undefined,undefined,undefined,undefined]
let resultArray = ar.map(  el => Array(5).fill(undefined) );

this resultArray will be a 5*5 array with every value of undefined...

/* 
[ [ undefined, undefined, undefined, undefined, undefined ],
  [ undefined, undefined, undefined, undefined, undefined ],
  [ undefined, undefined, undefined, undefined, undefined ],
  [ undefined, undefined, undefined, undefined, undefined ],
  [ undefined, undefined, undefined, undefined, undefined ] ]
*/

:)

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.