Lodash 源码 | 1 Lodash 如何判断数据类型 I

阅读 Lodash 的第一部分源码是关于如何可靠地判断数据类型的

Lodash 一共有 31 个 API 用于判断数据类型,今天分析其中 3 个,另外 28 个 API 会在后续的文章中分析。

JS 中对于对象类型的判断,极为复杂,不仅需要考虑语言本身的设计问题,还需要具备原型与原型链的知识,以及 ES6 相关的储备。

在 Lodash 中提供了 isObject(), isObjectLike() 和 isPlainObject() 三个函数进行可靠地判断,它们对对象如何进行分类?背后的原理又是什么?

Part 1. isObject()

广义对象

即引用类型。引用类型是一种数据结构,用于将数据和功能组织在一起,描述的是一类对象所具有的属性和方法。

通过上面的定义,我们可以清楚对象、数组、函数、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')
}

Part 2. isObjectLike()

函数有何不同?

上面我们已经知道,无法通过 typeof 来判断一个函数是一个对象,实际上真正返回的是:

typeof fn
// => function

对,没错。typeof 认为函数就是函数类型。(为什么呢?暂时没有得到结果。)

在 Lodash 中,isObjectLike() 就是在 isObject() 的基础上排除函数类型。

function isObjectLike(value) {
  return typeof value == 'object' && value !== null
}

Part 3. isPlainObject()

狭义对象

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。

Part 4. 小结

从 Lodash 对对象的判断,就可以看到判断 JS 的数据类型表面是一件可以解决的事,但是要了解其背后原理又不是一件轻松的事,这或许就是 JS 本身让人又爱又恨的魅力之一。

这篇文章通过分析 Lodash 中 3 个类型判断的 API(isObject, isObjectLike, isPlainObject)对于在 JS 中难以判断对象类型做了梳理。

下一篇,将探索 Lodash 中对其他常见数据类型的判断。

我的公众号

Lodash 源码 | 1 Lodash 如何判断数据类型 I_第1张图片

你可能感兴趣的:(lodash,源码,Lodash,源码)