Encontrei o seguinte código na lista de discussão es-discuss:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Isso produz
[0, 1, 2, 3, 4]
Por que esse é o resultado do código? O que está acontecendo aqui?
javascript
ecmascript-5
Benjamin Gruenbaum
fonte
fonte
Array.apply(null, Array(30)).map(Number.call, Number)
é mais fácil de ler, pois evita fingir que um objeto simples é uma matriz.Respostas:
Compreender esse "hack" requer entender várias coisas:
Array(5).map(...)
Function.prototype.apply
lida com argumentosArray
lida com vários argumentosNumber
função lida com argumentosFunction.prototype.call
fazEles são tópicos bastante avançados em javascript, portanto, isso será mais do que bastante longo. Vamos começar do topo. Preparar-se!
1. Por que não apenas
Array(5).map
?O que é uma matriz, realmente? Um objeto regular, contendo chaves inteiras, que são mapeadas para valores. Ele tem outros recursos especiais, por exemplo, a
length
variável mágica , mas, em sua essência, é umkey => value
mapa regular , como qualquer outro objeto. Vamos brincar um pouco com arrays, não é?Chegamos à diferença inerente entre o número de itens na matriz
arr.length
, e o número dekey=>value
mapeamentos que a matriz possui, que pode ser diferente dearr.length
.Expandir a matriz via
arr.length
não cria novoskey=>value
mapeamentos; portanto, não é que a matriz tenha valores indefinidos, ela não possui essas chaves . E o que acontece quando você tenta acessar uma propriedade inexistente? Você entendeuundefined
.Agora podemos levantar um pouco a cabeça e ver por que funções como
arr.map
não andam sobre essas propriedades. Searr[3]
fosse meramente indefinido e a chave existisse, todas essas funções da matriz passariam por cima dela como qualquer outro valor:Usei intencionalmente uma chamada de método para provar ainda mais que a chave em si nunca estava lá: a chamada
undefined.toUpperCase
teria gerado um erro, mas não aconteceu. Para provar isso :E agora chegamos ao meu ponto: como as
Array(N)
coisas acontecem. A seção 15.4.2.2 descreve o processo. Há um monte de mumbo jumbo com o qual não nos importamos, mas se você consegue ler nas entrelinhas (ou pode confiar em mim nesse caso, mas não sabe), basicamente se resume a isso:(opera sob a suposição (que é verificada na especificação real) que
len
é um uint32 válido e não apenas qualquer número de valor)Então agora você pode ver por que fazer
Array(5).map(...)
isso não funcionaria - não definimoslen
itens na matriz, não criamos oskey => value
mapeamentos, simplesmente alteramos alength
propriedade.Agora que temos isso fora do caminho, vejamos a segunda coisa mágica:
2. Como
Function.prototype.apply
funcionaO que
apply
faz é basicamente pegar uma matriz e desenrolá-la como argumentos de uma chamada de função. Isso significa que o seguinte é praticamente o mesmo:Agora, podemos facilitar o processo de ver como
apply
funciona simplesmente registrando aarguments
variável especial:É fácil provar minha afirmação no penúltimo exemplo:
(sim, trocadilhos). O
key => value
mapeamento pode não ter existido na matriz para a qual passamosapply
, mas certamente existe naarguments
variável É a mesma razão pela qual o último exemplo funciona: as chaves não existem no objeto que passamos, mas elas existemarguments
.Por que é que? Vejamos a Seção 15.3.4.3 , onde
Function.prototype.apply
está definido. Principalmente coisas com as quais não nos importamos, mas aqui está a parte interessante:O que basicamente significa:
argArray.length
. A especificação passa a fazer umfor
loop simples sobre oslength
itens, criando umlist
dos valores correspondentes (list
é um vodu interno, mas é basicamente uma matriz). Em termos de código muito, muito flexível:Então, tudo o que precisamos para imitar um
argArray
nesse caso é um objeto com umalength
propriedade. E agora podemos ver por que os valores não estão definidos, mas as chaves não estão ativadasarguments
: criamos oskey=>value
mapeamentos.Ufa, então isso pode não ter sido menor que a parte anterior. Mas haverá bolo quando terminarmos, então seja paciente! No entanto, após a seção a seguir (que será curta, prometo), podemos começar a dissecar a expressão. Caso você tenha esquecido, a questão era como funciona o seguinte:
3. Como
Array
lida com vários argumentosAssim! Vimos o que acontece quando você passa um
length
argumento paraArray
, mas na expressão, passamos várias coisas como argumentos (uma matriz de 5undefined
, para ser exato). A seção 15.4.2.1 nos diz o que fazer. O último parágrafo é tudo o que importa para nós, e é redigido de maneira muito estranha, mas meio que se resume a:Tada! Obtemos uma matriz de vários valores indefinidos e retornamos uma matriz desses valores indefinidos.
A primeira parte da expressão
Por fim, podemos decifrar o seguinte:
Vimos que ele retorna uma matriz contendo 5 valores indefinidos, com todas as chaves existentes.
Agora, para a segunda parte da expressão:
Essa será a parte mais fácil e não complicada, pois não depende tanto de hacks obscuros.
4. Como
Number
trata a entradaFazer
Number(something)
( seção 15.7.1 ) se convertesomething
em um número, e isso é tudo. Como isso é um pouco complicado, especialmente nos casos de strings, mas a operação é definida na seção 9.3 , caso você esteja interessado.5. Jogos de
Function.prototype.call
call
éapply
irmão de, definido na seção 15.3.4.4 . Em vez de pegar uma matriz de argumentos, apenas pega os argumentos recebidos e os passa adiante.As coisas ficam interessantes quando você encadeia mais de um
call
, aumenta o estranho até 11:Isso vale bastante até você entender o que está acontecendo.
log.call
é apenas uma função, equivalente aocall
método de qualquer outra função e, como tal, também possui umcall
método:E o que
call
faz? Ele aceitathisArg
vários argumentos e chama sua função pai. Podemos defini-lo viaapply
(novamente, código muito flexível, não funcionará):Vamos acompanhar como isso acontece:
A parte posterior, ou o
.map
de tudoAinda não acabou. Vamos ver o que acontece quando você fornece uma função para a maioria dos métodos de matriz:
Se nós mesmos não fornecermos um
this
argumento, o padrão seráwindow
. Tome nota da ordem em que os argumentos são fornecidos ao nosso retorno de chamada e vamos esquisitá-lo até o 11 novamente:Whoa whoa whoa ... vamos voltar um pouco. O que está acontecendo aqui? Podemos ver na seção 15.4.4.18 , onde
forEach
está definido, o seguinte praticamente acontece:Então, nós entendemos isso:
Agora podemos ver como
.map(Number.call, Number)
funciona:O que retorna a transformação do
i
índice atual em um número.Em conclusão,
A expressão
Funciona em duas partes:
A primeira parte cria uma matriz de 5 itens indefinidos. O segundo passa por cima dessa matriz e obtém seus índices, resultando em uma matriz de índices de elementos:
fonte
ahaExclamationMark.apply(null, Array(2)); //2, true
. Por que ele retorna2
etrue
respectivamente? Você não está passando apenas um argumento, ou seja,Array(2)
aqui?apply
, mas esse argumento é "dividido" em dois argumentos passados para a função. Você pode ver isso mais facilmente nos primeirosapply
exemplos. A primeiraconsole.log
mostra que, de fato, recebemos dois argumentos (os dois itens da matriz) e a segundaconsole.log
mostra que a matriz possui umkey=>value
mapeamento no 1º slot (como explicado na 1ª parte da resposta).log.apply(null, document.getElementsByTagName('script'));
não é necessária para funcionar e não funciona em alguns navegadores, e[].slice.call(NodeList)
transformar um NodeList em uma matriz também não funcionará neles.this
somente o padrão éWindow
no modo não estrito.Isenção de responsabilidade : Esta é uma descrição muito formal do código acima - é assim que eu sei como explicá-lo. Para uma resposta mais simples - verifique a ótima resposta de Zirak acima. Esta é uma especificação mais profunda em seu rosto e menos "aha".
Várias coisas estão acontecendo aqui. Vamos terminar um pouco.
Na primeira linha, o construtor da matriz é chamado como uma função com
Function.prototype.apply
.this
valor é onull
que não importa para o construtor Array (this
é o mesmothis
que no contexto, de acordo com 15.3.4.3.2.a.new Array
é chamado de ser passado um objeto com umalength
propriedade - que faz com que esse objeto seja uma matriz como para tudo o que importa.apply
devido à seguinte cláusula em.apply
:.apply
está passando argumentos de 0 para.length
, uma vez que[[Get]]
a chamada{ length: 5 }
com os valores de 0 a 4 produzundefined
o construtor de matriz é chamado com cinco argumentos cujo valor éundefined
(obter uma propriedade não declarada de um objeto).var arr = Array.apply(null, { length: 5 });
cria uma lista de cinco valores indefinidos.Nota : Observe a diferença aqui entre
Array.apply(0,{length: 5})
eArray(5)
, a primeira criando cinco vezes o tipo de valor primitivoundefined
e a segunda criando uma matriz vazia de comprimento 5. Especificamente, devido ao.map
comportamento de (8.b) e especificamente[[HasProperty]
.Portanto, o código acima em uma especificação compatível é o mesmo que:
Agora vamos para a segunda parte.
Array.prototype.map
chama a função de retorno de chamada (neste casoNumber.call
) em cada elemento da matriz e usa othis
valor especificado (nesse caso, definindo othis
valor como `Number).Number.call
) é o índice e o primeiro é esse valor.Number
é chamado comthis
asundefined
(o valor da matriz) e o índice como parâmetro. Portanto, é basicamente o mesmo que mapear cada umundefined
para seu índice de matriz (uma vez que a chamadaNumber
realiza a conversão de tipos, nesse caso, de número para número, sem alterar o índice).Portanto, o código acima pega os cinco valores indefinidos e mapeia cada um para seu índice na matriz.
É por isso que obtemos o resultado em nosso código.
fonte
Array.apply(null, { length: 2 })
e não, oArray.apply(null, [2])
que também chamaria oArray
construtor de passar2
como valor de comprimento? fiddleArray.apply(null,[2])
é como oArray(2)
que cria uma matriz vazia de comprimento 2 e não uma matriz contendo o valor primitivoundefined
duas vezes. Veja minha edição mais recente na nota após a primeira parte, deixe-me saber se está claro o suficiente e, caso contrário, vou esclarecer sobre isso.{length: 2}
falsifica uma matriz com dois elementos que oArray
construtor inserirá na matriz recém-criada. Como não existe um array real acessando os elementos não presentes, oundefined
qual é inserido. Bom truque :)Como você disse, a primeira parte:
cria uma matriz de 5
undefined
valores.A segunda parte está chamando a
map
função da matriz, que recebe 2 argumentos e retorna uma nova matriz do mesmo tamanho.O primeiro argumento que
map
leva é realmente uma função a ser aplicada em cada elemento da matriz; espera-se que seja uma função que pega 3 argumentos e retorna um valor. Por exemplo:se passarmos a função foo como o primeiro argumento, ela será chamada para cada elemento com
O segundo argumento utilizado
map
está sendo passado para a função que você passa como o primeiro argumento. Mas não seria a, b, nem c no caso defoo
, seriathis
.Dois exemplos:
e outro apenas para esclarecer:
E o Number.call?
Number.call
é uma função que recebe 2 argumentos e tenta analisar o segundo argumento em um número (não sei o que ele faz com o primeiro argumento).Como o segundo argumento que
map
está passando é o índice, o valor que será colocado na nova matriz nesse índice é igual ao índice. Assim como a funçãobaz
no exemplo acima.Number.call
tentará analisar o índice - retornará naturalmente o mesmo valor.O segundo argumento que você transmitiu para a
map
função em seu código não afeta o resultado. Corrija-me se eu estiver errado, por favor.fonte
Number.call
não é uma função especial que analisa argumentos em números. É apenas=== Function.prototype.call
. Apenas o segundo argumento, a função que é passada como othis
-valor paracall
, é relevante -.map(eval.call, Number)
,.map(String.call, Number)
e.map(Function.prototype.call, Number)
são todos equivalentes.Uma matriz é simplesmente um objeto que compreende o campo 'length' e alguns métodos (por exemplo, push). Portanto, arr in
var arr = { length: 5}
é basicamente o mesmo que uma matriz em que os campos 0..4 têm o valor padrão indefinido (ou seja,arr[0] === undefined
gera true).Quanto à segunda parte, mapear, como o nome indica, mapeia de uma matriz para uma nova. Isso é feito percorrendo a matriz original e chamando a função de mapeamento em cada item.
Tudo o que resta é convencê-lo de que o resultado da função de mapeamento é o índice. O truque é usar o método chamado 'call' (*) que invoca uma função com a pequena exceção de que o primeiro parâmetro é definido como o contexto 'this' e o segundo se torna o primeiro parâmetro (e assim por diante). Coincidentemente, quando a função de mapeamento é chamada, o segundo parâmetro é o índice.
Por último, mas não menos importante, o método invocado é o Number "Class" e, como sabemos em JS, uma "Class" é simplesmente uma função, e este (Number) espera que o primeiro parâmetro seja o valor.
(*) encontrado no protótipo da Function (e Number é uma função).
MASHAL
fonte
[undefined, undefined, undefined, …]
enew Array(n)
ou{length: n}
- os últimos são escassos , ou seja, eles não têm elementos. Isso é muito relevante paramap
, e é por isso que o ímparArray.apply
foi usado.