Definido ou Indefinido? Nuances da criação de matrizes em JavaScript

imagem

Há alguns meses, deparei-me com uma pergunta interessante sobre o stackoverflow , onde, em suma, uma pessoa queria criar uma matriz 5x5 vazia e, usando um método, conseguiu, mas o outro não. Na discussão que se seguiu, pensamentos interessantes foram citados sobre esse assunto.

É verdade que a pessoa que fez a pergunta, assim como as que a responderam, não prestaram atenção ao fato de que de fato a matriz não pôde ser criada e o resultado do cálculo estava incorreto. Tudo isso me interessou, e decidi ir um pouco mais fundo, e chegar a conclusões interessantes, que vou compartilhar com você agora.

Nota: Eu também respondi sob essa discussão, sob o apelido AndreyGS - lá respondi brevemente, aqui tentarei cobrir completamente o problema.

Em geral, o desafio é criar uma matriz. Como vamos fazer isso? Curiosamente, existem opções diferentes, dependendo do que queremos obter.

Sabemos que as funções em JavaScript têm dois métodos internos, Chamada e Construção . Se usarmos a nova palavra-chave, será usado o método Construct, que cria uma nova instância do objeto, atribui essa referência a ele e, em seguida, executa o corpo da função. Nem todas as funções possuem esse método, mas para nós isso não é tão importante no momento.

Ao criar matrizes, há uma peculiaridade: não importa se usamos Matriz (...) ou nova Matriz (...) - a especificação ECMAScript não distingue entre elas e, além disso, as considera equivalentes.

22.1.1 The Array Constructor The Array constructor is the %Array% intrinsic object and the initial value of the Array property of the global object. When called as a constructor it creates and initializes a new exotic Array object. When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments. 

Portanto, não filosofarei maliciosamente e, nos exemplos, usarei apenas a nova construção Array (...) , para não confundir ninguém.

Vamos começar.

Crie uma matriz:

 let arr = new Array(5); 

O que conseguimos?

 console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined 

Hmm ... bem, em princípio, deve ser assim - definimos o comprimento e obtivemos cinco células vazias, com o valor indefinido , que pode ser trabalhado mais adiante, certo? É verdade que existem alguns pontos que me confundem. Vamos conferir.

 let arr = new Array(5).map(function() { return new Array(5); }); console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined console.log(arr[0][0]); // TypeError: arr[0] is undefined 

Como é que, afinal, tivemos que obter uma matriz e, em cada célula, deveria haver uma matriz de 5 elementos ...

Vamos voltar à documentação do ECMAScript e ver o que está escrito sobre o método de criação de matrizes com um argumento:

 22.1.1.2 Array (len) This description applies if and only if the Array constructor is called with exactly one argument. 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs = 1. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(0, proto). 7. If Type(len) is not Number, then 1. Let defineStatus be CreateDataProperty(array, "0", len). 2. Assert: defineStatus is true. 3. Let intLen be 1. 8. Else, 1. Let intLen be ToUint32(len). 2. If intLen ≠ len, throw a RangeError exception. 9. Let setStatus be Set(array, "length", intLen, true). 10. Assert: setStatus is not an abrupt completion. 11. Return array. 

E, o que vemos, o objeto é criado, a propriedade length é criada no procedimento ArrayCreate (ponto 6), o valor na propriedade length é definido (etapa 9), e as células? Além do caso especial em que o argumento passado não é um número e uma matriz é criada com uma única célula "0" com o valor correspondente (ponto 7), não há uma palavra sobre eles ... Ou seja, há == 5 comprimento, mas não há cinco células. Sim, o compilador nos confunde quando tentamos acessar uma única célula; ele mostra que seu valor é indefinido , enquanto na verdade não é.

Aqui, para comparação, o método de criação de matrizes com vários argumentos enviados ao construtor:

 22.1.1.3 Array (...items ) This description applies if and only if the Array constructor is called with at least two arguments. When the Array function is called the following steps are taken: 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs ≥ 2. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(numberOfArgs, proto). 7. ReturnIfAbrupt(array). 8. Let k be 0. 9. Let items be a zero-origined List containing the argument items in order. 10. Repeat, while k < numberOfArgs 1. Let Pk be ToString(k). 2. Let itemK be items[k]. 3. Let defineStatus be CreateDataProperty(array, Pk, itemK). 4. Assert: defineStatus is true. 5. Increase k by 1. 11. Assert: the value of array's length property is numberOfArgs. 12. Return array. 

Aqui, por favor - 10 pontos, a criação dessas mesmas células.

E agora, o que Array.prototype.map () faz a seguir?

 22.1.3.15 Array.prototype.map ( callbackfn [ , thisArg ] ) 1. Let O be ToObject(this value). 2. ReturnIfAbrupt(O). 3. Let len be ToLength(Get(O, "length")). 4. ReturnIfAbrupt(len). 5. If IsCallable(callbackfn) is false, throw a TypeError exception. 6. If thisArg was supplied, let T be thisArg; else let T be undefined. 7. Let A be ArraySpeciesCreate(O, len). 8. ReturnIfAbrupt(A). 9. Let k be 0. 10. Repeat, while k < len 1. Let Pk be ToString(k). 2. Let kPresent be HasProperty(O, Pk). 3. ReturnIfAbrupt(kPresent). 4. If kPresent is true, then 1. Let kValue be Get(O, Pk). 2. ReturnIfAbrupt(kValue). 3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»). 4. ReturnIfAbrupt(mappedValue). 5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue). 6. ReturnIfAbrupt(status). 5. Increase k by 1. 11. Return A. 

Cláusula 7 - é criada uma cópia da matriz original, na cláusula 10 são realizadas iterações len em seus elementos e, em particular, a cláusula 10.2 verifica se existe uma célula específica na matriz de origem, para que, se for bem-sucedido, mapeie (10.4) e crie a célula apropriada na cópia - 10.4.5. Como o 10.2 fornece false para cada uma das 5 passagens, também não será criada uma única célula na cópia retornada da matriz.

Assim, como o construtor da matriz e o método Array.prototype.map () funcionam, descobrimos isso, mas a tarefa permaneceu não resolvida, porque a matriz não foi construída. Function.prototype.apply () virá em socorro!
Vamos verificar em ação imediatamente:

 let arr = Array.apply(null, new Array(5)); console.log(arr); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

Hurrah, todas as cinco células são claramente observadas aqui e também a primeira célula de teste com o número "0" possui um descritor.

Nesse caso, o programa funcionou da seguinte maneira:

  1. Chamamos o método Function.prototype.apply () e passamos o contexto nulo para ele, e como a matriz new Array (5) .
  2. nova Matriz (5) criou uma matriz sem células, mas com um comprimento de 5 .
  3. Function.prototype.apply () usou o método interno de dividir a matriz em argumentos separados; como resultado, passou cinco argumentos com valores indefinidos para o construtor Array .
  4. A matriz recebeu 5 argumentos com valores indefinidos e os adicionou às células correspondentes.

Tudo parece ser compreensível, exceto o que é esse método interno de Function.prototype.apply () , que produz 5 argumentos do nada - sugiro novamente que consulte a documentação do ECMAScript :

 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. 

Nós olhamos para os pontos mais interessantes:

19.2.3.1 - parágrafo 3: criando uma lista de argumentos a partir de um objeto semelhante a uma matriz (como lembramos, esses objetos devem ter uma propriedade length).

7.3.17 - o próprio método de criação de lista. Ele verifica se o objeto é ou não e, em caso afirmativo, uma solicitação para o campo de comprimento (parágrafo 4). Em seguida, é criado um índice igual a "0" (parágrafo 7). Um loop é criado com um incremento do índice para o valor retirado do campo length (parágrafo 8). Neste ciclo, nos referimos aos valores das células da matriz transmitida com os índices correspondentes (cláusulas 8a e 8b). E, como lembramos, ao acessar o valor de uma única célula em uma matriz na qual, na verdade, não há células, ele ainda fornece um valor - indefinido . O valor resultante é adicionado ao final da lista de argumentos (parágrafo 8e).

Bem, agora que tudo se encaixou, você pode construir com segurança a matriz muito vazia.

 let arr = Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); }); console.log(arr); // Array(5) [ (5) […], (5) […], (5) […], (5) […], (5) […] ] console.log(arr[0]); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: (5) […], writable: true, enumerable: true, configurable: true } console.log(arr[0][0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr[0],"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

Agora, como você pode ver, tudo converge e parece bem simples: nós, da maneira que já sabemos, criamos uma matriz Array.apply (null, new Array (5)) vazia e depois a passamos para o método map, que cria a mesma matriz em cada uma das células.

Além disso, você pode facilitar ainda mais. O operador spread - ... apareceu no ECMAScript6 e, o que é típico, também funciona especificamente com matrizes. Portanto, podemos simplesmente dirigir:

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

ou vamos simplificá-lo completamente, mesmo que eu tenha prometido que não tocaria ...

 let arr = Array(...Array(5)).map(() => Array(...Array(5))); 
nota: aqui também usamos funções de seta, pois ainda estamos lidando com um operador de spread que apareceu na mesma especificação que eles.

Não entraremos no princípio do operador de spread , no entanto, para o desenvolvimento geral, acredito que este exemplo também foi útil.

Além disso, é claro, podemos construir nossas próprias funções, que, usando a classificação Function.prototype.apply () , criarão matrizes normais para nós com células vazias, no entanto, entendemos os princípios internos do JavaScript e, portanto, o uso correto e adequado funções internas, é uma base para dominar, o que é uma prioridade. Bem, e, claro, é tão simples, mais rápido e mais conveniente.

E, finalmente, voltando à mesma pergunta no stackoverflow - ali, eu me lembro, a pessoa considerou erroneamente que o método que recebeu levou à resposta correta e que recebeu uma matriz 5x5 - um pequeno erro apareceu lá.

Ele dirigiu em:

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


O que você acha que o resultado real estará aqui?

A resposta
console.log (arr); // Matriz (5) [(5) [...], (5) [...], (5) [...], (5) [...], (5) [...]]
console.log (arr [0]); // Matriz (5) [<5 espaços vazios>]
console.log (Object.getOwnPropertyDescriptor (arr, "0")); // Objeto {valor: (5) [...], gravável: true, enumerável: true, configurável: true}
console.log (arr [0] [0]); // indefinido
console.log (Object.getOwnPropertyDescriptor (arr [0], "0")); // indefinido

não é, não era exatamente o que ele queria ...

Referências:

Especificação de idioma do ECMAScript 2015
O que o Array.apply está realmente fazendo

Source: https://habr.com/ru/post/pt463041/


All Articles