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

显示模式

登录
ARCHIVE DOCUMENTJS

浅谈 instanceof 和 typeof 的实现原理

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/108-浅谈 instanceof 和 typeof 的实现原理
本文目录7 个章节
  1. typeof 实现原理
  2. instanceof 操作符的实现原理
  3. 总结
  4. 原文勘误与现代化说明
  5. 图片来源与授权说明
  6. 原文归属
  7. 参考链接

浅谈 instanceof 和 typeof 的实现原理

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

本文保留了原文(2018 年)的讲解主线:typeof 的类型判断与底层存储的故事、Object.prototype.toString 的精确判断、instanceof 的原型链原理与经典 Object/Function/Foo 例题。整理时修复了被复制损坏的代码块,补全了 bigint 返回值,并给「机器码低位存类型」这段流传甚广的说法加上了引擎视角的说明。文末附勘误清单。

typeof 实现原理

typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断 numberstringobjectbooleanfunctionundefinedsymbol 这七种类型(2018 年原文时尚无 bigint,ES2020 之后 typeof 10n 会返回第八种 'bigint')。这种判断能帮助我们搞定一些问题,比如在判断不是 object 类型的数据的时候,typeof 能比较清楚地告诉我们具体是哪一类的类型。但是,很遗憾的一点是,typeof 在判断一个 object 的数据的时候只能告诉我们这个数据是 object,而不能细致地具体到是哪一种 object,比如👉

let s = new String('abc')
console.log(typeof s === 'object') // true
console.log(s instanceof String) // true

要想判断一个数据具体是哪一种 object 的时候,我们需要利用 instanceof 这个操作符来判断,这个我们后面会说到。

来谈谈关于 typeof 的原理吧,我们可以先想一个很有意思的问题,js 在底层是怎么存储数据的类型信息呢?或者说,一个 js 的变量,在它的底层实现中,它的类型信息是怎么实现的呢?

原文的说法是:js 在底层存储变量的时候,会在变量的机器码的低位 1-3 位存储其类型信息👉

  • 000:对象
  • 010:浮点数
  • 100:字符串
  • 110:布尔
  • 1:整数

but,对于 undefinednull 来说,这两个值的信息存储是有点特殊的。

null:所有机器码均为 0

undefined:用 −2^30 整数来表示

所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为 0,因此直接被当做了对象来看待。

现代注解:上面这段「机器码低位存类型标签」的说法描述的是某个历史版本引擎的实现细节(不同引擎的打标签方案并不相同:V8 用 Smi/堆对象指针打标签,JavaScriptCore 一度用 NaN-boxing,SpiderMonkey 用自己的 JSValue 打标签),它不是 ECMAScript 规范的内容。规范只是规定 typeof null 返回 'object'(详见 MDN:typeof 的「历史遗留」说明)——这个规定源自初版实现里 null 与对象共用标签位的历史,后来为了兼容旧代码一直保留。所以「为什么 typeof null 是 object」的历史解释可以保留,但要记住:现代规范里它就是表格里写死的行为,与今天引擎实际怎么存值无关。

然而用 instanceof 来判断的话👉

try {
  null instanceof null
} catch (e) {
  console.log(e) // TypeError: Right-hand side of 'instanceof' is not an object
}

(勘误:原文说「null 直接被判断为不是 object」,这个表述不对——null instanceof null 根本不返回结果,而是直接抛出 TypeError,因为规范要求 instanceof 的右侧必须是可调用的对象。typeof null === 'object' 才是那个历史遗留问题,两件事不要混在一起。typeof null === 'object' 可以参考 MDN:typeof。)

因此在用 typeof 来判断变量类型的时候,我们需要注意,最好是用 typeof 来判断基本数据类型(包括 symbolbigint),避免对 null 的判断。

现代补充typeof 有两个实用小特性。一是它对未声明的变量也安全:typeof notDeclaredVar 返回 'undefined' 而不会抛 ReferenceError,常用于检测全局变量/宿主 API 是否存在(如 typeof window !== 'undefined')。二是浏览器里有个著名的规范「故意违规」:typeof document.all === 'undefined',这是 HTML 规范为了兼容老代码特批的行为,仅此一例。

还有一个不错的判断类型的方法,就是 Object.prototype.toString,我们可以利用这个方法来对一个变量的类型来进行比较准确的判断:

console.log(Object.prototype.toString.call(1)) // "[object Number]"
console.log(Object.prototype.toString.call('hi')) // "[object String]"
console.log(Object.prototype.toString.call({ a: 'hi' })) // "[object Object]"
console.log(Object.prototype.toString.call([1, 'a'])) // "[object Array]"
console.log(Object.prototype.toString.call(true)) // "[object Boolean]"
console.log(Object.prototype.toString.call(() => {})) // "[object Function]"
console.log(Object.prototype.toString.call(null)) // "[object Null]"
console.log(Object.prototype.toString.call(undefined)) // "[object Undefined]"
console.log(Object.prototype.toString.call(Symbol(1))) // "[object Symbol]"
console.log(Object.prototype.toString.call(10n)) // "[object BigInt]"(ES2020+)

它的原理是读取对象内部的 Symbol.toStringTag:大多数内置对象使用默认标签,自定义类的实例默认仍是 "[object Object]",但可以通过定义 Symbol.toStringTag 改变输出(class Foo { get [Symbol.toStringTag]() { return 'Foo' } }"[object Foo]")。

instanceof 操作符的实现原理

之前我们提到了 instanceof 来判断对象的具体类型,其实 instanceof 主要的作用就是判断一个实例是否属于某种类型:

let person = function() {}
let nicole = new person()
console.log(nicole instanceof person) // true

当然,instanceof 也可以判断一个实例是否是其父类型或者祖先类型的实例。

let person = function() {}
let programmer = function() {}
programmer.prototype = new person()
let nicole = new programmer()
console.log(nicole instanceof person) // true
console.log(nicole instanceof programmer) // true

现代注解programmer.prototype = new person() 是 2018 年常见的「经典继承」写法,缺点是父类构造函数会在设定原型时被执行一次、且父类实例属性会泄漏到子类原型上。现代等价写法是 class/extends,或 programmer.prototype = Object.create(person.prototype)(再补回 constructor)。这里保留原文写法作历史示例。

这是 instanceof 的用法,但是 instanceof 的原理是什么呢?根据 ECMAScript 语言规范,我梳理了一下大概的思路,然后整理了一段代码如下:

function new_instance_of(leftValue, rightValue) {
  let rightProto = rightValue.prototype // 取右表达式的 prototype 值
  leftValue = leftValue.__proto__ // 取左表达式的 __proto__ 值
  while (true) {
    if (leftValue === null) {
      return false
    }
    if (leftValue === rightProto) {
      return true
    }
    leftValue = leftValue.__proto__
  }
}

(勘误:原文参数名误拼为 leftVaule/rightVaule,已更正。)

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

现代补充:这份手写版是教学简化,当前规范(ES2015+)的实际判定顺序是:

  1. 右侧必须是可调用对象(函数),否则抛 TypeError(这就是前面 null instanceof null 报错的来源);
  2. 若右侧定义了 Symbol.hasInstance 方法,优先调用它,由它决定结果(bind 返回的包装函数就是靠它把 instanceof 转发给目标函数的,用户也可自定义);
  3. 否则执行这里的原型链遍历(规范称 OrdinaryHasInstance)。

工程实践中还要注意跨 realm 问题:iframe / Worker 里的 Array 与主窗口的 Array 不是同一个构造器,跨 frame 判断数组请用 Array.isArray。更标准的取原型写法是 Object.getPrototypeOf(leftValue) 而非 __proto__

看几个很有趣的例子:

function Foo() {}

console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true
console.log(Foo instanceof Foo) // false
console.log(Foo instanceof Object) // true
console.log(Foo instanceof Function) // true

要想全部理解 instanceof 的原理,除了我们刚刚提到的实现原理,我们还需要知道 JavaScript 的原型继承原理。

关于原型继承的原理,我简单用一张图来表示:

原型继承关系图(原文插图)

我们知道每个 JavaScript 对象均有一个隐式的 __proto__ 原型(内部 [[Prototype]] 的访问器),而显式的原型属性是 prototype(只有函数才有),只有 Object.prototype.__proto__ 属性在未修改的情况下为 null 值(用 Object.create(null) 创建的对象连原型都没有,整个判定都会失效)。根据图上的原理,我们来梳理上面提到的几个有趣的 instanceof 使用的例子。

  • Object instanceof Object
    由图可知,Object 的 prototype 属性是 Object.prototype,而由于 Object 本身是一个函数,由 Function 所创建,所以 Object.__proto__ 的值是 Function.prototype,而 Function.prototype__proto__ 属性是 Object.prototype,所以我们可以判断出,Object instanceof Object 的结果是 true。用代码简单的表示一下:
    // 伪代码:跟踪左侧原型链与右侧 prototype 的比较过程
    leftValue = Object.__proto__ = Function.prototype
    rightValue = Object.prototype
    // 第一次判断 leftValue != rightValue
    leftValue = Function.prototype.__proto__ = Object.prototype
    // 第二次判断 leftValue === rightValue
    // 返回 true
    

    Function instanceof FunctionFunction instanceof Object 的运行过程与 Object instanceof Object 类似,故不再详说。
  • Foo instanceof Foo
    Foo 函数的 prototype 属性是 Foo.prototype,而 Foo 的 __proto__ 属性是 Function.prototype,由图可知,Foo 的原型链上并没有 Foo.prototype,因此 Foo instanceof Foo 也就返回 false。
    我们用代码简单的表示一下:
    // 伪代码
    leftValue = Foo.__proto__ = Function.prototype
    rightValue = Foo.prototype
    // 第一次判断 leftValue != rightValue
    leftValue = Function.prototype.__proto__ = Object.prototype
    // 第二次判断 leftValue != rightValue
    leftValue = Object.prototype.__proto__ // 即 null(原文误写为 leftValue = Object.prototype = null)
    // 第三次判断 leftValue === null
    // 返回 false
    
  • Foo instanceof Object
    // 伪代码
    leftValue = Foo.__proto__ = Function.prototype
    rightValue = Object.prototype
    // 第一次判断 leftValue != rightValue
    leftValue = Function.prototype.__proto__ = Object.prototype
    // 第二次判断 leftValue === rightValue
    // 返回 true
    
  • Foo instanceof Function
    // 伪代码
    leftValue = Foo.__proto__ = Function.prototype
    rightValue = Function.prototype
    // 第一次判断 leftValue === rightValue
    // 返回 true
    

总结

简单来说,我们使用 typeof 来判断基本数据类型是 ok 的,不过需要注意当用 typeof 来判断 null 类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof,但是 instanceof 也可能判断不准确,比如一个数组,它既可以被 instanceof 判断为 Array,也可以被判断为 Object(原型链上的每个构造器都返回 true)。所以我们要想比较准确地判断对象实例的具体类型时,可以采取 Object.prototype.toString.call 方法。


原文勘误与现代化说明

  1. 修复了所有被复制损坏的代码块:清除了掘金页面复制按钮残留的文字,恢复了换行、console.log 与注释(原文代码被压成一行,且多段示例的输出注释粘连在代码尾部)。
  2. typeof 的返回值列表从七种补全为八种:新增 ES2020 的 'bigint'typeof 10n);Object.prototype.toString 示例同步补充 "[object BigInt]"
  3. 「机器码低位 1-3 位存储类型信息」(000 对象 / 010 浮点数 / 100 字符串 / 110 布尔 / 1 整数)标注为历史引擎实现细节:这是对某个历史版本引擎打标签方案的记忆口诀,各引擎方案不同(V8 Smi/指针标签、JavaScriptCore NaN-boxing 等),规范只规定 typeof null === 'object' 这个结果本身。
  4. 「null 直接被判断为不是 object」是错误表述null instanceof null 不返回布尔值,而是抛 TypeError(右侧必须是可调用对象);typeof null === 'object' 才是历史遗留问题。已分开说明。
  5. new_instance_of 参数名 leftVaule/rightVaule 拼写更正;补充当前规范的实际判定顺序(可调用检查 → Symbol.hasInstance → OrdinaryHasInstance 原型链遍历)、Object.getPrototypeOf 标准写法、跨 realm 注意事项与 Array.isArray
  6. Foo instanceof Foo 推导中 leftValue = Object.prototype = null 一行误写(这样写会把 Object.prototype 真的置空),已更正为 Object.prototype.__proto__ === null 的伪代码表述,并全部标注「伪代码」。
  7. programmer.prototype = new person() 补充现代替代方案(class/extendsObject.create(person.prototype))说明。
  8. 新增:typeof 对未声明变量安全返回 'undefined'、浏览器中 typeof document.all === 'undefined' 的规范特批违规、Symbol.toStringTag 可自定义 Object.prototype.toString 输出。
  9. 失效的 link.juejin.cn 跳转链接替换为 MDN 直链。
  10. 原型继承图下载至 images/;原文末尾两张 URL 编码路径引用的附图(浅谈 instanceof 和 typeof 的实现原理/ 目录)已转换为 images/108-image-02.webpimages/108-image-03.webp

图片来源与授权说明

正文图片已本地化到 images/ 目录。图片的转载授权无法仅凭下载确认,公开发布前应向原作者确认许可:

  • images/108-image-01.webp:原型继承关系图。来源:原文插图(掘金图床)。
  • images/108-image-02.webp:原文末尾附图。来源:原文页面随附图片(原存放于文章同目录 浅谈 instanceof 和 typeof 的实现原理/image.png,内容未识别,疑为原文评论区/互动截图)。
  • images/108-image-03.webp:原文末尾附图。来源:同上(原 image 1.png)。

原文归属

作者:nicole_zhang18970。历史来源:掘金《浅谈 instanceof 和 typeof 的实现原理》。原文链接仅用于保留历史出处,当前语义以本文列出的规范和官方文档为准。

参考链接

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS