Front End

13 jun, 2011

Explorando loops for-in no JavaScript

Publicidade

O comando for-in é a única interface
compatível com ECMAScript (pdf) para
fazer a iteração de objetos genéricos. Há muita coisa escrita sobre os perigos
no uso do
for-in
para fazer a iteração de matrizes e sobre sua aplicação no filtro
hasOwnProperty,
mas, além disso, a documentação sobre essa construção comum é surpreendentemente
fragmentada. Este artigo tenta preencher algumas lacunas. Espero que seja útil.

Fundamentos

A especificação ECMAScript detalha duas sintaxes
distintas para o comando for-in:

1. for (var variable in objectExpression) {statement}

Este é o formato
familiar. Qualquer expressão que afete um objeto pode ser usada como objectExpression.
Se um primitive for fornecido, ele ficará condicionado a esse
objeto. As propriedades desse objeto são iteradas. Em cada iteração, o nome da
propriedade é atribuído à variável declarada e o statement (se presente)
é avaliado.

1	var myObj = {a: 1, b: 2, c: 3}, myKeys = [];
2
3 for (var property in myObj) {
4 myKeys.push(property);
5 }
6
7 myKeys; //['a','b','c'];

A variável pode opcionalmente ser definida fora
do for-in. As chaves somente são necessárias se o comando se prolonga por
linhas múltiplas e o próprio statement é opcional. Assim, o código
seguinte também é válido – embora não essencialmente útil, a menos que você
esteja interessado em gravar o nome da “última” propriedade do myObj´s (adiante,
mais sobre a sequência de iteração).

1	var myObj = {a: 1, b: 2, c: 3}, lastProperty;
2
3 for (lastProperty in myObj);
4
5 lastProperty; //"c";

Aqui mais um exemplo. Neste caso, objectExpression
transforma-se em um primitivo:

1	var str = "hello!", spreadOut = "";
2
3 for (var index in str) {
4 (index > 0) && (spreadOut += " ")
5 spreadOut += str[index];
6 }
7
8 spreadOut; //"h e l l o !"

Note que como
todos os property names, os índices no exemplo acima são na verdade strings
– dessa forma, nós não podemos somente fazer um simples teste de verificação na
linha 5. Mais tarde, veremos por que Strings e Arrays não são
sempre bons candidatos para iterações for-in.

2. for (var LeftHandSideExpression in objectExpression) {statement}

Esta sintaxe
interessante é raramente documentada (não há menção dela no MDC). Em
termos de ECMAScript, uma LeftHandSideExpression é qualquer expressão que
se define como referência de propriedade (pense como qualquer coisa que possa
ir no lado esquerdo de um atributo). Em cada iteração, o nome da próxima
propriedade fica atribuído à avaliação da LeftHandSideExpression. É perfeitamente válido para a  LeftHandSideExpression transformar
diferentes referências em cada iteração. Ocasionalmente isso é útil, e até
mesmo elegante – por exemplo, obter uma matriz de property name agora é
um alívio:

1	var myObj = {a: 1, b: 2, c: 3}, myKeys = [], i=0;
2
3 for (myKeys[i++] in myObj);
4
5 myKeys; //['a','b','c'];

Quais propriedades sofrem iteração?

Isso requer algum
conhecimento das propriedades internas do JavaScript. Objetos são coleções de
propriedades, e cada propriedade tem seus próprios padrões de propriedades
internas. (Podemos pensar nelas como propriedades abstratas – elas são usadas
pelo engine do JavaScript, mas não são diretamente acessíveis ao usuário.
ECMAScript usa o formato [[property]] para denotar
propriedades internas).

Uma dessas propriedades é [[Enumerable]]. 
O comando for-in fará a iteração para todas as propriedades para
as quais o valor de [[Enumerable]] for
verdadeiro. Isso inclui  propriedades
enumeráveis herdadas via prototype
chain. Propriedades com um valor [[Enumerable]]
falso, assim como propriedades shadowed (propriedades que são sobrepostas por
propriedades de mesmo nome de objetos descendentes), não sofrerão iteração.

Na prática, isso significa que,
por padrão, os loops for-in pegarão qualquer propriedade non-shadowed
definida pelo usuário (incluindo propriedades herdadas), mas não
propriedades internas. Por exemplo, funções internas Object (como toString)
não serão enumeradas.

Isso também significa
que se você tem o hábito de aumentar os prototypes dos objetos internos,
então suas extensões customizadas também aparecerão:

1	var arr = ['a','b','c'], indexes = [];
2 Array.prototype.each = function() {/*blah*/};
3
4 for (var index in arr) {
5 indexes.push(index);
6 }
7
8 indexes; //["0", "1", "2", "each"] whoops!

Alguns frameworks (por
exemplo, Prototype.js e Mootools) adicionam muitos prototypes
customizados, e o uso de for-in para iteração de Arrays
e Strings é geralmente considerado uma má ideia. O uso de um loop for comum é uma boa alternativa para iteração
de Arrays e Strings. Adicionalmente, ES5 define um grupo de iteradores
customizados (forEach, map
etc). Infelizmente, essas estratégias de iteração alternativa não funcionam com
o Objects comum, e esta é a razão porque é considerada uma prática muito
ruim aumentar o Object.prototype.

O bug “DontEnum”

As versões do IE abaixo da versão
9 sofrem uma enumeração peculiar, de modo que não possuem a propriedade de
shadow na forma nativa, (e consequentemente não enumeráveis ou [[DontEnum]] na linguagem ES3), que também
não serão enumeradas.

01	var obj = {
02 a: 2,
03 //shadow a non-enumerable
04 toString: "I'm an obj"
05 },
06 result = [];
07
08 for (result[result.length] in obj);
09
10 result.toString();
11 //IE -> "a"
12 //Other browsers -> "a,toString"

(Agradecimentos para @kangax pela lembrança e para @skilldrick
pela elegante variação em for (result[i++] in obj);

Posso impedir a
iteração de certas propriedades?

Sim. Há algumas técnicas
padrão para filtragem de elementos indesejáveis de nossos loops for-in:

1. Object.prototype.hasOwnProperty

Esta função ativará o método
interno property [[GetOwnProperty]] para
verificar se determinada propriedade é definida diretamente sobre o objeto (ao
invés de em alguma parte da prototype chain).

01	var arr = ['a','b','c'], indexes = [];
02 Array.prototype.each = function() {/*blah*/};
03
04 for (var index in arr) {
05 if (arr.hasOwnProperty(index)) {
06 indexes.push(index);
07 }
08 }
09
10 indexes; //["0", "1", "2"]

JSLint espera que você sempre
encapsule o corpo de um for-in com um comando if, mesmo fazendo a
iteração de um objeto comum (não se preocupe com o fato de que você poderia
facilmente conseguir essa condição com um &&, em vez de um if).

Se você está paranóico com a
possibilidade de que você mesmo ou alguém possa sobrescrever a definição local do hasOwnProperty, você pode recorrer à referência prototype
diretamente.

1	//snip...
2 for (var index in arr) {
3 if (Object.prototype.hasOwnProperty.call(arr, index)) {
4 indexes.push(index);
5 }
6 }

2. Object.defineProperty

ES5 introduz um novo método n o Object que permite que propriedades sejam
definidas com definições property internas customizadas (não suportadas em
FF<4 e IE<9).

1	var obj = {};
2
3 Object.defineProperty( obj, "value", {
4 value: true,
5 writable: false,
6 enumerable: true,
7 configurable: true
8 });

Podemos nivelar isso para inserirmos nosso próprio
valor para [[Enumerable]], permitindo-nos esconder aumentos customizados de
prototype do iterador for-in.

01	var arr = ['a','b','c'], indexes = [];
02 Object.defineProperty(Array.prototype, "each", {
03 value: function() {/*blah*/},
04 writable: false,
05 enumerable: false,
06 configurable: false
07 });
08
09 for (var index in arr) {
10 indexes.push(index);
11 }
12
13 indexes; //["0", "1", "2"]

O que é a sequência de iteração?

O padrão ECMA não
especifica uma ordem de enumeração, mas o padrão de fato para objetos non-array
é enumerar propriedades de acordo com a ordem de sua designação original.

01	var obj = {a: 1, b: 2, c: 3}, result = [];
02
03 obj.e; //referenced but not assigned
04 obj.f = 'bar'; //1st assignment
05 obj.e = 4;
06 obj.dd = 5;
07 obj.f = 'foo'; //2nd assignment
08
09 for (var prop in obj) {
10 result.push(prop);
11 }
12
13 result.toString(); //"a,b,c,f,e,dd"

Contudo, há atualmente algumas exceções importantes sobre as quais você deve estar alerta:

Apagando propriedades no
IE

A deleção de property em IE e sua redefinição não atualiza sua posição na sequência de
iteração. Isso contrasta com o comportamento observado em todos os outros
principais browsers:

01	var obj = {a: 1, b: 2, c: 3}, result = [];
02
03 delete obj.b;
04 obj.b = 4;
05
06 for (var prop in obj) {
07 result.push(prop);
08 }
09
10 result.toString();
11 //IE ->"a,b,c"
12 //Other browsers -> "a,c,b"

Propriedades numericamente
nomeadas no
Chrome

Navegadores Chrome processam primeiro chaves numericamente nomeadas, e em sequência
numérica, não em sequência de inserção.

1	var obj = {3:'a', 2:'b', 'foo':'c', 1:'d'}, result = [];
2
3 for (var prop in obj) {
4 result.push(prop);
5 }
6
7 result.toString();
8 //Chrome -&gt; "1,2,3,foo"
9 //Other browsers -&gt; "3,2,foo,1"

Há um bug registrado para isso, e existem comentários com argumentos pró e contra para
que ele seja consertado. Para mim, é um bug que precisa de conserto.
Propriedades fixas de objetos comuns são desordenadas por definição, e sim,
ECMA ainda não foi definida como padrão – mas como John Resig e Charles
Kendrick
ressaltam, a falta de um padrão ECMA não é desculpa – padrões
geralmente seguem implementações e não vice-versa – e, nesse caso, o Chrome está
fora da linha.

O in operator

Este primo elegante
do for-in usa o método interno [[HasProperty]] para checar a existência
de uma propriedade nomeada em um dado
objeto:

propertyNameExpression in objectExpression

Em termos de pseudo código, isso funciona mais ou
menos assim:

1	var name = //resolve [propertyNameExpression];
2 var obj = //resolve [objectExpression];
3
4 return obj.[[HasProperty]](name);

Note como ‘c’ in obj retorna como verdadeiro, ainda que o
valor de o.c seja undefined.
O método interno [[HasProperty]] retornará verdadeiro para qualquer propriedade atribuída para qualquer
valor. Isso é útil para distinguir essas propriedades que são deliberadamente
definidas como undefined de outras que
simplesmente não existem.

Como
no loop for-in
, o
operador in buscará no prototype chain do objeto.
Diferentemente do loop for-in, o operador in não distingue propriedades enumeráveis das não
enumeráveis.

01	var obj = {a:1, b:2, c:undefined, d:4}, aa = {};
02
03 'b' in obj; //true
04 'c' in obj; //true ('undefined' but still exists)
05 'e' in obj; //false (does not exist)
06
07 delete obj.c;
08 'c' in obj; //false (no longer exists)
09
10 obj.e;
11 'e' in obj; //false (referenced but not assigned)
12
13 //resolving expressions
14 aa.o = obj;
15 aa.a = 'a';
16 aa.a in aa.o; //true

E isso é tudo. Sinta-se à vontade para comentar com
sugestões, omissões e queixas.