Coerção, Closures e Protótipos: As Mecânicas do JavaScript que Você Usa Sem Entender
Resumo rápido
Este post cobre três mecânicas internas do JavaScript que a maioria dos devs usa diariamente sem compreender o que acontece por baixo: coerção de tipos, closures com escopo léxico e cadeia de protótipos. Não é um tutorial introdutório. É uma análise de por que essas mecânicas se comportam como se comportam, com código funcional, anti-patterns reais e contexto de produção.
O bug de R$ 14 mil que começou com um ==
Em 2018, eu mantinha um sistema de conciliação financeira em Node.js (v10 na época) para um marketplace com ~8 mil transações/dia. Um sábado de madrugada, o job de conciliação marcou 247 transações como "valor zerado" e disparou estornos automáticos. O valor total dos estornos indevidos: R$ 14.312,00.
A causa raiz era uma comparação com == entre um valor vindo de uma API externa (string "0.00") e o número 0. Em JavaScript, "0.00" == 0 retorna true. O sistema interpretou que o valor da transação era zero e seguiu o fluxo de estorno. O fix foi trocar um único operador. O prejuízo levou semanas para reverter.
Esse tipo de bug nasce de uma lacuna específica: entender o que a coerção faz, mas não quando e por que o motor a dispara.
Coerção de tipos: as regras que a spec define (e que ninguém lê)
A coerção em JavaScript não é aleatória. O motor V8 (e SpiderMonkey, e JavaScriptCore) segue o algoritmo Abstract Equality Comparison definido na seção 7.2.14 da ECMAScript spec. O problema é que esse algoritmo tem 12 passos com ramificações, e a intuição humana falha em pelo menos metade deles.
Os três cenários que causam mais bugs
// Cenário 1: string numérica vs número
// A spec converte a string para número antes de comparar (passo 5 do algoritmo)
console.log("42" == 42); // true — ToNumber("42") === 42
console.log("042" == 42); // true — ToNumber ignora zero à esquerda em decimal
console.log("0x2A" == 42); // true — ToNumber parseia hexadecimal
// Cenário 2: null e undefined são iguais APENAS entre si
// Passo 2 e 3 do algoritmo: retorna true se um é null e outro undefined
console.log(null == undefined); // true
console.log(null == 0); // false — null NÃO é convertido para número aqui
console.log(null == ""); // false — mesmo motivo
// Cenário 3: objetos vs primitivos
// A spec chama ToPrimitive no objeto, que invoca valueOf() e depois toString()
const createdAt = new Date("2025-01-15");
console.log(createdAt == "Wed Jan 15 2025 00:00:00 GMT+0000"); // true — toString() do Date
console.log(createdAt == 1736899200000); // false — porque toString() foi chamado, não valueOf()
Esse último cenário é contraintuitivo. Quando um objeto é comparado com uma string via ==, o motor chama ToPrimitive com hint "default", que para Date resulta em toString(). Quando comparado com um número, o hint continua "default" (não "number"), e Date sobrescreve o comportamento padrão para preferir string. Outros objetos (como arrays) preferem valueOf().
A tabela que resolve 90% das dúvidas
| Expressão | Resultado | Por quê |
|---|---|---|
"" == false | true | Ambos convertem para 0 via ToNumber |
"0" == false | true | ToNumber("0") → 0, ToNumber(false) → 0 |
[] == false | true | ToPrimitive([]) → "", ToNumber("") → 0 |
[] == ![] | true | ![] é false (objeto é truthy), depois [] == false |
NaN == NaN | false | Passo 1(c) da spec: NaN não é igual a nada, nem a si mesmo |
null == false | false | null só é == a undefined, nada mais |
" \t\n" == 0 | true | ToNumber ignora whitespace, string vazia vira 0 |
A solução não é "sempre use ==="
Eu uso === como padrão, sim. Mas o conselho cego de "nunca use ==" ignora que existem dois casos em que ==` é intencionalmente útil:
// Checar null OU undefined com uma única comparação
// Isso é idiomático e mais legível que (value === null || value === undefined)
function getConfig(value) {
if (value ==
---
Leia o artigo completo em [https://vivodecodigo.com.br/backend/coercao-closures-prototipos-mecanicas-javascript-producao](https://vivodecodigo.com.br/backend/coercao-closures-prototipos-mecanicas-javascript-producao)