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.applylida com argumentosArraylida com vários argumentosNumberfunção lida com argumentosFunction.prototype.callfazEles 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 lengthvariável mágica , mas, em sua essência, é um key => valuemapa 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=>valuemapeamentos que a matriz possui, que pode ser diferente de arr.length.
Expandir a matriz via arr.length não cria novos key=>valuemapeamentos; 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.mapnã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.toUpperCaseteria 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 lenitens na matriz, não criamos os key => valuemapeamentos, simplesmente alteramos a lengthpropriedade.
Agora que temos isso fora do caminho, vejamos a segunda coisa mágica:
Function.prototype.applyfuncionaO que applyfaz é 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 applyfunciona simplesmente registrando a argumentsvariá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 => valuemapeamento pode não ter existido na matriz para a qual passamos apply, mas certamente existe na argumentsvariá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.applyestá 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 forloop simples sobre os lengthitens, criando um listdos 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 argArraynesse caso é um objeto com uma lengthpropriedade. 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=>valuemapeamentos.
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);
Arraylida com vários argumentosAssim! Vimos o que acontece quando você passa um lengthargumento 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.
Numbertrata a entradaFazer Number(something)( seção 15.7.1 ) se converte somethingem 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.callcallé applyirmã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 callmétodo de qualquer outra função e, como tal, também possui um callmétodo:
log.call === log.call.call; //true
log.call === Function.call; //true
E o que callfaz? Ele aceita thisArgvá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}])
.mapde 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 thisargumento, 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 forEachestá 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 2e truerespectivamente? 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 applyexemplos. A primeira console.logmostra que, de fato, recebemos dois argumentos (os dois itens da matriz) e a segunda console.logmostra que a matriz possui um key=>valuemapeamento 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.
thissomente o padrão é Windowno 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.
thisvalor é o nullque não importa para o construtor Array ( thisé o mesmo thisque no contexto, de acordo com 15.3.4.3.2.a.new Arrayé chamado de ser passado um objeto com uma lengthpropriedade - que faz com que esse objeto seja uma matriz como para tudo o que importa .applydevido à seguinte cláusula em .apply:
.applyestá passando argumentos de 0 para .length, uma vez que [[Get]]a chamada { length: 5 }com os valores de 0 a 4 produz undefinedo 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 undefinede a segunda criando uma matriz vazia de comprimento 5. Especificamente, devido ao .mapcomportamento 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.mapchama a função de retorno de chamada (neste caso Number.call) em cada elemento da matriz e usa o thisvalor especificado (nesse caso, definindo o thisvalor como `Number).Number.call) é o índice e o primeiro é esse valor.Numberé chamado com thisas undefined(o valor da matriz) e o índice como parâmetro. Portanto, é basicamente o mesmo que mapear cada um undefinedpara seu índice de matriz (uma vez que a chamada Numberrealiza 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 undefinedduas 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 Arrayconstrutor inserirá na matriz recém-criada. Como não existe um array real acessando os elementos não presentes, o undefinedqual é inserido. Bom truque :)
Como você disse, a primeira parte:
var arr = Array.apply(null, { length: 5 });
cria uma matriz de 5 undefinedvalores.
A segunda parte está chamando a mapfunção da matriz, que recebe 2 argumentos e retorna uma nova matriz do mesmo tamanho.
O primeiro argumento que mapleva é 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 mapestá 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 mapestá passando é o índice, o valor que será colocado na nova matriz nesse índice é igual ao índice. Assim como a função bazno exemplo acima. Number.calltentará analisar o índice - retornará naturalmente o mesmo valor.
O segundo argumento que você transmitiu para a mapfunção em seu código não afeta o resultado. Corrija-me se eu estiver errado, por favor.
Number.callnã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] === undefinedgera 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.applyfoi usado.
Array.apply(null, Array(30)).map(Number.call, Number)é mais fácil de ler, pois evita fingir que um objeto simples é uma matriz.