技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

浅析instanceof 原理

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/107-浅析instanceof 原理
本文目录10 个章节
  1. 概述
  2. 一、JS 中变量类型
  3. 二、typeof 和 instanceof 判断变量类型
  4. 三、原型和原型链
  5. 四、instanceof 判断对象类型原理
  6. 五、总结
  7. 原文勘误与现代化说明
  8. 图片来源与授权说明
  9. 原文归属
  10. 参考链接

浅析instanceof 原理

Category(分类): JavaScript Status: 已整理

本文保留了原文(2020 年)从「变量类型 → typeof/instanceof → 原型与原型链 → instanceof 原理」的讲解主线,并补充了手写 myInstanceofSymbol.hasInstance、跨 realm 等现代边界。文末附勘误清单。

概述

JS 中可以通过 typeof 操作符和 instanceof 操作符判断变量的类型。typeof 在判断(大多数)基本类型的变量时能精准判断,但当变量是数组、对象时都只会返回 objecttypeof null 也是 object),无法准确说明该对象是什么类型的。而 instanceof 基于原型链可以判断一个引用是否属于某构造函数,能弥补 typeof 判断引用类型的缺点。本文旨在分析 instanceof 的底层原理,同时借此复习原型以及原型链的知识。

一、JS 中变量类型

本节先来回顾一下 JS 的变量类型,JS 变量可以分为基本类型(原始值)和引用类型。

(1)基本类型:字符串、数值、布尔值、undefinednull、Symbol(ES2015)、BigInt(ES2020)

(2)引用类型:对象(Object,包括数组、函数、Date、Map、Set 等)

基本类型和引用类型的区别

基本类型:

1、原始值本身按值保存和复制;引用类型的变量保存的是指向堆中对象的引用(地址),这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。(原文的「值在栈中、对象在堆中」是常见的教学简化,理解「复制值 vs 复制引用」即可。)

2、保存与复制的是值本身

3、使用 typeof 检测数据的类型(注意 typeof null === 'object' 是历史遗留问题)

引用类型:

1、对象保存在堆中,变量持有指向对象的引用

2、保存与复制的是指向对象的一个引用

3、使用 instanceof 检测数据类型

4、使用 new() 方法构造出的对象是引用型

在实际开发过程中,我们有时候需要判断变量的类型去做不同的逻辑处理,就会使用到 typeofinstanceof 操作符来判断变量类型。下面来看一下这两个方法的区别。

二、typeof 和 instanceof 判断变量类型

(1)typeof 判断变量类型

typeof 判断变量类型(原文截图)

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

result = variable instanceof constructor

如果变量的原型链上存在构造函数的 prototype,就会返回 true,否则返回 false

instanceof 语法示例(原文截图)

所有的引用类型的值(默认情况下)都是 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__ 是一个对象(原文截图)

可以看到 __proto__ 是一个对象,具有 constructorhasOwnProperty 等属性(原文误拼为 hansOwnProperty)。

勘误:原文说「浏览器为每一个可扩展的对象添加了一个 __proto__ 属性」,这不准确。__proto__ 不是每个对象的自有属性,而是 Object.prototype 上定义的访问器属性(ECMAScript 2015 起收录在附录 B 的 Web 兼容特性中),对象通过原型链继承它;Object.create(null) 创建的对象就没有 __proto__(访问得到 undefined)。标准 API 是 Object.getPrototypeOf(obj)

函数也是引用类型,因此每个函数都有一个 prototype 属性,叫作显式原型,也是一个普通的对象(仅用于 new 的时候给实例设定 [[Prototype]];箭头函数没有 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 方法的过程。

模拟 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 原理示意图(原文插图)

instanceof 底层实现的核心就是去判断对象的隐式原型(更准确说,整条原型链上依次出现的每个原型)是否等于构造函数的显式原型。找到就返回 true,整条链结束都没找到就返回 false。比如 test 是构造函数 Test 的实例化对象,因此 test.__proto__ === Test.prototype,因此返回 true,证明 testTest 类型。而 test.__proto__ !== Object.prototype,因此会继续沿链判断:此时传入的参数为 Test.prototypeObject,而 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 时还要注意这些边界:

  1. 跨 realm 问题:每个 iframe / Window / Worker 有自己的一套内置对象,iframe.contentWindow.Array !== window.Array,所以 iframe 里的数组 instanceof Array 会是 false。判断数组请用 Array.isArray(value)(它检查内部种类,不受 realm 影响)。
  2. 原始值始终 false(除非右侧自定义了 Symbol.hasInstance);包装对象 new Number(1) 则是 true,这也是装箱陷阱的来源之一。
  3. 右侧不是可调用对象会抛 TypeError1 instanceof 1 直接抛错,而不是返回 false
  4. 改过原型的对象会「说谎」Object.setPrototypeOf、整体重写 prototypeObject.create(null) 都会改变结果;Object.create(null) 出来的对象对任何构造函数都是 false
  5. 需要精确的「内置类型名」时,Object.prototype.toString.call(value)(如 '[object Array]')比 instanceof 更通用。

五、总结

本文通过分析原型以及原型链,探索了 instanceof 的核心思想:沿对象的 [[Prototype]] 链查找是否存在 constructor.prototype(或调用右侧的 Symbol.hasInstance。原型以及原型链是 JS 中很重要的知识点,也是自己一直以来不清晰的点。通过这次总结豁然开朗。文章中分析不到位之处望读者补充指正。


原文勘误与现代化说明

  1. 基本类型列表补充了 BigInt(ES2020),Null 的书写统一为 null
  2. 「typeof 在判断基本类型时能精准判断」补充了 typeof null === 'object' 这一历史例外的说明。
  3. 「浏览器为每一个可扩展的对象添加了一个 __proto__ 属性」是错误说法,已更正为「__proto__Object.prototype 上的访问器,对象通过继承获得;Object.create(null) 对象没有」。标准 API 为 Object.getPrototypeOf
  4. 「该操作符时钟会返回 false」为笔误,更正为「始终返回 false」,并补充:该结论仅适用于默认的原型链判定,Symbol.hasInstance 可以改变行为;右侧非函数时会抛 TypeError
  5. hansOwnProperty 拼写更正为 hasOwnProperty
  6. 「浏览器为了防止无限循环……找到最上层就是 object 了」更正为规范表述:属性查找沿 [[Prototype]] 链进行,Object.prototype[[Prototype]]null,查找自然终止。
  7. 新增内容:手写 myInstanceof 实现、Symbol.hasInstance 自定义判定、原始值/跨 realm/改原型等边界情况、Array.isArrayObject.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 原理》。原文链接仅用于保留历史出处,当前语义以本文列出的规范和官方文档为准。

参考链接

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS