Chapter 11

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 )

  1. 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.
  2. Let lnum be ? ToNumeric(lval).
  3. Let rnum be ? ToNumeric(rval).
  4. If Type(lnum) is different from Type(rnum), throw a TypeError exception.
  5. If lnum is a BigInt, then return BigInt::add(lnum, rnum).
  6. Return Number::add(lnum, rnum).

delete UnaryExpression Spec Algorithm

Spec Section 13.5.1:

13.5.1 The delete Operator

UnaryExpression : delete UnaryExpression

  1. Let ref be ? Evaluation of UnaryExpression.
  2. If ref is not a Reference Record, return true.
  3. If IsUnresolvableReference(ref) is true, then a. Assert: ref.[[Strict]] is false. b. Return true.
  4. 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.
  5. Let base be ref.[[Base]].
  6. Assert: base is an Environment Record.
  7. Return ? base.DeleteBinding(ref.[[ReferencedName]]).

RelationalExpression (in/instanceof) Spec Algorithms

Spec Section 13.10 (The in operator):

The in Operator

  1. Let rval be the right operand.
  2. If rval is not an Object, throw a TypeError exception.
  3. Return ? HasProperty(rval, ToPropertyKey(lval)).

Spec Section 13.10.2 (OrdinaryHasInstance):

OrdinaryHasInstance ( C, O )

  1. If IsCallable(C) is false, return false.
  2. If C has a [[BoundTargetFunction]] internal slot, then a. Let BC be C.[[BoundTargetFunction]]. b. Return ? InstanceofOperator(O, BC).
  3. If O is not an Object, return false.
  4. Let P be ? Get(C, "prototype").
  5. If P is not an Object, throw a TypeError exception.
  6. 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

  1. Let val be the result of evaluating UnaryExpression.
  2. If val is a Reference Record, then a. If IsUnresolvableReference(val) is true, return "undefined". b. Set val to ? GetValue(val).
  3. If val is undefined, return "undefined".
  4. If val is null, return "object".
  5. If val is a Boolean, return "boolean".
  6. If val is a Number, return "number".
  7. If val is a String, return "string".
  8. If val is a Symbol, return "symbol".
  9. If val is a BigInt, return "bigint".
  10. Assert: val is an Object.
  11. If val has a [[Call]] internal method, return "function".
  12. 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

  1. 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.

  2. delete removes properties, not variables: delete obj.prop calls [[Delete]], controlled by [[Configurable]]; delete varName has no effect on variables (returns false); non-configurable properties (like NaN, Math.PI) return false, and strict mode throws TypeError.

  3. in searches the entire prototype chain: 'toString' in {} is true because toString is on Object.prototype. To check own properties, use Object.hasOwn(obj, key) (ES2022+) rather than in.

  4. instanceof depends on the Realm: cross-iframe/Worker instanceof checks fail because different Realms have their own distinct built-in constructors. For cross-Realm checks, use Array.isArray(), Object.prototype.toString.call(), or custom Symbol.hasInstance.

  5. ?. short-circuits the entire chain; ?? only short-circuits for null/undefined: a?.b.c with a being null means b.c is never accessed; but if a has a value and a.b is null, accessing .c still throws — use a?.b?.c. ?? preserves 0, false, and "" as valid values, which is its core difference from ||, and is critical when working with legitimate zero values, empty strings, or booleans.

Rate this chapter
4.8  / 5  (32 ratings)

💬 Comments