Bizarrerie JavaScript «nouveau tableau (n)» et «Array.prototype.map»

209

J'ai observé cela dans Firefox-3.5.7 / Firebug-1.5.3 et Firefox-3.6.16 / Firebug-1.6.2

Quand je lance Firebug:

var x = new Array(3)
console.log(x) 
// [undefined, undefined, undefined]

var y = [undefined, undefined, undefined]
console.log(y) 
// [undefined, undefined, undefined]

console.log( x.constructor == y.constructor) // true

console.log( 
  x.map(function() { return 0; })
)
// [undefined, undefined, undefined]

console.log(
  y.map(function() { return 0; })
)
// [0, 0, 0]

Que se passe t-il ici? Est-ce un bug ou est-ce que je comprends mal comment l'utiliser new Array(3)?

rampion
la source
Je n'obtiens pas les mêmes résultats que vous voyez de la notation littérale du tableau. J'obtiens toujours undefined au lieu de 0. Je n'obtiens le résultat 0 que si je définis quelque chose comme var y = x.map(function(){return 0; });, et j'obtiens ceci pour la nouvelle méthode Array () et le littéral de tableau. J'ai testé dans Firefox 4 et Chrome.
RussellUresti
également éclaté dans Chrome, cela pourrait être défini dans le langage, même si cela n'a aucun sens, donc j'espère vraiment que ce n'est pas
Hashbrown

Réponses:

125

Il semble que le premier exemple

x = new Array(3);

Crée un tableau avec des pointeurs non définis.

Et le second crée un tableau avec des pointeurs vers 3 objets non définis, dans ce cas, les pointeurs eux-mêmes ne sont PAS non définis, seuls les objets vers lesquels ils pointent.

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

Comme la carte est exécutée dans le contexte des objets du tableau, je pense que la première carte ne parvient pas du tout à exécuter la fonction tandis que la seconde parvient à s'exécuter.

David Mårtensson
la source
86
De MDC (accentuation mine): " mapappelle une fonction de rappel fournie une fois pour chaque élément dans un tableau, dans l'ordre, et construit un nouveau tableau à partir des résultats. callbackEst invoqué uniquement pour les index du tableau qui ont des valeurs assignées ; il n'est pas invoqué pour les index qui ont été supprimés ou qui n'ont jamais reçu de valeurs. " Dans ce cas, xles valeurs de n'ont pas explicitement affecté de valeurs, tandis que yles valeurs ont été attribuées, même si c'était la valeur undefined.
Martijn
2
Est-ce donc un échec de JavaScript qu'il est impossible de vérifier s'il s'agit d'un pointeur non défini ou d'un pointeur vers non défini? Je veux dire (new Array(1))[0] === [undefined][0].
Trevor Norris
Eh bien, un tableau d'objets non définis est différent d'un tableau de pointeurs sur des objets non définis. Un tableau d'indéfinis serait comme un tableau de valeurs nulles, [null, null, null] tandis qu'un tableau de pointeurs vers undefined serait comme [343423, 343424, 343425] pointant vers null et null et null. Les secondes solutions ont de vrais pointeurs pointant vers des adresses mémoire tandis que les premières ne pointent nulle part. Si c'est un échec de JS, c'est probablement un sujet de discussion, mais pas ici;)
David Mårtensson
4
@TrevNorris, vous pouvez facilement tester cela avec hasOwnPropertysauf si hasOwnPropertylui-même a un bug: (new Array(1)).hasOwnProperty(0) === falseet [undefined].hasOwnProperty(0) === true. En fait, vous pouvez faire exactement la même chose avec in: 0 in [undefined] === trueet 0 in new Array(0) === false.
squid314
3
Parler de "pointeurs non définis" en JavaScript confond le problème. Le terme que vous recherchez est "élisions" . x = new Array(3);est équivalent à x = [,,,];, non x = [undefined, undefined, undefined].
Matt Kantor
118

J'avais une tâche dont je ne connaissais que la longueur du tableau et dont j'avais besoin pour transformer les éléments. Je voulais faire quelque chose comme ça:

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

Pour créer rapidement un tableau comme celui-ci:

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

Mais cela n'a pas fonctionné parce que: (voir la réponse de Jonathan Lonowski quelques réponses ci-dessus)

La solution pourrait être de remplir les éléments du tableau avec n'importe quelle valeur (même avec undefined) en utilisant Array.prototype.fill ()

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

Mettre à jour

Une autre solution pourrait être:

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

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

cstuncsik
la source
28
à noter que vous n'avez pas besoin d'indiquer undefineddans la .fill()méthode, simplifiant très légèrement le code pourlet arr = new Array(10).fill().map((val,idx) => idx);
Yann Eves
De même, vous pouvez utiliserArray.from(Array(10))
84

Avec ES6, vous pouvez le faire [...Array(10)].map((a, b) => a), rapidement et facilement!

Manuel Beaudru
la source
9
Pré-ES6, vous pouvez utiliser new Array(10).fill(). Même résultat que[...Array(10)]
Molomby
Avec de grands tableaux, la syntaxe de propagation crée des problèmes, il vaut donc mieux éviter
ou[...Array(10).keys()]
Chungzuwalla
25

Solution ES6:

[...Array(10)]

Ne fonctionne pas sur le tapuscrit (2.3), cependant

Serge Intern
la source
6
Array(10).fill("").map( ...est ce qui a fonctionné pour moi avec Typescript 2.9
bouquetin
19

Les tableaux sont différents. La différence est que new Array(3)crée un tableau avec une longueur de trois mais aucune propriété, tandis que [undefined, undefined, undefined]crée un tableau avec une longueur de trois et trois propriétés appelées "0", "1" et "2", chacune avec une valeur de undefined. Vous pouvez voir la différence en utilisant l' inopérateur:

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

Cela découle du fait légèrement déroutant que si vous essayez d'obtenir la valeur d'une propriété inexistante d'un objet natif en JavaScript, il renvoie undefined(plutôt que de lancer une erreur, comme cela se produit lorsque vous essayez de faire référence à une variable inexistante ), qui est identique à ce que vous obtenez si la propriété a précédemment été explicitement définie sur undefined.

Tim Down
la source
17

Depuis la page MDC pour map:

[...] callbackn'est invoqué que pour les index du tableau auxquels une valeur a été affectée; [...]

[undefined]applique en fait le setter sur l'index (s) de sorte qu'il mapva itérer, tandis que new Array(1)juste initialise l'index (es) avec une valeur par défaut de undefinedsorte l' mapignore.

Je pense que c'est la même chose pour toutes les méthodes d'itération .

Jonathan Lonowski
la source
8

Dans la spécification ECMAScript 6e édition.

new Array(3)définissez uniquement la propriété lengthet ne définissez pas les propriétés d'index comme {length: 3}. voir https://www.ecma-international.org/ecma-262/6.0/index.html#sec-array-len Étape 9.

[undefined, undefined, undefined]définira les propriétés d'index et la propriété de longueur comme {0: undefined, 1: undefined, 2: undefined, length: 3}. voir https://www.ecma-international.org/ecma-262/6.0/index.html#sec-runtime-semantics-arrayaccumulation ElementList Étape 5.

méthodes map, every, some, forEach, slice, reduce, reduceRight, filterde tableau vérifiera la propriété d'index par HasPropertyméthode interne, donc new Array(3).map(v => 1)ne invoquera le rappel.

pour plus de détails, voir https://www.ecma-international.org/ecma-262/6.0/index.html#sec-array.prototype.map

Comment réparer?

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);
wenshin
la source
Très belle explication.
Dheeraj Rao
7

Je pense que la meilleure façon d'expliquer cela est de regarder la façon dont Chrome le gère.

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

Donc, ce qui se passe réellement, c'est que new Array () retourne un tableau vide qui a une longueur de 3, mais pas de valeurs. Par conséquent, lorsque vous exécutez x.mapsur un tableau techniquement vide, il n'y a rien à définir.

Firefox «remplit» simplement ces emplacements vides undefinedmême s'il n'a pas de valeurs.

Je ne pense pas que ce soit explicitement un bug, juste une mauvaise façon de représenter ce qui se passe. Je suppose que Chrome est "plus correct" car il montre qu'il n'y a en fait rien dans le tableau.

bonjour
la source
4

Je suis juste tombé sur ça. Il serait certainement pratique de pouvoir l'utiliser Array(n).map.

Array(3) donne à peu près {length: 3}

[undefined, undefined, undefined]crée les propriétés numérotées:
{0: undefined, 1: undefined, 2: undefined, length: 3}.

L'implémentation map () n'agit que sur les propriétés définies.

Vezquex
la source
3

Pas un bug. C'est ainsi que le constructeur Array est défini pour fonctionner.

De MDC:

Lorsque vous spécifiez un seul paramètre numérique avec le constructeur Array, vous spécifiez la longueur initiale du tableau. Le code suivant crée un tableau de cinq éléments:

var billingMethod = new Array(5);

Le comportement du constructeur Array dépend si le paramètre unique est un nombre.

La .map()méthode inclut uniquement dans les éléments d'itération du tableau auxquels des valeurs ont été explicitement affectées. Même une affectation explicite de undefinedfera qu'une valeur sera considérée comme éligible à l'inclusion dans l'itération. Cela semble étrange, mais c'est essentiellement la différence entre une undefinedpropriété explicite sur un objet et une propriété manquante:

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

L'objet xn'a pas de propriété appelée "z", et l'objet en ypossède. Cependant, dans les deux cas, il semble que la "valeur" de la propriété soit undefined. Dans un tableau, la situation est similaire: la valeur de lengtheffectue implicitement une affectation de valeur à tous les éléments de zéro à length - 1. La .map()fonction ne fera donc rien (n'appellera pas le rappel) lorsqu'elle est appelée sur un tableau nouvellement construit avec le constructeur Array et un argument numérique.

Pointu
la source
C'est défini pour être cassé? Il est conçu pour produire un tableau de trois éléments qui seront toujours undefinedéternels?
Courses de légèreté en orbite
Oui, c'est exact, sauf pour la partie "pour toujours". Vous pouvez ensuite attribuer des valeurs aux éléments.
Pointy
3
C'est pourquoi vous devriez utiliser à la x = []place dex = new Array()
Rocket Hazmat
3

Si vous faites cela afin de remplir facilement un tableau avec des valeurs, ne pouvez pas utiliser fill pour des raisons de support du navigateur et ne voulez vraiment pas faire de boucle for, vous pouvez également le faire x = new Array(3).join(".").split(".").map(...ce qui vous donnera un tableau de vide cordes.

Assez moche je dois dire, mais au moins le problème et l'intention sont assez clairement communiqués.

Alex
la source
1

Puisque la question est de savoir pourquoi, cela a à voir avec la façon dont JS a été conçu.

Il y a 2 raisons principales auxquelles je peux penser pour expliquer ce comportement:

  • Performances: étant donné x = 10000et new Array(x)il est sage pour le constructeur d'éviter de boucler de 0 à 10000 pour remplir le tableau avec des undefinedvaleurs.

  • Implicitement «non défini»: Donnez a = [undefined, undefined]et b = new Array(2), a[1]et b[1]reviendront tous les deux undefined, mais a[8]et b[8]reviendront également undefinedmême s'ils sont hors de portée.

En fin de compte, la notation empty x 3est un raccourci pour éviter de définir et d'afficher une longue liste de undefinedvaleurs qui sont de undefinedtoute façon parce qu'elles ne sont pas déclarées explicitement.

Remarque: Étant donné le tableau a = [0]et a[9] = 9, console.log(a)retournera (10) [0, empty x 8, 9], combler automatiquement l'écart en renvoyant la différence entre les deux valeurs déclarées explicitement.

BPS Julien
la source
1

Pour des raisons expliquées en détail dans d'autres réponses, Array(n).mapne fonctionne pas. Cependant, dans ES2015 Array.fromaccepte une fonction de carte:

let array1 = Array.from(Array(5), (_, i) => i + 1)
console.log('array1', JSON.stringify(array1)) // 1,2,3,4,5

let array2 = Array.from({length: 5}, (_, i) => (i + 1) * 2)
console.log('array2', JSON.stringify(array2)) // 2,4,6,8,10

Eden Landau
la source
0

Voici une méthode utilitaire simple comme solution de contournement:

Carte simplePour

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]

Exemple complet

Voici un exemple plus complet (avec vérifications d'intégrité) qui permet également de spécifier un index de départ facultatif:

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]

Compte à rebours

La manipulation de l'index transmis au rappel permet de compter à rebours:

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