运算符的规范算法:+、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 |
检查属性是否存在(含原型链) | 检查自有属性(用 hasOwnProperty 或 Object.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 )
- 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 规范算法
规范第 13.5.1 节:
13.5.1 The delete Operator
Runtime Semantics: Evaluation
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. (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.
- Let base be ref.[[Base]].
- Assert: base is an Environment Record.
- Return ? base.DeleteBinding(ref.[[ReferencedName]]).
RelationalExpression(in/instanceof)规范算法
规范第 13.10.1 节(The in operator):
Runtime Semantics: IsLessThan
The in Operator
- Let rval be the right operand.
- If rval is not an Object, throw a TypeError exception.
- Return ? HasProperty(rval, ToPropertyKey(lval)).
规范第 13.10.2 节(The instanceof operator),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 规范算法
规范第 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 · 边界与陷阱
陷阱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 报错)
本章小结
-
+运算符的规则:有字符串就拼接:规范在调用 ToPrimitive 后,只要任一操作数是字符串,就走字符串拼接路径(ToString + ToString),否则走数值相加路径。这与操作数的原始类型无关,取决于 ToPrimitive 的结果。 -
delete删除属性,不删除变量:delete obj.prop调用[[Delete]],受[[Configurable]]控制;delete varName对变量无效(返回 false);对不可配置属性(如NaN、Math.PI)返回 false,在严格模式下还会 TypeError。 -
in沿整个原型链搜索:'toString' in {}为 true,因为 toString 在 Object.prototype 上。检查自有属性应用Object.hasOwn(obj, key)(ES2022+)而非in。 -
instanceof依赖 Realm:跨 iframe/Worker 的instanceof检查会失败,因为不同 Realm 有各自的内置构造函数。跨 Realm 应使用Array.isArray()、Object.prototype.toString.call(),或自定义Symbol.hasInstance。 -
?.短路整个后续链,??只对 null/undefined 短路:a?.b.c在a为 null 时,b.c也不会执行;但如果a有值而a.b为 null,访问.c仍然 TypeError,需要a?.b?.c。??保留0、false、""这些假值,这是它与||的核心区别,在处理合法的零值/空字符串/false 时至关重要。