Criando intervalo em JavaScript - sintaxe estranha

129

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?

Benjamin Gruenbaum
fonte
2
O IMO Array.apply(null, Array(30)).map(Number.call, Number)é mais fácil de ler, pois evita fingir que um objeto simples é uma matriz.
Fncomp
10
@fncomp Por favor, não use nenhum deles para realmente criar um intervalo. Não é apenas mais lento que a abordagem direta - não é tão fácil de entender. É difícil entender a sintaxe (bem, realmente a API e não a sintaxe) aqui, o que torna essa uma pergunta interessante, mas o terrível código de produção IMO.
Benjamin Gruenbaum 25/09
Sim, não sugerindo que alguém o use, mas achei que ainda era mais fácil de ler, em relação à versão literal do objeto.
Fncomp
1
Não sei por que alguém iria querer fazer isso. A quantidade de tempo que leva para criar a matriz desta forma poderia ter sido feito em um pouco menos sexy, mas muito maneira mais rápida: jsperf.com/basic-vs-extreme
Eric Hodonsky

Respostas:

263

Compreender esse "hack" requer entender várias coisas:

  1. Por que não fazemos apenas Array(5).map(...)
  2. Como Function.prototype.applylida com argumentos
  3. Como Arraylida com vários argumentos
  4. Como a Numberfunção lida com argumentos
  5. O que Function.prototype.callfaz

Eles 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 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:

2. Como Function.prototype.applyfunciona

O 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:

  1. 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);

3. Como Arraylida com vários argumentos

Assim! 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.

A primeira parte da expressão

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.

4. Como Numbertrata a entrada

Fazer 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.

5. Jogos de Function.prototype.call

callé 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}])

A parte posterior, ou o .mapde tudo

Ainda 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.

Em conclusão,

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]
Zirak
fonte
@ Zirak Por favor, ajude-me a entender o seguinte 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?
25413 Geek
4
@ Geek Nós passamos apenas um argumento para 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).
Zirak 25/09
4
Devido a (alguns) pedidos , agora você pode apreciar a versão em áudio: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak
1
Observe que a passagem de um NodeList, que é um objeto host, para um método nativo como in 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.
RobG
2
Uma correção: thissomente o padrão é Windowno modo não estrito.
ComFreek
21

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.

  • O 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.
  • Em seguida, 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:
    • Seja o resultado da chamada do método interno [[Get]] interno do argArray com o argumento "length".
  • Como tal, .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).
  • O construtor de matriz é chamado com 0, 2 ou mais argumentos . A propriedade length da matriz recém-construída é definida como o número de argumentos de acordo com a especificação e os valores com os mesmos valores.
  • Assim, 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).
  • O segundo parâmetro do retorno de chamada no mapa (nesse caso Number.call) é o índice e o primeiro é esse valor.
  • Isso significa que 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.

Benjamin Gruenbaum
fonte
1
Para documentos: Especificação de como o mapa funciona: es5.github.io/#x15.4.4.19 , o Mozilla possui um exemplo de script que funciona de acordo com essa especificação em developer.mozilla.org/en-US/docs/Web/JavaScript/ Referência /…
Patrick Evans
1
Mas por que funciona apenas com Array.apply(null, { length: 2 })e não, o Array.apply(null, [2])que também chamaria o Arrayconstrutor de passar 2como valor de comprimento? fiddle
Andreas
@Andreas 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.
Benjamin Gruenbaum 22/09
Não entendi como funciona na primeira execução ... Depois da segunda leitura, faz sentido. {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 :)
Andreas
5

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

  • a como o valor do elemento iterado atual
  • b como o índice do elemento iterado atual
  • c como toda a matriz original

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.

Tal Z
fonte
1
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.
Bergi 22/09/2013
0

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

shex
fonte
1
Há uma enorme diferença entre [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.
Bergi