Skip to main content
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!

GET IN TOUCH

Let's work together

I build fast, accessible, and delightful digital experiences for the web.
Whether you have a project in mind or just want to connect, I'd love to hear from you.

or reach out directly at hello@mohammadshehadeh.com