TL;DR

Suas reclamações sobre x == y serem estranhos, com bugs ou totalmente quebrados foram todos culpados pela == como o culpado. Não, na verdade não é. == é bastante útil, na verdade.

Os problemas que o senhor está tendo não são com o == em si, mas sim com os valores subjacentes e como eles são coerentes com os diferentes tipos, especialmente nos casos de cantos estranhos.

Em vez de resolver seus problemas evitando == (e sempre usando ===), devemos concentrar nossos esforços em evitar – ou corrigir! – as coerções de valor dos casos extremos. De fato, é delas que vêm todos os WTFs.

Quick Jump:




Esta postagem anuncia o lançamento do último livro do meu O senhor não conhece JS série de livros, YDKJS: Tipos e gramática, que pode ser lido gratuitamente on-line!

Tipos & Gramática inclui um Prefácio por nosso próprio e incrível David Walshe também está disponível para para compra através da O’Reilly e outros vendedores, como Amazon. Se o senhor gostou de alguma parte deste post, confira Tipos e gramática para obter uma cobertura muito mais detalhada dos tipos, coerção e regras gramaticais do JS.


Aviso: Aqui reside uma polêmica, e muito longo, uma discussão que provavelmente o incomodará. Este post defende e endossa o tão odiado JavaScript coerção mecanismo. Tudo o que o senhor já ouviu ou sentiu sobre o que há de errado com a coerção será desafiado pelo que vou expor aqui. Certifique-se de reservar bastante tempo para ler este artigo.

A coerção já não está morta?

Por que diabos estou falando sobre – e muito menos defendendo e endossando – um mecanismo que foi tão universalmente criticado como sendo horrível, maligno, mágico, cheio de bugs e com um design de linguagem ruim? O barco já não partiu há muito tempo? Será que todos nós já não seguimos em frente e deixamos a coerção para trás? Se Crockford diz que é ruim, então deve ser.

Hummm… não. Pelo contrário, acho que a coerção nunca teve uma chance justa, porque nunca foi falada ou ensinada corretamente. Não é de surpreender que o senhor odeie a coerção quando tudo o que já viu sobre ela é a maneira completamente errada de entendê-la.

Para o bem ou para o mal, quase toda a razão para o Tipos & Gramática bem como o livro muitas de minhas palestras em conferênciasé justamente para apresentar esse caso.

Mas talvez eu esteja perdendo meu tempo tentando convencer o senhor. Talvez o senhor nunca mude de ideia.

De fato, o Sr. Crockford tem algo a dizer diretamente sobre esse ponto:




Douglas Crockford – Eles se foram? “The Better Parts” (As melhores partes), Nordic.js 2014




E a razão pela qual essas coisas levam uma geração é porque, em última análise, não mudamos a mente das pessoas. Temos de esperar que a geração anterior se aposente ou morra para que possamos obter massa crítica para a próxima ideia. Então, é como se olhássemos ao redor: “Eles já se foram?”

–Douglas Crockford

Então, ele está certo? Mais especificamente, a coerção poderia ser “a próxima ideia” que simplesmente não se cansou da antiga geração de tipagem estática? morrer para que o senhor faça um exame justo e objetivo?

Acho que talvez sim.

Essencialmente, há anos venho observando os opositores da coerção e perguntando: “Eles já se foram?”

Uma Verdade Inconveniente

“Faça o que eu digo, não o que eu faço”.

Seus pais lhe disseram isso quando o senhor era criança, e isso o irritou na época, não foi? Aposto que o senhor ficaria irritado hoje se alguém em nossa profissão tivesse essa postura.

Portanto, quando o senhor ouve Douglas Crockford falar negativamente sobre coerção, certamente presume que ele também evita usá-la em seu próprio código. Certo? Hummm… como posso dizer isso? Como posso explicar isso ao senhor?

Crockford usa coerções. Pronto, eu disse isso. O senhor não acredita em mim?

// L292 - 294 of json2.js
for (i = 0; i < length; i += 1) {
    partial[i] = str(i, value) || 'null';
}


json2.js, L293

O senhor vê a coerção? str(..) || 'null'. Como isso funciona?

Para o || o primeiro operando (str(..)) é implicitamente coagido a boolean se já não o for, e que o senhor true / false é então usado para a seleção do primeiro operando (str(..)) ou o segundo ('null'). Leia mais sobre como || e && trabalho e os usos idiomáticos comuns desses operadores.

Nesse caso, definitivamente não se espera que o primeiro operando seja um boolean, pois ele documenta anteriormente o str(..) desta forma:

function str(key, holder) {

// Produce a string from holder[key].

..


Portanto, seu próprio código está se baseando em um implícito coerção aqui. Exatamente o que ele passou uma década nos ensinando é ruim. E por quê? Por que ele usa isso?

Mais importante ainda, por que o o senhor o senhor usa essas expressões idiomáticas? Porque eu sei que o senhor usa. A maioria dos desenvolvedores de JS usa isso || para definir um valor padrão para uma variável. É muito útil.

Em vez disso, ele poderia ter escrito esse código da seguinte forma:

tmp = str(i, value);
partial[i] = (tmp !== '') ? tmp : 'null';


Isso evita totalmente a coerção. O !== (na verdade, todos os operadores de igualdade, incluindo == e !=) sempre retorna um boolean da verificação de igualdade. O ? : primeiro verifica o primeiro operando e, em seguida, escolhe o segundo (tmp) ou o terceiro ('null'). Não há coerção.

Então, por que o senhor não faz isso?

Porque o str(..) || 'null' é comum, mais curto/simples de escrever (sem necessidade de uma variável temporária), o tmp) e, em geral, é fácil de entender, certamente em comparação com a forma sem coerção.

Em outras palavras, a coerção, especialmente implícita coerção, tem usos em que realmente melhora a legibilidade do nosso código.

OK, então essa é apenas uma exceção isolada que ele fez, certo? Não é bem assim.

Nesse único arquivo “json2.js”, aqui está uma lista (não necessariamente completa) de lugares que Crockford usa explícito ou implícito coerções: L234, L275, L293, L301-302, L310, L316-317, L328-329, L340-341, L391, L422e L442.

Oh, espere. Essa é apenas a antiga biblioteca “json2.js”. Isso é injusto, certo? Que tal sua própria biblioteca JSLint, que ele ainda mantém (EDIT: ele está lançando em breve um atualização para ES6): L671, L675, L713, L724, L782, … O senhor entendeu o ponto, certo?

Doug Crockford usa coerção para tornar seu código mais legível. Eu o aplaudo por isso.

Ignore o que ele diz sobre a coerção ser má ou ruim. Ela é útil, e ele prova isso com seu código, independentemente dos slides que ele coloca em suas palestras em conferências.

Mas… == O mal é o senhor?

OK, o senhor está certo, não há uma única instância de == em seu código. E sempre que ele ridiculariza a coerção, é quase certo que esteja falando sobre == especificamente.

Então, será que estou sendo injusto ao destacar um monte de não== coerções? Na verdade, eu diria que ele é que está sendo injusto, ao destacar constantemente equiparando == com coerção (trocadilho intencional, é claro!). Ele não está sozinho. Eu diria que quase todos os desenvolvedores de JS fazem o mesmo. Quando ouvem “coerção”, inevitavelmente invocam ==.

A coerção é um mecanismo que pode funcionar quando == é usado, e impedido de ser usado quando o === é usado. Mas essa constatação deve deixar claro que o == e a coerção são preocupações ortogonais. Em outras palavras, o senhor pode ter reclamações sobre == que são separadas das reclamações sobre a coerção em si.

Não estou apenas tentando fazer picuinhas aqui. Isso é muito importante para entender o restante desta postagem: temos que considerar a coerção separadamente da consideração do ==. Chamada == “coerção da igualdade” se o senhor quiser, mas não a confunda com a coerção em si.

De modo geral, acho que quase todas as reclamações feitas contra o == são, na verdade, problemas de coerção, que abordaremos mais adiante. Também voltaremos a falar sobre o ==e analisá-lo um pouco mais. Continue lendo!

O senhor precisa de coerção?

Coerção é o que acontece no JavaScript quando você precisa ir de um tipo (como string) para outro (como boolean). No entanto, isso não é exclusivo do JS. Toda linguagem de programação tem valores de diferentes tipose a maioria dos programas exige que o senhor converta de um para o outro. Em linguagens estaticamente tipadas (com imposição de tipos), a conversão é geralmente chamada de “casting” e é explícita. Mas, mesmo assim, a conversão acontece.

A coerção do JavaScript pode ser intencional e explícita, ou pode acontecer implicitamente como um efeito colateral.

Mas não há praticamente nenhum programa JS não trivial por aí que não dependa, em algum momento ou outro, de alguma forma de coerção. Quando as pessoas odeiam a coerção, geralmente estão odiando o implícita coerção, mas explícita a coerção é geralmente vista como OK.

var x = 42;

var y = x + "";     // implicit coercion!
y;                  // "42"

var z = String(x);  // explicit coercion!
z;                  // "42"


Mesmo para aqueles que são publicamente contra o implícita coerção, por alguma razão eles geralmente são muito bem com a x + "" formulário aqui. Sinceramente, não entendo por que o senhor este implícito coerção é aceitável e muitas outras não são.

O senhor pode se concentrar em decidir se prefere explícito ou implícito mas o senhor não pode argumentar de forma razoável que a maioria dos programas JS pode ser escrita sem nenhuma coerção.

Muitos desenvolvedores dizem que não deveríamos ter coerção, mas quase nunca se dão ao trabalho de pensar em todos os casos extremos que isso apresentaria. O senhor não pode simplesmente dizer que o mecanismo não deve existir sem ter uma resposta para o que deve fazer em vez disso.

Este artigo, de certa forma, é um exercício nessa busca, para examinar o quão sensata é essa posição. Dica: não muito.

Por que a coerção?

O caso da coerção é muito mais amplo do que o que vou expor aqui. Confira Capítulo 4 de Tipos & Gramática para um muito mais detalhesmas deixe-me tentar desenvolver brevemente o que vimos anteriormente.

Além do x || y (e x && y), que podem ser bastante úteis para expressar a lógica de uma forma mais simples do que o x ? x : y há outros casos em que a coerção, mesmo implícita coerção, é útil para melhorar a legibilidade e a compreensão do nosso código.

// no coercion
if (x === 3 || x === "3") {
    // do something
}

// explicit coercion
if (Number(x) == 3) {
    // do something
}

// implicit coercion
if (x == 3) {
    // do something
}


A primeira forma da condicional evita totalmente a coerção. Mas ela também é mais longa e mais “complicada”, e eu diria que introduz detalhes extras aqui que podem muito bem ser desnecessários.

Se a intenção deste código for fazer algo se x for o três independentemente de estar em seu string ou na forma de number precisamos realmente conhecer esse detalhe e pensar sobre ele aqui? Depende um pouco.

Muitas vezes, não. Muitas vezes, esse fato será um detalhe de implementação que foi abstraído na forma como o x foi definido (a partir de um elemento de formulário de página da Web, ou uma resposta JSON, ou …). Devemos deixá-lo abstraído e usar alguma coerção para simplificar esse código, mantendo essa abstração.

Então, será que o Number(x) == 3 é melhor ou pior do que o x == 3? Nesse caso muito limitado, eu diria que é uma incógnita. Eu não discutiria com aqueles que preferem o explícito sobre o implícito. Mas eu até gosto do implícito formulário aqui.

Aqui está outro exemplo de que gosto ainda mais:

// no coercion
if (x === undefined || x === null) {
    // do something
}

// implicit coercion
if (x == null) {
    // do something
}


O implícita funciona aqui porque a especificação diz que null e undefined são coercitivamente iguais entre si, e a nenhum outro valor na linguagem. Ou seja, é perfeitamente seguro tratar o undefined e null como indistinguíveis, e de fato eu recomendo enfaticamente que o que.

O x == null é completamente seguro em relação a qualquer outro valor que possa estar no x coagindo a null, garantido pela especificação. Então, por que não usar a forma mais curta para que possamos abstrair esse detalhe estranho de implementação de ambos os undefined e do null valores vazios?

Usando === impede que o senhor tire proveito de todos os benefícios da coerção. E o senhor foi informado de que essa é a resposta para todos os problemas de coerção, certo?

Aqui está um segredo sujo: o <, <=, > e >= operadores de comparação, bem como o +, -, *, e / operadores matemáticos, não têm como desativar a coerção. Portanto, basta usar o === não corrige nem remotamente o todos os problemas do senhor, mas elimina as instâncias realmente úteis da igualdade coercitiva == ferramenta.

Se o senhor odeia a coerção, ainda tem que lidar com todos os lugares onde a === não pode ajudar o senhor. Ou o senhor pode aceitar e aprender a usar a coerção a seu favor, de modo que == o ajude em vez de lhe dar problemas.

Este post tem muito mais a acrescentar, por isso não vou me alongar mais sobre o caso para coerção e ==. Novamente, Capítulo 4, Tipos & Gramática aborda o assunto com muito mais detalhes, caso o senhor tenha interesse.

Uma história de dois valores

Acabei de exaltar por que a coerção é tão boa. Mas todos nós sabemos que a coerção tem algumas partes feias – não há como negar isso. Vamos à dor, que é realmente o objetivo deste artigo.

Vou fazer uma afirmação talvez duvidosa: a raiz da maior parte do mal na coerção é Number("") resultando em 0.

O senhor pode se surpreender ao ver quantos outros casos de coerção se resumem a esse. Sim, sim, há outros também. Já chegaremos lá.

Eu disse isso antes, mas vale a pena repetir: todas as linguagens precisam lidar com conversões de tipos e, portanto, todas as linguagens precisam lidar com casos de canto que produzem resultados estranhos. Cada um deles.

// C
char s[] = "";
int num = atoi(s);
printf("%d",num);                   // 0

// Java
String s = "";
Integer num = Integer.valueOf(s);
System.out.println(num);            // java.lang.NumberFormatException


C opta por converter "" para 0. Mas o Java reclama e lança uma exceção. O JavaScript claramente não é o único atormentado por essa questão.

Para o bem ou para o mal, o JavaScript teve que tomar decisões para todos esses tipos de casos e, francamente, algumas dessas decisões são as reais fonte de nossos problemas atuais.

Mas nessas decisões havia uma filosofia de design inegável – e, na minha opinião, admirável. Pelo menos nos primeiros dias, o JS optou por se afastar da filosofia “vamos simplesmente lançar uma exceção toda vez que o senhor fizer algo estranho”, que vem de linguagens como Java. Essa é a mentalidade “garbage in, garbage out”.

Simplificando, o JS tenta adivinhar da melhor forma possível o que o senhor pediu que ele fizesse. Ele só lança um erro nos casos extremos em que não consegue chegar a um comportamento razoável. E muitas outras linguagens escolheram caminhos semelhantes. O JS é mais como “entra lixo, sai material reciclado”.

Portanto, quando JS estava considerando o que fazer com strings como "", " ", e "\n\n" quando solicitado a coagi-los a um número, ele escolheu aproximadamente: cortar todos os espaços em branco; se apenas "" restar, retorne 0. O JS não lança exceções em todos os lugares, e é por isso que hoje a maioria dos códigos JS não precisa de try..catch em quase todas as declarações. Acho que essa foi uma boa direção. Esse pode ser o principal motivo pelo qual gosto do JS.

Então, vamos considerar: é razoável que o "" se torne 0? A resposta do senhor é diferente para " " ou para "\n\n"? Se sim, por que, exatamente? É estranho que ambos os "" e "0" coagir o senhor a fazer o mesmo 0 número? O senhor acha que isso é suspeito? Parece suspeito para mim.

Permita-me fazer a pergunta inversa: seria razoável que o String(0) produzir ""? Claro que não, esperamos claramente que o "0" lá. Hmmm.

Mas quais são os outros comportamentos possíveis? Deveria Number("") lançar uma exceção (como em Java)? Não. Isso viola de forma intolerável a filosofia do projeto. O único outro comportamento sensato que consigo conceber é que ele retorne NaN.

NaN não deve ser considerado como “não é um número”; mais precisamente, é o estado de número inválido. Normalmente, o senhor obtém NaN ao realizar uma operação matemática sem que o(s) valor(es) necessário(s) seja(m) números (ou semelhantes a números), como 42 / "abc". O raciocínio simétrico da coerção se encaixa perfeitamente: qualquer coisa que o senhor tente coagir a um número que não seja claramente uma representação de número válido deve resultar em um número inválido NaN-de fato Number("I like maths") produz NaN.

Acredito firmemente que o Number("") deve ter resultado em NaN.

Coerção "" para NaN?

E se pudéssemos mudar apenas um aspecto do JavaScript?

Uma das igualdades coercitivas comuns que causam estragos é a 0 == "" igualdade. E adivinhe? Isso vem diretamente do fato de que a == diz que, nesse caso, para o "" se torne um número (0 já é um), então ele termina como 0 == 0, que, obviamente, é true.

Portanto, se o "" em vez disso, o senhor for coagido ao NaN em vez do valor numérico do 0, a verificação de igualdade seria 0 == NaN, que é, obviamente, false (porque nada é igual a NaN, nem mesmo a si mesmo!).

Aqui, o senhor pode ver a base da minha tese geral: o problema com o 0 == "" não é a == em si – seu comportamento, pelo menos nesse caso, é bastante sensato. Não, o problema é com o Number("") coerção em si. Usando === para evitar esses casos é como colocar um band-aid na testa para tratar a dor de cabeça.

O senhor está apenas tratando o sintoma (ainda que mal!), não corrigindo o problema. A coerção de valores é o problema. Portanto, conserte o problema. Sair == sozinho.

Louco, o senhor diz? Não há como consertar o Number("") produzindo 0. O senhor tem razão, seria aparecer não há como fazer isso, não sem quebrar milhões de programas JavaScript. Tenho uma ideia, mas voltaremos a ela mais tarde. Temos muito mais a explorar para entender meu ponto mais amplo.

Array To String

E quanto ao 0 == []? Esse parece estranho, certo? Esses valores são claramente diferentes. E mesmo que o senhor estivesse pensando em verdade/falsidade aqui, [] deveria ser verdadeiro e 0 deve ser falso. Então, WTF?

O == diz que se ambos os operandos forem objetos (objetos, matrizes, funções etc.), basta fazer uma comparação de referência. [] == [] sempre falha, pois são sempre duas referências de matriz diferentes. Mas se um dos operandos for não um objeto, mas sim um primitivo, == tenta fazer com que ambos os lados sejam primitivos e, de fato, primitivos do mesmo tipo.

Em outras palavras, == prefere comparar valores do mesmo tipo. Isso é bastante sensato, eu diria, porque comparar valores de tipos diferentes é um absurdo. Nós, desenvolvedores, também temos esse instinto, certo? Maçãs e laranjas e tudo o mais.

Portanto [] precisa se tornar um primitivo. [] torna-se um primitivo de string por padrão, pois não tem coerção padrão para número. Em que string ele se transforma? Aqui está outra coerção que eu diria que foi quebrada pelo design original: String([]) é "".

Por alguma razão, o comportamento padrão dos arrays é que eles se restringem apenas à representação de string de seu conteúdo. Se eles não tiverem conteúdo, isso deixa apenas o "". É claro que é mais complicado do que isso, pois o null e undefined, se presentes nos valores de uma matriz, também são representados como "" em vez do muito mais sensato "null" e "undefined" o que seria de se esperar.

Basta dizer que a stringificação de matrizes é muito estranha. O que eu preferiria? String([]) deveria ser "[]". E a propósito, String([1,2,3]) deveria ser "[1,2,3]"e não apenas "1,2,3" como o comportamento atual.

Então, de volta ao 0 == []. Isso se torna 0 == ""que já consideramos quebrado e precisando de um trabalho de correção. Se o String([]) ou Number("") (ou ambos!) foram corrigidos, a loucura que é a 0 == [] desapareceria. O mesmo aconteceria com o 0 == [0] e 0 == ["0"] e assim por diante.

Novamente: == não é o problema, mas sim a stringificação de matrizes. Corrija o problema, não o sintoma. Sair == sozinho.

Nota: A stringificação de objetos também é estranha. String({ a: 42 }) produz "[object Object]" estranhamente, quando o {a:42} faria muito mais sentido. Não vamos nos aprofundar mais nesse caso aqui, pois ele não é normalmente associado a problemas de coerção. Mas, mesmo assim, é um WTF.

Mais pegadinhas (que não são ==)

Se o senhor não entender o == etapas do algoritmoAcho que seria bom que o senhor as lesse algumas vezes para se familiarizar. Acho que o senhor ficará surpreso com a sensatez == é.

Um ponto importante é que o == só faz uma comparação de cadeias de caracteres se ambos os lados já forem cadeias de caracteres ou se tornarem cadeias de caracteres a partir de um objeto que coagula um primitivo. Portanto, o 42 == "42" pode parecer que é tratado como "42" == "42"mas, na verdade, ele é tratado como 42 == 42.

Assim como quando seu professor de matemática o repreendia por obter a resposta certa pelo motivo errado, o senhor não deve se contentar em prever acidentalmente == mas, em vez disso, certificar-se de entender o que ele realmente faz.

E quanto a muitos outros citados comumente == gotchas?



  • false == "": Não são muitos os senhores que vão reclamar deste. Os dois são falsos, portanto, é pelo menos um pouco sensato. Mas, na verdade, a falsidade deles é irrelevante. Ambos se tornam números, o 0 valor. Já demonstramos o que precisa ser mudado aqui.


  • false == []: O que o senhor acha? [] é verdade, como é possível que seja == false? Aqui, o senhor provavelmente está tentado a pensar [] deve ser coagido a um true / false, mas não é. Em vez disso, false torna-se um número (0 naturalmente), e então é 0 == [], e acabamos de ver esse caso na seção anterior.


    Devemos mudar Number(false) de 0 para NaN (e, simetricamente, Number(true) para NaN)? Certamente, se estivermos mudando Number("") para NaN, eu poderia defender esse caso. Especialmente porque podemos observar Number(undefined) é NaN, Number({}) é NaNe Number(function(){}) é NaN. A consistência pode ser mais importante aqui?


    Ou não. A forte tradição da linguagem C é que o false para 0e o inverso Boolean(0) claramente deve ser false. Acho que essa é uma incógnita.

    Mas de qualquer forma, false == [] seria corrigido se os outros problemas de stringificação de array ou de string numérica vazia mencionados anteriormente fossem corrigidos!

  • [] == ![]: Nuts! Como algo pode ser igual à negação de si mesmo?

    Infelizmente, essa é a pergunta errada. A ! acontece antes que o == seja sequer considerado. ! força a boolean (e inverte sua paridade), de modo que o ![] torna-se false. Portanto, este caso é apenas [] == false, que acabamos de abordar.

A raiz de tudo == Males

OK, espere. Vamos revisar por um momento.

Acabamos de passar por um monte de artigos comumente citados == WTFs. O senhor pode continuar procurando ainda mais == mas é bem provável que o senhor acabe voltando a um desses casos que acabamos de citar, ou a alguma variação deles.

Mas a única coisa que o tudo esses casos têm em comum é que se o Number("") fosse alterado para NaN, todos eles magicamente ser consertado. Tudo se resume ao 0 == ""!!

Opcionalmente, poderíamos também corrigir String([]) para "[]" e Number(false) para NaN, para garantir. Ou não. Poderíamos simplesmente corrigir o 0 == "". Sim, Estou dizendo que praticamente todas as frustrações em torno do == na verdade, decorrem desse caso isoladoe, além disso, não têm quase nada a ver com o == em si.

Respire fundo e deixe o senhor entender.

Adicionando às nossas frustrações

Eu realmente gostaria de poder terminar o artigo aqui. Mas não é tão simples assim. Sim, consertar Number("") corrige praticamente todos os == mas o == é apenas um dos muitos lugares em que as pessoas tropeçam na coerção em JS.

A próxima fonte mais comum de problemas de coerção ocorre quando se usa o + . Novamente, veremos que as reclamações geralmente são feitas contra o +mas, na realidade, a culpa é das coerções de valor subjacentes.

Algumas pessoas estão bastante incomodadas com a sobrecarga do + para ser tanto adição matemática quanto concatenação de strings. Para ser sincero, não amo nem odeio esse fato. Para mim, está ótimo, mas também não me importaria se tivéssemos um operador diferente. Infelizmente, não temos, e provavelmente nunca teremos.

Em poucas palavras, + faz a concatenação de strings se um dos operandos for uma string. Caso contrário, adição. Se + for usado com um ou ambos os operandos que não estejam em conformidade com essa regra, eles serão implicitamente coagido para corresponder ao tipo esperado (ou string ou number).

À primeira vista, parece que, se não for por outra razão que a consistência com o ==, que + deveria concatenar somente se ambos já fossem cadeias de caracteres (sem coerção). E, por extensão, o senhor poderia dizer que ele adiciona somente se ambos os operandos já forem números (sem coerção).

Mas mesmo se nós fizéssemos mudança + como essa, não resolveria os casos extremos de misturar dois tipos diferentes com +:

42 + "";    // "42" or 42?
41 + "1";   // "411" or 42?


O que deve + fazer aqui? Lançar um erro é tão Java. 1994 acabou de ser chamado.

É um acréscimo realmente mais preferível do que a concatenação aqui, ou vice-versa? Meu palpite é que a maioria das pessoas prefere a concatenação ("42") para a primeira operação, mas a adição (42) para a segunda. Entretanto, a inconsistência dessa posição é tola. A única posição sensata é que essas operações devem resultar em "42" e "411" (como atualmente) ou 42 e 42 (conforme a hipótese).

Na verdade, como argumentei anteriormente, se o primeiro + for adição, essa operação deverá resultar em NaNe não 42, como o "" deve se tornar NaN em vez de 0. O senhor ainda prefere NaN / 42 para "42" / "411"O senhor não tem certeza? Eu duvido.

Não acho que haja um comportamento melhor que possamos mudar + para.

Então, como podemos explicar + se não for o + a culpa é do operador? Exatamente como antes: coerções de valor!

Por exemplo:

null + 1;           // 1
undefined + 1;      // NaN


Antes de explicar, qual dos dois parece mais sensato? Eu diria sem reservas que a segunda é muito mais razoável do que a primeira. Nenhum dos dois null nem undefined são números (nem strings), portanto + não pode ser visto como uma operação válida com eles.

Nos dois casos acima + acima, nenhum dos operandos é uma cadeia de caracteres, portanto, ambos são adições numéricas. Além disso, vemos que Number(null) é 0 mas Number(undefined) é NaN. Devemos consertar um deles, para que sejam pelo menos consistentes, mas qual?

Acredito firmemente que devemos mudar Number(null) para ser NaN.

Outros WTFs de coerção

Já destacamos a maioria dos casos com os quais o senhor provavelmente se deparará na codificação diária de JS. Até mesmo nos aventuramos em alguns casos de nicho malucos que são citados popularmente, mas nos quais a maioria dos desenvolvedores raramente tropeça.

Mas, no interesse de uma abrangência exaustiva, compilei uma tabela enorme e malfeita com um monte de valores diferentes de casos extremos e todas as coerções implícitas e explícitas que o senhor pode usar. Pegue uma garrafa de álcool forte (ou seu mecanismo de enfrentamento favorito) e mergulhe de cabeça.


coercions-grid


Se o senhor estiver procurando um caso para criticar a coerção, ele (ou sua raiz) quase certamente será encontrado nessa lista. Há algumas outras surpresas escondidas nessa tabela, mas já cobrimos aquelas com as quais o senhor precisa se preocupar.

Podemos consertar?

Discorri longamente sobre por que a coerção é incrível e por que ela tem problemas. É importante lembrar que, do meu ponto de vista, as operadoras não são culpadas, embora recebam toda a atenção negativa.

A verdadeira culpa é de algumas das regras de coerção de valor. Na verdade, a lista de problemas raiz é bastante curta. Se os corrigirmos, eles se transformarão em cascata para corrigir uma série de outros problemas não relacionados à raiz que atrapalham os desenvolvedores.

Vamos recapitular as coerções de valor do problema raiz com as quais estamos preocupados:



  • Number("") é 0

    Deveria ser: NaN (corrige a maioria dos problemas!)


  • String([]) é "", String([null]) é "", String([undefined]) é ""

    Deveria ser: "[]", "[null]", "[undefined]"


  • Number(false) é 0, Number(true) é 1

    Deveria ser (opcional/debatível): NaN, NaN


  • Number(null) é 0

    Deveria ser: NaN



OK, então o que podemos fazer para corrigir esses problemas (coerções de valor) em vez de tratar os sintomas (operadores)?

Admito que não há uma bala mágica que eu possa usar. Não há nenhum truque (bem… nós poderia monkey-patch Array.prototype.toString() para corrigir esses casos). Não há nenhum insight profundo.

Não, para corrigir isso, teremos que usar força bruta.

Propor ao TC39 uma alteração direta em qualquer um desses itens fracassaria na primeira etapa. Há literalmente zero chance de esse tipo de proposta ser bem-sucedida. Mas há outra maneira de introduzir essas alterações, e ela pode, apenas pode, ter uma pequena fração de uma chance percentual. Provavelmente zero, mas talvez seja como 1e-9.

"use proper";

Esta é a minha ideia. Vamos introduzir um novo modo, ativado pela função "use proper"; (simétrico ao "use strict", "use asm", etc.), o que altera essas coerções de valor para suas próprias comportamento.

Por exemplo:

function foo(x) {
    "use proper";

    return x == 0;
}

foo("");    // false
foo([]);    // false
foo(false); // false

foo("0");   // true


O senhor percebe por que isso é diferente – e eu estou argumentando, melhor – do que ===? Porque ainda podemos usar o == para coerções seguras como "0" == 0, que a grande maioria de nós diria que ainda é um comportamento sensato.

Além disso, todas essas correções estariam em vigor:

"use proper";

Number("");             // NaN
Number("  ");           // NaN
Number("\n\n");         // NaN
Number(true);           // NaN
Number(false);          // NaN
Number(null);           // NaN
Number([]);             // NaN

String([]);             // "[]"
String([null]);         // "[null]"
String([undefined]);    // "[undefined]"

0 == false;             // false
1 == true;              // false
-1 < "";                // false

1 * "";                 // NaN
1 + null;               // NaN


O senhor ainda pode usar o === para desativar totalmente todas as coerções, mas o "use proper" garantiria que todas essas coerções de valor incômodas que têm atormentado o senhor == e + são fixas, portanto o senhor pode usar o == sem toda essa preocupação!

E agora?

A proposta teórica que acabei de fazer, que provavelmente tem quase nenhuma chance de ser adotada, mesmo que eu fizesse formalmente proposto, não parece que o senhor tenha muito a tirar de toda essa leitura. Mas, se um número suficiente de senhores se apegar às ideias aqui expostas e ajudar a criar um impulso, isso pode ter uma chance remota.

Mas deixe-me sugerir algumas outras possibilidades, além da trilha de padrões, para o senhor analisar:

  1. "use proper" poderia se tornar uma nova linguagem transpile-to-JavaScript (“ProperScript”, “CoercionScript”, etc.), no mesmo espírito que o TypeScript, Dardo, SoundScript, etc. Poderia ser uma ferramenta que transforma o código ao envolver todas as operações de valor em verificações de tempo de execução que aplicam as novas regras. Poderíamos reduzir bastante o impacto óbvio sobre o desempenho especificando anotações (novamente, no estilo TypeScript) que indiquem à ferramenta quais operações ela deve envolver.
  2. Poderíamos pegar esses conjuntos de regras de coerção de novos valores desejados e transformá-los em asserções para um processo de compilação que faz verificações simuladas em tempo de execução (com dados de teste) para “lintar” seu código, em um espírito semelhante ao do RestrictMode um dos meus projetos favoritos. Essa ferramenta emitiria avisos se detectasse locais em seu código que esperam resultados de coerção que não se mantêm.

Conscientização

Por fim, deixe-me dizer que, mesmo que nenhuma dessas propostas venha a se concretizar, acredito que ainda há valor a ser extraído deste artigo. Ao saber exatamente o que está dando errado em sua == e + ou seja, os casos de canto de coerção de valor em si, o senhor agora está capacitado a escrever um código melhor e mais robusto que lida (ou pelo menos evita) esses casos de forma robusta.

Acredito que é muito mais saudável estar ciente dos prós e contras da coerção e usar o == e === de forma responsável e intencional, do que simplesmente usar === porque é mais fácil não pensar e não aprender.

Se o senhor leva a sério a escrita da JS, e espero que leve, não vale a pena internalizar essa disciplina? Será que o senhor não vai isso fazem mais para melhorar seu código do que qualquer regra de linting aplicada cegamente jamais fará?



Não se esqueça de dar uma olhada no meu O senhor não conhece o JS e, especificamente, a série de livros YDKJS: Tipos e gramática que pode ser lido gratuitamente on-line ou adquirido por meio da O’Reilly e outros vendedores.

Kyle Simpson

Sobre Kyle Simpson

Kyle Simpson é um engenheiro de software orientado para a Web, amplamente aclamado por sua série de livros “You Don’t Know JS” e por quase 1 milhão de horas vistas de seus cursos on-line. O superpoder de Kyle é fazer perguntas melhores, e ele acredita profundamente no uso máximo das ferramentas minimamente necessárias para qualquer tarefa. Como um “tecnólogo centrado no ser humano”, ele é apaixonado por unir humanos e tecnologia, desenvolvendo organizações de engenharia para resolver os problemas certos, de maneiras mais simples. Kyle sempre lutará pelas pessoas por trás dos pixels.