news 2026/10/7 20:16:39

JavaScript 的 instanceof 之谜:为什么对象不是由 B() 创建的,`a instanceof B` 却返回 true

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript 的 instanceof 之谜:为什么对象不是由 B() 创建的,`a instanceof B` 却返回 true
  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

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

  1. 每个函数(包括构造函数)都默认带有一个名为prototype的普通属性;
  2. 当执行new F()时,JavaScript 会创建新对象,并把F.prototype赋给该对象的隐藏属性[[Prototype]](可通过__proto__读取,见 原型继承一章);
  3. 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

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 20:16:03

【WorkBuddy从入门到精通实战教程】实战案例 第 80 章 从能用到好用:工作台的三阶段迭代

【WorkBuddy从入门到精通实战教程】实战案例 第 80 章 从能用到好用:工作台的三阶段迭代 一、搭了三个月的工作台,用的人越来越少 一位做内容运营的同学,花了不少力气搭了一个内容管理工作台:选题表、排期表、数据表、素材库,一应俱全。 前两周团队新鲜感强,用得挺勤。…

作者头像 李华
网站建设 2026/10/7 20:15:27

10分钟搞懂e2e:让测试跟上发布速度的AI E2E框架

10分钟搞懂e2e:让测试跟上发布速度的AI E2E框架 【免费下载链接】e2e Next generation e2e testing framework for web and mobile apps. 项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e e2e 是一款面向 Web 和移动应用的新一代开源 AI E2E 测试框…

作者头像 李华
网站建设 2026/10/7 20:14:42

Agent Skills 工程化实战:从提示词封装到 GKE 容器化部署

1. 从"skills"这个模糊词说起:它到底指什么第一次看到"skills"这个标题,加上一堆热搜词里混着 Google Cloud、Agent Skills、npx、GKE,我脑子里第一反应是:这大概率不是指人类职业技能,而是指智能…

作者头像 李华
网站建设 2026/10/7 20:12:39

TMAC v6.0.7 实战:Windows 网卡 MAC 地址修改原理、避坑与批量脚本

简介:TMACv6.0.7 是一款面向 Windows 平台的 MAC 地址修改工具,全称 Terminal MAC Address Changer,兼容 XP、Win7、Win8 与 Win10 系统。它适合网络管理员、测试人员及注重隐私的普通用户,用于在局域网中临时或永久更换网卡物理地…

作者头像 李华
网站建设 2026/10/7 20:12:16

VS Code MCP 服务接入 TaoToken:把 Base URL 改到统一 Key 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华