- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
instanceof是 JavaScript 中用于判断对象是否属于某个类的运算符,但它真正检查的并不是构造函数本身,而是原型链上与Class.prototype的匹配关系。本文基于《Modern JavaScript Tutorial》中 1-js/09-classes/06-instanceof 一节的 "Strange instanceof" 任务,从"看似诡异"的代码出发,逐步拆解instanceof的底层算法,说明prototype才是类型判定的真正依据,并延伸到Symbol.hasInstance自定义逻辑、原型替换后的连锁效应,以及与Object.prototype.toString的对比选型。
任务原文:一段"奇怪"的代码
原任务(task.md)给出了这样一段代码:
function A() {} function B() {} A.prototype = B.prototype = {}; let a = new A(); alert( a instanceof B ); // true第一眼看上去确实违反直觉:a明明是通过new A()创建的,构造函数是A,为什么a instanceof B却返回true?我们完全可以"看出"a不是由B()构造出来的。要解释这个现象,必须先弄清楚instanceof到底在比较什么。
instanceof不看构造函数,只看原型链
原任务的官方解答(solution.md)一语道破:
instanceof并不关心函数本身,它关心的是函数的prototype,并将该prototype与对象的原型链进行匹配。 在这里a.__proto__ == B.prototype,所以instanceof返回true。 因此,按照instanceof的逻辑,真正定义类型的是prototype,而不是构造函数。
也就是说,a instanceof B的判定与B这个函数是否"构造"过a完全无关,它只回答一个问题:B.prototype是否出现在a的原型链上?
在本例中,A.prototype = B.prototype = {}这一行让两个构造函数共享了同一个原型对象。而new A()创建对象时,new运算符会把A.prototype赋给新对象的[[Prototype]](参见 F.prototype 一章 对new F()机制的说明)。于是:
a.__proto__ === A.prototype成立;- 而
A.prototype === B.prototype(两者是同一个对象引用); - 所以
a.__proto__ === B.prototype也成立; instanceof在原型链第一步就匹配成功,返回true。
逐行拆解new A()到底做了什么
要彻底理解上面的结论,需要回顾构造函数与prototype属性的协作机制(详见 1-js/08-prototypes/02-function-prototype/article.md):
- 每个函数(包括构造函数)都默认带有一个名为
prototype的普通属性; - 当执行
new F()时,JavaScript 会创建新对象,并把F.prototype赋给该对象的隐藏属性[[Prototype]](可通过__proto__读取,见 原型继承一章); F.prototype只在new F()被调用的那一刻生效,之后对F.prototype的改写不会追溯影响已创建对象。
对照本任务的代码,执行顺序是:
function A() {} function B() {} // 关键一行:让两个函数共享同一个原型对象 A.prototype = B.prototype = {}; // new 的时刻:a.[[Prototype]] = A.prototype = {}(也就是 B.prototype) let a = new A(); // 原型链第一步:a.__proto__ === B.prototype => true alert( a instanceof B ); // true注意这里A.prototype = B.prototype = {}的赋值是自右向左执行的:先把{}赋给B.prototype,再把同一个对象引用赋给A.prototype。因此二者指向同一个对象,而非两个内容相同但互不相等的对象。这正是"诡异"现象的根源——两个构造函数共享了同一份原型。
instanceof的标准算法:沿着原型链逐级比对
obj instanceof Class的完整算法在 06-instanceof/article.md 中有详细说明,分为两步:
第一步:检查Symbol.hasInstance静态方法。如果Class上定义了静态方法Symbol.hasInstance,则直接调用ClassSymbol.hasInstance,用其返回值(true/false)作为最终结果,instanceof的默认行为被完全接管。
第二步:默认逻辑——原型链比对。大多数类没有定义Symbol.hasInstance,此时按标准逻辑执行,等价于逐级比较:
obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // 任意一步为 true,立即返回 true // 走到链尾仍无匹配,返回 false文中还给出了一个继承场景的验证:rabbit instanceof Animal在第二步匹配成功,因为rabbit.__proto__是Rabbit.prototype,而rabbit.__proto__.__proto__ === Animal.prototype。下面这张出自本章的示意图直观展示了rabbit instanceof Animal与Animal.prototype逐级比较的过程:
由此可以得出一个关键等价式:obj instanceof Class实质上等价于Class.prototype.isPrototypeOf(obj)。构造函数本身(包括其内部的代码、名字、是否真的构造过该对象)完全不参与判定——"类型"完全由原型对象决定。
用代码验证:改写原型后,"兔子"不再是"兔子"
理解了"真正定义类型的是 prototype"之后,另一个经典现象就顺理成章了:在对象创建之后替换Class.prototype,会导致已存在的对象"失去"该类身份。文章中的示例:
function Rabbit() {} let rabbit = new Rabbit(); // 创建 rabbit 之后才替换原型 Rabbit.prototype = {}; // ...不再是兔子了! alert( rabbit instanceof Rabbit ); // false原因正是new时刻rabbit.__proto__被固定为旧的Rabbit.prototype对象;此后Rabbit.prototype被替换成新对象,rabbit的原型链上自然再也找不到新的Rabbit.prototype,于是instanceof返回false。这与本任务中的现象互为镜像:任务里两个函数共享原型导致"不是 B 构造的也算 B 的实例";这里则是"是 B 构造的,但原型被换掉后就不再算 B 的实例"。两者共同印证同一个结论——身份跟随 prototype,而非构造函数。
需要说明的是:__proto__属于规范 Annex B(主要面向浏览器)的历史写法,现代代码推荐使用Object.getPrototypeOf/Object.setPrototypeOf/Object.create来读写原型(参见 1-js/08-prototypes/04-prototype-methods/article.md)。上面的示例使用__proto__仅用于直观展示比对逻辑,生产代码中应改用Object.getPrototypeOf(a) === B.prototype来显式验证。
延伸一:用Symbol.hasInstance自定义instanceof行为
既然instanceof默认走"原型链比对",那能否让它"不看原型、只看属性"?可以——通过静态方法Symbol.hasInstance完全接管判定。Symbol.hasInstance是规范定义的 well-known symbol 之一(参见 1-js/04-object-basics/08-symbol/article.md 中的系统 symbol 列表),它属于类静态成员,定义方式与 static 方法 一致。
文章中的示例:
// 自定义 instanceof 判定:只要对象有 canEat 属性,就视为动物 class Animal { static Symbol.hasInstance { if (obj.canEat) return true; } } let obj = { canEat: true }; alert(obj instanceof Animal); // true: AnimalSymbol.hasInstance 被调用注意此时obj与Animal.prototype毫无原型关系,判定完全来自自定义静态方法。这从另一个侧面印证:instanceof的语义本质上是"由Class侧定义的一种可定制判定协议",默认实现恰好是原型链匹配罢了。
延伸二:Object.prototype.toString——返回字符串的"高级 typeof"
instanceof返回布尔值,适合"是否属于某类"的判断;但当我们想要的是类型名字符串时,Object.prototype.toString是更合适的选择。它比instanceof更强的点在于:不仅能识别自定义类和内建对象,还能通过Symbol.toStringTag自定义输出。
let s = Object.prototype.toString; alert( s.call(123) ); // [object Number] alert( s.call(null) ); // [object Null] alert( s.call([]) ); // [object Array]自定义标签的写法:
let user = { [Symbol.toStringTag]: "User" }; alert( {}.toString.call(user) ); // [object User]浏览器环境中的内建对象通常自带Symbol.toStringTag(如window[Symbol.toStringTag]为"Window"),因此{}.toString.call(window)返回[object Window]。其内部原理是:toString算法读取this的Symbol.toStringTag属性,把结果包进[object ...]中。
三种类型检查手段的取舍
结合原文章末尾的总结表,三种手段各有适用场景:
| 手段 | 适用对象 | 返回值 |
|---|---|---|
typeof | 原始类型(primitive) | 字符串 |
{}.toString | 原始类型、内建对象、带Symbol.toStringTag的对象 | 字符串 |
instanceof | 对象(考虑继承链) | true/false |
- 需要判断"是否为原始类型"时用
typeof; - 需要拿到"类型名字符串"(尤其是内建对象,如区分
Array与Object)时用{}.toString.call; - 需要在类继承层级中判断归属(例如
rabbit instanceof Animal对子类实例也返回true,参见 class 继承)时,instanceof是唯一直接支持继承语义的选择。
小结:记住 prototype 才是"类型"的化身
回到开头的任务,最值得记住的一句话就是:在instanceof的视角里,类型不是构造函数,而是构造函数所挂载的prototype对象。a instanceof B === true并不要求a由B()创建,只要求B.prototype出现在a的原型链上。本任务中A.prototype = B.prototype = {}让两个构造函数共享原型,从而"制造"出了这个看似诡异、实则完全符合规范的结果。理解了这一机制,你在阅读或编写涉及原型替换、类继承、多态分发的代码时,就能准确预判instanceof的每一次判定。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
JavaScript 浮点数精度之谜:为什么 `6.35.toFixed(1)` 返回 `6.3` 而不是 `6.4`
JavaScript 浮点数精度之谜:为什么 6.35.toFixed 1 返回 6.3 而不是 6.4 6.35.toFixed 1 按"四舍五入"的直觉应当
文档/教程前端JavaScript Map 迭代器之谜:为什么 `map.keys()` 返回的不是数组,以及如何修复 `keys.push is not a function`
JavaScript Map 迭代器之谜:为什么 map.keys 返回的不是数组,以及如何修复 keys.push is not a function 导读
文档/教程前端JavaScript 浮点精度陷阱:为什么 6.35.toFixed(1) 返回 6.3 而不是 6.4
JavaScript 浮点精度陷阱:为什么 6.35.toFixed 1 返回 6.3 而不是 6.4 导读 6.35.toFixed 1 返回 "6.3" 而
文档教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考