阅读 Lodash 的第一部分源码是关于如何可靠地判断数据类型的。
Lodash 一共有 31 个 API 用于判断数据类型,今天分析其中 3 个,另外 28 个 API 会在后续的文章中分析。
JS 中对于对象类型的判断,极为复杂,不仅需要考虑语言本身的设计问题,还需要具备原型与原型链的知识,以及 ES6 相关的储备。
在 Lodash 中提供了 isObject(), isObjectLike() 和 isPlainObject() 三个函数进行可靠地判断,它们对对象如何进行分类?背后的原理又是什么?
广义对象
即引用类型。引用类型是一种数据结构,用于将数据和功能组织在一起,描述的是一类对象所具有的属性和方法。
通过上面的定义,我们可以清楚对象、数组、函数、Set、Map 等都是引用类型。OK,现在我们明确了范围,那么怎样可以判断出来呢?
原生 JS 提供了 typeof 操作符,但是却存在着致命缺陷!
typeof null === 'object'
// => true
function fn() {}
typeof fn === 'object'
// => false
null 不满足广义对象的定义,却被 typeof 判断为一个对象;另外,函数满足广义对象的定义,却无法被 typeof 判断为一个对象。
通常,我们去判断广义对象(引用类型),必须要考虑这两种情况。通过下面 isObject() 的源码可以看到,Lodash 也是这样做的:
function isObject(value) {
const type = typeof value
return value != null && (type == 'object' || type == 'function')
}
函数有何不同?
上面我们已经知道,无法通过 typeof 来判断一个函数是一个对象,实际上真正返回的是:
typeof fn
// => function
对,没错。typeof 认为函数就是函数类型。(为什么呢?暂时没有得到结果。)
在 Lodash 中,isObjectLike() 就是在 isObject() 的基础上排除函数类型。
function isObjectLike(value) {
return typeof value == 'object' && value !== null
}
狭义对象
PlainObject 的定义是指由 Object 创建的值,以及 __proto__
属性指向 null 的值。
先看在 Lodash 中如何实现的:
function isPlainObject(value) {
if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') {
return false
}
if (Object.getPrototypeOf(value) === null) {
return true
}
let proto = value
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(value) === proto
}
从 3 个维度来分析:
1、不满足 isObjectLike 或者 baseGetTag 不为 ‘[object Object]’ 的值。
重点分析 baseGetTag,这是 Lodash 不对外暴露的一个内部函数:
const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined
function baseGetTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
if (!(symToStringTag && symToStringTag in Object(value))) {
return toString.call(value)
}
// 篇幅有限,省略 ...
// 完整代码https://github.com/lodash/lodash/blob/master/.internal/baseGetTag.js
}
其实,baseGetTag() 函数的核心就是利用了 Object.prototype.toString.call() 来检测类型,这也是 ES6 推荐的一种方式。
不过,baseGetTag() 对 Symbol 进行了单独地处理。关于这一点,我有两处不理解:如果 value 自身有 symToStringTag,那么就会被赋值为 undefined,这是为什么?接着,再去 toString.call(value) 得到的结果又是什么?(未解之谜 …)
分析到这里,其实我们完全可以自己封装一个工具函数来判断类型:
function getType(value) {
return Object.prototype.toString.call(value);
}
有时候,你可能不需要第三方工具库。
按理来说,凭 [object Object] 就可以完全确定一个值是绝对的对象,不会是函数、数组、JSON、日期等等。
但是 isPlainObject() 的实现还有两个条件,这里合在一起分析。
2/3、需要考虑 Object.prototype 以及继承
凭借第一个条件,成功排除 JS 中所有的内置函数。但是无法正确地判断 Object.prototype 和继承的情况。
先看例子:
class A {};
const a = new A();
Object.prototype.toString.call(a);
// => [object Object]
a 的类型是 [object Object]。在开发中,我们通常会自定义一些类,却通常不会定义该类的类型(可以通过 Symbol.toStringTag 自定义)。所以这些自定义类的实例的类型始终是 [object Object],即继承自 Object。
而 PlainObject 的定义要求该对象必须是由对象字面量创建或者由 new Object() 创建,这就是源码中 while 循环的意义,通过最终对比,只有直接继承自 Object 的实例才会被返回 true。
但是,还需要注意的是,Object.prototype 就会被排除在外,而 Object.prototype 满足定义:
Object.getPrototypeOf(Object.prototype);
// => null
另外 Object.create(null) 也属于这种情况:
Object.getPrototypeOf(Object.create(null));
// => null
所以对于 __proto__
为 null 的情况,在源码中提前返回 true。
从 Lodash 对对象的判断,就可以看到判断 JS 的数据类型表面是一件可以解决的事,但是要了解其背后原理又不是一件轻松的事,这或许就是 JS 本身让人又爱又恨的魅力之一。
这篇文章通过分析 Lodash 中 3 个类型判断的 API(isObject, isObjectLike, isPlainObject)对于在 JS 中难以判断对象类型做了梳理。
下一篇,将探索 Lodash 中对其他常见数据类型的判断。