JavaScript “new Array(n)” and “Array.prototype.map

2018-12-31 14:47发布

问题:

I\'ve observed this in Firefox-3.5.7/Firebug-1.5.3 and Firefox-3.6.16/Firebug-1.6.2

When I fire up Firebug:

    >>> x = new Array(3)
    [undefined, undefined, undefined]
    >>> y = [undefined, undefined, undefined]
    [undefined, undefined, undefined]

    >>> x.constructor == y.constructor
    true

    >>> x.map(function(){ return 0; })
    [undefined, undefined, undefined]
    >>> y.map(function(){ return 0; })
    [0, 0, 0]

What\'s going on here? Is this a bug, or am I misunderstanding how to use new Array(3)?

回答1:

It appears that the first example

x = new Array(3);

Creates an array with undefined pointers.

And the second creates an array with pointers to 3 undefined objects, in this case the pointers them self are NOT undefined, only the objects they point to.

y = [undefined, undefined, undefined]
// The following is not equivalent to the above, it\'s the same as new Array(3)
y = [,,,];

As map is run in the context of the objects in the array I believe the first map fails to run the function at all while the second manages to run.



回答2:

I had a task that I only knew the length of the array and needed to transform the items. I wanted to do something like this:

let arr = new Array(10).map((val,idx) => idx);

To quickly create an array like this:

[0,1,2,3,4,5,6,7,8,9]

But it didn\'t worked because: see Jonathan Lonowski\'s answer a few answers upper.

Solution could be to fill up the array items with any value (even with undefined) using Array.prototype.fill()

let arr = new Array(10).fill(undefined).map((val,idx) => idx);

console.log(new Array(10).fill(undefined).map((val, idx) => idx));

Update

Another solution could be:

let arr = Array.apply(null, Array(10)).map((val, idx) => idx);

console.log(Array.apply(null, Array(10)).map((val, idx) => idx));



回答3:

With ES6, you can do [...Array(10)].map((a, b) => a) , quick and easy!



回答4:

ES6 solution:

[...Array(10)]

Doesn\'t work on typescript (2.3), though



回答5:

The arrays are different. The difference is that new Array(3) creates an array with a length of three but no properties, while [undefined, undefined, undefined] creates an array with a length of three and three properties called \"0\", \"1\" and \"2\", each with a value of undefined. You can see the difference using the in operator:

\"0\" in new Array(3); // false
\"0\" in [undefined, undefined, undefined]; // true

This stems from the slightly confusing fact that if you try to get the value of a non-existent property of any native object in JavaScript, it returns undefined (rather than throwing an error, as happens when you try to refer to a non-existent variable), which is the same as what you get if the property has previously been explictly set to undefined.



回答6:

From the MDC page for map:

[...] callback is invoked only for indexes of the array which have assigned value; [...]

[undefined] actually applies the setter on the index(es) so that map will iterate, whereas new Array(1) just initializes the index(es) with a default value of undefined so map skips it.

I believe this is the same for all iteration methods.



回答7:

I think the best way to explain this is to look at the way that Chrome handles it.

>>> x = new Array(3)
[]
>>> x.length
3

So what is actually happening is that new Array() is returning an empty array that has a length of 3, but no values. Therefore, when you run x.map on a technically empty array, there is nothing to be set.

Firefox just \'fills in\' those empty slots with undefined even though it has no values.

I don\'t think this is explicitly a bug, just a poor way of representing what is going on. I suppose Chrome\'s is \"more correct\" because it shows that there isn\'t actually anything in the array.



回答8:

In ECMAScript 6th edition specification.

new Array(3) only define property length and do not define index properties like {length: 3}. see https://www.ecma-international.org/ecma-262/6.0/index.html#sec-array-len Step 9.

[undefined, undefined, undefined] will define index properties and length property like {0: undefined, 1: undefined, 2: undefined, length: 3}. see https://www.ecma-international.org/ecma-262/6.0/index.html#sec-runtime-semantics-arrayaccumulation ElementList Step 5.

methods map, every, some, forEach, slice, reduce, reduceRight, filter of Array will check the index property by HasProperty internal method, so new Array(3).map(v => 1) will not invoke the callback.

for more detail, see https://www.ecma-international.org/ecma-262/6.0/index.html#sec-array.prototype.map

How to fix?

let a = new Array(3);
a.join(\'.\').split(\'.\').map(v => 1);

let a = new Array(3);
a.fill(1);

let a = new Array(3);
a.fill(undefined).map(v => 1);

let a = new Array(3);
[...a].map(v => 1);


回答9:

Just ran into this. It sure would be convenient to be able to use Array(n).map.

Array(3) yields roughly {length: 3}

[undefined, undefined, undefined] creates the numbered properties:
{0: undefined, 1: undefined, 2: undefined, length: 3}.

The map() implementation only acts on defined properties.



回答10:

Not a bug. That\'s how the Array constructor is defined to work.

From MDC:

When you specify a single numeric parameter with the Array constructor, you specify the initial length of the array. The following code creates an array of five elements:

var billingMethod = new Array(5);

The behavior of the Array constructor depends on whether the single parameter is a number.

The .map() method only includes in the iteration elements of the array that have explicitly had values assigned. Even an explicit assignment of undefined will cause a value to be considered eligible for inclusion in the iteration. That seems odd, but it\'s essentially the difference between an explicit undefined property on an object and a missing property:

var x = { }, y = { z: undefined };
if (x.z === y.z) // true

The object x does not have a property called \"z\", and the object y does. However, in both cases it appears that the \"value\" of the property is undefined. In an array, the situation is similar: the value of length does implicitly perform a value assignment to all the elements from zero through length - 1. The .map() function therefore won\'t do anything (won\'t call the callback) when called on an array newly constructed with the Array constructor and a numeric argument.



回答11:

If you are doing this in order to easily fill up an array with values, can\'t use fill for browser support reasons and really don\'t want to do a for-loop, you can also do x = new Array(3).join(\".\").split(\".\").map(... which will give you an array of empty strings.

Quite ugly I have to say, but at least the problem and intention are quite clearly communicated.



回答12:

Here\'s a simple utility method as a workaround:

Simple mapFor

function mapFor(toExclusive, callback) {
    callback = callback || function(){};
    var arr = [];
    for (var i = 0; i < toExclusive; i++) {
        arr.push(callback(i));
    }
    return arr;
};

var arr = mapFor(3, function(i){ return i; });
console.log(arr); // [0, 1, 2]
arr = mapFor(3);
console.log(arr); // [undefined, undefined, undefined]

Complete Example

Here\'s a more complete example (with sanity checks) which also allows specifying an optional starting index:

function mapFor() {
var from, toExclusive, callback;
if (arguments.length == 3) {
    from = arguments[0];
    toExclusive = arguments[1];
    callback = arguments[2];
} else if (arguments.length == 2) {
    if (typeof arguments[1] === \'function\') {
        from = 0;
        toExclusive = arguments[0];
        callback = arguments[1];
    } else {
        from = arguments[0];
        toExclusive = arguments[1];
    }
} else if (arguments.length == 1) {
    from = 0;
    toExclusive = arguments[0];
}

callback = callback || function () {};

var arr = [];
for (; from < toExclusive; from++) {
    arr.push(callback(from));
}
return arr;
}

var arr = mapFor(1, 3, function (i) { return i; });
console.log(arr); // [1, 2]
arr = mapFor(1, 3);
console.log(arr); // [undefined, undefined]
arr = mapFor(3);
console.log(arr); // [undefined, undefined, undefined]

Counting Down

Manipulating the index passed to the callback allows counting backwards:

var count = 3;
var arr = arrayUtil.mapFor(count, function (i) {
    return count - 1 - i;
});
// arr = [2, 1, 0]


回答13:

In Chrome, if I do new Array(3) I get [], so my guess is that you\'ve come across a browser bug.