Operator Algorithms: +, delete, in, instanceof, typeof, ?. and ??
typeof undeclaredVariable does not throw a ReferenceError — this is a special exemption written specifically for typeof in the specification. It is the only operator in JavaScript that carries this privilege.
🔹 Level 1 · What You Need to Know
Surprising Behavior of Each Operator
// + operator: result depends on operand types — not always what you expect
1 + "2" // "12" (string concatenation, not addition)
"3" + 4 + 5 // "345" (left to right: concatenate, then concatenate)
3 + 4 + "5" // "75" (3+4=7 first, then concatenate "5")
[] + {} // "[object Object]" (both objects converted to strings)
{} + [] // 0 (statement context: {} is a block, +[] is unary)
// delete: removes properties, not variables
const obj = { a: 1 };
delete obj.a // true (successfully deleted)
delete obj.b // true (non-existent property also returns true)
let x = 1;
delete x // false (cannot delete variables!)
// delete window.NaN // false (non-configurable property)
// in: checks the prototype chain, not just own properties
'toString' in {} // true (toString is on Object.prototype)
'length' in [] // true (arrays have length)
'constructor' in {} // true (comes from the prototype chain)
// instanceof: checks constructor on the prototype chain
[] instanceof Array // true
[] instanceof Object // true (Object is also on the prototype chain)
// Cross-iframe fails:
// frameArr instanceof Array // false (Array from different Realm)
// typeof: the only operator that doesn't throw for undeclared variables
typeof undeclaredVar // "undefined" (special exemption!)
// undeclaredVar // ReferenceError! (everything else throws)
// ?. Optional chaining: short-circuits the ENTIRE chain, not just current node
const obj2 = null;
obj2?.a // undefined (short-circuits)
obj2?.a.b // undefined (entire chain short-circuits, .b not accessed)
obj2?.a.b.c // undefined (entire chain)
// ?? Nullish coalescing: only short-circuits for null/undefined
null ?? "default" // "default"
undefined ?? "default" // "default"
0 ?? "default" // 0 (0 is not null/undefined!)
"" ?? "default" // ""
false ?? "default" // false
// vs || operator (short-circuits for ALL falsy values)
0 || "default" // "default" (0 is falsy)
"" || "default" // "default" ("" is falsy)
false || "default" // "default" (false is falsy)
When to Use Each Operator
| Operator | Recommended use | Avoid |
|---|---|---|
+ |
Explicit concatenation/addition; parenthesize complex expressions | Mixed-type operands without explicit conversion |
delete |
Removing dynamic object properties | Deleting variables (no effect); deleting array elements (use splice) |
in |
Checking if property exists (including prototype chain) | Checking own properties only (use Object.hasOwn) |
instanceof |
Type checking within the same Realm | Cross-iframe/Worker type checking |
typeof |
Checking undeclared variables; basic type checks | Checking null (returns "object"); checking arrays (returns "object") |
?. |
Deep property access; chained method calls | Known-to-exist properties (hides errors) |
?? |
Meaningful defaults (excluding only null/undefined) | When 0/false/"" also need defaults (use || instead) |
🔸 Level 2 · How It Actually Runs
+ Operator Execution Steps
Decision flow for the addition/concatenation + operator:
AdditiveExpression: lval + rval
Step 1: Call ToPrimitive on both operands (hint = "default")
lprim = ToPrimitive(lval)
rprim = ToPrimitive(rval)
Step 2: Check if either side is a String
If lprim is a String OR rprim is a String:
→ String concatenation: ToString(lprim) + ToString(rprim)
Otherwise:
→ Numeric addition: ToNumber(lprim) + ToNumber(rprim)
Key: Step 2 is an OR check — if EITHER operand is a string, concatenation wins
// Execution process examples
1 + 2 // ToPrimitive(1)=1, ToPrimitive(2)=2, no string → 3
1 + "2" // ToPrimitive(1)=1, ToPrimitive("2")="2", string → "1"+"2"="12"
[] + 1 // ToPrimitive([])="", ToPrimitive(1)=1, "" is string → ""+1="1"
[1,2] + [3,4] // ToPrimitive([1,2])="1,2", ToPrimitive([3,4])="3,4" → "1,23,4"
null + 1 // ToPrimitive(null)=null, ToNumber(null)=0 → 0+1=1
undefined + 1 // ToPrimitive(undefined)=undefined, ToNumber(undefined)=NaN → NaN
delete Operator Execution Steps
delete operator execution flow:
delete UnaryExpression
1. Evaluate the expression to get a Reference Record
Reference Record = {
[[Base]]: object or environment record,
[[ReferencedName]]: property name,
[[Strict]]: strict mode flag
}
2. If Reference's [[Base]] is unresolvable:
→ Non-strict mode: return true
→ Strict mode: throw ReferenceError
3. If Reference's [[Base]] is an Environment Record (variable declaration):
→ Non-strict mode: return false (cannot delete variables)
→ Strict mode: throw SyntaxError
4. Call [[Base]].[[Delete]](ReferencedName)
Check property's [[Configurable]]:
→ [[Configurable]] = true: delete property, return true
→ [[Configurable]] = false:
Non-strict mode: return false
Strict mode: throw TypeError
// delete behavior examples
const obj = { a: 1 };
Object.defineProperty(obj, 'b', { value: 2, configurable: false });
delete obj.a // true ([[Configurable]] defaults to true)
delete obj.b // false ([[Configurable]] = false)
// Non-configurable global properties
delete Math.PI // false (Math.PI is non-configurable)
delete NaN // false (window.NaN is non-configurable)
delete undefined // false (window.undefined is non-configurable)
// Configurable global properties (created via assignment)
globalThis.myVar = 42;
delete myVar // true (can delete globals created by assignment)
// Deleting array elements with delete (not recommended!)
const arr = [1, 2, 3];
delete arr[1];
// arr = [1, empty, 3], length is still 3! Creates a "hole"
// Correct deletion uses splice
arr.splice(1, 1); // [1, 3], length is 2
in Operator Execution Steps
in operator execution flow:
RelationalExpression: key in obj
1. Evaluate right operand; if not an Object → throw TypeError
2. Call obj.[[HasProperty]](key)
3. [[HasProperty]] walks the prototype chain:
- Check obj's own properties
- If not found, follow [[Prototype]] upward
- Until null (end of chain)
4. Found → return true; otherwise → return false
// in vs hasOwn
const child = Object.create({ inherited: true });
child.own = 1;
'own' in child // true (own property)
'inherited' in child // true (inherited property!)
'toString' in child // true (from Object.prototype)
Object.hasOwn(child, 'own') // true
Object.hasOwn(child, 'inherited') // false (own properties only!)
Object.hasOwn(child, 'toString') // false
// Common misconception: thinking in only checks own properties
const arr = [1, 2, 3];
0 in arr // true (index 0 is an own property)
'push' in arr // true! (push is on Array.prototype)
'length' in arr // true (length is an own property of arrays)
instanceof Execution Steps
instanceof operator execution flow:
RelationalExpression: obj instanceof Constructor
1. Check if Constructor[Symbol.hasInstance] exists
→ Exists: call Constructor[Symbol.hasInstance](obj), return result
→ Doesn't exist:
2. Execute OrdinaryHasInstance(Constructor, obj):
a. If Constructor has no [[Call]] → TypeError (not a function)
b. If Constructor has [[BoundTargetFunction]]:
→ Get the bound function's target, continue
c. Get Constructor.prototype
d. Walk obj's [[Prototype]] chain upward:
- If some [[Prototype]] === Constructor.prototype → true
- If [[Prototype]] is null → false (end of chain, not found)
// Why instanceof fails across iframes
// In a browser with embedded iframe:
// const arr = new iframe.contentWindow.Array();
// arr instanceof Array // false!
// Because arr's prototype chain is:
// arr → iframeWindow.Array.prototype → iframeWindow.Object.prototype → null
// And Array (current Realm) is a different object!
// Cross-Realm type checking solutions:
Array.isArray(arr) // true (checks [[IsArray]], Realm-safe)
Object.prototype.toString.call(arr) // "[object Array]" (tag-based)
// Custom instanceof behavior
const between0and100 = {
[Symbol.hasInstance](num) {
return typeof num === 'number' && num >= 0 && num <= 100;
}
};
50 instanceof between0and100 // true
150 instanceof between0and100 // false
typeof's Special Exemption
typeof's special handling of undeclared variables:
typeof UnaryExpression
1. Evaluate the expression
- If the result is a Reference Record AND [[Base]] is unresolvable:
→ Return "undefined" (NO ReferenceError!)
- Otherwise, execute GetValue(Reference), which may throw ReferenceError
2. Apply type detection to the value:
Undefined → "undefined"
Null → "object"
Boolean → "boolean"
Number → "number"
String → "string"
Symbol → "symbol"
BigInt → "bigint"
Object (no [[Call]]) → "object"
Object (has [[Call]]) → "function"
?. Optional Chain Execution Steps
Optional chaining ?. short-circuit mechanism:
a?.b.c.d is equivalent to:
if (a == null) { // null or undefined
undefined
} else {
a.b.c.d ← the ENTIRE remainder of the chain executes when a != null
}
Important: the short-circuit point is at 'a', NOT at 'a?.b'!
If a is null, the entire .b.c.d is not executed.
If a has a value but a.b is null, accessing a.b.c WILL throw TypeError!
// Correct understanding of optional chaining
const obj = {
a: null
};
obj?.a // null (a exists, value is null)
obj?.a.b // TypeError! (a is null, .b is NOT protected by ?.)
obj?.a?.b // undefined (need ?. at each potentially-null location)
obj?.b // undefined (b doesn't exist)
obj?.b.c // undefined (entire chain short-circuits)
// Various forms of ?.
obj?.['key'] // optional property access (bracket form)
arr?.[0] // optional array index
func?.() // optional function call (func might not be a function)
obj?.method() // optional method call
// Short-circuit with assignment
let result = obj?.nonexist?.nested ?? "default";
// obj?.nonexist → undefined
// undefined?.nested → undefined (can continue chaining)
// undefined ?? "default" → "default"
🔺 Level 3 · How the Spec Defines It
AdditiveExpression (Addition/Concatenation) Spec Algorithm
Spec Section 13.15.3 (ApplyStringOrNumericBinaryOperator):
ApplyStringOrNumericBinaryOperator ( lval, opText, rval )
- If opText is +, then a. Let lprim be ? ToPrimitive(lval). b. Let rprim be ? ToPrimitive(rval). c. If lprim is a String or rprim is a String, then i. Let lstr be ? ToString(lprim). ii. Let rstr be ? ToString(rprim). iii. Return the String that is the result of concatenating lstr and rstr. d. Set lval to lprim. e. Set rval to rprim.
- Let lnum be ? ToNumeric(lval).
- Let rnum be ? ToNumeric(rval).
- If Type(lnum) is different from Type(rnum), throw a TypeError exception.
- If lnum is a BigInt, then return BigInt::add(lnum, rnum).
- Return Number::add(lnum, rnum).
delete UnaryExpression Spec Algorithm
Spec Section 13.5.1:
13.5.1 The delete Operator
UnaryExpression : delete UnaryExpression
- Let ref be ? Evaluation of UnaryExpression.
- If ref is not a Reference Record, return true.
- If IsUnresolvableReference(ref) is true, then a. Assert: ref.[[Strict]] is false. b. Return true.
- If IsPropertyReference(ref) is true, then a. Assert: IsPrivateReference(ref) is false. b. If IsSuperReference(ref) is true, throw a ReferenceError exception. c. Let baseObj be ? ToObject(ref.[[Base]]). d. If ref.[[ReferencedName]] is a Private Name, throw a TypeError exception. e. Let deleteStatus be ? baseObj.[Delete]. f. If deleteStatus is false and ref.[[Strict]] is true, throw a TypeError exception. g. Return deleteStatus.
- Let base be ref.[[Base]].
- Assert: base is an Environment Record.
- Return ? base.DeleteBinding(ref.[[ReferencedName]]).
RelationalExpression (in/instanceof) Spec Algorithms
Spec Section 13.10 (The in operator):
The in Operator
- Let rval be the right operand.
- If rval is not an Object, throw a TypeError exception.
- Return ? HasProperty(rval, ToPropertyKey(lval)).
Spec Section 13.10.2 (OrdinaryHasInstance):
OrdinaryHasInstance ( C, O )
- If IsCallable(C) is false, return false.
- If C has a [[BoundTargetFunction]] internal slot, then a. Let BC be C.[[BoundTargetFunction]]. b. Return ? InstanceofOperator(O, BC).
- If O is not an Object, return false.
- Let P be ? Get(C, "prototype").
- If P is not an Object, throw a TypeError exception.
- Repeat, a. Set O to ? O.[GetPrototypeOf]. b. If O is null, return false. c. If SameValue(P, O) is true, return true.
typeof UnaryExpression Spec Algorithm
Spec Section 13.5.3:
13.5.3 The typeof Operator
UnaryExpression : typeof UnaryExpression
- Let val be the result of evaluating UnaryExpression.
- If val is a Reference Record, then a. If IsUnresolvableReference(val) is true, return "undefined". b. Set val to ? GetValue(val).
- If val is undefined, return "undefined".
- If val is null, return "object".
- If val is a Boolean, return "boolean".
- If val is a Number, return "number".
- If val is a String, return "string".
- If val is a Symbol, return "symbol".
- If val is a BigInt, return "bigint".
- Assert: val is an Object.
- If val has a [[Call]] internal method, return "function".
- Return "object".
💎 Level 4 · Edge Cases and Traps
Trap 1: typeof undeclaredVar === 'undefined' — No Error Thrown
// Ordinary variable access: throws ReferenceError for undeclared vars
console.log(undeclaredVar); // ReferenceError: undeclaredVar is not defined
// typeof's special exemption: undeclared variable returns "undefined"
typeof undeclaredVar // "undefined" (no error!)
// Practical use: feature detection (checking if a global API exists)
if (typeof fetch === 'undefined') {
// fetch API not available, use a polyfill
window.fetch = myFetch;
}
// Checking for Node.js environment
if (typeof process === 'undefined') {
console.log("Not a Node.js environment");
}
// Safe environment detection in modules (without relying on window)
function isNodejs() {
return typeof process !== 'undefined' &&
typeof process.versions !== 'undefined' &&
typeof process.versions.node !== 'undefined';
}
Trap 2: delete window.NaN Returns false
// NaN is a non-configurable property on the global object
Object.getOwnPropertyDescriptor(globalThis, 'NaN');
// { value: NaN, writable: false, enumerable: false, configurable: false }
delete window.NaN // false (non-configurable!)
delete window.Infinity // false (also non-configurable)
delete window.undefined // false (also non-configurable)
// Verification:
window.NaN = 42; // silently fails (strict mode would throw TypeError)
console.log(NaN); // NaN (unchanged!)
// But globals added via assignment can be deleted
window.myCustomProp = "hello";
delete window.myCustomProp // true
var myVarDecl = "world";
delete myVarDecl // false (var-declared variables cannot be deleted)
Trap 3: 'toString' in {} Is true
// in checks the entire prototype chain
'toString' in {} // true (Object.prototype has toString)
'hasOwnProperty' in {} // true (Object.prototype has hasOwnProperty)
'constructor' in {} // true (Object.prototype has constructor)
'__proto__' in {} // true (in most environments)
// Correct way to check own properties
const obj = { a: 1 };
Object.hasOwn(obj, 'a') // true (ES2022+, recommended)
obj.hasOwnProperty('a') // true (older style, but may be overridden)
Object.prototype.hasOwnProperty.call(obj, 'a') // safest approach
// Why might obj.hasOwnProperty fail?
const safeObj = Object.create(null); // pure object, no Object.prototype
safeObj.key = 1;
safeObj.hasOwnProperty('key') // TypeError! safeObj has no hasOwnProperty
Object.hasOwn(safeObj, 'key') // true (correct! Object.hasOwn doesn't use prototype)
// Practical use of in with the DOM
'style' in document.body // true (elements have a style property)
'nonexistent' in document.body // false (property doesn't exist)
Trap 4: Cross-iframe instanceof Failure
// This is a common problem in single-page apps that embed iframes
// Each iframe has its own independent JavaScript execution environment (Realm)
// Hypothetical scenario:
// const iframe = document.getElementById('myFrame');
// const iframeWindow = iframe.contentWindow;
// const iframeArr = iframeWindow.eval('[1, 2, 3]');
// iframeArr instanceof Array // false!
// Why: iframeArr's prototype chain is:
// iframeArr → iframeWindow.Array.prototype → iframeWindow.Object.prototype → null
// The current Realm's Array is a different object — they don't match!
// Solutions:
// 1. Array.isArray (checks [[IsArray]] internally, Realm-safe)
// Array.isArray(iframeArr) // true
// 2. Object.prototype.toString.call (based on @@toStringTag)
// Object.prototype.toString.call(iframeArr) // "[object Array]"
// 3. Cross-Realm type-safe utility functions
function isArraySafe(val) {
return Array.isArray(val); // most recommended
}
function isTypeSafe(val, type) {
return Object.prototype.toString.call(val) === `[object ${type}]`;
}
isTypeSafe([1,2,3], 'Array') // true (matches any Realm's array)
isTypeSafe({}, 'Object') // true
Trap 5: The Critical Difference Between ?? and ||
// || short-circuits for ALL falsy values (0, "", false, null, undefined, NaN)
const count = 0;
const total1 = count || 100; // 100 (0 is falsy, gets replaced)
// ?? short-circuits ONLY for null and undefined
const total2 = count ?? 100; // 0 (0 is not null/undefined, preserved)
// Real business example: pagination component
function Pagination({ page, pageSize, total }) {
const currentPage = page || 1; // ❌ if page=0, replaced by 1 (bad if 0 is valid)
const size = pageSize || 10; // ❌ if pageSize=0, replaced by 10
const currentPage2 = page ?? 1; // ✅ only uses default when page is null/undefined
const size2 = pageSize ?? 10; // ✅ correct
}
// ?? with config objects
const config = {
timeout: 0, // valid timeout value (0 = no timeout)
retries: null, // explicitly not set
};
const timeout = config.timeout ?? 5000; // 0 (preserved, 0 is not nullish)
const retries = config.retries ?? 3; // 3 (null triggers default)
// ❌ Mixing ?? with || or && requires parentheses
// true || false ?? false // SyntaxError! Need parentheses
(true || false) ?? false // true
true || (false ?? false) // true
Trap 6: Where a?.b.c Actually Short-Circuits
// Easy-to-confuse short-circuit behavior
const a = null;
a?.b // undefined (a is null, short-circuits)
a?.b.c // undefined (a is null, entire .b.c not executed!)
a?.b.c.d // undefined (entire chain not executed)
// But when a has a value:
const b = { x: null };
b?.x // null (b has a value, returns x which is null)
b?.x.y // TypeError! (b has a value so no short-circuit; x is null, .y throws)
b?.x?.y // undefined (need ?. at each potentially-null location)
// The correct pattern: ?. at each possible null
const user = {
profile: null
};
user?.profile?.avatar?.url // undefined (safe)
user?.profile.avatar.url // TypeError! (profile is null)
// Optional chaining with function calls
const obj2 = {
method: null
};
obj2.method?.() // undefined (method is null, not called)
obj2.noMethod?.() // undefined (noMethod doesn't exist, not called)
obj2?.method() // TypeError! (obj2 has a value, method is null, calling null throws)
Chapter Summary
-
The
+operator rule: string wins: after calling ToPrimitive on both operands, if either result is a string, the spec takes the string concatenation path (ToString + ToString). Otherwise it takes the numeric addition path. This depends on what ToPrimitive returns, not on the original types. -
deleteremoves properties, not variables:delete obj.propcalls[[Delete]], controlled by[[Configurable]];delete varNamehas no effect on variables (returns false); non-configurable properties (likeNaN,Math.PI) return false, and strict mode throws TypeError. -
insearches the entire prototype chain:'toString' in {}is true because toString is on Object.prototype. To check own properties, useObject.hasOwn(obj, key)(ES2022+) rather thanin. -
instanceofdepends on the Realm: cross-iframe/Workerinstanceofchecks fail because different Realms have their own distinct built-in constructors. For cross-Realm checks, useArray.isArray(),Object.prototype.toString.call(), or customSymbol.hasInstance. -
?.short-circuits the entire chain;??only short-circuits for null/undefined:a?.b.cwithabeing null meansb.cis never accessed; but ifahas a value anda.bis null, accessing.cstill throws — usea?.b?.c.??preserves0,false, and""as valid values, which is its core difference from||, and is critical when working with legitimate zero values, empty strings, or booleans.