在项目开发中,我们时常会遇到判断某个变量是否为一个有效值,或者根据变量的类型,根据不同的类型进行不同的操作的情况。
比如最常见的,判断一个变量是否为 Truthy
值(什么是 Truthy 值):
if (value !== null && value !== undefined) {
// 搞事情
}
咋看之下,也就短短两个语句,但这个事情需要进行 10 次、100 次的时候,或许你会开始想到封装:
function isDefined(value) {
return value !== undefined && value !== null
}
既然有了这个想法,何不一干到底,我们就直接来封装一个自己的 is
方法库。
下列的方法,将使用标准的TypeScript
编写,你将会看到:泛型、类型谓词is
。
一些常规的 is
方法
通过类型谓词 is
可以在 TypeScript
收窄类型,帮助更好的类型推断,这里不展开。
判断 Truthy
和 Falsy
:
// 可以思考一下 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
}
判断其他基本类型(除了 null
和 undefined
):
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 文档,可能需要直接查阅源码食用~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。