Skip to main content
Mohammad Shehadeh — home (MSH monogram, letter M filled with the Palestinian flag)

JavaScript Weird Parts: The Quirks That Keep Us Guessing

Published on
4 min read

JavaScript has bizarre behaviors that 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.

Understanding these weird parts isn't just academic. It helps you write bug-free code and avoid the subtle traps that break your app in production.

Why This Matters

These quirks aren't bugs - they're features. Understanding them helps you write more predictable code and debug 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 the IEEE 754 floating-point standard - +0 and -0 are equal in comparisons but keep 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

Type coercion can surprise you:

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 coerces types before comparing, which leads 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, whose type coercion 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 is 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 type coercion and equality issues before they reach production.

Wrapping Up

JavaScript's weird parts make it both fascinating and challenging. These behaviors seem counterintuitive, but understanding them is key to writing robust 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

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 by following best practices.

Next Steps

Practice with the examples above, then build your own JavaScript apps while keeping these quirks in mind.

Related Articles

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.

Get in touch

or reach out directly at hello@mohammadshehadeh.com