Executando verificação de segurança...
6
kht
4 min de leitura ·

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

Carregando publicação patrocinada...
1
0

Não achei bizarrice nenhuma.
Ele executa funções internas para comparar.
Você mexeu em sua função interna e fez 💩
Mais especificamente mexeu na função toString().
Foi só isso que ocorreu.

Ei! Toda vez que for converter para toString esta variável, soma + 1 nela, tá.

🤪

2

Bom, se eu visse um código desses em qualquer sistema sério, acharia bizarro. A menos, é claro, que houvesse uma justificativa muito boa (mas não consigo imaginar nenhuma).