Built in 10 Days: Brendan Eich's Design Trade-offs and JavaScript's Historical Debt
typeof null === 'object' is not a bug — it is a bit-tagging scheme written by one engineer in May 1995 to meet a deadline, and for 30 years nobody has dared fix it, because doing so would crash millions of already-running web pages.
🔹 Level 1 · What You Need to Know
Several constructs you write every day in JavaScript are direct artifacts of decisions made in 1995. Recognizing them lets you judge when to reach for a modern alternative — and when what you're seeing is a known trap rather than a bug you introduced.
5 Historical Artifacts at a Glance
| Artifact | Introduced | Modern Alternative | Notes |
|---|---|---|---|
typeof null === 'object' |
ES1 (1997) | Not fixable | Type-tag implementation quirk |
== implicit coercion |
ES1 (1997) | === |
12 conversion rules, most devs can't recite them |
var hoisting |
ES1 (1997) | let / const |
Hoisted to function top, not block-scoped |
arguments object |
ES1 (1997) | Rest params ...args |
Array-like but not an array; hidden perf cost |
with statement |
ES1 (1997) | Object destructuring | Banned in strict mode; blocks compiler analysis |
5 Mistakes Beginners Make Repeatedly
Mistake 1: Trusting typeof to detect null
// Wrong: this branch will never fire for null
function process(value) {
if (typeof value === 'null') { // the string 'null' is never returned
return 'empty';
}
return value;
}
// Correct: null must be caught with strict equality
function process(value) {
if (value === null) { // correct
return 'empty';
}
if (typeof value === 'object') { // now it really is an object
return JSON.stringify(value);
}
return String(value);
}
Mistake 2: Mixing == and getting surprising truthy results
console.log(0 == false); // true — both coerced to number 0
console.log('' == false); // true — both coerced to number 0
console.log(null == undefined); // true — special-cased in the spec
console.log(null == 0); // false — does NOT follow general coercion
console.log([] == false); // true — [] → '' → 0, false → 0
console.log([] == ![]); // true — both become 0 (real interview question)
// Rule of thumb: use == only to check null/undefined simultaneously
if (value == null) { /* value is null OR undefined */ } // the one acceptable use
Mistake 3: Assuming var is block-scoped
// Classic loop-closure trap (run in Node.js or DevTools)
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() { return i; }); // captures the variable, not the value
}
console.log(funcs[0]()); // 3, not 0
console.log(funcs[1]()); // 3, not 1
console.log(funcs[2]()); // 3, not 2
// ES2015 fix: let creates a new binding per iteration
var funcs2 = [];
for (let i = 0; i < 3; i++) {
funcs2.push(function() { return i; });
}
console.log(funcs2[0]()); // 0
console.log(funcs2[1]()); // 1
console.log(funcs2[2]()); // 2
Mistake 4: Treating arguments as an array
// arguments is array-like but lacks array methods
function sum() {
return arguments.reduce((a, b) => a + b); // TypeError
}
// ES5 workaround
function sum() {
return Array.prototype.reduce.call(arguments, (a, b) => a + b, 0);
}
// ES2015 correct approach: rest parameters
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0); // nums is a real Array
}
Mistake 5: Not knowing the hoisting difference between declarations and expressions
// Function declaration: fully hoisted (name + body)
console.log(add(1, 2)); // 3 — works before the declaration
function add(a, b) { return a + b; }
// Function expression: only the var name is hoisted (as undefined)
console.log(multiply(2, 3)); // TypeError: multiply is not a function
var multiply = function(a, b) { return a * b; };
🔸 Level 2 · How It Really Works
The 10 Days in May 1995
Brendan Eich joined Netscape in April 1995 with a job description that said "implement Scheme in the browser." By the time he arrived, the brief had changed completely. Netscape had signed a partnership agreement with Sun Microsystems to promote Java in the browser, so the company needed "a scripting language that looks like Java" — something non-programmers and web designers could write simple logic in.
Over 10 days in May 1995, Eich built the first prototype of JavaScript (then called Mocha, later LiveScript, renamed JavaScript in December 1995). Each decision made in those 10 days left a mark that persists 30 years later.
Decision 1: Prototype-based instead of class-based inheritance
Eich's decision tree:
Object-oriented paradigm
/ \
Class-based (class) Prototype-based (prototype)
/ \
Java style Self language style
Strict inheritance hierarchy Objects delegate directly
Eich did NOT choose this ← → Eich chose this
(Java was popular but Eich thought
a browser scripting language
didn't need that complexity)
Why prototypes? Eich admired Self's simplicity: objects delegate behavior directly to other objects with no "class" abstraction in between. But marketing pressure from Netscape forced him to make the syntax look Java-like, so he added the new keyword and this — while keeping prototypes underneath. The result is a strange hybrid:
// Java-like surface syntax (using new)
function Animal(name) {
this.name = name; // Java-style constructor syntax
}
Animal.prototype.speak = function() {
return this.name + ' makes a noise.'; // but the engine uses prototype chain
};
var dog = new Animal('Rex');
// dog.__proto__ === Animal.prototype (prototype lookup)
// dog has no speak method; the engine walks up the chain
ES2015's class keyword is purely syntactic sugar — the underlying mechanism never changed:
class Animal {
constructor(name) { this.name = name; }
speak() { return this.name + ' makes a noise.'; }
}
// Animal.prototype.speak still exists
// new Animal('Rex') still uses [[Prototype]] lookup
Decision 2: The type-tag scheme that created typeof null
The original Mocha engine represented every JS value as a 32-bit integer. The lowest 3 bits were a type tag:
Original type-tag layout (Mocha, 1995):
┌────────────────────────────────────────────────────────┐
│ 32-bit value representation │
│ ┌──────────────────────────────────┬────────────────┐ │
│ │ Actual value or pointer (29 hi) │ Type tag (3 lo)│ │
│ └──────────────────────────────────┴────────────────┘ │
│ │
│ Type tags: │
│ 000 = object (pointer to heap object) │
│ 001 = int (31-bit integer) │
│ 010 = double (pointer to a float) │
│ 100 = string (pointer to a string) │
│ 110 = boolean │
│ │
│ null representation: NULL pointer = 0x00000000 │
│ Low 3 bits = 000 → type tag = object │
└────────────────────────────────────────────────────────┘
null was represented as all-zero bits (the C language convention for a NULL pointer). The type-check code read the low 3 bits, found 000, and returned "object". The oversight was discovered weeks after the code was written, but Netscape was already demoing the language and couldn't make breaking changes.
Decision 3: Two equality operators
JavaScript originally had only ==, which performed implicit type coercion. By 1998, the ES1 spec had been published and vast amounts of web code already depended on =='s behavior. ES3 (1999) introduced === as a stricter alternative — but the behavior of == could not be changed without breaking existing sites.
== history:
1995 → only ==, performs type coercion
1999 → === added in ES3
2009 → 'use strict' arrives, but == stays
2015 → ESLint's eqeqeq rule popularizes ===
Now → virtually every style guide mandates ===
Decision 4: The Netscape–Sun naming deal
On December 4, 1995, Netscape and Sun jointly announced "JavaScript." The name was a marketing decision, not a technical one — designed to ride Java's momentum. Eich has said in multiple interviews he was unhappy with the name because it has caused confusion ever since. The relationship between Java and JavaScript is like the relationship between "Car" and "Carpet."
The Browser Wars (1995–2001): The Root of Fragmented Implementations
Browser war timeline:
1995 → Netscape Navigator 2.0: first browser to include JavaScript
1996 → IE 3.0 ships JScript (Microsoft's reverse-engineered JavaScript)
↓
JScript and JavaScript are incompatible in subtle ways
Same code behaves differently in IE vs Netscape
↓
1997 → ECMAScript 1 (ES1) — attempt to unify standards
1999 → ECMAScript 3 (ES3) — lays the modern JS foundation
↓
IE holds ~90% market share, stops updating for 5 years
Netscape acquired by AOL; browser development nearly halts
↓
2004 → Firefox 1.0 released (Mozilla Foundation)
2008 → Chrome launched; V8 engine with JIT delivers 10-100x perf gains
2009 → IE's monopoly era ends; ES5 ships
2011 → IE9 supports ES5 strict mode
Fragmented implementations spawned the "browser compatibility" problem: developers wrote if (document.all) to detect IE; one of jQuery's primary goals was to paper over these differences.
ES4 (2003–2008): The Failed Revolution
ES4 was championed by Adobe (the Flash/ActionScript team), Mozilla, and Opera. It proposed:
- An optional static type system
- A real
classkeyword (not prototype sugar — actual classes) - Package and namespace system
- Interfaces
- Fixed types like
int,uint,double
Microsoft opposed strongly — implementing ES4 would require rewriting IE's JScript engine at enormous cost. Google had no major browser yet. After five years of standoff, ES4 was formally abandoned in 2008. This is the largest techno-political event in JavaScript's history.
ES4 failure chain:
Too technically ambitious (static types = rewrite the engine)
+ Factional split (Microsoft vs Adobe/Mozilla)
+ "Don't break existing code" constraint impossible to satisfy
= August 2008: TC39 formally abandons ES4
↓
ES3.1 (renamed ES5) proceeds on "repair, not revolution" principles
ES5 (2009): Fixing Things Within Constraints
ES5's design principle: all ES3 code must run identically on an ES5 engine. Within that constraint, ES5 delivered:
// 1. Strict mode: opt-in subset that fixes the worst design issues
'use strict';
// In strict mode:
// - with statement throws
// - Accidental globals throw
// - Deleting non-deletable properties throws
// - arguments/caller access restricted
// 2. Object.defineProperty: foundation of meta-programming
const obj = {};
Object.defineProperty(obj, 'readOnly', {
value: 42,
writable: false,
enumerable: true,
configurable: false
});
obj.readOnly = 100; // silent fail (sloppy mode) / throws (strict mode)
console.log(obj.readOnly); // 42
// 3. Native JSON (previously required a third-party library)
JSON.parse('{"key": "value"}');
JSON.stringify({ key: 'value' });
// 4. New Array methods
[1, 2, 3].forEach(x => x);
[1, 2, 3].map(x => x * 2);
[1, 2, 3].filter(x => x > 1);
[1, 2, 3].reduce((acc, x) => acc + x, 0);
🔺 Level 3 · What the Spec Says
ES1 (1997) Design Principles — From the Source
The preamble of ECMAScript Edition 1 (ECMA-262, June 1997) states:
"ECMAScript is an object-oriented programming language for performing computations and manipulating computational objects within a host environment. ECMAScript as defined here is not intended to be computationally self-sufficient; indeed, there are no provisions in this specification for input of external data or output of computed results. Instead, it is expected that the computational environment of an ECMAScript program will provide not only the objects and other facilities described in this specification but also certain environment-specific host objects..."
Section 1 (Scope) makes the relationship explicit:
"This standard defines the ECMAScript scripting language. It does not define facilities for obtaining external data, specifying the format of files, providing graphical user interfaces, or interacting with the user."
This means ES1 defined itself as a computation layer inside a host environment — not a standalone language. document, window, and DOM APIs are not in the spec; they belong to the browser host.
Web Compatibility in the Spec — Annex B
ES5 (ECMA-262 5th edition, December 2009) Section 16 (Errors) includes:
"An implementation may report syntax errors in the program text supplied to it using the script mechanism described in section 14.1. An implementation shall report all errors as specified, except for the following: A host may leave certain behaviors 'implementation-defined'..."
More importantly, Annex B collects all features that must be retained for web compatibility but are not recommended:
Annex B (normative): Web compatibility features in ES5
- String.prototype.substr()
(non-standard but too widely used to remove)
- Non-standard Date.parse() string formats
(implementations differ but must support certain formats)
- HTML-style comments <!-- and -->
(originally let old browsers hide JS as HTML comments;
retained in modern spec)
- escape() / unescape()
(deprecated but not removable)
Annex B is essentially a "historical debt storage room" — the spec acknowledges these features are poorly designed but mandates their preservation because so much code depends on them. This is the direct spec-level expression of the Web Compatibility constraint.
The Type System in ES1
ES1 Section 8 (Types) defined 6 types:
ES1's 6 types (1997):
1. Undefined
2. Null
3. Boolean
4. Number (64-bit IEEE 754)
5. String (UTF-16)
6. Object
typeof operator return values (ES1 Section 11.4.3):
Type of val Result
Undefined "undefined"
Null "object" ← the spec codified the implementation bug
Boolean "boolean"
Number "number"
String "string"
Object "object"
By writing typeof null === 'object' into ES1, the spec transformed a bug into a specified behavior — an acknowledged-but-codified design mistake.
💎 Level 4 · Edge Cases and Traps
Trap 1: The Full Derivation of typeof null === 'object'
The original C implementation (Netscape, 1995)
Based on descriptions from Eich and other Netscape engineers, the original Mocha engine represented JS values like this:
/* Pseudocode reconstructing the 1995 design */
typedef uint32_t JSValue;
/* Type tags (lowest 3 bits) */
#define JSTAG_OBJECT 0 /* 000 */
#define JSTAG_INT 1 /* 001 */
#define JSTAG_DOUBLE 2 /* 010 */
#define JSTAG_STRING 4 /* 100 */
#define JSTAG_BOOLEAN 6 /* 110 */
/* NULL in C is ((void*)0), i.e. all-zero bits */
#define JS_NULL 0 /* 0x00000000 */
const char* js_typeof(JSValue val) {
if (val == JS_NULL) {
/* BUG: should return "null" here,
but this check was never added */
}
switch (val & 0x7) { /* read lowest 3 bits */
case JSTAG_OBJECT: return "object"; /* null falls here */
case JSTAG_INT: return "number";
case JSTAG_DOUBLE: return "number";
case JSTAG_STRING: return "string";
case JSTAG_BOOLEAN: return "boolean";
default: return "undefined";
}
}
Why it can never be fixed
In 2011, Brendan Eich confirmed on Twitter that a fix was theoretically possible. TC39 testing showed the change would break vast amounts of code matching this pattern:
// Millions of websites have code like this
if (typeof someValue === 'object') {
// handles both null and real objects
if (someValue !== null) {
someValue.doSomething();
}
}
// If typeof null changed to 'null', the outer guard
// would no longer include null — and code that relies on
// typeof null === 'object' to gate null-and-object paths
// together would silently change behavior.
// The more dangerous breaking pattern:
function isObject(val) {
return typeof val === 'object'; // used as "not a primitive"
}
// If fixed: isObject(null) goes from true to false
// All callers that pass null and expect true → break silently
Conclusion: typeof null === 'object' is JavaScript's most famous wontfix bug, codified in ECMAScript's Annex B spirit and unlikely to ever change.
Trap 2: Why NaN !== NaN (It's Not JavaScript's Fault)
NaN !== NaN is IEEE 754-1985's specification, not JavaScript's design. JavaScript merely conforms to it.
IEEE 754's rationale
Sources of NaN:
0 / 0 = NaN
Math.sqrt(-1) = NaN
Infinity - Infinity= NaN
parseInt('abc') = NaN
Why NaN !== NaN?
The IEEE 754 committee paper (1985) argued:
"NaN represents an unknown value. Are two unknowns equal?
Unknown. An unknown answer should be false."
Analogy:
If you don't know who is in a room (call them NaN1 and NaN2)
you cannot assert NaN1 === NaN2
So IEEE 754 mandates: NaN compares false to everything, including itself
Cross-language comparison — all IEEE 754 languages behave the same
# Python
import math
nan = float('nan')
print(nan != nan) # True
print(math.isnan(nan)) # True
// Java
double nan = Double.NaN;
System.out.println(nan != nan); // true
System.out.println(Double.isNaN(nan)); // true
// JavaScript — identical to Python and Java
const nan = NaN;
console.log(nan !== nan); // true
console.log(Number.isNaN(nan)); // true — ES2015, no coercion
console.log(isNaN('abc')); // true — DANGEROUS: coerces first
console.log(Number.isNaN('abc')); // false — correct: 'abc' is not NaN
Trap 3: 0.1 + 0.2 !== 0.3 — Not JavaScript's Bug
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
IEEE 754 double-precision is the cause
Decimal → Binary
0.1 → 0.0001100110011... (infinite repeating)
0.2 → 0.0011001100110... (infinite repeating)
64-bit double has only 52 mantissa bits; truncation is mandatory:
0.1 actual stored value: 0.1000000000000000055511151231257827...
0.2 actual stored value: 0.2000000000000000111022302462515654...
Their sum ≠ 0.3's stored approximation
Same behavior across languages:
# Python
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
// Java
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
// Correct JavaScript approaches:
// Option 1: epsilon comparison
function nearlyEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON; // Number.EPSILON ≈ 2.22e-16
}
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true
// Option 2: integer arithmetic (recommended for financial calculation)
const priceInCents = 10; // $0.10 stored as 10 cents
const taxInCents = 20; // $0.20 stored as 20 cents
const totalInCents = priceInCents + taxInCents; // 30 — exact
// Option 3: BigInt for integers only (ES2020)
// Option 4: decimal.js or big.js for arbitrary precision
Trap 4: Legacy Code Patterns and Their Modern Replacements
These 5 patterns appear in production code written before 2010 and still live in old codebases:
// ═══════════════════════════════════════════════════════════
// Pattern 1: arguments for variadic functions (ES3 era)
// ═══════════════════════════════════════════════════════════
// Legacy (pre-2015)
function logAll() {
var args = Array.prototype.slice.call(arguments);
args.forEach(function(arg) { console.log(arg); });
}
// Modern (ES2015+)
function logAll(...args) {
args.forEach(arg => console.log(arg)); // args is a real Array
}
// ═══════════════════════════════════════════════════════════
// Pattern 2: IIFE for module encapsulation (pre-ES modules)
// ═══════════════════════════════════════════════════════════
// Legacy (2009–2015)
var MyModule = (function() {
var privateVar = 'secret';
return { getSecret: function() { return privateVar; } };
})();
// Modern (ES2015 native modules)
// myModule.js
const privateVar = 'secret';
export function getSecret() { return privateVar; }
// ═══════════════════════════════════════════════════════════
// Pattern 3: Manual prototype chain for inheritance
// ═══════════════════════════════════════════════════════════
// Legacy (ES5, seen throughout jQuery source)
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return this.name; };
function Dog(name) { Animal.call(this, name); }
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() { return 'Woof!'; };
// Modern (ES2015+)
class Animal {
constructor(name) { this.name = name; }
speak() { return this.name; }
}
class Dog extends Animal {
bark() { return 'Woof!'; }
}
// ═══════════════════════════════════════════════════════════
// Pattern 4: Callback hell (pre-Promise async)
// ═══════════════════════════════════════════════════════════
// Legacy (Node.js 0.x, 2009–2014)
fs.readFile('a.txt', function(err, dataA) {
if (err) return handleError(err);
fs.readFile('b.txt', function(err, dataB) {
if (err) return handleError(err);
fs.writeFile('c.txt', dataA + dataB, function(err) {
if (err) return handleError(err);
console.log('done');
});
});
});
// Modern (ES2017+)
async function mergeFiles() {
const dataA = await fs.promises.readFile('a.txt');
const dataB = await fs.promises.readFile('b.txt');
await fs.promises.writeFile('c.txt', dataA + dataB);
console.log('done');
}
// ═══════════════════════════════════════════════════════════
// Pattern 5: Hand-written polyfills (for IE8 and below)
// ═══════════════════════════════════════════════════════════
// Legacy (ES5 era)
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback) {
for (var i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
}
// Modern: core-js + Babel handle everything automatically
// babel.config.json:
// { "targets": "> 0.5%, last 2 versions, not dead" }
Chapter Summary
-
JavaScript was designed in 10 days (May 1995). Brendan Eich made countless trade-offs under commercial pressure from Netscape. The vast majority of those trade-offs could not be reversed in the subsequent 30 years because of the Web Compatibility constraint.
-
typeof null === 'object'is an artifact of a 32-bit type-tag scheme: the NULL pointer (all-zero bits) collided with the object type tag (low 3 bits = 000). The oversight was discovered weeks later but could not be fixed, and was formally written into the ECMAScript specification in Annex B. -
NaN !== NaNfollows IEEE 754-1985, not a JavaScript design choice. Python, Java, and every other IEEE 754-conforming language behave identically. The correct detection method isNumber.isNaN()(ES2015), not the legacy globalisNaN(). -
0.1 + 0.2 !== 0.3is the inevitable result of 64-bit IEEE 754 floating-point representation, not a JavaScript bug. Every language using 64-bit floats has this property. Use integer arithmetic or a dedicated library for financial calculations. -
ES4's failure (2003–2008) was JavaScript's turning point: it forced the community to embrace "incremental improvement" over "radical redesign," laying the groundwork for ES5's "repair without revolution" strategy and ultimately for ES2015's "large but backward-compatible" release model.