如何手写 instanceof?
简化版
常规情况下,left instanceof Right 会判断 Right.prototype 是否出现在 left 的原型链上:从 Object.getPrototypeOf(left) 开始逐层向上找,命中返回 true,到 null 返回 false。但完整语义还允许右侧通过 Symbol.hasInstance 自定义判断。
详细版
只覆盖普通构造函数的基础版:
function ordinaryInstanceof(left, Right) {
if (typeof Right !== 'function') throw new TypeError('Right must be callable')
if (left === null || (typeof left !== 'object' && typeof left !== 'function')) {
return false
}
const target = Right.prototype
if ((typeof target !== 'object' && typeof target !== 'function') || target === null) {
throw new TypeError('Right.prototype must be an object')
}
for (let current = Object.getPrototypeOf(left); current !== null; current = Object.getPrototypeOf(current)) {
if (current === target) return true
}
return false
}
这解释了原型链主体,却没覆盖自定义 Symbol.hasInstance 和绑定函数。面试回答应主动区分“普通构造器算法”和完整运算符语义。
完整版教学
一、instanceof 判断的是原型身份
function Person() {}
const ada = new Person()
ada instanceof Person // true
原因不是对象里存了字符串类型,而是:
Object.getPrototypeOf(ada) === Person.prototype
| 左侧 | 右侧 | 常见结果 |
|---|---|---|
new Person() | Person | true |
{} | Person | false |
1 | Number | false |
Object.create(Person.prototype) | Person | true |
最后一行说明 instanceof 通常不能证明对象真的执行过构造函数;只要原型身份吻合就可能为真。
完整算法会先查右侧的
Symbol.hasInstance。只有没有自定义方法时,才退回普通函数的原型链判断,不能把后者误写成运算符的全部语义。
二、普通原型链算法
function ordinaryInstanceof(left, Right) {
if (typeof Right !== 'function') throw new TypeError('Right must be callable')
if (left === null || (typeof left !== 'object' && typeof left !== 'function')) {
return false
}
const target = Right.prototype
if (target === null || (typeof target !== 'object' && typeof target !== 'function')) {
throw new TypeError('Right.prototype must be an object')
}
let current = Object.getPrototypeOf(left)
while (current !== null) {
if (current === target) return true
current = Object.getPrototypeOf(current)
}
return false
}
必须从左侧的原型开始,而不是从左侧本身开始。比较必须使用对象身份,不能比较构造器名称或把原型对象做深比较。
三、补上 Symbol.hasInstance
右侧只要是对象,就有机会提供自定义方法:
class EvenNumber {
static [Symbol.hasInstance](value) {
return typeof value === 'number' && value % 2 === 0
}
}
2 instanceof EvenNumber // true
较完整的教学实现为:
function myInstanceof(left, right) {
const rightIsObject = right !== null &&
(typeof right === 'object' || typeof right === 'function')
if (!rightIsObject) throw new TypeError('right operand must be an object')
const method = right[Symbol.hasInstance]
if (method != null) {
if (typeof method !== 'function') {
throw new TypeError('Symbol.hasInstance must be callable')
}
return Boolean(Reflect.apply(method, right, [left]))
}
return ordinaryInstanceof(left, right)
}
普通函数从 Function.prototype 继承了默认的 Symbol.hasInstance,调用它最终仍执行普通构造器检查;自定义静态方法则能完全改变判定标准。
四、异常边界为什么重要
({}) instanceof 1 // TypeError:右侧不是对象
({}) instanceof {} // TypeError:没有 hasInstance,且右侧不可调用
({}) instanceof (() => {}) // TypeError:箭头函数没有对象类型的 prototype
“右侧必须是构造函数”是便于记忆的说法,但更精确的入口是:右侧先要是对象;若有可调用的 @@hasInstance 就使用它,否则必须是函数并走普通算法。
函数可调用也不代表可构造。箭头函数可调用,却没有正常的 prototype 对象,因此默认检查会抛错。
五、修改 prototype 会改变未来结果
function A() {}
const oldObject = new A()
A.prototype = {}
const newObject = new A()
oldObject instanceof A // false
newObject instanceof A // true
instanceof 在判断当下读取当前的 A.prototype。旧实例仍指向旧原型对象,因此替换整个 prototype 会让旧实例不再匹配。若只是给原型新增方法而不替换对象,身份不变,结果也不变。
这也是为什么不能缓存“构造时的类名”来模拟 instanceof。
六、跨 realm、绑定函数与品牌检查
iframe 等不同 realm 有各自的内建构造器和原型。另一个窗口创建的数组可能不满足当前窗口的 value instanceof Array,因为两个 Array.prototype 不是同一对象;数组检测应优先 Array.isArray(value)。
绑定函数的默认 instanceof 会穿透到目标函数:
function User() {}
const BoundUser = User.bind(null)
new User() instanceof BoundUser // true
另外,伪造 Object.create(C.prototype) 能通过普通检查,却不具有类的私有字段品牌。安全或能力判断不应只依赖 instanceof;内建品牌检查、私有字段 #x in obj 或显式校验往往更可靠。
七、常见误区与追问
- 误区:
instanceof能证明构造函数确实执行过。 手动把对象原型设为Ctor.prototype也能通过普通检查。 - 误区:右侧只要
typeof === 'function'就不会报错。 箭头函数虽可调用,但默认算法读取到非对象 prototype 时仍会抛错。 - 误区:只沿原型链查找就是完整实现。 运算符会优先调用右侧的
Symbol.hasInstance。 - 追问:为什么基本类型左值返回 false? 普通算法只在对象原型链上查找,原始值没有可遍历的对象原型链。
- 追问:为什么跨 iframe 的数组判断会失败? 每个 realm 拥有不同的
Array.prototype对象,身份比较不相等。 - 追问:重设构造函数 prototype 后旧实例怎样? 旧实例仍连着旧原型,因此通常不再满足对新 prototype 的检查。
- 追问:类型判断应该一律用
instanceof吗? 不应;数组用Array.isArray,结构数据可做字段校验,安全品牌要用更可靠机制。
八、加强记忆
- 主体:普通算法查的是
Right.prototype在不在左值原型链。 - 起点:从
Object.getPrototypeOf(left)开始,不比较 left 本身。 - 优先级:完整运算符先看
Symbol.hasInstance。 - 异常:右侧非对象、不可调用或 prototype 非对象都可能抛
TypeError。 - 身份:替换 prototype 会影响旧实例,跨 realm 原型身份也不同。
- 边界:
instanceof不是可靠的构造证明,更不是通用安全品牌检查。