JavaScript: The Good, The Bad, and The Weird
JavaScript: The Good, The Bad, and The Weird
JavaScript powers nearly everything on the modern web. It’s flexible, expressive, and incredibly widespread. Yet, beneath its approachable syntax lies a collection of behaviors that can feel charmingly clever—or deeply confusing.
If you’ve ever wondered why [] + [] becomes an empty string, or why 0.1 + 0.2 !== 0.3, this guide will walk you through JavaScript’s quirkiest features and, more importantly, explain exactly why they behave the way they do.
TL;DR
- Type Coercion: JavaScript converts types implicitly, sometimes leading to surprises
- NaN Paradox:
NaNis a number, but it's not equal to itself - Floating Point Limitations: IEEE 754 makes
0.1 + 0.2imprecise - Hoisting: Declarations move to the top of their scope
- Closures: Inner functions retain access to outer-scope variables
Type Coercion — JavaScript’s Silent Transformer
JavaScript automatically converts values from one type to another when it thinks it’s helpful. This flexibility is powerful but often the source of frustrating bugs.
The Double Personality of the + Operator
Depending on its operands, + changes its role:
console.log(5 + 10); // 15 (number addition)
console.log('Hello' + ' World'); // "Hello World" (string concatenation)
console.log(1 + '1'); // "11" (number → string)
console.log(1 - '1'); // 0 (string → number)
Key insight:
If either operand is a string, + prefers concatenation. Other operators (-, *, /) convert values to numbers before operating.
When Objects Enter the Equation
Here’s where it gets truly weird:
console.log([] + []); // ""
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0 (in some contexts)
console.log({} + {}); // "[object Object][object Object]" OR NaN
Why this happens:
- JavaScript tries
valueOf()thentoString()on objects []becomes""{}becomes"[object Object]"- A standalone
{}can be interpreted as a block statement, not an object literal
This combination produces results that look nonsensical at first glance—but they’re consistent once you understand the underlying rules.
Equality: Why === Is Your Friend
console.log(0 == false); // true
console.log('' == 0); // true
console.log(0 === false); // false
console.log('' === 0); // false
Rule of thumb:
Use === unless you absolutely need the quirks of loose equality.
NaN — The Number That Isn’t a Number
NaN (“Not a Number”) sits in JavaScript’s number type—but behaves unlike any number:
console.log(typeof NaN); // "number"
console.log(NaN === NaN); // false
This is by design, following the IEEE 754 floating-point spec.
How to Check for NaN Properly
const value = 0 / 0;
Number.isNaN(value); // Best option: strict
isNaN(value); // Works but coerces first
value === NaN will always be false.
Floating-Point Arithmetic — The Classic 0.1 + 0.2 Problem
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
This isn’t a JavaScript bug—it’s a binary floating-point limitation.
Just like 1/3 can't be represented exactly in decimal, 0.1 cannot be represented exactly in binary.
Workarounds
(0.1 + 0.2).toFixed(2); // "0.30"
function areEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
areEqual(0.1 + 0.2, 0.3); // true
Hoisting — What JavaScript Lifts for You
JavaScript "hoists" declarations to the top of their scope before running your code.
var: Hoisted and Initialized
console.log(myVar); // undefined
var myVar = 5;
Behind the scenes:
var myVar; // hoisted
console.log(myVar);
myVar = 5;
let and const: Hoisted but Uninitialized (TDZ)
console.log(myLet); // ReferenceError
let myLet = 10;
They exist but cannot be accessed until execution reaches their declaration—this gap is the Temporal Dead Zone.
Function Hoisting
greet(); // Works
function greet() { console.log("Hello!"); }
sayHi(); // TypeError
var sayHi = function() { console.log("Hi!"); };
Function declarations are hoisted entirely; function expressions are not.
Closures — JavaScript’s Hidden Superpower
Closures allow functions to remember variables from their defining scope—even after that scope has closed.
Classic Example
function createCounter() {
let count = 0;
return () => ++count;
}
const counter = createCounter();
counter(); // 1
counter(); // 2
Practical Use Case: Private State
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
return amount > balance ? "Insufficient funds" : (balance -= amount);
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.balance; // undefined
Closures give you encapsulation without classes.
Conclusion — Embrace the Quirks
JavaScript’s unusual behaviors stem from its design philosophy:
- Backward compatibility: old scripts must still run
- Flexibility: the language tries to “help” instead of failing
- Accessibility: easy for beginners, expressive for experts
Understanding why these quirks exist turns them from bugs into tools. Master the rules, and JavaScript becomes far more predictable—and far more powerful.
Recommended Reading
- You Don’t Know JS (Yet) – Kyle Simpson
- MDN Web Docs – The authoritative reference
- JavaScript.info – Modern deep-dive tutorials
- WAT Talk – Gary Bernhardt’s hilarious breakdown of JS oddities