浅析instanceof 原理
Category(分类): JavaScript Status: 已整理
本文保留了原文(2020 年)从「变量类型 → typeof/instanceof → 原型与原型链 → instanceof 原理」的讲解主线,并补充了手写
myInstanceof、Symbol.hasInstance、跨 realm 等现代边界。文末附勘误清单。
概述
JS 中可以通过 typeof 操作符和 instanceof 操作符判断变量的类型。typeof 在判断(大多数)基本类型的变量时能精准判断,但当变量是数组、对象时都只会返回 object(typeof null 也是 object),无法准确说明该对象是什么类型的。而 instanceof 基于原型链可以判断一个引用是否属于某构造函数,能弥补 typeof 判断引用类型的缺点。本文旨在分析 instanceof 的底层原理,同时借此复习原型以及原型链的知识。
一、JS 中变量类型
本节先来回顾一下 JS 的变量类型,JS 变量可以分为基本类型(原始值)和引用类型。
(1)基本类型:字符串、数值、布尔值、undefined、null、Symbol(ES2015)、BigInt(ES2020)
(2)引用类型:对象(Object,包括数组、函数、Date、Map、Set 等)
基本类型和引用类型的区别
基本类型:
1、原始值本身按值保存和复制;引用类型的变量保存的是指向堆中对象的引用(地址),这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。(原文的「值在栈中、对象在堆中」是常见的教学简化,理解「复制值 vs 复制引用」即可。)
2、保存与复制的是值本身
3、使用 typeof 检测数据的类型(注意 typeof null === 'object' 是历史遗留问题)
引用类型:
1、对象保存在堆中,变量持有指向对象的引用
2、保存与复制的是指向对象的一个引用
3、使用 instanceof 检测数据类型
4、使用 new() 方法构造出的对象是引用型
在实际开发过程中,我们有时候需要判断变量的类型去做不同的逻辑处理,就会使用到 typeof 和 instanceof 操作符来判断变量类型。下面来看一下这两个方法的区别。
二、typeof 和 instanceof 判断变量类型
(1)typeof 判断变量类型

typeof 操作符是确定一个变量是字符串、数值、布尔值、还是 undefined 的最佳工具。如果一个变量是一个对象(或 null),那么 typeof 就会返回 object;typeof 函数 返回 function(这在规范中是对可调用对象特设的返回值)。虽然 typeof 在检测基本数据类型时是非常得力的助手,但是在检测对象类型的时候用处就不大了——它无法区分数组、Date、Map 等具体种类,此时我们就需要 instanceof 操作符了,语法如下:
result = variable instanceof constructor
如果变量的原型链上存在构造函数的 prototype,就会返回 true,否则返回 false。

所有的引用类型的值(默认情况下)都是 Object 的实例,因此在检测一个引用类型值和 Object 的构造函数时,instanceof 都会返回 true(用 Object.create(null) 创建或改过原型的对象除外)。如果使用 instanceof 操作符检测基本类型的值,则该操作符始终会返回 false(原文误写为「时钟」),因为原始值不是对象、没有原型链:
console.log(1 instanceof Number) // false,原始值没有 [[Prototype]]
console.log(new Number(1) instanceof Number) // true,包装对象才有原型链
那么 instanceof 的原理是什么?为什么能判断变量是否来源于某个构造函数?涉及到原型以及原型链的知识。下一节研究什么是原型以及原型链。搞懂原型和原型链后 instanceof 的原理就明白了。
三、原型和原型链
1、原型
原型分为隐式原型(__proto__)和显式原型(prototype)。
由于所有的引用类型(数组、对象、函数)都具有对象特性,即可自由扩展属性(除了 null),浏览器给对象模型加上了原型机制:每个对象都有一个内部原型,规范记作 [[Prototype]],在绝大多数对象上可以通过继承自 Object.prototype 的 __proto__ 访问器观察到。该属性称为隐式原型,指向另一个对象。

可以看到 __proto__ 是一个对象,具有 constructor、hasOwnProperty 等属性(原文误拼为 hansOwnProperty)。
勘误:原文说「浏览器为每一个可扩展的对象添加了一个
__proto__属性」,这不准确。__proto__不是每个对象的自有属性,而是Object.prototype上定义的访问器属性(ECMAScript 2015 起收录在附录 B 的 Web 兼容特性中),对象通过原型链继承它;Object.create(null)创建的对象就没有__proto__(访问得到undefined)。标准 API 是Object.getPrototypeOf(obj)。
函数也是引用类型,因此每个函数都有一个 prototype 属性,叫作显式原型,也是一个普通的对象(仅用于 new 的时候给实例设定 [[Prototype]];箭头函数没有 prototype)。

该对象包含了 constructor 和 __proto__ 属性。隐式原型和显式原型的关系用一句话概述就是:对象的隐式原型指向构造该对象的构造函数的显式原型。

对象 student 是通过构造函数 People 实例化而来的。因此 student 的隐式原型(__proto__)与构造函数 People 的显式原型(prototype)完全相等。
2、原型链
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的隐式原型 __proto__(即它的构造函数的 prototype)中寻找。如果它的构造函数的 prototype 中没有该属性,那么就会通过 prototype.__proto__ 继续去寻找。以此类推,直到找到该属性,或整条链结束。最上层就是 Object.prototype(「祖先」),它的 [[Prototype]] 是 null,查找到此终止,说明此时不存在该属性。这个寻找的过程会形成一条链路,就是原型链。如下图所示:

其中 Foo 是构造函数,f 是其实例化对象。曲线 __proto__ 形成的就是原型链。
现代注解:规范语言是「属性查找沿
[[Prototype]]链进行,Object.prototype的[[Prototype]]为null」。这条链是引擎查找规则的自然结果,不是「为了防止无限循环而故意设置」的补丁。
3、原型链的应用
简单模拟 jQuery 封装 DOM 方法的过程。

div1 是构造函数 Elem 的实例化对象,调用 html()(div1.html())方法时,首先会在 div1 上寻找该方法。但是该对象上并没有该方法,因此会通过 div1.__proto__ 即 Elem.prototype 去寻找该方法。正好 Elem.prototype 上有 html 方法,停止寻找并执行该方法。当调用 toString() 方法时还是通过同样的方式去寻找,但是 Elem.prototype 上也不存在该方法,于是就通过 Elem.prototype.__proto__ 向上寻找。Elem.prototype 是普通对象,其原型是 Object.prototype,所以此时会去 Object.prototype 上寻找 toString() 方法。正好该对象上存在该方法,于是停止寻找并执行该方法。
四、instanceof 判断对象类型原理
上面分析了原型以及原型链的知识,现在我们看看 instanceof 底层是如何工作的。

instanceof 底层实现的核心就是去判断对象的隐式原型(更准确说,整条原型链上依次出现的每个原型)是否等于构造函数的显式原型。找到就返回 true,整条链结束都没找到就返回 false。比如 test 是构造函数 Test 的实例化对象,因此 test.__proto__ === Test.prototype,因此返回 true,证明 test 是 Test 类型。而 test.__proto__ !== Object.prototype,因此会继续沿链判断:此时传入的参数为 Test.prototype 和 Object,而 Test.prototype.__proto__ === Object.prototype,因此返回 true,证明 test 也是 Object 类型。
因此 instanceof 是从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。这样就弥补了 typeof 判断引用类型不准确的缺点。
(现代补充)手写 myInstanceof
把上面的描述翻译成代码:
function myInstanceof(left, right) {
// 右侧必须是可调用的构造器,否则规范要求抛 TypeError
if (typeof right !== 'function') {
throw new TypeError('Right-hand side of instanceof is not callable')
}
// 若右侧定义了 Symbol.hasInstance,则规范优先调用它
if (typeof right[Symbol.hasInstance] === 'function') {
return right[Symbol.hasInstance](left)
}
// 沿 left 的原型链查找 right.prototype
let proto = Object.getPrototypeOf(left)
while (proto !== null) {
if (proto === right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}
function Test() {}
const test = new Test()
console.log(myInstanceof(test, Test)) // true
console.log(myInstanceof(test, Object)) // true
console.log(myInstanceof([], Array)) // true
console.log(myInstanceof(1, Number)) // false,原始值没有原型链
(现代补充)Symbol.hasInstance 与边界情况
instanceof 并非只会做原型链比较,ES2015 之后右侧对象可以通过 Symbol.hasInstance 自定义判定逻辑(内置类型和 bind 返回的函数也在使用它):
class Even {
static [Symbol.hasInstance](value) {
return typeof value === 'number' && value % 2 === 0
}
}
console.log(4 instanceof Even) // true
console.log(3 instanceof Even) // false
使用 instanceof 时还要注意这些边界:
- 跨 realm 问题:每个 iframe / Window / Worker 有自己的一套内置对象,
iframe.contentWindow.Array !== window.Array,所以iframe 里的数组 instanceof Array会是false。判断数组请用Array.isArray(value)(它检查内部种类,不受 realm 影响)。 - 原始值始终
false(除非右侧自定义了Symbol.hasInstance);包装对象new Number(1)则是true,这也是装箱陷阱的来源之一。 - 右侧不是可调用对象会抛
TypeError:1 instanceof 1直接抛错,而不是返回false。 - 改过原型的对象会「说谎」:
Object.setPrototypeOf、整体重写prototype、Object.create(null)都会改变结果;Object.create(null)出来的对象对任何构造函数都是false。 - 需要精确的「内置类型名」时,
Object.prototype.toString.call(value)(如'[object Array]')比instanceof更通用。
五、总结
本文通过分析原型以及原型链,探索了 instanceof 的核心思想:沿对象的 [[Prototype]] 链查找是否存在 constructor.prototype(或调用右侧的 Symbol.hasInstance)。原型以及原型链是 JS 中很重要的知识点,也是自己一直以来不清晰的点。通过这次总结豁然开朗。文章中分析不到位之处望读者补充指正。
原文勘误与现代化说明
- 基本类型列表补充了 BigInt(ES2020),
Null的书写统一为null。 - 「typeof 在判断基本类型时能精准判断」补充了
typeof null === 'object'这一历史例外的说明。 - 「浏览器为每一个可扩展的对象添加了一个
__proto__属性」是错误说法,已更正为「__proto__是Object.prototype上的访问器,对象通过继承获得;Object.create(null)对象没有」。标准 API 为Object.getPrototypeOf。 - 「该操作符时钟会返回 false」为笔误,更正为「始终返回 false」,并补充:该结论仅适用于默认的原型链判定,
Symbol.hasInstance可以改变行为;右侧非函数时会抛TypeError。 hansOwnProperty拼写更正为hasOwnProperty。- 「浏览器为了防止无限循环……找到最上层就是 object 了」更正为规范表述:属性查找沿
[[Prototype]]链进行,Object.prototype的[[Prototype]]为null,查找自然终止。 - 新增内容:手写
myInstanceof实现、Symbol.hasInstance自定义判定、原始值/跨 realm/改原型等边界情况、Array.isArray与Object.prototype.toString.call的替代方案。
图片来源与授权说明
正文图片已本地化到 images/ 目录,均来自原文(掘金 Thequestion《浅析 instanceof 原理》)的图床资源。图片的转载授权无法仅凭下载确认,公开发布前应向原作者确认许可:
images/107-image-01.webp:typeof 判断变量类型示例。来源:原文截图。images/107-image-02.webp:instanceof 语法示例。来源:原文截图。images/107-image-03.webp:__proto__结构截图。来源:原文截图。images/107-image-04.webp:函数prototype截图。来源:原文截图。images/107-image-05.webp:隐式原型与显式原型关系图。来源:原文插图。images/107-image-06.webp:原型链示意图。来源:原文插图。images/107-image-07.webp:模拟 jQuery 封装 DOM 方法代码。来源:原文截图。images/107-image-08.webp:instanceof 原理示意图。来源:原文插图。
其中控制台截图反映的是历史引擎输出,具体输出随引擎与版本变化。
原文归属
作者:Thequestion。历史来源:掘金《浅析 instanceof 原理》。原文链接仅用于保留历史出处,当前语义以本文列出的规范和官方文档为准。