先记住这个答案
x instanceof C 并不是直接查原型链。引擎先去 C 身上找 Symbol.hasInstance 方法,找到就调用 C[Symbol.hasInstance](x),返回值转成布尔就是结果。普通函数都从 Function.prototype 继承了一个默认实现,默认实现才去做原型链查找。所以只要在 class 里写 static [Symbol.hasInstance](value),就能让 instanceof 按你的规则判断。
hasInstance可改写实例判断- 用静态符号方法定义匹配钩子
- 改写后原型链判断被整个替换掉
- 右边不是对象会抛
TypeError
用静态方法接管 instanceof
最直观的例子:定义一个 Even 类,让 instanceof Even 判断一个数是不是偶数。
instanceof 判断偶数JavaScriptclass Even {
// instanceof 右边是 Even 时,会调用这个静态方法
static [Symbol.hasInstance](value) {
return Number.isInteger(value) && value % 2 === 0
}
}
console.log(2 instanceof Even) // 原始值也能判断了
console.log(3 instanceof Even)
console.log(new Even() instanceof Even) // 原型链判断被整个替换掉了查看输出与解释
true
false
false第一行说明原始值也能参与判断。正常情况下 2 instanceof Number 是 false,因为原始值没有原型链可查,但自定义的方法想怎么判断都行。最后一行容易被忽略:new Even() 明明是 Even 的实例,结果却是 false。因为方法里只判断了数字,原来的原型链逻辑已经不会执行了。
实际能拿它做什么
真实项目里很少为了炫技改 instanceof,常见的是下面两种需求。
- 鸭子类型判断
- 「鸭子类型」是说不管对象是谁创建的,只要长得像就当它是。比如定义一个
Iterable类,static [Symbol.hasInstance](obj)里判断obj != null && typeof obj[Symbol.iterator] === 'function'。之后[] instanceof Iterable、new Map() instanceof Iterable、字符串对象都能返回 true,写类型守卫时读起来很自然。 - 兼容跨 iframe 或多份库的对象
instanceof的原型链判断有个老问题:每个 iframe 有自己的一套全局对象,iframe 里创建的数组,它的原型是那个 iframe 的Array.prototype,在主页面用instanceof Array判断就是 false。项目里打包进两份同一个库也会有类似情况。这时可以在自己的类上改写Symbol.hasInstance,改成检查一个品牌标记,比如对象上有没有某个特定 Symbol 属性,不再依赖原型对象是不是同一个。数组本身直接用Array.isArray就行,它不受 iframe 影响。
容易答错的地方
- 直接给函数赋值
Symbol.hasInstance - 写
Foo[Symbol.hasInstance] = () => true不会生效。Foo从Function.prototype继承的那个Symbol.hasInstance是不可写的,按规则,继承来的属性不可写时,给子对象赋同名属性也会失败。非严格模式下静默忽略,严格模式下抛TypeError。正确做法是 class 里用static声明,或者用Object.defineProperty(Foo, Symbol.hasInstance, { value: fn })。 - 以为方法定义在实例原型上也有用
instanceof只查右边那个对象,也就是构造函数本身。把[Symbol.hasInstance]写成实例方法,它在C.prototype上,instanceof不会去看,所以一定要写static。- 以为右边必须是函数
- 有了
Symbol.hasInstance之后,右边只要是对象就行。x instanceof { [Symbol.hasInstance]: v => true }合法,返回 true。但右边既不是函数、又没有这个方法时会抛TypeError。
面试官还会怎么问?
手写 instanceof 时要不要考虑 Symbol.hasInstance?
面试手写通常只写原型链那部分:用 Object.getPrototypeOf 从左边对象一层层往上找,遇到等于右边 prototype 的就返回 true,找到 null 返回 false。能补一句「完整实现要先检查右边的 Symbol.hasInstance」,说明你知道规范里的真实顺序。
除了 Symbol.hasInstance,还有哪些 well-known Symbol 能改语言行为?
常见的有 Symbol.iterator 控制 for...of 和展开运算符,Symbol.toPrimitive 控制对象转原始值,Symbol.toStringTag 控制 Object.prototype.toString 输出的类型标签。它们都是给对象开的「钩子」,让语言内置操作调用你定义的方法。
改写 instanceof 有什么风险?
它会让一个看起来很基础的运算符变得不可预测,读代码的人很难想到 instanceof 被改过。TypeScript 的类型收窄也只看类的声明,不知道你的自定义规则,可能推出错误的类型。所以除非是库内部需要统一判断,一般直接写一个 isXxx 函数更清楚。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。