[Bizarrices do JavaScript] É possível que uma variável seja comparada com 3 valores diferentes e dê true? Ex: (a == 1 && a == 2 && a == 3)
Sim, é possível.
Mas antes, fica o aviso. Nenhum dos códigos abaixo deveria ser usado em aplicações sérias. Estão mais para curiosidades, que servem para mostrar os meandros e limites da linguagem.
Então vamos às bizarrices:
Objetos
var a = {
i: 1,
toString: function() { return this.i++; }
}
console.log(a == 1 && a == 2 && a == 3); // true
Isso funciona porque, ao comparar um objeto com um número, primeiro o objeto é convertido para número. E para isso, ele segue uma série de regras descritas na especificação da linguagem.
Só para resumir, ele verifica se o objeto possui o método @@toPrimitive
. Se não existir, procura o método valueOf
, e se não existir, procura por toString
. Então ele chama o primeiro que encontrar e o resultado é usado na comparação (se nenhum desses métodos for encontrado, o resultado da comparação é false
).
E no caso acima o método toString
sempre retorna um número diferente a cada invocação (pois ele retorna o valor e depois incrementa, graças ao operador ++
). Por isso na primeira comparação ele retorna 1
, na segunda 2
e na terceira 3
.
Lembrando que os métodos sempre são procurados nesta ordem: @@toPrimitive
, valueOf
e toString
. Ou seja, assim também funciona:
var a = {
i: 1,
valueOf: function() { return this.i++; }
}
console.log(a == 1 && a == 2 && a == 3); // true
E assim também (o @@toPrimitive
é mais chato porque precisamos usar o respectivo Symbol
):
var a = {
i: 1,
[Symbol.toPrimitive]: function() { return this.i++; }
}
console.log(a == 1 && a == 2 && a == 3); // true
Para ler a explicação completa sobre porque funciona, veja aqui.
Generalização
De forma geral, podemos criar qualquer mecanismo que retorne um valor diferente a cada invocação, como por exemplo, usando uma regex que retorne o próximo match de uma string contendo dígitos:
var a = {
r: /\d/g,
valueOf: function() {
// cada vez que exec é chamado, retorna o próximo dígito
return this.r.exec('123')[0];
}
};
if (a == 1 && a == 2 && a == 3) {
console.log("todos iguais");
}
Ou retornar números aleatórios (não vai dar certo sempre, claro, mas alguma hora acontecerá):
var a = {
valueOf: function () {
return Math.floor(Math.random() * 4);
}
};
// tenta mil vezes (se não der certo nenhuma, é muito azar)
for (var i = 1; i <= 1000; i++) {
if (a == 1 && a == 2 && a == 3) {
console.log(`Todos iguais, depois de ${i} tentativas`);
break;
}
}
Array
Ao comparar um array com um número usando o operador ==
, primeiro é feita a conversão do array para primitivo. E conforme explicado aqui (e também acima), para fazer esta conversão, ele procura pelos métodos @@toPrimitive
, valueOf
e toString
, nesta ordem, e chama o primeiro que for encontrado. Depois, o retorno deste método é usado na comparação.
No caso específico de arrays, podemos nos valer do fato de que toString()
chama internamente o método join
(conforme descrito na especificação da linguagem), então podemos alterá-lo assim:
var a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true
Ou seja, quando o array é comparado com um número, o método toString()
chama o join
, que por sua vez chamará shift
, que é um método que remove o primeiro elemento do array e o retorna. Assim, cada vez que a comparação é feita, ele retorna o próximo elemento do array.
Unicode (sempre ele)
Isso é meio que "trapaça", já que o nome de duas variáveis não é exatamente a
:
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if (aᅠ==1 && a== 2 &&ᅠa==3) {
console.log("todos iguais");
}
Note que a primeira e terceira variáveis possuem um espaçamento diferente (a primeira tem um espaço a mais depois do a
, a terceira tem antes). Este "espaço" na verdade é o caractere HALFWIDTH HANGUL FILLER (um caractere coreano). Apesar de ser renderizado como um espaço, ele não é reconhecido como um whitespace, pois pertence à categoria "Letter, Other".
Além disso, segundo a especificação da linguagem, identificadores podem começar com qualquer caractere Unicode que tenha a propriedade ID_Start
, e depois podem ter quaisquer caracteres que tenham a propriedade ID_Continue
, e o HALFWIDTH HANGUL FILLER se encaixa nesses critérios. Portanto, "a" seguido do HALFWIDTH HANGUL FILLER é um nome válido, e HALFWIDTH HANGUL FILLER seguido de "a" também.
Ou seja, neste caso estamos na verdade com três variáveis diferentes. Mas como o nome de duas delas possuem um caractere que é renderizado como espaço, fica parecendo que estamos comparando a mesma variável.
Além disso, também é possível (embora nada recomendável) usar os caracteres ZERO WIDTH JOINER e ZERO WIDTH NON-JOINER nos nomes de variáveis. Isso deixa mais obscuro ainda, já que eles não são renderizados (afinal, "zero width" indica que eles não têm largura):
var a= 1;
var a= 2; // depois do "a" tem um zero width joiner
var a= 3; // depois do "a" tem um zero width non-joiner
if (a==1 && a==2 && a==3) {
console.log("todos iguais");
}
Enfim, há muitos outros truques similares nesta pergunta do Stack Overflow. E mais uma vez, nenhum deles é recomendado em aplicações sérias. Estão mais para curiosidades, que servem para mostrar os limites da linguagem.
O conteúdo acima é baseado em minhas respostas (essa e essa, sendo que uma delas é uma adaptação do original em inglês), junto com algumas partes de outras respostas.