第 11 章

运算符的规范算法:+、delete、in、instanceof、typeof、?.、??

typeof undeclaredVariable 不会报 ReferenceError——这是规范里专门为 typeof 写的特殊豁免,JavaScript 中只有这一个运算符拥有这种特权。

🔹 Level 1 · 你需要知道的

各运算符的意外行为速查

// + 运算符:结果取决于操作数类型,不是你想的那样
1 + "2"         // "12"(字符串拼接,不是相加)
"3" + 4 + 5     // "345"(左到右,先拼接再...拼接)
3 + 4 + "5"     // "75"(先算 3+4=7,再拼接 "5")
[] + {}         // "[object Object]"(两个对象转字符串后拼接)
{} + []         // 0(语句上下文:{} 是代码块,+[] 是一元运算符)

// delete:删除属性,不是变量
const obj = { a: 1 };
delete obj.a    // true(成功删除)
delete obj.b    // true(不存在的属性也返回 true)
let x = 1;
delete x        // false(无法删除变量!)
// delete window.NaN  // false(不可配置属性)

// in:检查原型链,不只是自有属性
'toString' in {}        // true(toString 在 Object.prototype 上)
'length' in []          // true(数组有 length)
'constructor' in {}     // true(来自原型链)

// instanceof:检查原型链上的构造函数
[] instanceof Array     // true
[] instanceof Object    // true(Object 也在原型链上)
// 跨 iframe 失效:
// frameArr instanceof Array  // false(不同 Realm 的 Array 是不同的对象)

// typeof:唯一不对未声明变量报错的运算符
typeof undeclaredVar    // "undefined"(特殊豁免!)
// undeclaredVar          // ReferenceError!(其他情况就会报错)

// ?. 可选链:整链短路,不只是当前节点短路
const obj2 = null;
obj2?.a         // undefined(短路)
obj2?.a.b       // undefined(整个链路短路,不访问 .b)
obj2?.a.b.c     // undefined(整个链路)

// ?? 空值合并:只对 null/undefined 短路,不是所有假值
null ?? "default"       // "default"
undefined ?? "default"  // "default"
0 ?? "default"          // 0(0 不是 null/undefined!)
"" ?? "default"         // ""
false ?? "default"      // false

// 对比 || 运算符(所有假值都触发短路)
0 || "default"          // "default"(0 是假值)
"" || "default"         // "default"("" 是假值)
false || "default"      // "default"(false 是假值)

各运算符适用场景

运算符 推荐用法 避免误用
+ 明确时才拼接/相加;复杂表达式加括号 混合类型操作数不加转换
delete 删除对象的动态属性 删除变量(无效果),删除数组元素(用 splice)
in 检查属性是否存在(含原型链) 检查自有属性(用 hasOwnPropertyObject.hasOwn
instanceof 同一 Realm 内的类型检查 跨 iframe/Worker 的类型检查
typeof 检查未声明变量是否存在;基本类型检查 检查 null(返回 "object");检查数组(返回 "object")
?. 深层属性访问;链式方法调用 明知存在的属性(掩盖错误)
?? 有意义的默认值(只排除 null/undefined) 当 0/false/"" 也需要默认值时(应用 ||

🔸 Level 2 · 它是怎么运行的

+ 运算符的执行步骤

加法/拼接运算符 + 的决策流程:

AdditiveExpression: lval + rval

步骤1:对两个操作数调用 ToPrimitive(hint = "default")
  lprim = ToPrimitive(lval)
  rprim = ToPrimitive(rval)

步骤2:检查是否有字符串类型
  如果 lprim 是 String 或 rprim 是 String:
    → 字符串拼接:ToString(lprim) + ToString(rprim)
  否则:
    → 数值相加:ToNumber(lprim) + ToNumber(rprim)

关键:步骤2是 OR 判断,只要有一个字符串就走拼接路径
// 实际执行过程演示
1 + 2           // ToPrimitive(1)=1, ToPrimitive(2)=2, 无字符串 → 3
1 + "2"         // ToPrimitive(1)=1, ToPrimitive("2")="2", 有字符串 → "1"+"2"="12"
[] + 1          // ToPrimitive([])="", ToPrimitive(1)=1, "" 是字符串 → ""+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 运算符的执行步骤

delete 运算符执行流程:

delete UnaryExpression

1. 对表达式求值,得到 Reference Record
   Reference Record = {
     [[Base]]: 对象或环境记录,
     [[ReferencedName]]: 属性名,
     [[Strict]]: 是否严格模式
   }

2. 如果 Reference 的 [[Base]] 是 unresolvable:
   → 在非严格模式:return true
   → 在严格模式:抛出 ReferenceError

3. 如果 Reference 的 [[Base]] 是 Environment Record(变量声明):
   → 非严格模式:return false(无法删除变量)
   → 严格模式:抛出 SyntaxError

4. 调用 [[Base]].[[Delete]](ReferencedName)
   检查属性的 [[Configurable]]:
   → [[Configurable]] = true:删除属性,return true
   → [[Configurable]] = false:
     非严格模式:return false
     严格模式:抛出 TypeError
// delete 行为演示
const obj = { a: 1 };
Object.defineProperty(obj, 'b', { value: 2, configurable: false });

delete obj.a    // true([[Configurable]] 默认 true)
delete obj.b    // false([[Configurable]] = false)

// 不可配置的全局属性
delete Math.PI  // false(Math.PI 是不可配置的)
delete NaN      // false(window.NaN 是不可配置的)
delete undefined // false(window.undefined 是不可配置的)

// 可配置的全局属性(通过赋值创建)
globalThis.myVar = 42;
delete myVar    // true(可以删除通过赋值创建的全局变量)

// 数组的 delete(不推荐!)
const arr = [1, 2, 3];
delete arr[1];
// arr = [1, empty, 3],length 仍为 3!产生"空洞"
// 正确的删除应该用 splice
arr.splice(1, 1);  // [1, 3],length 为 2

in 运算符的执行步骤

in 运算符执行流程:

RelationalExpression: key in obj

1. 对右操作数求值,如果不是对象 → 抛出 TypeError
2. 调用 obj.[[HasProperty]](key)
3. [[HasProperty]] 沿原型链向上查找:
   - 检查 obj 的自有属性
   - 如果没有,沿 [[Prototype]] 向上继续
   - 直到 null(链末端)
4. 找到则返回 true,否则返回 false
// in 与 hasOwn 的区别
const child = Object.create({ inherited: true });
child.own = 1;

'own' in child           // true(自有属性)
'inherited' in child     // true(继承属性!)
'toString' in child      // true(来自 Object.prototype)

Object.hasOwn(child, 'own')          // true
Object.hasOwn(child, 'inherited')    // false(只检查自有!)
Object.hasOwn(child, 'toString')     // false

// 常见误解:认为 in 只检查自有属性
const arr = [1, 2, 3];
0 in arr          // true(索引0是自有属性)
'push' in arr     // true!(push 在 Array.prototype 上)
'length' in arr   // true(length 是数组的自有属性)

instanceof 的执行步骤

instanceof 运算符执行流程:

RelationalExpression: obj instanceof Constructor

1. 检查 Constructor[Symbol.hasInstance] 是否存在
   → 存在:调用 Constructor[Symbol.hasInstance](obj),返回结果
   → 不存在:

2. 执行 OrdinaryHasInstance(Constructor, obj):
   a. 如果 Constructor 没有 [[Call]] → TypeError(不是函数)
   b. 如果 Constructor 有 [[BoundTargetFunction]]:
      → 取 bound function 的目标,继续
   c. 获取 Constructor.prototype
   d. 沿 obj 的 [[Prototype]] 链逐级向上:
      - 如果某个 [[Prototype]] === Constructor.prototype → true
      - 如果 [[Prototype]] 为 null → false(链末端,未找到)
// instanceof 跨 Realm 失败的原因
// 浏览器中的 iframe 示例:
// const arr = new iframe.contentWindow.Array();
// arr instanceof Array    // false!
// 因为:arr 的原型链上是 iframe 的 Array.prototype
// 而不是当前 Realm 的 Array.prototype

// 正确的跨 Realm 检测方法
Array.isArray(arr)      // true(内部检查 [[Class]],不依赖 Realm)
typeof arr === 'object' // true(基本类型检查)
Object.prototype.toString.call(arr)  // "[object Array]"(tag 检查)

// 自定义 instanceof 行为
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num >= this.start && num <= this.end;
  }
}
// 注意:这里 this 是 Range 类,不是 num
// 上面的写法有问题,更合适的写法:

const between0and100 = {
  [Symbol.hasInstance](num) {
    return typeof num === 'number' && num >= 0 && num <= 100;
  }
};
50 instanceof between0and100   // true
150 instanceof between0and100  // false

typeof 的特殊豁免

typeof 对未声明变量的特殊处理:

typeof UnaryExpression

1. 对表达式求值
   - 如果得到 Reference Record 且 [[Base]] 是 unresolvable:
     → 返回 "undefined"(不抛 ReferenceError!)
   - 否则执行 GetValue(Reference),可能抛 ReferenceError

2. 对值执行类型检测:
   Undefined → "undefined"
   Null → "object"
   Boolean → "boolean"
   Number → "number"
   String → "string"
   Symbol → "symbol"
   BigInt → "bigint"
   Object(无[[Call]])→ "object"
   Object(有[[Call]])→ "function"

?. 可选链的执行步骤

可选链 ?. 的短路机制:

a?.b.c.d  等价于:

if (a == null) {  // null 或 undefined
  undefined
} else {
  a.b.c.d  ← 整个后续链在 a != null 时全部执行
}

重要:短路点是 a 处,而不是 a?.b 处!
如果 a 为 null,整个 .b.c.d 都不执行。
如果 a 有值但 a.b 是 null,访问 a.b.c 会报 TypeError!
// 可选链的正确理解
const obj = {
  a: null
};

obj?.a          // null(a 存在,值是 null)
obj?.a.b        // TypeError!(a 是 null,.b 不受 ?. 保护)
obj?.a?.b       // undefined(需要在每个可能为 null 的地方加 ?.)
obj?.b          // undefined(b 不存在)
obj?.b.c        // undefined(整链短路)

// ?. 的多种形式
obj?.['key']    // 可选属性访问(方括号形式)
arr?.[0]        // 可选数组索引
func?.()        // 可选函数调用(func 可能不是函数)
obj?.method()   // 可选方法调用

// 短路后的赋值
let result = obj?.nonexist?.nested ?? "default";
// obj?.nonexist → undefined
// undefined?.nested → undefined(可以继续链)
// undefined ?? "default" → "default"

🔺 Level 3 · 规范怎么定义的

AdditiveExpression(加法/拼接)规范算法

规范第 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 规范算法

规范第 13.5.1 节:

13.5.1 The delete Operator

Runtime Semantics: Evaluation

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. (Private fields cannot be deleted) 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)规范算法

规范第 13.10.1 节(The in operator):

Runtime Semantics: IsLessThan

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

规范第 13.10.2 节(The instanceof operator),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 规范算法

规范第 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 · 边界与陷阱

陷阱1:typeof undeclaredVar === 'undefined'(不报错)

// 普通变量访问:未声明变量会 ReferenceError
console.log(undeclaredVar);  // ReferenceError: undeclaredVar is not defined

// typeof 的特殊豁免:未声明变量返回 "undefined"
typeof undeclaredVar         // "undefined"(不报错!)

// 实际应用:特性检测(检查全局 API 是否存在)
if (typeof fetch === 'undefined') {
  // fetch API 不可用,使用 polyfill
  window.fetch = myFetch;
}

// 检查 Node.js 环境
if (typeof process === 'undefined') {
  console.log("不是 Node.js 环境");
}

// 模块内的安全检测(不依赖 window)
function isNodejs() {
  return typeof process !== 'undefined' &&
         typeof process.versions !== 'undefined' &&
         typeof process.versions.node !== 'undefined';
}

陷阱2:delete window.NaN 为 false

// NaN 是 window 上的不可配置属性
Object.getOwnPropertyDescriptor(globalThis, 'NaN');
// { value: NaN, writable: false, enumerable: false, configurable: false }

delete window.NaN      // false(不可配置!)
delete window.Infinity // false(同样不可配置)
delete window.undefined // false(同样不可配置)

// 可以验证:
window.NaN = 42;     // 静默失败(严格模式会 TypeError)
console.log(NaN);    // NaN(没变!)

// 但通过 var/赋值 添加的全局变量可以删除
window.myCustomProp = "hello";
delete window.myCustomProp  // true
var myVarDecl = "world";
delete myVarDecl            // false(var 声明的变量不可删除)

陷阱3:'toString' in {} 为 true

// in 检查整个原型链
'toString' in {}         // true(Object.prototype 上有 toString)
'hasOwnProperty' in {}  // true(Object.prototype 上有)
'constructor' in {}      // true(Object.prototype 上有)
'__proto__' in {}        // true(大多数环境中)

// 检查自有属性的正确方式
const obj = { a: 1 };
Object.hasOwn(obj, 'a')          // true(ES2022+,推荐)
obj.hasOwnProperty('a')          // true(老方式,但可能被重写)
Object.prototype.hasOwnProperty.call(obj, 'a')  // 最安全的写法

// 为什么 obj.hasOwnProperty 可能失效?
const safeObj = Object.create(null);  // 无 Object.prototype 的纯对象
safeObj.key = 1;
safeObj.hasOwnProperty('key')  // TypeError! safeObj 没有 hasOwnProperty
Object.hasOwn(safeObj, 'key')  // true(正确!Object.hasOwn 不依赖原型)

// in 在 DOM 中的实用性
'style' in document.body         // true(元素有 style 属性)
'nonexistent' in document.body   // false(不存在的属性)

陷阱4:跨 iframe 的 instanceof 失效

// 单页面应用中内嵌 iframe 时,这是常见问题
// iframe 拥有自己独立的 JavaScript 执行环境(Realm)

// 假设场景:
// const iframe = document.getElementById('myFrame');
// const iframeWindow = iframe.contentWindow;

// const iframeArr = iframeWindow.eval('[1, 2, 3]');
// iframeArr instanceof Array  // false!
// 原因:iframeArr 的原型链:
//   iframeArr → iframeWindow.Array.prototype → iframeWindow.Object.prototype → null
// 而 Array(当前Realm)是另一个对象,两者不同!

// 解决方案:
// 1. Array.isArray(内部检查 [[IsArray]],跨 Realm 安全)
// Array.isArray(iframeArr)  // true

// 2. Object.prototype.toString.call(基于 @@toStringTag)
// Object.prototype.toString.call(iframeArr)  // "[object Array]"

// 3. 比较构造函数名(不够严格但实用)
// iframeArr.constructor.name === 'Array'  // true(但名字可以伪造)

// 现代替代方案(跨 Realm 的类型检查)
function isArraySafe(val) {
  return Array.isArray(val);  // 最推荐
}
function isObjectSafe(val) {
  return val !== null && typeof val === 'object';  // 简单类型检查
}
function isTypeSafe(val, type) {
  return Object.prototype.toString.call(val) === `[object ${type}]`;
}
isTypeSafe([1,2,3], 'Array')  // true(任何 Realm 的数组都匹配)

陷阱5:?? vs || 的关键差异

// || 对所有"假值"短路(0, "", false, null, undefined, NaN)
const count = 0;
const total1 = count || 100;  // 100(0 是假值,被替换)

// ?? 只对 null 和 undefined 短路
const total2 = count ?? 100;  // 0(0 不是 null/undefined,保留)

// 实际业务示例:分页组件
function Pagination({ page, pageSize, total }) {
  const currentPage = page || 1;      // ❌ 如果 page=0,会被替换为1(虽然0不是合法页码)
  const size = pageSize || 10;        // ❌ 如果 pageSize=0,会被替换为10
  const currentPage2 = page ?? 1;    // ✅ 只有 page 是 null/undefined 时才用默认值
  const size2 = pageSize ?? 10;      // ✅ 正确
}

// ?? 与 && 的组合
const value = null;
value ?? "default"           // "default"
value ?? false               // false!(null → "default" 是 ??,null → false 也是 ??)
// 因为 false 不是 null/undefined,所以不会被 ?? 的右边替换
// 等等,null ?? false 的意思是:如果 value 是 null,返回 false
null ?? false    // false(null 触发 ??,返回右边的 false)

// 更复杂的场景
const config = {
  timeout: 0,      // 合法的超时值(0 = 不超时)
  retries: null,   // 明确未设置
};
const timeout = config.timeout ?? 5000;   // 0(保留,因为 0 不是 nullish)
const retries = config.retries ?? 3;      // 3(null 触发默认值)

// ❌ 危险:?? 和 && 或 || 不能直接混用(需要括号)
// true || false ?? false  // SyntaxError! 需要括号明确优先级
(true || false) ?? false  // true
true || (false ?? false)  // true

陷阱6:a?.b.c 的短路位置

// 容易混淆的短路行为
const a = null;

a?.b        // undefined(a 是 null,短路)
a?.b.c      // undefined(a 是 null,整个 .b.c 都不执行!)
a?.b.c.d    // undefined(同样,整个后续链路不执行)

// 但如果 a 有值:
const b = { x: null };
b?.x        // null(b 有值,返回 x 的值 null)
b?.x.y      // TypeError!(b 有值所以不短路,x 是 null,访问 .y 报错)
b?.x?.y     // undefined(需要在每个可能 null 的地方都加 ?.)

// 正确的写法:每个"可能为 null"的节点都需要 ?.
const user = {
  profile: null
};
user?.profile?.avatar?.url   // undefined(安全)
user?.profile.avatar.url     // TypeError!(profile 是 null)

// 带函数调用的可选链
const obj2 = {
  method: null
};
obj2.method?.()       // undefined(method 是 null,不调用)
obj2.noMethod?.()     // undefined(noMethod 不存在,不调用)
obj2?.method()        // TypeError!(obj2 有值,method 是 null,调用 null 报错)

本章小结

  1. + 运算符的规则:有字符串就拼接:规范在调用 ToPrimitive 后,只要任一操作数是字符串,就走字符串拼接路径(ToString + ToString),否则走数值相加路径。这与操作数的原始类型无关,取决于 ToPrimitive 的结果。

  2. delete 删除属性,不删除变量delete obj.prop 调用 [[Delete]],受 [[Configurable]] 控制;delete varName 对变量无效(返回 false);对不可配置属性(如 NaNMath.PI)返回 false,在严格模式下还会 TypeError。

  3. in 沿整个原型链搜索'toString' in {} 为 true,因为 toString 在 Object.prototype 上。检查自有属性应用 Object.hasOwn(obj, key)(ES2022+)而非 in

  4. instanceof 依赖 Realm:跨 iframe/Worker 的 instanceof 检查会失败,因为不同 Realm 有各自的内置构造函数。跨 Realm 应使用 Array.isArray()Object.prototype.toString.call(),或自定义 Symbol.hasInstance

  5. ?. 短路整个后续链,?? 只对 null/undefined 短路a?.b.ca 为 null 时,b.c 也不会执行;但如果 a 有值而 a.b 为 null,访问 .c 仍然 TypeError,需要 a?.b?.c?? 保留 0false"" 这些假值,这是它与 || 的核心区别,在处理合法的零值/空字符串/false 时至关重要。

本章评分
4.8  / 5  (32 评分)

💬 留言讨论