Lodash 源码 | 2 Lodash 如何判断数据类型 II

本文详细探讨了Lodash中如何判断数值、字符串、布尔值、Symbol、null和undefined等基本数据类型,以及如何处理JS中的类型检测陷阱。通过分析Lodash的源码,揭示了其在处理new操作符创建的基本数据类型时的独特处理方式,并介绍了增强类API,如isArrayLike()、isEmpty()、isNative()等,强调了原生API在类型判断中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇文章分析了在 Lodash 中如何可靠地检测对象类型(纯对象)。在 JS 中,引用类型不仅仅有纯对象,常见的还有数组、函数、类数组对象等等。除了引用类型,还有一个大类叫做基本数据类型,包括数值、字符串、布尔值、null、undefnied、Symbol。

所以,在这一篇文章中,就要来分析在 Lodash 中如何判断其他常见的数据类型的?以及在原生 JS 中,这些数据类型又存在哪些“坑”?

Part 1. 数值、字符串、布尔值、Symbol

先来看这三个最基本的类型。

OK,它们三种类型可以被 typeof 操作符准确地检测。但是,在分析 Lodash 源码时,我发现 Lodash 可不是只利用 typeof 进行地检测。

function isNumber(value) {
  return typeof value == 'number' ||
    (isObjectLike(value) && baseGetTag(value) == '[object Number]')
}

先看 isNumber() 函数。可以看到,Lodash 用到了 isObjectLike() 和 baseGetTag() 函数,这两个函数在上一篇文章中已经分析过了。

function isString(value) {
  const type = typeof value
  return type == 'string' || (type == 'object' && value != null && !Array.isArray(value) && getTag(value) == '[object String]')
}
function isBoolean(value) {
  return value === true || value === false ||
    (isObjectLike(value) && baseGetTag(value) == '[object Boolean]')
}

再看看,我们发现 Lodash 在判断数值、字符串、布尔值都用到了 isObjectLike() 和 baseGetTag() 函数。Why?

这得从 JS 语言本身来考虑。

在 JS 中,可以通过字面量的形式来直接创建数值、字符串、布尔值:

var num = 1;
var str = 'a';
var flag = true;

这种形式创建的数据均可以直接被 typeof 操作符检测,不会存在歧义:

typeof num // => number
typeof str // => string
typeof flag // => boolean

但是,JS 允许我们通过内置函数 Number()、String()、Boolean() 来创建相应的数据,只不过这时用 typeof 检测不到,只能返回 ‘object’:

typeof new Number(1) // => object
typeof new String('a') // => object
typeof new Boolean(true) // => object

这从开发者的感知上会产生歧义,这是原生 JS 存在的问题。问题的原因在于 new 操作符本身的原理是创建一个对象并返回(这里不展开,会在其他专题中总结),所以导致了 typeof 检测为 ‘object’。

在开发中最佳实践是要求我们避免这样创建基本数据类型。但是,Lodash 采用了主动出击的方式,在源码中认为以 new 形式创建的基本数据类型依旧应该是相应的数据类型。这就解释了这一行代码存在的意义:

isObjectLike(value) && baseGetTag(value) == '[object Number]'

对于 Symbol,Lodash 也采用了同样的处理方式:

function isSymbol(value) {
  const type = typeof value
  return type == 'symbol' || (type == 'object' && value != null && getTag(value) == '[object Symbol]')
}

Part 2. null 与 undefined

判断这两货是非常容易的,可以直接利用 === 关系。

function isNull(value) {
  return value === null
}
function isUndefined(value) {
  return value === undefined
}

提个醒:void 0 是个标准的 undefined 值

在 JS 中,我们知道 null 与 undefined 是相等的,即:

null == undefined // => true

为什么呢?它们存在转换?

答案是不。**在 ECMA-262 中规定 null 与 undefined 的相等性测试要返回 true。**诶,没错,这是规定!

如果你不信,再看看 == 的转换规则:

1、布尔值转为数值;

2、字符串与数值相比,将字符串转为数值;

3、对象,调用 valueOf() 得到结果后按照前面的规则转换;

4、null 与 undefined 是相等的;

5、null 与 undefined 在比较相等性时不可以转换为任何值。

再看看例子:

null == 0 // => false
undefined == false // => false

Lodash 除了提供 isNull() 和 isUndefined() 函数,还提供了一个 isNil() 函数:

function isNil(value) {
  return value == null
}

Part 3. 增强类 API

Lodash 除了判断基本数据类型,还增加了对 JS 中许多特定的类型做出判断的 API。

1、isArrayLike()

Lodash 对于 ArrayLike 类数组的定义是:

A value is considered array-like if it’s not a function and has a value.length that’s an integer greater than or equal to 0 and less than or equal to Number.MAX_SAFE_INTEGER.

首先,类数组不是一个函数。最重要的是,它具有 length 属性,其值是一个大于等于 0 并且小于等于 MAX_SAFE_INTEGER 最大安全正整数。

function isArrayLike(value) {
  return value != null && typeof value != 'function' && isLength(value.length)
}

这里涉及 isLength() API,来看一下:

function isLength(value) {
  return typeof value == 'number' &&
    value > -1 &&
    value % 1 == 0 && value <= MAX_SAFE_INTEGER
}

提个醒:MAX_SAFE_INTEGER 是多少?

在 ES6 中引入两个常量值:Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 表示 JS 能精确表示的安全正整数范围。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// => true

Number.MIN_SAFE_INTEGER === 1 - Math.pow(2, 53)
// => true

再来看看哪些常见的满足 ArrayLike 定义的数据类型,以下都将返回 true:

// arguments 对象
_.isArrayLike(function(){return arguments}());

// 数组
_.isArrayLike([1, 2, 3])

// NodeList
_.isArrayLike(document.body.children)

// 字符串
_.isArrayLike('abc')

Lodash 中还有一个 isArrayObjectLike() API,它其实就是 isObjectLike() 和 isArrayLike() 的并集:

function isArrayLikeObject(value) {
  return isObjectLike(value) && isArrayLike(value)
}

这样来看,字符串就不满足 isArrayLikeObject()。

_.isArrayLikeObject('abc')
// => false

2、isEmpty()

在 Lodash 中是这样定义 Empty 的:

Checks if value is an empty object, collection, map, or set.

Objects are considered empty if they have no own enumerable string keyed properties.

Array-like values such as arguments objects, arrays, buffers, strings, or jQuery-like collections are considered empty if they have a length of 0. Similarly, maps and sets are considered empty if they have a size of 0.

对于对象来说,Lodash 认为没有可枚举的字符串键值就属于 Empty;对于数组或者类数组来说,length 属性为 0 就属于 Empty;以及 size 属性为 0 的 Set 和 Map。

function isEmpty(value) {
  if (value == null) {
    return true
  }
  if (isArrayLike(value) &&
      (Array.isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||
        isBuffer(value) || isTypedArray(value) || isArguments(value))) {
    return !value.length
  }
  const tag = getTag(value)
  if (tag == '[object Map]' || tag == '[object Set]') {
    return !value.size
  }
  if (isPrototype(value)) {
    return !Object.keys(value).length
  }
  for (const key in value) {
    if (hasOwnProperty.call(value, key)) {
      return false
    }
  }
  return true
}

在 isEmpty() 的源码中,重点来说一下 isPrototype()。这是一个内部函数。

const objectProto = Object.prototype

function isPrototype(value) {
  const Ctor = value && value.constructor
  const proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto

  return value === proto
}

这个函数用于判断一个值是否是原型对象。它的实现逻辑还是很容易理解的,就是判断 value 等不等于 value.constructor.prototype,如果等于,那么这个 value 就是原型对象。(关于原型的知识,这里不做阐述)

关键是它在 isEmpty() 中的作用是什么?即下面这段代码在 isEmpty() 中的作用是什么?

if (isPrototype(value)) {
  return !Object.keys(value).length
}

这个问题我现在不是很能理解,为什么要把原型对象单独拿出来?而不是合在下面的 in 操作符中去处理,并且我知道在这里的 Object.keys() 确实只迭代对象自身的可枚举属性。

我们再来分析一下,判断可枚举性是如何在 isEmpty() 中实现的?

它就是利用了 in 操作符和 Object.hasOwnProperty() 这两个 JS 原生的语法进行的判断。in 操作符会迭代一个对象中所有可枚举的属性,包括从原型链上继承而来的属性,所以需要通过设置一层 Object.hasOwnProperty() 来判断某个属性是否是自身的。

3、isNative()

这个 API 用来判断是否是 JS 的一个原生函数。这里举个例子:

_.isNative(Array.prototype.push)
// => true

来看一下实现:

const reRegExpChar = /[\\^$.*+?()[\]{}|]/g

const reIsNative = RegExp(`^${
  Function.prototype.toString.call(Object.prototype.hasOwnProperty)
    .replace(reRegExpChar, '\\$&')
    .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?')
}$`)

function isNative(value) {
  return isObject(value) && reIsNative.test(value)
}

(⊙o⊙)… 正则表达式,这一块触及到我的知识盲区了。来一点一点的分析。

首先,Function.prototype.toString() 是继承自 Object.prototype.toString(),并且重写,作用是返回一个代表函数的字符串。

Function.prototype.toString.call(Object.prototype.hasOwnProperty)
// => "function hasOwnProperty() { [native code] }"

接着:

Function.prototype.toString.call(Object.prototype.hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')

// => "function hasOwnProperty\(\) \{ \[native code\] \}"

这里可以理解为将 () {} [] 前添加 \,目的是转义这些符号。

再来:

Function.prototype.toString.call(Object.prototype.hasOwnProperty)
    .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
    .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?')

// => "function.*?\(\) \{ \[native code\] \}"

最终通过 RegExp() 将 "function.*?\(\) \{ \[native code\] \}" 生成一个正则表达式,就可以去判断一个函数是否是原生 JS 的内部函数了。其实分析到这里整个思路已经清晰了,对于正则表达式的细节,不是这篇文章探讨的。

4、isElement()

这个 API 是用来判断 DOM 元素的。

function isElement(value) {
  return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value)
}

关于 isObjectLike() 和 isPlainObject() 在上一篇文章中分析过了,也就是说判断一个值是 DOM 元素,它不能是一个纯对象,但是是一个类对象,并且具有 nodeType 值为 1 的属性。

5、其他

除上面的一些 API,还有一些高级类型判断 API,比如:isEqualWith(), isMatch(), isMatchWith()。这三个 API 将放在后续其他专题中去分析。

关于 isTypedArray() 也会在我学习 TypedArray 之后再来分析。

另外,以下类型可以直接通过 Object.prototype.toString.call() 来判断:Set, Map, WeakSet, WeakMap, RegExp, Function, Array, Error, Date, ArrayBuffer, arguments。这些类型就不再一一分析。

Part 4. 小结

今天分析了 Lodash 中如何判断基本数据类型,以及 Lodash 其他的增强型的类型判断 API。

纵观 Lodash 整套判断数据类型的 API,可以说现在 ES6 官方推荐的 Object.prototype.toString() 方法已经可以满足绝大多数开发者的需求,这个原生 API 也是 Lodash 判断数据类型的核心思想。

在分析的过程中,我的偏向主要还是回到 JS 里去寻求根本原因。比如上一篇文章分析 isPlainObject() 时,往原型与原型链、继承的方向去思考;又比如这一篇文章中对形如 new Number() 这样的语言本身缺陷的思考。

最后,我还有一点体会就是:有时,你可能真的不需要第三方工具库。随着 ES6+ 的普及与实现,原生 API 可能在某种程度上已经满足你的需求。

关于 Lodash 判断数据类型的文章就告一段落,后续会对这两篇文章不断地修改,因为还有一些谜题没有解决(比如上一篇文章中提出的 typeof 是如何判断函数的?这篇文章中的 isPrototype() 在 isEmpty() 中起到的什么作用?)。

如果你已经有思路了,或者说有其他见解。欢迎 Email 给我:

swpu.leo@gmail.com

文章首发在微信公众号 cameraee,另外文章同步在我的 优快云 上,优快云 传送门:

https://blog.youkuaiyun.com/swpu_leo

转载请注明

Next

分析 Lodash 源码的下一站,Array

关注

文章首发在微信公众号 cameraee
cameraee

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值