From dedc9f127689d8bb7d124b332b2988d7b0025daa Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Fri, 21 Mar 2025 14:36:50 +0800 Subject: [PATCH] feat(utils): use cursor to add comments, ts type declarations, and vitest test cases to utils functions (#3138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(utils): use cursor to add comments and vitest test cases to utils functions * feat(utils): 添加类型声明 * test(utils/type): 优化单元测试 --- packages/utils/src/dom/index.ts | 302 +++++++++++++----- .../utils/src/type/__tests__/type.test.ts | 198 ++++++++++++ packages/utils/src/type/index.ts | 268 +++++++++++----- 3 files changed, 608 insertions(+), 160 deletions(-) create mode 100644 packages/utils/src/type/__tests__/type.test.ts diff --git a/packages/utils/src/dom/index.ts b/packages/utils/src/dom/index.ts index 34c869524..399556245 100644 --- a/packages/utils/src/dom/index.ts +++ b/packages/utils/src/dom/index.ts @@ -17,33 +17,66 @@ import { isServer } from '../globalConfig' const SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g const MOZ_HACK_REGEXP = /^moz([A-Z])/ -/** 处理style的名字。 - * 把 moz : - _ 等位置,转换为大写驼峰格式, 比如 :camelCase("moz:moz_abc:def-hjk_lmnOpqRst") = MozMozAbcDefHjkLmnOpqRst +/** + * 将字符串转换为驼峰格式 + * 处理style的名字,把 moz : - _ 等位置,转换为大写驼峰格式 + * 例如:camelCase("moz:moz_abc:def-hjk_lmnOpqRst") = MozMozAbcDefHjkLmnOpqRst + * @param name 需要转换的字符串 + * @returns 转换后的驼峰格式字符串 */ -const camelCase = (name: string) => +const camelCase = (name: string): string => name - .replace(SPECIAL_CHARS_REGEXP, (_, separator, letter, offset) => (offset ? letter.toUpperCase() : letter)) + .replace(SPECIAL_CHARS_REGEXP, (_: string, separator: string, letter: string, offset: number) => + offset ? letter.toUpperCase() : letter + ) .replace(MOZ_HACK_REGEXP, 'Moz$1') -/** 绑定事件 */ -export const on = (el: EventTarget, event: any, handler: (this: HTMLElement, ev: any) => any, options = false) => { +/** + * 为元素绑定事件监听器 + * @param el 目标DOM元素 + * @param event 事件名称 + * @param handler 事件处理函数 + * @param options 事件选项,默认为false + */ +export const on = ( + el: EventTarget, + event: string, + handler: (this: HTMLElement, ev: Event) => any, + options: boolean | AddEventListenerOptions = false +): void => { if (el && event && handler) { el.addEventListener(event, handler, options) } } -/** 移除事件 */ -export const off = (el: EventTarget, event: any, handler: (this: HTMLElement, ev: any) => any, options = false) => { + +/** + * 移除元素的事件监听器 + * @param el 目标DOM元素 + * @param event 事件名称 + * @param handler 事件处理函数 + * @param options 事件选项,默认为false + */ +export const off = ( + el: EventTarget, + event: string, + handler: (this: HTMLElement, ev: Event) => any, + options: boolean | EventListenerOptions = false +): void => { if (el && event) { el.removeEventListener(event, handler, options) } } -/** 执行一次就立即移除事件 */ -export const once = (el: HTMLElement, event: any, fn: (this: HTMLElement, ev: any) => any) => { - const listener = function () { +/** + * 为元素绑定一次性事件,触发后自动移除 + * @param el 目标DOM元素 + * @param event 事件名称 + * @param fn 事件处理函数 + */ +export const once = (el: HTMLElement, event: string, fn: (this: HTMLElement, ev: Event) => any): void => { + const listener = function (this: HTMLElement, ev: Event): void { if (fn) { - // eslint-disable-next-line prefer-rest-params - fn.apply(this, arguments) + fn.call(this, ev) } off(el, event, listener) @@ -52,8 +85,14 @@ export const once = (el: HTMLElement, event: any, fn: (this: HTMLElement, ev: an on(el, event, listener) } -/** 判断是否有class, 只能查询单个类名, 且不能有空格 */ -export const hasClass = (el: HTMLElement, clazz: string) => { +/** + * 判断元素是否包含指定的类名 + * 只能查询单个类名,且不能有空格 + * @param el 目标DOM元素 + * @param clazz 要检查的类名 + * @returns 如果元素包含该类名则返回true,否则返回false + */ +export const hasClass = (el: HTMLElement, clazz: string): boolean => { if (!el || !clazz) { return false } @@ -65,34 +104,52 @@ export const hasClass = (el: HTMLElement, clazz: string) => { if (el.classList) { return el.classList.contains(clazz) } + + return false } -/** 给el添加一组classes, clazz 允许为用空格分隔的多个类名 */ -export const addClass = (el: HTMLElement, clazz = '') => { +/** + * 为元素添加一个或多个类名 + * clazz允许为用空格分隔的多个类名 + * @param el 目标DOM元素 + * @param clazz 要添加的类名,多个类名用空格分隔 + */ +export const addClass = (el: HTMLElement, clazz = ''): void => { if (!el) { return } - const classes = clazz.split(' ').filter((name) => name) + const classes: string[] = clazz.split(' ').filter((name: string) => name) - classes.forEach((clsName) => el.classList.add(clsName)) + classes.forEach((clsName: string) => el.classList.add(clsName)) } -/** 移除el上的classes, clazz 允许为用空格分隔的多个类名 */ -export const removeClass = (el: HTMLElement, clazz: string) => { +/** + * 从元素移除一个或多个类名 + * clazz允许为用空格分隔的多个类名 + * @param el 目标DOM元素 + * @param clazz 要移除的类名,多个类名用空格分隔 + */ +export const removeClass = (el: HTMLElement, clazz: string): void => { if (!el || !clazz) { return } - const classes = clazz.split(' ').filter((name) => name) + const classes: string[] = clazz.split(' ').filter((name: string) => name) - classes.forEach((clsName) => el.classList.remove(clsName)) + classes.forEach((clsName: string) => el.classList.remove(clsName)) } -/** 查询元素的style的值。 优先找el.style, 找不到则调用getComputedStyle(el) */ -export const getStyle = (el: HTMLElement, styleName: string) => { +/** + * 获取元素的样式值 + * 优先查找el.style,找不到则调用getComputedStyle(el) + * @param el 目标DOM元素 + * @param styleName 样式属性名 + * @returns 样式属性值 + */ +export const getStyle = (el: HTMLElement, styleName: string): string | null | undefined => { if (isServer) { - return + return undefined } if (!el || !styleName) { return null @@ -105,21 +162,24 @@ export const getStyle = (el: HTMLElement, styleName: string) => { } try { - if (el.style[styleName]) { - return el.style[styleName] + if (el.style[styleName as any]) { + return el.style[styleName as any] } - const computed = window.getComputedStyle(el) - return computed ? computed[styleName] : null + const computed: CSSStyleDeclaration = window.getComputedStyle(el) + return computed ? computed[styleName as any] : null } catch (e) { - return el.style[styleName] + return el.style[styleName as any] } } -/** 给元素赋值style。 - * @param name 当它是对象时,遍历所有属性;当它是字符串时,需要传入第3个参数 value +/** + * 设置元素的样式 + * @param el 目标DOM元素 + * @param name 样式属性名或样式对象。当它是对象时,遍历所有属性;当它是字符串时,需要传入第3个参数value + * @param value 样式属性值,当name为字符串时使用 */ -export const setStyle = (el: HTMLElement, name: string | object, value?: any) => { +export const setStyle = (el: HTMLElement, name: string | Record, value?: any): void => { if (!el || !name) { return } @@ -133,24 +193,28 @@ export const setStyle = (el: HTMLElement, name: string | object, value?: any) => } else { name = camelCase(name) - el.style[name as string] = value + el.style[name as any] = value } } -/** 判断元素是否有滚动的style TINY_NO_USED - * @param vertical true时,只判断overflow-y属性; false时,只判断overflow-x属性; 不传入时,只判断overflow属性! +/** + * 判断元素是否有滚动样式 + * @param el 目标DOM元素 + * @param vertical true时只判断overflow-y属性;false时只判断overflow-x属性;不传入时只判断overflow属性 + * @returns 如果元素有滚动样式则返回匹配结果,否则返回null或undefined */ -export const isScroll = (el: HTMLElement, vertical?: boolean) => { +export const isScroll = (el: HTMLElement, vertical?: boolean): RegExpMatchArray | null | undefined => { if (isServer) { - return + return undefined } - /** 是否需要判断方向 - * 它的值为false: 当vertical = null / undefinded。 - * 它的值为 true: 当vertical =true /false + /** + * 是否需要判断方向 + * 它的值为false: 当vertical = null / undefinded + * 它的值为true: 当vertical = true / false */ const determinedDirection = !isNull(vertical) - let overflow + let overflow: string | null | undefined if (determinedDirection) { overflow = vertical ? getStyle(el, 'overflow-y') : getStyle(el, 'overflow-x') @@ -158,44 +222,53 @@ export const isScroll = (el: HTMLElement, vertical?: boolean) => { overflow = getStyle(el, 'overflow') } - return overflow.match(/(scroll|auto)/) + return overflow ? overflow.match(/(scroll|auto)/) : null } -/** 查找离元素最近的父级滚动元素 - * @param vertical true时,只判断overflow-y属性; false时,只判断overflow-x属性; 不传入时,只判断overflow属性! +/** + * 查找离元素最近的可滚动父元素 + * @param el 目标DOM元素 + * @param vertical true时只判断overflow-y属性;false时只判断overflow-x属性;不传入时只判断overflow属性 + * @returns 最近的可滚动父元素,如果没有则返回元素自身 */ -export const getScrollContainer = (el: HTMLElement, vertical?: boolean) => { +export const getScrollContainer = (el: HTMLElement, vertical?: boolean): Window | HTMLElement | undefined => { if (isServer) { return } - let parent = el + let parent: HTMLElement | Window | Node = el while (parent) { - if (~[window, document, document.documentElement].indexOf(parent)) { + if (~[window, document, document.documentElement].indexOf(parent as any)) { return window } - if (isScroll(parent, vertical)) { - return parent + if (isScroll(parent as HTMLElement, vertical)) { + return parent as HTMLElement } - parent = parent.parentNode as any + parent = (parent as HTMLElement).parentNode as any } - return parent + return parent as HTMLElement } -/** 判断是否 el 完全在 container 中。 四个边有重合都不行,必须完全在里面。 */ -export const isInContainer = (el: HTMLElement, container: HTMLElement) => { +/** + * 判断元素是否完全在容器内部 + * 四个边有重合都不行,必须完全在里面 + * @param el 目标DOM元素 + * @param container 容器元素 + * @returns 如果元素完全在容器内部则返回true,否则返回false + */ +export const isInContainer = (el: HTMLElement, container: HTMLElement): boolean => { if (isServer || !el || !container) { return false } - const elRect = el.getBoundingClientRect() - let containerRect + const elRect: DOMRect = el.getBoundingClientRect() + let containerRect: { top: number; right: number; bottom: number; left: number } - if (~[window, document, document.documentElement].indexOf(container) || isNull(container)) { + if (~[window, document, document.documentElement].indexOf(container as any) || isNull(container)) { containerRect = { top: 0, right: window.innerWidth, @@ -214,16 +287,23 @@ export const isInContainer = (el: HTMLElement, container: HTMLElement) => { ) } -/** 查询页面的位置和尺寸 - * @returns scrollTop : document 或 body的滚动位置 - * @returns scrollLeft : document 或 body的滚动位置 - * @returns visibleHeight : 可视区高度 (不含滚动条) - * @returns visibleWidth : 可视区宽度(不含滚动条) +/** + * 获取页面的位置和尺寸信息 + * @returns 包含滚动位置和可视区域尺寸的对象 + * - scrollTop: document或body的垂直滚动位置 + * - scrollLeft: document或body的水平滚动位置 + * - visibleHeight: 可视区高度(不含滚动条) + * - visibleWidth: 可视区宽度(不含滚动条) */ -export const getDomNode = () => { - const viewportWindow = globalConfig.viewportWindow || window - let documentElement = viewportWindow.document.documentElement - let bodyElem = viewportWindow.document.body +export const getDomNode = (): { + scrollTop: number + scrollLeft: number + visibleHeight: number + visibleWidth: number +} => { + const viewportWindow: Window = globalConfig.viewportWindow || window + let documentElement: HTMLElement = viewportWindow.document.documentElement + let bodyElem: HTMLElement = viewportWindow.document.body return { scrollTop: documentElement.scrollTop || bodyElem.scrollTop, @@ -233,15 +313,30 @@ export const getDomNode = () => { } } -export const getScrollTop = (el) => { - const top = 'scrollTop' in el ? el.scrollTop : el.pageYOffset +/** + * 获取元素的垂直滚动位置 + * 处理iOS滚动反弹导致的负scrollTop值 + * @param el 目标DOM元素 + * @returns 元素的垂直滚动位置,最小为0 + */ +export const getScrollTop = (el: HTMLElement | Window): number => { + const top: number = 'scrollTop' in el ? (el as HTMLElement).scrollTop : (el as Window).pageYOffset // iOS scroll bounce cause minus scrollTop return Math.max(top, 0) } -export const stopPropagation = (event) => event.stopPropagation() +/** + * 阻止事件冒泡 + * @param event 事件对象 + */ +export const stopPropagation = (event: Event): void => event.stopPropagation() -export const preventDefault = (event, isStopPropagation) => { +/** + * 阻止事件默认行为 + * @param event 事件对象 + * @param isStopPropagation 是否同时阻止事件冒泡 + */ +export const preventDefault = (event: Event, isStopPropagation?: boolean): void => { /* istanbul ignore else */ if (typeof event.cancelable !== 'boolean' || event.cancelable) { event.preventDefault() @@ -253,31 +348,61 @@ export const preventDefault = (event, isStopPropagation) => { } const overflowScrollReg = /scroll|auto|overlay/i -const defaultRoot = isServer ? undefined : window +const defaultRoot: Window | undefined = isServer ? undefined : window -const isElement = (node) => node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === 1 +/** + * 判断节点是否为元素节点 + * @param node DOM节点 + * @returns 如果是元素节点则返回true,否则返回false + */ +const isElement = (node: Node): boolean => { + const element = node as Element + return element.tagName !== 'HTML' && element.tagName !== 'BODY' && node.nodeType === 1 +} -export const getScrollParent = (el, root = defaultRoot) => { - let node = el +/** + * 获取元素的可滚动父元素 + * @param el 目标DOM元素 + * @param root 根元素,默认为window + * @returns 可滚动的父元素,如果没有则返回root + */ +export const getScrollParent = ( + el: HTMLElement, + root: Window | HTMLElement | undefined = defaultRoot +): Window | HTMLElement | null => { + let node: Node | null = el while (node && node !== root && isElement(node)) { - const { overflowY } = window.getComputedStyle(node) + const { overflowY }: CSSStyleDeclaration = window.getComputedStyle(node as HTMLElement) if (overflowScrollReg.test(overflowY)) { - return node + return node as HTMLElement } node = node.parentNode } - return root + return root || null } +interface Hooks { + onMounted: (callback: () => void) => void + ref: () => { value?: T } + watch: (source: { value?: T }, callback: () => void) => void +} + +/** + * 创建一个用于获取元素可滚动父元素的组合式函数 + * @param hooks 包含onMounted、ref和watch的对象 + * @returns 返回一个函数,该函数接收元素引用和根元素,返回可滚动父元素的ref + */ export const useScrollParent = - ({ onMounted, ref, watch }) => - (elRef, root = defaultRoot) => { - const scrollParent = ref() - const setScrollParent = () => (scrollParent.value = getScrollParent(elRef.value, root)) + ({ onMounted, ref, watch }: Hooks) => + (elRef: { value?: T }, root: Window | HTMLElement | undefined = defaultRoot) => { + const scrollParent = ref() + const setScrollParent = (): void => { + scrollParent.value = elRef.value ? getScrollParent(elRef.value, root) : null + } watch(elRef, setScrollParent) onMounted(() => elRef.value && setScrollParent()) @@ -285,18 +410,23 @@ export const useScrollParent = return scrollParent } -// 判断body的后代元素是否是隐藏的 -export const isDisplayNone = (elm) => { +/** + * 判断元素是否处于隐藏状态 + * 递归检查元素及其父元素的display和position属性 + * @param elm 目标DOM元素 + * @returns 如果元素处于隐藏状态则返回true,否则返回false + */ +export const isDisplayNone = (elm: HTMLElement | null): boolean => { if (isServer) return false if (elm) { - const computedStyle = getComputedStyle(elm) + const computedStyle: CSSStyleDeclaration = getComputedStyle(elm) if (computedStyle.getPropertyValue('position') === 'fixed') { if (computedStyle.getPropertyValue('display') === 'none') { return true } else if (elm.parentNode !== document.body) { - return isDisplayNone(elm.parentNode) + return isDisplayNone(elm.parentNode as HTMLElement) } } else { return elm.offsetParent === null diff --git a/packages/utils/src/type/__tests__/type.test.ts b/packages/utils/src/type/__tests__/type.test.ts new file mode 100644 index 000000000..168f3fc36 --- /dev/null +++ b/packages/utils/src/type/__tests__/type.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it } from 'vitest' +import { + isNull, + typeOf, + isObject, + isFunction, + isPlainObject, + isEmptyObject, + isNumber, + isNumeric, + isDate, + isSame, + isRegExp, + isPromise +} from '..' + +describe('类型判断工具函数测试', () => { + describe('isNull', () => { + it('应该正确判断null和undefined', () => { + expect(isNull(null)).toBe(true) + expect(isNull(undefined)).toBe(true) + expect(isNull('')).toBe(false) + expect(isNull(0)).toBe(false) + expect(isNull(false)).toBe(false) + expect(isNull({})).toBe(false) + }) + }) + + describe('typeOf', () => { + it('应该正确返回各种类型的字符串表示', () => { + expect(typeOf(undefined)).toBe('undefined') + expect(typeOf(null)).toBe('null') + expect(typeOf(true)).toBe('boolean') + expect(typeOf(3)).toBe('number') + expect(typeOf('test')).toBe('string') + expect(typeOf(function () {})).toBe('function') + expect(typeOf(async function () {})).toBe('asyncFunction') + expect(typeOf([])).toBe('array') + expect(typeOf(new Date())).toBe('date') + expect(typeOf(new Error('测试错误'))).toBe('error') + expect(typeOf(/test/)).toBe('regExp') + expect(typeOf({})).toBe('object') + }) + }) + + describe('isObject', () => { + it('应该正确判断对象类型', () => { + expect(isObject({})).toBe(true) + expect(isObject({})).toBe(true) + expect(isObject(null)).toBe(false) + expect(isObject(undefined)).toBe(false) + expect(isObject([])).toBe(false) + expect(isObject(new Date())).toBe(false) + expect(isObject(function () {})).toBe(false) + }) + }) + + describe('isFunction', () => { + it('应该正确判断函数类型', () => { + expect(isFunction(function () {})).toBe(true) + expect(isFunction(async function () {})).toBe(true) + expect(isFunction(() => {})).toBe(true) + expect(isFunction(async () => {})).toBe(true) + expect(isFunction({})).toBe(false) + expect(isFunction(null)).toBe(false) + expect(isFunction(undefined)).toBe(false) + }) + }) + + describe('isPlainObject', () => { + it('应该正确判断纯粹的对象', () => { + expect(isPlainObject({})).toBe(true) + expect(isPlainObject({})).toBe(true) + expect(isPlainObject(Object.create(null))).toBe(true) + expect(isPlainObject([])).toBe(false) + expect(isPlainObject(new Date())).toBe(false) + expect(isPlainObject(null)).toBe(false) + expect(isPlainObject(undefined)).toBe(false) + + // 自定义类的实例不是纯粹对象 + class TestClass {} + expect(isPlainObject(new TestClass())).toBe(false) + }) + }) + + describe('isEmptyObject', () => { + it('应该正确判断空对象', () => { + expect(isEmptyObject({})).toBe(true) + expect(isEmptyObject([])).toBe(true) + expect(isEmptyObject({ a: 1 })).toBe(false) + expect(isEmptyObject([1, 2])).toBe(false) + expect(isEmptyObject(null)).toBe(true) + expect(isEmptyObject(undefined)).toBe(true) + }) + }) + + describe('isNumber', () => { + it('应该正确判断数字类型', () => { + expect(isNumber(1)).toBe(true) + expect(isNumber(0)).toBe(true) + expect(isNumber(-1)).toBe(true) + expect(isNumber(1.5)).toBe(true) + expect(isNumber(NaN)).toBe(false) // NaN不是有效数字 + expect(isNumber(Infinity)).toBe(false) // Infinity不是有效数字 + expect(isNumber('1')).toBe(false) + expect(isNumber(null)).toBe(false) + expect(isNumber(undefined)).toBe(false) + }) + }) + + describe('isNumeric', () => { + it('应该正确判断数值', () => { + expect(isNumeric('-10')).toBe(true) + expect(isNumeric('16')).toBe(true) + expect(isNumeric(0xff)).toBe(true) + expect(isNumeric('0xFF')).toBe(true) + expect(isNumeric('8e5')).toBe(true) + expect(isNumeric(3.1415)).toBe(true) + expect(isNumeric(+10)).toBe(true) + expect(isNumeric('')).toBe(false) + expect(isNumeric({})).toBe(false) + expect(isNumeric(NaN)).toBe(false) + expect(isNumeric(null)).toBe(false) + expect(isNumeric(true)).toBe(false) + expect(isNumeric(Infinity)).toBe(false) + expect(isNumeric(undefined)).toBe(false) + }) + }) + + describe('isDate', () => { + it('应该正确判断日期类型', () => { + expect(isDate(new Date())).toBe(true) + expect(isDate(new Date('2023-01-01'))).toBe(true) + expect(isDate(Date.now())).toBe(false) + expect(isDate('2023-01-01')).toBe(false) + expect(isDate({})).toBe(false) + expect(isDate(null)).toBe(false) + expect(isDate(undefined)).toBe(false) + }) + }) + + describe('isSame', () => { + it('应该正确判断两个值是否相同', () => { + expect(isSame(1, 1)).toBe(true) + expect(isSame('a', 'a')).toBe(true) + expect(isSame(true, true)).toBe(true) + expect(isSame(null, null)).toBe(true) + expect(isSame(undefined, undefined)).toBe(true) + expect(isSame(NaN, NaN)).toBe(true) // 特殊情况:NaN === NaN 应该为 true + expect(isSame({}, {})).toBe(false) // 引用不同 + expect(isSame([], [])).toBe(false) // 引用不同 + expect(isSame(1, '1')).toBe(false) // 类型不同 + expect(isSame(0, false)).toBe(false) // 类型不同 + }) + }) + + describe('isRegExp', () => { + const testCases = [ + { input: /test/, expected: true, description: '字面量正则表达式应该返回true' }, + { input: /test/i, expected: true, description: '带修饰符的正则表达式应该返回true' }, + { input: /\d+/g, expected: true, description: '带特殊字符和修饰符的正则表达式应该返回true' }, + { input: '/test/', expected: false, description: '字符串不应该被识别为正则表达式' }, + { input: {}, expected: false, description: '空对象不应该被识别为正则表达式' }, + { + input: { source: 'test', flags: 'g' }, + expected: false, + description: '类似正则表达式的对象不应该被识别为正则表达式' + }, + { input: null, expected: false, description: 'null不应该被识别为正则表达式' }, + { input: undefined, expected: false, description: 'undefined不应该被识别为正则表达式' }, + { input: 123, expected: false, description: '数字不应该被识别为正则表达式' }, + { input: true, expected: false, description: '布尔值不应该被识别为正则表达式' } + ] + + it.each(testCases)('$description', ({ input, expected }) => { + expect(isRegExp(input)).toBe(expected) + }) + }) + + describe('isPromise', () => { + it('应该正确判断Promise对象', () => { + expect(isPromise(Promise.resolve())).toBe(true) + expect(isPromise(new Promise(() => {}))).toBe(true) + + // 模拟Promise-like对象 + const promiseLike = { + then: () => {}, + catch: () => {} + } + expect(isPromise(promiseLike)).toBe(true) + + expect(isPromise({})).toBe(false) + expect(isPromise({ then: () => {} })).toBe(false) // 缺少catch方法 + expect(isPromise(null)).toBe(false) + expect(isPromise(undefined)).toBe(false) + }) + }) +}) diff --git a/packages/utils/src/type/index.ts b/packages/utils/src/type/index.ts index 6d78d183d..b6006b5ba 100644 --- a/packages/utils/src/type/index.ts +++ b/packages/utils/src/type/index.ts @@ -10,14 +10,41 @@ * */ -export const toString = Object.prototype.toString -export const hasOwn = Object.prototype.hasOwnProperty +/** + * 获取对象的字符串表示形式 + * 用于类型判断,等同于 Object.prototype.toString + */ +export const toString: () => string = Object.prototype.toString -const getProto = Object.getPrototypeOf -const fnToString = hasOwn.toString -const ObjectFunctionString = fnToString.call(Object) +/** + * 检查对象是否具有指定的属性 + * 等同于 Object.prototype.hasOwnProperty + */ +export const hasOwn: (prop: PropertyKey) => boolean = Object.prototype.hasOwnProperty -const class2type = { +/** + * 获取对象的原型 + * 等同于 Object.getPrototypeOf + */ +const getProto: (obj: T) => object | null = Object.getPrototypeOf + +/** + * 获取函数的字符串表示 + * 用于判断对象构造函数 + */ +const fnToString: () => string = hasOwn.toString + +/** + * Object函数的字符串表示 + * 用于判断纯粹对象 + */ +const ObjectFunctionString: string = fnToString.call(Object) + +/** + * 类型映射表 + * 将Object.prototype.toString的结果映射为对应的类型字符串 + */ +const class2type: Record = { '[object Error]': 'error', '[object Object]': 'object', '[object RegExp]': 'regExp', @@ -30,78 +57,119 @@ const class2type = { '[object Boolean]': 'boolean' } -/** 判断是否为 null / undefined */ -export const isNull = (x: any) => x === null || x === undefined +/** + * 判断值是否为 null 或 undefined + * + * @param {any} x - 要检查的值 + * @returns {boolean} - 如果值为null或undefined则返回true,否则返回false + * + * @example + * isNull(null) // true + * isNull(undefined) // true + * isNull(0) // false + * isNull('') // false + */ +export const isNull = (x: any): boolean => x === null || x === undefined /** * 返回 JavaScript 对象的类型。 * * 如果对象是 undefined 或 null,则返回相应的'undefined'或'null'。 + * 其他一切都将返回它的类型字符串表示。 * - * 其他一切都将返回它的类型'object'。 - * - * typeOf( undefined ) === 'undefined' - * typeOf() === 'undefined' - * typeOf( window.notDefined ) === 'undefined' - * typeOf( null ) === 'null' - * typeOf( true ) === 'boolean' - * typeOf( 3 ) === 'number' - * typeOf( "test" ) === 'string' - * typeOf( function (){} ) === 'function' - * typeOf( [] ) === 'array' - * typeOf( new Date() ) === 'date' - * typeOf( new Error() ) === 'error' - * typeOf( /test/ ) === 'regExp' + * @param {any} obj - 要检查类型的对象 + * @returns {string} - 类型的字符串表示 * + * @example + * typeOf(undefined) // 'undefined' + * typeOf(null) // 'null' + * typeOf(true) // 'boolean' + * typeOf(3) // 'number' + * typeOf("test") // 'string' + * typeOf(function(){}) // 'function' + * typeOf([]) // 'array' + * typeOf(new Date()) // 'date' + * typeOf(new Error()) // 'error' + * typeOf(/test/) // 'regExp' + * typeOf({}) // 'object' */ -export const typeOf: (obj: any) => string = (obj) => - isNull(obj) ? String(obj) : class2type[toString.call(obj)] || 'object' +export const typeOf = (obj: any): string => (isNull(obj) ? String(obj) : class2type[toString.call(obj)] || 'object') /** - * 判断对象是否为 object 类型。 + * 判断对象是否为纯粹的对象类型(通过{}或new Object创建的对象) * - * isObject({}) // true + * @param {any} obj - 要检查的对象 + * @returns {boolean} - 如果是纯粹的对象类型则返回true,否则返回false + * + * @example + * isObject({}) // true + * isObject(new Object()) // true + * isObject([]) // false + * isObject(null) // false */ -export const isObject = (obj: any) => typeOf(obj) === 'object' +export const isObject = (obj: any): boolean => typeOf(obj) === 'object' /** - * 判断对象是否为 function 类型。 + * 判断对象是否为函数类型(包括普通函数和异步函数) * - * isObject(function (){) // true - + * @param {any} fn - 要检查的对象 + * @returns {boolean} - 如果是函数类型则返回true,否则返回false + * + * @example + * isFunction(function(){}) // true + * isFunction(async function(){}) // true + * isFunction(() => {}) // true + * isFunction({}) // false */ -export const isFunction = (fn: any) => ['asyncFunction', 'function'].includes(typeOf(fn)) +export const isFunction = (fn: any): boolean => ['asyncFunction', 'function'].includes(typeOf(fn)) /** - * 判断对象是否为简单对象。 + * 判断对象是否为简单对象(纯粹的对象) * - * 即不是 HTML 节点对象,也不是 window 对象,而是纯粹的对象(通过 '{}' 或者 'new Object' 创建的)。 + * 即不是 HTML 节点对象,也不是 window 对象,而是纯粹的对象 + * (通过 '{}' 或者 'new Object' 创建的,或者原型为null的对象) * - * let obj = {} - * isPlainObject(obj) //true + * @param {any} obj - 要检查的对象 + * @returns {boolean} - 如果是简单对象则返回true,否则返回false + * + * @example + * isPlainObject({}) // true + * isPlainObject(new Object()) // true + * isPlainObject(Object.create(null)) // true + * isPlainObject([]) // false + * isPlainObject(new Date()) // false */ -export const isPlainObject = (obj: any) => { +export const isPlainObject = (obj: any): boolean => { if (!obj || toString.call(obj) !== '[object Object]') { return false } - const proto = getProto(obj) + const proto: object | null = getProto(obj) if (!proto) { return true } - const Ctor = hasOwn.call(proto, 'constructor') && proto.constructor + const Ctor: any = hasOwn.call(proto, 'constructor') && proto.constructor return typeof Ctor === 'function' && fnToString.call(Ctor) === ObjectFunctionString } /** - * 检查对象是否为空(不包含任何属性)。 + * 检查对象是否为空(不包含任何属性) * - * let obj = {} - * isEmptyObject(obj) // true + * 对于对象和数组,检查是否有自有属性 + * 对于其他类型,直接返回true + * + * @param {any} obj - 要检查的对象 + * @returns {boolean} - 如果对象为空则返回true,否则返回false + * + * @example + * isEmptyObject({}) // true + * isEmptyObject([]) // true + * isEmptyObject({a: 1}) // false + * isEmptyObject([1, 2]) // false */ -export const isEmptyObject = (obj: any) => { - const type = typeOf(obj) +export const isEmptyObject = (obj: any): boolean => { + const type: string = typeOf(obj) if (type === 'object' || type === 'array') { for (const name in obj) { @@ -115,52 +183,104 @@ export const isEmptyObject = (obj: any) => { } /** - * 判断对象是否为数字类型。 + * 判断对象是否为数字类型(有限数字) * - * isNumber(369) // true + * 注意:NaN和Infinity不被视为有效数字 + * + * @param {any} value - 要检查的值 + * @returns {boolean} - 如果是有效数字则返回true,否则返回false + * + * @example + * isNumber(369) // true + * isNumber(0) // true + * isNumber(-1.5) // true + * isNumber(NaN) // false + * isNumber(Infinity) // false + * isNumber('123') // false */ -export const isNumber = (value: any) => typeof value === 'number' && isFinite(value) +export const isNumber = (value: any): boolean => typeof value === 'number' && isFinite(value) /** - * 判断对象是否代表一个数值。 + * 判断对象是否代表一个数值(可以是数字类型或可转换为数字的字符串) * - * isNumeric('-10') // true - * isNumeric(16) // true - * isNumeric(0xFF) // true - * isNumeric('0xFF') // true - * isNumeric('8e5') // true - * isNumeric(3.1415) // true - * isNumeric(+10) // true - * isNumeric('') // false - * isNumeric({}) // false - * isNumeric(NaN) // false - * isNumeric(null) // false - * isNumeric(true) // false - * isNumeric(Infinity) // false - * isNumeric(undefined) // false + * @param {any} value - 要检查的值 + * @returns {boolean} - 如果是数值则返回true,否则返回false + * + * @example + * isNumeric('-10') // true + * isNumeric(16) // true + * isNumeric(0xFF) // true + * isNumeric('0xFF') // true + * isNumeric('8e5') // true + * isNumeric(3.1415) // true + * isNumeric(+10) // true + * isNumeric('') // false + * isNumeric({}) // false + * isNumeric(NaN) // false + * isNumeric(null) // false + * isNumeric(true) // false + * isNumeric(Infinity) // false + * isNumeric(undefined) // false */ -export const isNumeric = (value: any) => value - parseFloat(value) >= 0 +export const isNumeric = (value: any): boolean => value - parseFloat(value) >= 0 /** - * 判断对象是否为日期类型。 + * 判断对象是否为日期类型 * - * let date = new Date() - * isDate(date) // true + * @param {any} value - 要检查的值 + * @returns {boolean} - 如果是日期类型则返回true,否则返回false + * + * @example + * isDate(new Date()) // true + * isDate(Date.now()) // false + * isDate('2023-01-01') // false */ -export const isDate = (value) => typeOf(value) === 'date' +export const isDate = (value: any): boolean => typeOf(value) === 'date' /** - * 判断两个值是否值相同且类型相同。 + * 判断两个值是否值相同且类型相同 * - * 注:在 JavaScript 里 NaN === NaN 为 false,因此不能简单的用 === 来判断。 + * 特别处理了NaN的情况,在JavaScript中NaN !== NaN,但在此函数中认为它们相同 * - * isSame(1, 1) // true - * isSame(NaN, NaN) // true + * @param {any} x - 第一个值 + * @param {any} y - 第二个值 + * @returns {boolean} - 如果两个值相同则返回true,否则返回false + * + * @example + * isSame(1, 1) // true + * isSame(NaN, NaN) // true + * isSame('a', 'a') // true + * isSame({}, {}) // false (引用不同) + * isSame(1, '1') // false (类型不同) */ -export const isSame = (x: any, y: any) => +export const isSame = (x: any, y: any): boolean => x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)) -/** 判断是否是正则表达式 */ -export const isRegExp = (value: any) => typeOf(value) === 'regExp' +/** + * 判断值是否是正则表达式 + * + * @param {any} value - 要检查的值 + * @returns {boolean} - 如果是正则表达式则返回true,否则返回false + * + * @example + * isRegExp(/test/) // true + * isRegExp(new RegExp('test')) // true + * isRegExp('/test/') // false (这是字符串) + */ +export const isRegExp = (value: any): boolean => typeOf(value) === 'regExp' -export const isPromise = (val) => isObject(val) && isFunction(val.then) && isFunction(val.catch) +/** + * 判断值是否是Promise对象或类Promise对象 + * + * 类Promise对象需要有then和catch方法 + * + * @param {any} val - 要检查的值 + * @returns {boolean} - 如果是Promise对象则返回true,否则返回false + * + * @example + * isPromise(Promise.resolve()) // true + * isPromise(new Promise(() => {})) // true + * isPromise({then: () => {}, catch: () => {}}) // true + * isPromise({then: () => {}}) // false (缺少catch方法) + */ +export const isPromise = (val: any): boolean => isObject(val) && isFunction(val.then) && isFunction(val.catch)