ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

JavaScript 的 instanceof 之谜:为什么对象不是由 B() 创建的,`a instanceof B` 却返回 true 文档/教程前端【免费下载链接】en.javascript.infoModern 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每个函数包括构造函数都默认带有一个名为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——返回字符串的高级 typeofinstanceof返回布尔值适合是否属于某类的判断但当我们想要的是类型名字符串时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.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐JavaScript 浮点数精度之谜为什么 6.35.toFixed(1) 返回 6.3 而不是 6.4JavaScript 浮点数精度之谜为什么 6.35.toFixed 1 返回 6.3 而不是 6.4 6.35.toFixed 1 按四舍五入的直觉应当文档/教程前端JavaScript Map 迭代器之谜为什么 map.keys() 返回的不是数组以及如何修复 keys.push is not a functionJavaScript Map 迭代器之谜为什么 map.keys 返回的不是数组以及如何修复 keys.push is not a function 导读文档/教程前端JavaScript 浮点精度陷阱为什么 6.35.toFixed(1) 返回 6.3 而不是 6.4JavaScript 浮点精度陷阱为什么 6.35.toFixed 1 返回 6.3 而不是 6.4 导读 6.35.toFixed 1 返回 6.3 而文档教程前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表