MSH Logo

JavaScript Weird Parts: The Quirks That Keep Us Guessing

Published on

JavaScript has some truly bizarre behaviors that can make even experienced developers scratch their heads. From [] == ![] being true to NaN not equaling itself, these quirks are part of what makes JavaScript both fascinating and frustrating.

But here's the thing - understanding these weird parts isn't just academic curiosity. It's essential for writing bug-free code and avoiding the subtle traps that can break your application in production.

Why This Matters

These quirks aren't bugs - they're features! Understanding them helps you write more predictable code and debug issues faster.

Equality Quirks: When Things Aren't What They Seem

Zero and Negative Zero

JavaScript treats 0 and -0 as equal, but they're not identical:

equality.js
1console.log(0 === -0); // true 2console.log(Object.is(0, -0)); // false 3console.log(1 / 0); // Infinity 4console.log(1 / -0); // -Infinity
IEEE 754 Standard

JavaScript follows IEEE 754 floating-point standard - +0 and -0 are equal in comparisons but preserve their sign in math operations.

NaN Equality

NaN is the only value that's not equal to itself:

nan-equality.js
1console.log(NaN === NaN); // false 2console.log(NaN == NaN); // false 3console.log(Object.is(NaN, NaN)); // true 4console.log(Number.isNaN(NaN)); // true

Type Coercion Magic: When JavaScript Changes Types

String to Number Conversion

JavaScript's type coercion can be surprising:

type-coercion.js
1console.log(Number("")); // 0 2console.log(Number(" ")); // 0 3console.log(Number("123")); // 123 4console.log(Number("abc")); // NaN 5console.log(Number(null)); // 0 6console.log(Number(undefined)); // NaN 7console.log(Number(false)); // 0 8console.log(Number(true)); // 1

Boolean Coercion

JavaScript has exactly 7 falsy values - everything else is truthy:

boolean-coercion.js
1// Falsy values 2console.log(Boolean("")); // false 3console.log(Boolean(0)); // false 4console.log(Boolean(null)); // false 5console.log(Boolean(undefined)); // false 6console.log(Boolean(NaN)); // false 7 8// Truthy values 9console.log(Boolean("hello")); // true 10console.log(Boolean(42)); // true 11console.log(Boolean({})); // true 12console.log(Boolean([])); // true
Falsy Values

JavaScript has exactly 7 falsy values: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else is truthy.

Object Type Coercion: When Objects Become Primitives

The ValueOf and ToString Methods

Objects have special methods that JavaScript calls during type coercion:

object-coercion.js
1const obj1 = { 2 valueOf: () => 42, 3 toString: () => 27 4}; 5console.log(obj1 + ''); // "42" 6console.log(obj1 + 10); // 52 7console.log(String(obj1)); // "27" 8 9const obj2 = { 10 toString: () => 27 11}; 12console.log(obj2 + ''); // "27" 13console.log(obj2 + 10); // 37

Coercion Order: valueOf() first, then toString() if needed.

Array Coercion

Arrays are converted to strings by joining their elements with commas:

array-coercion.js
1console.log([] + []); // "" 2console.log([] + {}); // "[object Object]" 3console.log({} + []); // "[object Object]" 4console.log([1, 2, 3] + [4, 5, 6]); // "1,2,34,5,6" 5console.log([1, 2, 3] + 4); // "1,2,34" 6console.log([1, 2, 3] - 4); // NaN

The Double Equals Operator: The Abstract Equality Algorithm

The Famous [] == ![] Example

The == operator performs type coercion before comparison, leading to surprising results:

abstract-equality.js
1console.log([] == ![]); // true 2 3// Step by step: 4// 1. ![] evaluates to false 5// 2. [] == false 6// 3. false is converted to 0: [] == 0 7// 4. [] is converted to string: "" == 0 8// 5. "" is converted to number: 0 == 0 9// 6. Result: true
Abstract Equality Algorithm

The == operator follows the Abstract Equality Comparison Algorithm, which performs type coercion that can be unpredictable. Always prefer === for explicit comparisons.

More Abstract Equality Examples

Here are more examples of the == operator's behavior:

more-equality.js
1console.log(null == undefined); // true 2console.log(null == 0); // false 3console.log(undefined == 0); // false 4 5console.log("0" == 0); // true 6console.log("0" == false); // true 7console.log(0 == false); // true 8 9console.log("" == 0); // true 10console.log("" == false); // true 11 12console.log([1, 2] == "1,2"); // true 13console.log([null] == ""); // true 14console.log([undefined] == ""); // true

Hoisting: When JavaScript Moves Things Around

Function Declarations vs Function Expressions

Function declarations are hoisted, function expressions are not:

hoisting.js
1// Function declaration - hoisted 2console.log(hoistedFunction()); // "Hello from hoisted function" 3 4function hoistedFunction() { 5 return "Hello from hoisted function"; 6} 7 8// Function expression - not hoisted 9try { 10 console.log(notHoisted()); // TypeError: notHoisted is not a function 11} catch (e) { 12 console.log("Error:", e.message); 13} 14 15var notHoisted = function() { 16 return "Hello from not hoisted function"; 17};

Variable Hoisting

var declarations are hoisted but initialized as undefined, while let and const create a "temporal dead zone":

variable-hoisting.js
1console.log(x); // undefined (not ReferenceError) 2var x = 5; 3 4// Let and const are not hoisted the same way 5try { 6 console.log(y); // ReferenceError: Cannot access 'y' before initialization 7} catch (e) { 8 console.log("Error:", e.message); 9} 10let y = 10;
Hoisting Behavior

Function declarations are fully hoisted, while function expressions and variables declared with var are hoisted but initialized as undefined. let and const are hoisted but not initialized, creating a "temporal dead zone."

The This Keyword: Context-Dependent Behavior

The this keyword in JavaScript can be confusing because it depends on how a function is called:

this-keyword.js
1// Global context 2console.log(this === window); // true (in browser) 3 4// Function context 5function regularFunction() { 6 console.log(this); 7} 8regularFunction(); // window (in non-strict mode) 9 10// Method context 11const obj = { 12 name: "Object", 13 method: function() { 14 console.log(this.name); 15 } 16}; 17obj.method(); // "Object" 18 19// Arrow function context 20const arrowObj = { 21 name: "Arrow Object", 22 method: () => { 23 console.log(this.name); 24 } 25}; 26arrowObj.method(); // undefined (this refers to outer scope)

Best Practices: Avoiding the Weird Parts

  1. Always use === instead of == - Avoid type coercion surprises
  2. Use Object.is() for special cases - When you need to distinguish between +0 and -0 or check for NaN
  3. Understand hoisting - Know how function declarations and variable declarations behave
  4. Be explicit about types - Use explicit conversions when needed
  5. Test edge cases - Always test your code with unexpected input types
Pro Tip

Use TypeScript or ESLint rules to catch potential issues with type coercion and equality comparisons before they reach production.

Wrapping Up

JavaScript's weird parts make it both fascinating and challenging. While these behaviors might seem counterintuitive, understanding them is crucial for writing robust JavaScript code.

Key takeaways:

  • Prefer strict equality (===) over loose equality (==)
  • Understand how type coercion works
  • Know the difference between function declarations and expressions
  • Be aware of hoisting behavior
  • Use modern JavaScript features that provide more predictable behavior

Remember, these quirks aren't bugs—they're features of the language. Once you understand them, you can use them to your advantage or avoid them entirely by following best practices.

Next Steps

Practice with the interactive examples above, then try building your own JavaScript applications while keeping these quirks in mind!

Buy Me a Coffee at ko-fi.com
GET IN TOUCH

Let's work together

I build exceptional and accessible digital experiences for the web

WRITE AN EMAIL

or reach out directly at hello@mohammadshehadeh.com