Comparação de valores no Javascript: Entendendo features que parecem bugs

Neste update resolvi falar um pouco sobre os métodos de comparação no Javascript, este pode parecer um assunto trivial para os mais habituados com a linguagem, mas o intuito aqui é ir além do óbvio, meu objetivo é ir a fundo no funcionamento destes operadores, pois mesmo que o nome seja muito sugestivo, alguns deles fazem mais do que apenas comparar valores.
No Javascript existem dois tipos principais de operadores de comparação, o operador de comparação estrita "===" e comparação abstrata "==", ao utilizá-los é como se estivéssemos fazendo uma pergunta, "este primeiro valor é igual ao segundo?". Há dois valores que podem ser retornados na operação, true ou false, respectivamente verdadeiro e falso. O mesmo vale para os operadores de desigualdade, mas eles só retornarão verdadeiro se os valores não forem iguais.
Durante o processo de comparação abstrata primeiro o algoritmo verifica se os operandos são do mesmo tipo, se forem de tipos diferentes esses valores passaram por uma coerção de tipos, que é o processo onde ambos os valores serão convertidos para o mesmo tipo.
Tabela de igualdade abstrata

Fonte: https://dorey.github.io/JavaScript-Equality-Table
O operador de comparação estrita não executa nenhum tipo de conversão implícita e o resultado será verdadeiro apenas se ambos os valores forem iguais e do mesmo tipo.
Tabela de igualdade estrita

Fonte: https://dorey.github.io/JavaScript-Equality-Table
É preciso sempre se atentar antes de decidir qual operador será utilizado pois a coerção de tipos em muitos casos pode ser difícil de entender, o que pode acabar causando inconsistências, por isso geralmente o recomendado é evitá-la. Inclusive muitos linters irão alertá-lo quando tais operadores forem utilizados para evitar casos onde o resultado pode ser um pouco confuso.
'1' == true; // true
'' == 0 // true
O exemplo acima é um caso claro onde a coerção de tipos pode trazer resultados estranhos, isso acontece pois qualquer número maior que zero é considerado verdadeiro, e no segundo caso, tanto a string vazia quanto zero são valores considerados falsos.
'string' === 'string'; // true
'string' == 'string; // true
23 === 23; // true
23 === '23'; // false
23 == 23; // true
23 == '23'; // true
// Negando as expressões
'string' !== 'string'; // false
'string' != 'string; // false
23 !== 23; // false
23 !== '23'; // false
23 != 23; // false
23 != '23'; // false
A maioria dos exemplos utilizados são comparações básicas que podem ocorrer no dia a dia de qualquer programador, mas e se compararmos uma string e um array com apenas uma posição? Este é um exemplo da coerção de tipos que o operador == aplica, o resultado da operação 'string' == ['string']
pode parecer inesperado, mas isso ocorre pois o array é convertido em uma String, e como o resultado da coerção é um texto exatamente igual ao valor na direita, o resultado da operação será true.
'string' === ['string']; // false
'string' == ['string']; // true
Existem ainda outras peculiaridades que podem ocorrer, e que constantemente causam confusão entre programadores, é muito comum ouvi-los dizendo que Javascript é uma linguagem ruim simplesmente porque ela não funciona da forma como eles queriam que fosse.
[] === []; // false
[] == []; // false
{} === {}; // false
{} == {}; // false
O fato é que nenhuma linguagem será boa o suficiente se você não conhecê-la a fundo, à primeira vista pode parecer estranho que a comparação de operandos que parecem ser iguais tenham um resultado negativo, mas acredite, existe uma explicação completamente racional por trás dessas features que parecem bugs.
No primeiro caso a operação retornará false porque ao utilizar [] um novo array será gerado, e a comparação feita não levará em conta o seu valor, mas as referências em memória de cada vetor, que não serão iguais por serem instâncias diferentes. O segundo exemplo segue a mesma lógica, pois {} também irá criar uma nova instância de objeto.
NaN === NaN; // false
NaN == NaN; // false
De acordo com a especificação ECMA-262, a comparação entre operandos NaN sempre retornará false, isso é um comportamento esperado pois ao compararmos os resultados de dois cálculos distintos, espera-se que os resultados sejam considerados iguais exclusivamente se ambas resultarem no mesmo valor, como NaN indica apenas que houve um erro no cálculo, ou uma operação ilegal foi encontrada. Não é possível afirmar que os dois valores são iguais, pois o valor NaN não informa a causa do erro.
'string' / 1; // NaN
null / 0; // NaN
undefined / -1; NaN
Existem duas formas de verificar se um valor é igual a NaN, mas é preciso se atentar às peculiaridades de cada uma. A primeira forma é utilizando o método global isNaN(), este método deve ser utilizado com cuidado pois realizará a coerção para o tipo Number, portanto qualquer valor não pertencente ao conjunto dos números reais passado como argumento fará o método retornar true, isNaN(null) e isNaN(undefined) são casos onde isso ocorre.
A segunda forma possível, é utilizando o método isNaN presente na classe Number, Number.isNaN(), esta normalmente é a opção mais recomendada pois o retorno é mais consistente e apenas será verdadeiro caso o parâmetro recebido seja NaN.
Uma adição mais recente à especificação do EcmaScript é o método Object.is(), que é bastante parecido com o operador === pois também não aplica nenhuma conversão nos operandos, porém, a operação Object.is(NaN, NaN)
será verdadeira.