likes
comments
collection
share

【封装小技巧】is 系列方法的封装

作者站长头像
站长
· 阅读数 19

在项目开发中,我们时常会遇到判断某个变量是否为一个有效值,或者根据变量的类型,根据不同的类型进行不同的操作的情况。

比如最常见的,判断一个变量是否为 Truthy 值(什么是 Truthy 值):

if (value !== null && value !== undefined) {
  // 搞事情
}

咋看之下,也就短短两个语句,但这个事情需要进行 10 次、100 次的时候,或许你会开始想到封装:

function isDefined(value) {
  return value !== undefined && value !== null
}

既然有了这个想法,何不一干到底,我们就直接来封装一个自己的 is 方法库。

下列的方法,将使用标准的 TypeScript 编写,你将会看到:泛型、类型谓词 is

一些常规的 is 方法

通过类型谓词 is 可以在 TypeScript 收窄类型,帮助更好的类型推断,这里不展开。

判断 TruthyFalsy

// 可以思考一下 value !== undefined 和 typeof value !== 'undefined' 有什么区别?
// null 呢?
function isDefined<T = unknown>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null
}

function isNull(value: unknown): value is null | undefined {
  return value === undefined || value === null
}

判断其他基本类型(除了 nullundefined):

function isNumber(value: unknown): value is number {
  return typeof value === 'number'
}

// 提问:NaN 是不是一个基本类型呢?
function isNaN(value: unknown): value is number {
  return Number.isNaN(value)
}

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function isBoolean(value: unknown): value is boolean {
  return typeof value === 'boolean'
}

// 严格判断 true
function isTrue(value: unknown): value is true {
  return value === true
}

// 严格判断 false
function isFalse(value: unknown): value is false {
  return value === false
}

// 别忘了 Symbol
function isSymbol(value: unknown): value is symbol {
  return typeof value === 'symbol'
}

// 还有一个基本类型,它是谁呢?

除开基本类型后,接下来就是一些常见对象类型的判断的,在这个之前可以先思考一个问题:

typeof object === 'object' 能不能有效的判断一个变量是否为对象呢?

从广义上来讲,只要这个成立,那该变量确实是一个对象,但这往往不是我们所需要和期望的,因为这样并不能区分数组 [] 和 对象 {} 的区别,包括一些其他对象如 Date

所以我们要借助一个大家都知道的绕一点的方式来判断:Object.prototype.toString,这里我们就直接上了,不知道具体原理的请自行搜索。

常见对象的判断:

// 存一下,减少对象属性的读取
const toString = Object.prototype.toString

function is(value: unknown, type: string) {
  return toString.call(value) === `[object ${type}]`
}

// 这里可以思考对象类型的收窄,用 Record<string, any> 是否合适?
function isObject<T extends Record<string, any> = Record<string, any>>(
  value: unknown
): value is T {
  return is(value, 'Object')
}

// 数组可以使用原生的方法获得更高的效率
function isArray(value: unknown): value is any[] {
  return Array.isArray(value)
}

// 插播一个 function
function isFunction(value: unknown): value is (...any: any[]) => any {
  return typeof value === 'function'
}

// 补充上面被遗忘的 BigInt 基本类型
function isBigInt(value: unknown): value is bigint {
  return typeof value === 'bigint'
}

// 这里如果想要同时支持 PromiseLike 的类型收窄的话要怎么写呢?
function isPromise(value: unknown): value is Promise<any> {
  return (
    !!value &&
    typeof (value as any).then === 'function' &&
    typeof (value as any).catch === 'function'
  )
}

function isSet(value: unknown): value is Set<any> {
  return is(value, 'Set')
}

function isMap(value: unknown): value is Map<any, any> {
  return is(value, 'Map')
}

function isDate(value: unknown): value is Date {
  return is(value, 'Date')
}

function isRegExp(value: unknown): value is RegExp {
  return is(value, 'RegExp')
}

注意到这里单独封装了一个 is 方法,这个方法是可以进行任意的拓展的,比如想判断一些自定义类的时候,可以基于该 is 再封装(上面的方法都是这个原则):

function isMyClass(value: unknown): value is MyClass {
  return is(value, 'MyClass')
}

一些不太常规的 is 方法

除了一些类型的判断,我们时常会有出现像是判断该变量是否为一个 Empty 值:

什么是 Empty 指的,常规一点来讲就是包括:空数组、空字符串、空 Map、空 Set、空对象 {}
function isEmpty(value: unknown) {
  if (Array.isArray(value) || typeof value === 'string') {
    return value.length === 0
  }

  if (value instanceof Map || value instanceof Set) {
    return value.size === 0
  }

  if (isObject(value)) {
    return Object.keys(value).length === 0
  }

  return false
}

还有一个比较常见的场景是,判断某个变量是否是否个对象的键值,我们可以借助 Object.prototype.hasOwnProperty 来判断:

const hasOwnProperty = Object.prototype.hasOwnProperty

function has(value: Record<string, any>, key: string | symbol): key is keyof typeof value {
  return hasOwnProperty.call(value, key)
}

整合一下

好了,到此为止一个包含了基本类型和一些常见类型的 is 函数库就大功告成了,最后附上一份整合后的完整代码,大家可以在这个基础上做一些自己的拓展(应该没人需要纯 js 版本的吧):

const toString = Object.prototype.toString
const hasOwnProperty = Object.prototype.hasOwnProperty

export function is(value: unknown, type: string) {
  return toString.call(value) === `[object ${type}]`
}

export function has(value: Record<string, any>, key: string | symbol): key is keyof typeof value {
  return hasOwnProperty.call(value, key)
}

export function isDefined<T = unknown>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null
}

export function isNull(value: unknown): value is null | undefined {
  return value === undefined || value === null
}

export function isNumber(value: unknown): value is number {
  return typeof value === 'number'
}

export function isNaN(value: unknown): value is number {
  return Number.isNaN(value)
}

export function isString(value: unknown): value is string {
  return typeof value === 'string'
}

export function isBoolean(value: unknown): value is boolean {
  return typeof value === 'boolean'
}

export function isTrue(value: unknown): value is true {
  return value === true
}

export function isFalse(value: unknown): value is false {
  return value === false
}

export function isSymbol(value: unknown): value is symbol {
  return typeof value === 'symbol'
}

export function isBigInt(value: unknown): value is bigint {
  return typeof value === 'bigint'
}

export function isArray(value: unknown): value is any[] {
  return Array.isArray(value)
}

export function isObject<T extends Record<string, any> = Record<string, any>>(
  value: unknown
): value is T {
  return is(value, 'Object')
}

export function isPromise(value: unknown): value is Promise<any> {
  return (
    !!value &&
    typeof (value as any).then === 'function' &&
    typeof (value as any).catch === 'function'
  )
}

export function isFunction(value: unknown): value is (...any: any[]) => any {
  return typeof value === 'function'
}

export function isSet(value: unknown): value is Set<any> {
  return is(value, 'Set')
}

export function isMap(value: unknown): value is Map<any, any> {
  return is(value, 'Map')
}

export function isDate(value: unknown): value is Date {
  return is(value, 'Date')
}

export function isRegExp(value: unknown): value is RegExp {
  return is(value, 'RegExp')
}

export function isEmpty(value: unknown) {
  if (Array.isArray(value) || typeof value === 'string') {
    return value.length === 0
  }

  if (value instanceof Map || value instanceof Set) {
    return value.size === 0
  }

  if (isObject(value)) {
    return Object.keys(value).length === 0
  }

  return false
}

一些碎碎念

最近回想了这几年的工作,发现自己封装过各种各样的工具函数,但很多都是零零散散地遍布在项目中。

也是出于整理和复习的目的,想着分享一下自己写过的一些东西,于是便尝试写了这篇文章,希望能帮助到一些人。

更新:

【封装小技巧】列表处理函数的封装【封装小技巧】数字处理函数的封装

最后来推荐一下我的个人开源项目 Vexip UI - GitHub

一个比较齐全的 Vue3 组件库,支持全面的 css 变量,内置暗黑主题,全量 TypeScript 和组合式 Api,其特点是所有组件几乎每个属性都支持通过配置(传一个对象)来修改其默认值,这应该是目前其他组件库不具备的特性~

现正招募小伙伴来使用或者参与维护与发展这个项目,我一个人的力量非常有限,文档、单元测试、服务端渲染支持、周边插件、使用案例等等,只要你有兴趣都可以从各个切入点参与进来,非常欢迎~

这几期【封装小技巧】的内容源码都包含在了 @vexip-ui/utils 包下面,GitHub,这个包也有单独发布,不过目前还没有 Api 文档,可能需要直接查阅源码食用~