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?
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?
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!
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, é um key => value
mapa regular , como qualquer outro objeto. Vamos brincar um pouco com arrays, não é?
var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined
//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']
Chegamos à diferença inerente entre o número de itens na matriz arr.length
, e o número de key=>value
mapeamentos que a matriz possui, que pode ser diferente de arr.length
.
Expandir a matriz via arr.length
não cria novos key=>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ê entendeu undefined
.
Agora podemos levantar um pouco a cabeça e ver por que funções como arr.map
não andam sobre essas propriedades. Se arr[3]
fosse meramente indefinido e a chave existisse, todas essas funções da matriz passariam por cima dela como qualquer outro valor:
//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';
arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']
arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]
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 :
arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined
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:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(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 definimos len
itens na matriz, não criamos os key => value
mapeamentos, simplesmente alteramos a length
propriedade.
Agora que temos isso fora do caminho, vejamos a segunda coisa mágica:
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:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Agora, podemos facilitar o processo de ver como apply
funciona simplesmente registrando a arguments
variável especial:
function log () {
console.log(arguments);
}
log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
//["mary", "had", "a", "little", "lamb"]
//arguments is a pseudo-array itself, so we can use it as well
(function () {
log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
//["mary", "had", "a", "little", "lamb"]
//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
//[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]
//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!
log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]
É fácil provar minha afirmação no penúltimo exemplo:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(sim, trocadilhos). O key => value
mapeamento pode não ter existido na matriz para a qual passamos apply
, mas certamente existe na arguments
variável É a mesma razão pela qual o último exemplo funciona: as chaves não existem no objeto que passamos, mas elas existem arguments
.
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:
- Seja o resultado da chamada do método interno [[Get]] interno do argArray com o argumento "length".
O que basicamente significa: argArray.length
. A especificação passa a fazer um for
loop simples sobre os length
itens, criando um list
dos valores correspondentes ( list
é um vodu interno, mas é basicamente uma matriz). Em termos de código muito, muito flexível:
Function.prototype.apply = function (thisArg, argArray) {
var len = argArray.length,
argList = [];
for (var i = 0; i < len; i += 1) {
argList[i] = argArray[i];
}
//yeah...
superMagicalFunctionInvocation(this, thisArg, argList);
};
Então, tudo o que precisamos para imitar um argArray
nesse caso é um objeto com uma length
propriedade. E agora podemos ver por que os valores não estão definidos, mas as chaves não estão ativadas arguments
: criamos os key=>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:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Array
lida com vários argumentosAssim! Vimos o que acontece quando você passa um length
argumento para Array
, mas na expressão, passamos várias coisas como argumentos (uma matriz de 5 undefined
, 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:
function Array () {
var ret = [];
ret.length = arguments.length;
for (var i = 0; i < arguments.length; i += 1) {
ret[i] = arguments[i];
}
return ret;
}
Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]
Tada! Obtemos uma matriz de vários valores indefinidos e retornamos uma matriz desses valores indefinidos.
Por fim, podemos decifrar o seguinte:
Array.apply(null, { length: 5 })
Vimos que ele retorna uma matriz contendo 5 valores indefinidos, com todas as chaves existentes.
Agora, para a segunda parte da expressão:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Essa será a parte mais fácil e não complicada, pois não depende tanto de hacks obscuros.
Number
trata a entradaFazer Number(something)
( seção 15.7.1 ) se converte something
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.
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:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Isso vale bastante até você entender o que está acontecendo. log.call
é apenas uma função, equivalente ao call
método de qualquer outra função e, como tal, também possui um call
método:
log.call === log.call.call; //true
log.call === Function.call; //true
E o que call
faz? Ele aceita thisArg
vários argumentos e chama sua função pai. Podemos defini-lo via apply
(novamente, código muito flexível, não funcionará):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Vamos acompanhar como isso acontece:
log.call.call(log, {a:4}, {a:5});
this = log.call
thisArg = log
args = [{a:4}, {a:5}]
log.call.apply(log, [{a:4}, {a:5}])
log.call({a:4}, {a:5})
this = log
thisArg = {a:4}
args = [{a:5}]
log.apply({a:4}, [{a:5}])
.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:
function log () {
console.log(this, arguments);
}
var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^ ^-----------------------^
// this arguments
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:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
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:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Então, nós entendemos isso:
log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);
Agora podemos ver como .map(Number.call, Number)
funciona:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
O que retorna a transformação do i
índice atual em um número.
A expressão
Array.apply(null, { length: 5 }).map(Number.call, Number);
Funciona em duas partes:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
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:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true
. Por que ele retorna 2
e true
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 primeiros apply
exemplos. A primeira console.log
mostra que, de fato, recebemos dois argumentos (os dois itens da matriz) e a segunda console.log
mostra que a matriz possui um key=>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.
var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Na primeira linha, o construtor da matriz é chamado como uma função com Function.prototype.apply
.
this
valor é o null
que não importa para o construtor Array ( this
é o mesmo this
que no contexto, de acordo com 15.3.4.3.2.a.new Array
é chamado de ser passado um objeto com uma length
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 produz undefined
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})
e Array(5)
, a primeira criando cinco vezes o tipo de valor primitivo undefined
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:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Agora vamos para a segunda parte.
Array.prototype.map
chama a função de retorno de chamada (neste caso Number.call
) em cada elemento da matriz e usa o this
valor especificado (nesse caso, definindo o this
valor como `Number).Number.call
) é o índice e o primeiro é esse valor.Number
é chamado com this
as undefined
(o valor da matriz) e o índice como parâmetro. Portanto, é basicamente o mesmo que mapear cada um undefined
para seu índice de matriz (uma vez que a chamada Number
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.
Array.apply(null,[2])
é como o Array(2)
que cria uma matriz vazia de comprimento 2 e não uma matriz contendo o valor primitivo undefined
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 o Array
construtor inserirá na matriz recém-criada. Como não existe um array real acessando os elementos não presentes, o undefined
qual é inserido. Bom truque :)
Como você disse, a primeira parte:
var arr = Array.apply(null, { length: 5 });
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:
function foo(a,b,c){
...
return ...
}
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 de foo
, seria this
.
Dois exemplos:
function bar(a,b,c){
return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]
function baz(a,b,c){
return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]
e outro apenas para esclarecer:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
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ção baz
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.
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 o this
-valor para call
, é 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
[undefined, undefined, undefined, …]
e new Array(n)
ou {length: n}
- os últimos são escassos , ou seja, eles não têm elementos. Isso é muito relevante para map
, e é por isso que o ímpar Array.apply
foi usado.
Array.apply(null, Array(30)).map(Number.call, Number)
é mais fácil de ler, pois evita fingir que um objeto simples é uma matriz.