JavaScript 实用方法/API (提供源码🥳) — 02

281 阅读15分钟

系列

简介

本篇主要是自定义了日常中经常需要的JS方法或Api (例如: 节流/防抖,柯里化,字符串格式转换等), 并已提供API源码。


Animater (节流/防抖)

API 列表

Api ScopeApi NameApi Description
Animaterdebounce防抖动函数
Animaterthrottle节流函数, debounce函数的变种

API 使用

  import js from 'js-simpler'
  
  const wait = 500
  const func = () => console.log('print log')
  
  js.debounce(func, wait)
  js.debounce(func, wait, { leading: true, trailing: true })
  js.debounce(func, wait, { leading: true, trailing: true, maxWait: 1000 })
  
  js.throttle(func, wait)
  js.throttle(func, wait, { leading: true, trailing: true }) // 等同于 js.debounce(func, wait, { leading: true, trailing: true, maxWait: wait })

API 源码

import { isUndef } from './~Nullable'
import { isObject } from './-Object'
import { isFunction } from './-Function'
import { isFiniteNumber } from './-Number'
import { toFiniteNumber } from './-Number'

const debounce = (func: Function, wait: number, options: { leading?: boolean; trailing?: boolean; maxWait?: number; } = {}) => {
  let result: any
  let timerId: any
  let lastArgs: any
  let lastThis: any
  let lastCallTime = 0
  let lastInvokeTime = 0

  if (!isFunction(func)) {
    throw new TypeError('#<func> is not a function')
  }

  if (!isObject(options)) {
    options = {}
  }

  wait = Math.max(toFiniteNumber(wait), 0)

  const leading = options.leading === true
  const trailing = options.trailing !== false
  const useFrame = wait < 20 && typeof window.requestAnimationFrame === 'function'
  const maxWait = isFiniteNumber(options.maxWait) && Math.max(options.maxWait, wait)


  function timerExpired() {
    const time = Date.now()

    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }

    useFrame
      ? timerId = window.requestAnimationFrame(timerExpired)
      : timerId = setTimeout(timerExpired, remainingWait(time))
  }

  function remainingWait(time: number) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const remainingCallTime = Math.max(wait - timeSinceLastCall, 0)
    const remainingInvokeTime = Math.max(+maxWait! - timeSinceLastInvoke, 0)
    return maxWait === false ? remainingCallTime : Math.min(remainingCallTime, remainingInvokeTime)
  }

  function shouldInvoke(time: number) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime

    return (
      lastCallTime === 0 ||
      timeSinceLastCall < 0 ||
      timeSinceLastCall >= wait ||
      (maxWait !== false && timeSinceLastInvoke >= maxWait)
    )
  }

  function leadingEdge(time: number) {
    if (leading) {
      return invokeFunc(time)
    }

    lastInvokeTime = time

    useFrame
      ? timerId = window.requestAnimationFrame(timerExpired)
      : timerId = setTimeout(timerExpired, wait)

    return result
  }

  function trailingEdge(time: number) {
    timerId = undefined

    if (trailing && lastArgs) {
      return invokeFunc(time)
    }

    lastArgs = lastThis = undefined

    return result
  }

  function invokeFunc(time: number) {
    const args = lastArgs
    const context = lastThis
    lastInvokeTime = time
    lastArgs = lastThis = undefined
    result = func.apply(context, args)
    return result
  }

  function cancel() {
    if (!isUndef(timerId)) {
      useFrame
        ? window.cancelAnimationFrame(timerId)
        : clearTimeout(timerId)
    }

    lastArgs = lastThis = timerId = undefined
    lastCallTime = lastInvokeTime = 0
  }

  function flush() {
    return isUndef(timerId) ? result : trailingEdge(Date.now())
  }

  function debounced(this: any, ...rest: any[]) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastThis = this
    lastArgs = rest
    lastCallTime = time

    if (isInvoking && isUndef(timerId)) {
      return leadingEdge(lastCallTime)
    }

    if (isInvoking && maxWait !== false) {
      useFrame || clearTimeout(timerId)
      useFrame && window.cancelAnimationFrame(timerId)

      useFrame && (timerId = window.requestAnimationFrame(timerExpired))
      useFrame || (timerId = setTimeout(timerExpired, remainingWait(time)))

      return invokeFunc(lastCallTime)
    }

    if (isUndef(timerId)) {
      useFrame
        ? timerId = window.requestAnimationFrame(timerExpired)
        : timerId = setTimeout(timerExpired, wait)
    }

    return result
  }

  debounced.cancel = cancel
  debounced.flush = flush

  return debounced
}

const throttle = (func: Function, wait: number, options: { leading?: boolean; trailing?: boolean } = {}) => {
  const leading = !isObject(options) || options.leading !== false
  const trailing = !isObject(options) || options.trailing !== false

  if (!isFunction(func)) {
    throw new TypeError('#<func> is not a function')
  }

  return debounce(func, wait, {
    maxWait: wait,
    leading: leading,
    trailing: trailing
  })
}

Currylize (柯里化)

API 列表

Api ScopeApi NameApi Description
Currylizecurry柯里化 新的函数, 函数参数数量 <=5 个时, 支持 typescript 类型提示

API 使用

  import js from 'js-simpler'
  
  const x = 30 // 第一个参数
  const y = 20 // 第二个参数
  const m = 10 // 第三个参数
  const n = 5 // 第四个参数
  
  // 原函数
  const fn = (x, y, m, n) => {
    return x * y - y * m - n
  }
  
  // 柯里化后函数
  const _fn_ = js.curry(fn)
  
  /**
   * 说明: 参数中 js.curry, 充当一个占位符,其意是该参数后续补充
   */
  _fn_(js.curry, 20)(js.curry, js.curry, 5)(30, 10) // 395
  _fn_(js.curry, js.curry, 10)(js.curry, 20)(30, 5) // 395
  _fn_(js.curry, js.curry, 10)(30, 20)(5) // 395
  _fn_(30, 20, 10, 5) // 395

API 源码

import { isFunction } from './-Function'
import { isInteger } from './-Number'

export interface Curry {
  <T1, R>(func: (this: any, t1: T1) => R, length?: number): CurryFn1<T1, R>;
  <T1, T2, R>(func: (this: any, t1: T1, t2: T2) => R, length?: number): CurryFn2<T1, T2, R>;
  <T1, T2, T3, R>(func: (this: any, t1: T1, t2: T2, t3: T3) => R, length?: number): CurryFn3<T1, T2, T3, R>;
  <T1, T2, T3, T4, R>(func: (this: any, t1: T1, t2: T2, t3: T3, t4: T4) => R, length?: number): CurryFn4<T1, T2, T3, T4, R>;
  <T1, T2, T3, T4, T5, R>(func: (this: any, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R, length?: number): CurryFn5<T1, T2, T3, T4, T5, R>;
  (func: (this: any, ...args: any[]) => any, length?: number): (this: any, ...args: any[]) => any
}

export interface CurryFn1<T1, R> {
  (this: any): CurryFn1<T1, R>;
  (this: any, t1: T1): R;
}

export interface CurryFn2<T1, T2, R> {
  (this: any): CurryFn2<T1, T2, R>;
  (this: any, t1: T1): CurryFn1<T2, R>;
  (this: any, t1: Curry, t2: T2): CurryFn1<T1, R>;
  (this: any, t1: T1, t2: T2): R;
}

export interface CurryFn3<T1, T2, T3, R> {
  (this: any): CurryFn3<T1, T2, T3, R>;
  (this: any, t1: T1): CurryFn2<T2, T3, R>;
  (this: any, t1: Curry, t2: T2): CurryFn2<T1, T3, R>;
  (this: any, t1: T1, t2: T2): CurryFn1<T3, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3): CurryFn2<T1, T2, R>;
  (this: any, t1: T1, t2: Curry, t3: T3): CurryFn1<T2, R>;
  (this: any, t1: Curry, t2: T2, t3: T3): CurryFn1<T1, R>;
  (this: any, t1: T1, t2: T2, t3: T3): R;
}

export interface CurryFn4<T1, T2, T3, T4, R> {
  (this: any): CurryFn4<T1, T2, T3, T4, R>;
  (this: any, t1: T1): CurryFn3<T2, T3, T4, R>;
  (this: any, t1: Curry, t2: T2): CurryFn3<T1, T3, T4, R>;
  (this: any, t1: T1, t2: T2): CurryFn2<T3, T4, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3): CurryFn3<T1, T2, T4, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3): CurryFn2<T2, T4, R>;
  (this: any, t1: Curry, t2: T2, t3: T3): CurryFn2<T1, T4, R>;
  (this: any, t1: T1, t2: T2, t3: T3): CurryFn1<T4, R>;
  (this: any, t1: Curry, t2: Curry, t3: Curry, t4: T4): CurryFn3<T1, T2, T3, R>;
  (this: any, t1: T1, t2: Curry, t3: Curry, t4: T4): CurryFn2<T2, T3, R>;
  (this: any, t1: Curry, t2: T2, t3: Curry, t4: T4): CurryFn2<T1, T3, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3, t4: T4): CurryFn2<T1, T2, R>;
  (this: any, t1: T1, t2: T2, t3: Curry, t4: T4): CurryFn1<T3, R>;
  (this: any, t1: T1, t2: Curry, t3: T3, t4: T4): CurryFn1<T2, R>;
  (this: any, t1: Curry, t2: T2, t3: T3, t4: T4): CurryFn1<T1, R>;
  (this: any, t1: T1, t2: T2, t3: T3, t4: T4): R;
}

export interface CurryFn5<T1, T2, T3, T4, T5, R> {
  (this: any): CurryFn5<T1, T2, T3, T4, T5, R>;
  (this: any, t1: T1): CurryFn4<T2, T3, T4, T5, R>;
  (this: any, t1: Curry, t2: T2): CurryFn4<T1, T3, T4, T5, R>;
  (this: any, t1: T1, t2: T2): CurryFn3<T3, T4, T5, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3): CurryFn4<T1, T2, T4, T5, R>;
  (this: any, t1: T1, t2: Curry, t3: T3): CurryFn3<T2, T4, T5, R>;
  (this: any, t1: Curry, t2: T2, t3: T3): CurryFn3<T1, T4, T5, R>;
  (this: any, t1: T1, t2: T2, t3: T3): CurryFn2<T4, T5, R>;
  (this: any, t1: Curry, t2: Curry, t3: Curry, t4: T4): CurryFn4<T1, T2, T3, T5, R>;
  (this: any, t1: T1, t2: Curry, t3: Curry, t4: T4): CurryFn3<T2, T3, T5, R>;
  (this: any, t1: Curry, t2: T2, t3: Curry, t4: T4): CurryFn3<T1, T3, T5, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3, t4: T4): CurryFn3<T1, T2, T5, R>;
  (this: any, t1: T1, t2: T2, t3: Curry, t4: T4): CurryFn2<T3, T5, R>;
  (this: any, t1: T1, t2: Curry, t3: T3, t4: T4): CurryFn2<T2, T5, R>;
  (this: any, t1: Curry, t2: T2, t3: T3, t4: T4): CurryFn2<T1, T5, R>;
  (this: any, t1: T1, t2: T2, t3: T3, t4: T4): CurryFn1<T5, R>;
  (this: any, t1: Curry, t2: Curry, t3: Curry, t4: Curry, t5: T5): CurryFn4<T1, T2, T3, T4, R>;
  (this: any, t1: T1, t2: Curry, t3: Curry, t4: Curry, t5: T5): CurryFn3<T2, T3, T4, R>;
  (this: any, t1: Curry, t2: T2, t3: Curry, t4: Curry, t5: T5): CurryFn3<T1, T3, T4, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3, t4: Curry, t5: T5): CurryFn3<T1, T2, T4, R>;
  (this: any, t1: Curry, t2: Curry, t3: Curry, t4: T4, t5: T5): CurryFn3<T1, T2, T3, R>;
  (this: any, t1: T1, t2: T2, t3: Curry, t4: Curry, t5: T5): CurryFn2<T3, T4, R>;
  (this: any, t1: T1, t2: Curry, t3: T3, t4: Curry, t5: T5): CurryFn2<T2, T4, R>;
  (this: any, t1: T1, t2: Curry, t3: Curry, t4: T4, t5: T5): CurryFn2<T2, T3, R>;
  (this: any, t1: Curry, t2: T2, t3: T3, t4: Curry, t5: T5): CurryFn2<T1, T4, R>;
  (this: any, t1: Curry, t2: T2, t3: Curry, t4: T4, t5: T5): CurryFn2<T1, T3, R>;
  (this: any, t1: Curry, t2: Curry, t3: T3, t4: T4, t5: T5): CurryFn2<T1, T2, R>;
  (this: any, t1: T1, t2: T2, t3: T3, t4: Curry, t5: T5): CurryFn1<T4, R>;
  (this: any, t1: T1, t2: T2, t3: Curry, t4: T4, t5: T5): CurryFn1<T3, R>;
  (this: any, t1: T1, t2: Curry, t3: T3, t4: T4, t5: T5): CurryFn1<T2, R>;
  (this: any, t1: Curry, t2: T2, t3: T3, t4: T4, t5: T5): CurryFn1<T1, R>;
  (this: any, t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): R;
}


export const curry: Curry = function(this: any, fn: Function, length?: number) {
  if (!isFunction(fn)) {
    throw new TypeError('#<fn> is not a function')
  }

  const currying = (fn: Function, length: number, holder: any, params: any[], holders: any[]) => {
    const wrapper = (...rest: any[]) => {
      const _params = params.slice()
      const _holders = holders.slice()

      rest.forEach(arg => {
        if (!holders.length && arg === holder) {
          _params.push(arg)
          _holders.push(_params.length - 1)
          return
        }

        if (!holders.length && arg !== holder) {
          _params.push(arg)
          return
        }

        if (holders.length && arg !== holder) {
          const index = holders.shift()
          _holders.splice(_holders.indexOf(index), 1)
          _params[index] = arg
          return
        }

        holders.shift()
      })

      const isPass = (
        _params.length >= length &&
        _params.slice(0, length).every(v => v !== holder)
      )

      return !isPass ? currying(fn, length, holder, _params, _holders) : fn.apply(this, _params)
    }

    return wrapper
  }

  return currying(fn, isInteger(length) && length >= 0 ? length : fn.length, curry, [], [])
}


Formater (字符串格式转换)

API 列表

Api ScopeApi NameApi Description
FormaterlowerCase转换成 小写字符串
FormaterupperCase转换成 大写字符串
FormatercamelCase转换成 驼峰式字符串
FormaterunderCase转换成 下划线字符串
FormaterhyphenCase转换成 连字符字符串

API 使用

  import js from 'js-simpler'
  
  js.lowerCase('Text_Content') // 'text_content'
  js.lowerCase('text_content') // 'text_content'
  
  js.upperCase('Text_Content') // 'TEXT_CONTENT'
  js.upperCase('text_content') // 'TEXT_CONTENT'
  
  js.camelCase('text_content') // 'textContent'
  js.camelCase('_text_content') // 'TextContent'
  js.camelCase('-text-content') // 'TextContent'
  js.camelCase('text_content', true) // 'TextContent', true 会首字母转换成大写
  js.camelCase('_text_content', true) // 'TextContent'
  js.camelCase('-text-content', true) // 'TextContent'
 
  js.underCase('textContent') // 'text_content'
  js.underCase('TextContent') // '_text_content'
  js.underCase('_text_content') // '_text_content'
  js.underCase('-text-content') // '_text_content'
  js.underCase('TextContent', true) // 'text_content', true 如果首个字符是`-`或`_`, 会移除
  js.underCase('_text_content', true) // 'text_content'
  js.underCase('-text-content', true) // 'text_content'
  
  js.hyphenCase('textContent') // 'text-content'
  js.hyphenCase('TextContent') // '-text-content'
  js.hyphenCase('_text_content') // '-text-content'
  js.hyphenCase('-text-content') // '-text-content'
  js.hyphenCase('TextContent', true) // 'text-content', true 如果首个字符是`-`或`_`, 会移除
  js.hyphenCase('_text_content', true) // 'text-content'
  js.hyphenCase('-text-content', true) // 'text-content'

API 源码

import { isNonEmptyString } from './-String'

export const lowerCase = <T = any>(string: T): T => {
  return (isNonEmptyString(string) ? string.replace(/[A-Z]/g, t1 => t1 && t1.toLowerCase()) : string) as T
}

export const upperCase = <T = any>(string: T): T => {
  return (isNonEmptyString(string) ? string.replace(/[a-z]/g, t1 => t1 && t1.toUpperCase()) : string) as T
}

export const camelCase = <T = any>(string: T, first = false): T => {
  return (
    first === true
      ? isNonEmptyString(string) ? string.replace(/(^|[_-])([a-z])/g, (_t1, _t2, t3) => t3 && t3.toUpperCase()) : string
      : isNonEmptyString(string) ? string.replace(/[_-]([a-z])/g, (_t1, t2) => t2 && t2.toUpperCase()) : string
  ) as T
}

export const underCase = <T = any>(string: T, first = false): T => {
  return (
    first === true
      ? isNonEmptyString(string) ? string.replace(/([A-Z])/g, '_$1').replace(/([_-])([a-zA-Z])/g, '_$2').toLowerCase().replace(/^[_-]+/, '') : string
      : isNonEmptyString(string) ? string.replace(/([A-Z])/g, '_$1').replace(/([_-])([a-zA-Z])/g, '_$2').toLowerCase() : string
  ) as T
}

export const hyphenCase = <T = any>(string: T, first = false): T => {
  return (
    first === true
      ? isNonEmptyString(string) ? string.replace(/([A-Z])/g, '-$1').replace(/([_-])([a-zA-Z])/g, '-$2').toLowerCase().replace(/^[_-]+/, '') : string
      : isNonEmptyString(string) ? string.replace(/([A-Z])/g, '-$1').replace(/([_-])([a-zA-Z])/g, '-$2').toLowerCase() : string
  ) as T
}

Generalize (克隆/合并/比较)

API 列表

Api ScopeApi NameApi Description
Generalizeomit克隆某个对象, 并剔除 omits 选项中的字段, 属于clone函数变种
Generalizepick克隆某个对象, 仅保留在 picks 选项中的字段, 与omit函数相反
Generalizeequal用于比两个值是否相等, 如果两个对象想深度比对, 可设置第三个参数为true
Generalizeclone克隆某个对象, 默认是浅拷贝, 深度克隆可设置第二个参数为true
Generalizeassign合并多个对象到某一对象, 类似Object.assgin, 设置最后一个参数为true, 即可深度assign
GeneralizedeepAssignassign 函数的deep版本
GeneralizedeepCloneclone 函数的deep版本
GeneralizedeepEqualequal 函数的deep版本

API 使用

  import js from 'js-simpler'
  
 /**
  * omit
  */
  const omit_obj = {
    a: 'a',
    b: 'b',
    c: { a: 'a', b: 'b' }
  }
  const omit_arr1 = [/a/]
  const omit_arr2 = ['a']
  
  js.omit(omit_obj, omit_arr1) // { b: 'b', c: { a: 'a', b: 'b' } }
  js.omit(omit_obj, omit_arr2, true) // { b: 'b', c: { b: 'b' } }
 
 
 
 /**
  * pick
  */
  const pick_obj = {
    a: 'a',
    b: 'b',
    c: { a: 'a', b: 'b' }
  }
  const pick_arr = ['a', /c/]
  
  js.pick(pick_obj, pick_arr) // { a: 'a', c: { a: 'a', b: 'b' } }
  js.pick(pick_obj, pick_arr, true) // { a: 'a', c: { a: 'a' } }
  
  
  
 /**
  * clone
  */
  const clone_obj = {
    a: 'a',
    b: 'b',
    c: { a: 'a', b: 'b' }
  }
  
  js.clone(clone_obj, true) // 深度拷贝 { a: 'a', b: 'b', c: { a: 'a', b: 'b' } }
  
  
  
  /**
   * equal
   */
  const equal_obj1 = {
    a: 'a',
    b: 'b',
    c: { a: 'a', b: 'b' }
  }
  
  const equal_obj2 = {
    a: 'a',
    b: 'b',
    c: { a: 'a', b: 'b' }
  }
  
  js.equal(equal_obj1, equal_obj2) // false
  js.equal(equal_obj1, equal_obj2, true) // true
    
  
  
  /**
   * assign
   */
   const assign_target1 = { a: "a1", b: { b1: "b1" } }
   const assign_source1 = { a: "a2", b: { b2: "b2" }, c: "c2" }
   
   const assign_target2 = { a: "a1", b: { b1: "b1" } }
   const assign_source2 = { a: "a2", b: { b2: "b2" }, c: "c2" }
  
   js.assign(assign_target1, assign_source1) // { a: "a2", b: { b2: "b2" }, c: "c2" }
   js.assign(assign_target2, assign_source2, true) // // { a: "a2", b: { b1: "b1", b2: "b2" }, c: "c2" }

API 源码

import { isFiniteNumber, isNumber, isNaN } from './-Number'
import { isWeakMap } from './-WeakMap'
import { isUndef } from './~Nullable'
import { isRegExp } from './-RegExp'
import { isObject } from './-Object'
import { isString } from './-String'
import { isArray } from './-Array'
import { isDate } from './-Date'
import { isMap } from './-Map'
import { isSet } from './-Set'

export type DeepType = boolean | number
export type FilterType = string | number | RegExp
export type FilterTypes = Array<string | number | RegExp>
export type CloneOptionsType = { omits?: FilterTypes; picks?: FilterTypes; cache?: WeakMap<object, unknown>; deep?: DeepType; }
export type EqualOptionsType = { strict?: FilterTypes; include?:FilterTypes; exclude?: FilterTypes; deep?: DeepType; }


export const omit = <T = unknown>(val: T, arr?: FilterTypes | FilterType, deep: DeepType = false): T => {
  return clone(val, { omits: isArray(arr) ? arr : !isUndef(arr) ? [arr] : [], deep })
}

export const pick = <T = unknown>(val: T, arr?: FilterTypes | FilterType, deep: DeepType = false): T => {
  return clone(val, { picks: isArray(arr) ? arr : !isUndef(arr) ? [arr] : [], deep })
}

export const equal = (one: unknown, two: unknown, opts: EqualOptionsType | DeepType = false): boolean => {
  if (one === two) {
    return true
  }

  if (Object.is(one, two)) {
    return true
  }

  if (isFiniteNumber(one) && isFiniteNumber(two)) {
    return Math.abs(one - two) < Number.EPSILON
  }

  if (isRegExp(one) && isRegExp(two)) {
    return one.source === two.source && one.flags === two.flags && one.lastIndex === two.lastIndex
  }

  if (isObject(one) && isObject(two)) {
    if (Object.keys(one).length !== Object.keys(two).length) {
      return false
    }

    const deep = isObject(opts) && !isUndef(opts.deep) ? opts.deep : opts
    const strict = isObject(opts) && isArray(opts.strict) ? opts.strict.filter(key => isRegExp(key) || isFiniteNumber(key) || isString(key)) : []
    const exclude = isObject(opts) && isArray(opts.exclude) ? opts.exclude.filter(key => isRegExp(key) || isFiniteNumber(key) || isString(key)) : []
    const include = isObject(opts) && isArray(opts.include) ? opts.include.filter(key => isRegExp(key) || isFiniteNumber(key) || isString(key)) : [/(?:)/]

    for (const key of Object.keys(one)) {
      const val1 = one[key]
      const val2 = two[key]
      const level = isNumber(deep) ? deep : deep === true ? Infinity : 0
      const stricted = strict.length > 0 && strict.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))
      const excluded = exclude.length > 0 && exclude.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))
      const included = include.length === 0 || include.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))

      if (excluded || !included) {
        continue
      }

      if (val1 === val2) {
        continue
      }

      if (stricted) {
        return false
      }

      const reuslt = level >= 1
        ? equal(val1, val2, level - 1)
        : false

      if (!reuslt) {
        return false
      }
    }

    return true
  }

  if (isArray(one) && isArray(two)) {
    if (one.length !== two.length) {
      return false
    }

    const deep = isObject(opts) && !isUndef(opts.deep) ? opts.deep : opts
    const strict = isObject(opts) && isArray(opts.strict) ? opts.strict.filter(key => isRegExp(key) || isFiniteNumber(key) || isString(key)) : []
    const exclude = isObject(opts) && isArray(opts.exclude) ? opts.exclude.filter(key => isRegExp(key) || isFiniteNumber(key) || isString(key)) : []
    const include = isObject(opts) && isArray(opts.include) ? opts.include.filter(key => isRegExp(key) || isFiniteNumber(key) || isString(key)) : [/(?:)/]

    for (const key of one.keys()) {
      const val1 = one[key]
      const val2 = two[key]
      const level = isNumber(deep) ? deep : deep === true ? Infinity : 0
      const stricted = strict.length > 0 && strict.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))
      const excluded = exclude.length > 0 && exclude.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))
      const included = include.length === 0 || include.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))

      if (excluded || !included) {
        continue
      }

      if (val1 === val2) {
        continue
      }

      if (stricted) {
        return false
      }

      const reuslt = level >= 1
        ? equal(val1, val2, level - 1)
        : false

      if (!reuslt) {
        return false
      }
    }

    return true
  }

  if (isDate(one) && isDate(two)) {
    return +one === +two
  }

  if (isMap(one) && isMap(two)) {
    return one.size === two.size && equal([...one.entries()], [...two.entries()], opts)
  }

  if (isSet(one) && isSet(two)) {
    return one.size === two.size && equal([...one.values()], [...two.values()], opts)
  }

  return false
}

export const clone = <T = unknown>(val: T, opts: CloneOptionsType | DeepType = false) : T => {
  const deep = isObject(opts) && !isUndef(opts.deep) ? opts.deep : opts
  const omits = isObject(opts) && isArray(opts.omits) ? opts.omits.filter(key => isRegExp(key) || isString(key) || isFiniteNumber(key)) : []
  const picks = isObject(opts) && isArray(opts.picks) ? opts.picks.filter(key => isRegExp(key) || isString(key) || isFiniteNumber(key)) : []
  const cache = isObject(opts) && isWeakMap(opts.cache) ? opts.cache : new WeakMap()
  const level = isNumber(deep) ? deep : deep === true ? Infinity : 1

  const taking = (val: any): any[] => {
    return isObject(val) ? Object.entries(val) : val.entries()
  }

  const cloning = (val: T, level: number): T => {
    const value: any = isArray(val) ? [] : {}

    for (const [key, source] of taking(val)) {
      if (picks.length > 0 && !picks.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))) {
        continue
      }

      if (omits.length > 0 && omits.some(k => isRegExp(k) ? k.test(String(key)) : String(k) === String(key))) {
        continue
      }

      let record = source

      const isCopySet = isSet(source)
      const isCopyMap = isMap(source)
      const isCopyDate = isDate(source)
      const isCopyArray = isArray(source)
      const isCopyObject = isObject(source)
      const isCopyRegExp = isRegExp(source)

      if (!isCopySet && !isCopyMap && !isCopyDate && !isCopyArray && !isCopyObject && !isCopyRegExp) {
        isArray(value) ? value.push(record) : value[key] = record
        continue
      }

      if (level > 1 && !cache.has(source)) {
        cache.set(source, clone(source, { omits, picks, cache, deep: level - 1 }))
      }

      if (level > 1 && cache.has(source)) {
        record = cache.get(source)
      }

      isArray(value) ? value.push(record) : value[key] = record
    }

    return value as T
  }

  if (isNaN(level) || level < 1) {
    return val
  }

  if (isRegExp(val)) {
    const flags = val.flags
    const source = val.source
    const regexp = new RegExp(source, flags)
    regexp.lastIndex = val.lastIndex
    return regexp as T
  }

  if (isObject(val)) {
    return cloning(val, level)
  }

  if (isArray(val)) {
    return cloning(val, level)
  }

  if (isDate(val)) {
    return new Date(+val) as T
  }

  if (isMap(val)) {
    const maps = Array.from(val.entries()) as any
    return new Map(cloning(maps, level) as any) as T
  }

  if (isSet(val)) {
    const sets = Array.from(val.values()) as any
    return new Set(cloning(sets, level) as any) as T
  }

  return val
}

export const assign = <T = unknown>(val: T, ...rest: any[]) => {
  const empty = {}
  const cache = new WeakMap()
  const state = rest.slice(-1)[0]
  const level = isNumber(state) ? state : state === true ? Infinity : 1

  const taking = (val: any) => {
    return isObject(val) ? Object.entries(val) : val.entries()
  }

  const merging = (val: T, obj: T, level: number) => {
    const refly: any = isMap(val) ? Array.from(val.entries()) : isSet(val) ? Array.from(val.values()) : val
    const newly: any = isMap(obj) ? Array.from(obj.entries()) : isSet(obj) ? Array.from(obj.values()) : obj

    for (const [key, source] of taking(newly)) {
      if (level < 1) {
        refly[key] = source
        continue
      }

      const isCopySet = isSet(source)
      const isCopyMap = isMap(source)
      const isCopyDate = isDate(source)
      const isCopyArray = isArray(source)
      const isCopyObject = isObject(source)
      const isCopyRegExp = isRegExp(source)
      const isNotSameType = empty.toString.call(refly[key]) !== empty.toString.call(source)

      if (!isCopySet && !isCopyMap && !isCopyDate && !isCopyArray && !isCopyObject && !isCopyRegExp) {
        refly[key] = source
        continue
      }

      if (!cache.has(source)) {
        cache.set(source, clone(source, { deep: level, cache }))
      }

      if (isNotSameType) {
        refly[key] = cache.get(source)
        continue
      }

      merging(refly[key], newly[key], level - 1)
    }

    if (isMap(val) || isSet(val)) {
      val.clear()
    }

    if (isMap(val)) {
      for (const [key, source] of refly) {
        val.set(key, source)
      }
    }

    if (isSet(val)) {
      for (const source of refly) {
        val.add(source)
      }
    }
  }

  if (isObject(val) || isArray(val) || isMap(val) || isSet(val)) {
    for (const obj of rest) {
      if (empty.toString.call(val) !== empty.toString.call(obj)) {
        continue
      }
      merging(val, obj, level - 1)
    }
  }

  return val
}

export const deepAssign = <T = unknown>(val: T, ...rest: any[]): T => {
  return assign<T>(val, ...rest, true)
}

export const deepClone = <T = unknown>(val: T, opts: CloneOptionsType = {}): T => {
  return clone<T>(val, { ...opts, deep: true })
}

export const deepEqual = (one: unknown, two: unknown, opts: EqualOptionsType = {}): boolean => {
  return equal(one, two, { ...opts, deep: true })
}

Nullable (null/undefined判断)

API 列表

Api ScopeApi NameApi Description
NullableisNull是否是 null
NullableisUndef是否是 undefined
NullableisNullable是否是 null或undefined

API 使用

  import js from 'js-simpler'
  
  js.isNull(null) // true
  js.isNull(undefined) // false
  
  js.isUndef(null) // false
  js.isUndef(undefined) // true

  js.isNullable(null) // true
  js.isNullable(undefined) // true

API 源码

export const isNull = (val: unknown): val is null => {
  return Object.prototype.toString.call(val) === '[object Null]'
}

export const isUndef = (val: unknown): val is undefined => {
  return Object.prototype.toString.call(val) === '[object Undefined]'
}

export const isNullable = (val: unknown): val is null | undefined => {
  return isNull(val) || isUndef(val)
}

Uniquer (uuid生成器)

API 列表

Api ScopeApi NameApi Description
Uniqueruniquer生成 uuid或指定格式的UniqueId

API 使用

  import js from 'js-simpler'
  
  js.uniquer() // '4479ee7c-eaeb-3fc4-b58e-7e76361f9af0'
  js.uniquer({ radix: 8 }) // '73350133-4650-3613-8556-234657352754'
  js.uniquer({ format: '???-??' }) // '287-d8'

API 源码

import { isFunction } from './-Function'
import { isArray } from './-Array'
import { isSet } from './-Set'

export type UniquerOptions = {
  radix?: 2 | 8 | 10 | 16 | 26 | 36;
  format?: string | null;
  random?: '?' | '*' | '#';
  usedUniques?: Array<string> | Set<string> | null;
  listenCacherHandler?: ListenCacherHandler | null;
  reduplicateHandler?: ReduplicateHandler | null;
  reduplicateExit?: boolean | null;
  onlyUpdate?: boolean | null;
}

export type RandomizeOptions = {
  bytes: string[];
  max: number;
  min: number;
}

export type CacherOptions = Set<string>
export type ListenCacherHandler = (options: CacherOptions) => void;
export type ReduplicateHandler = (options: UniquerOptions) => UniquerOptions;
export type Randomize = (options: RandomizeOptions) => string;
export type Uniquer = (options?: UniquerOptions) => string;


/**
 * Cacher
 */
const Cacher: CacherOptions = new Set([''])


/**
 * Uniquer
 */
export const uniquer: Uniquer = (options = {}) => {
  const onlyUpdate = options.onlyUpdate === true
  const usedUniques = isArray(options.usedUniques) || isSet(options.usedUniques) ? options.usedUniques : []
  const isExitOnRegenerate = options.reduplicateExit !== false || !isFunction(options.reduplicateHandler)
  const listenCacherHandler = isFunction(options.listenCacherHandler) ? options.listenCacherHandler : (_: CacherOptions) => {}
  const reduplicateHandler = isFunction(options.reduplicateHandler) ? options.reduplicateHandler : undefined

  const radix = options.radix !== undefined ? options.radix : 16
  const random = options.random !== undefined ? options.random : '?'
  const format = options.format !== undefined ? options.format : ('????????-????-[1-5]???-[8-b]???-????????????').replace(/\?/g, random)

  if (![2, 8, 10, 16, 26, 36].includes(radix)) {
    throw new Error('#<Options.radix> is not in [2, 8, 10, 16, 26, 36]')
  }

  if (!['?', '*', '#'].includes(random)) {
    throw new Error('#<Options.random> is not in ["?", "*", "#"]')
  }

  if (String(format) !== format) {
    throw new Error('#<Options.format> is not string')
  }

  if (usedUniques instanceof Array) {
    usedUniques.forEach(key => typeof key === 'string' && Cacher.add(key.trim()))
    listenCacherHandler(new Set(Cacher))
  }

  if (usedUniques instanceof Set) {
    usedUniques.forEach(key => typeof key === 'string' && Cacher.add(key.trim()))
    listenCacherHandler(new Set(Cacher))
  }

  if (onlyUpdate === true) {
    return ''
  }

  let unique = ''
  let tryCount = 10
  let regenerate = true
  const byteOffset = radix === 26 ? 10 : 0
  const characters = Array.from({ length: 36 }, (_, key) => key.toString(36))
  const appendCacher = Cacher.add.bind(Cacher)

  const randomizer: Randomize = opt => {
    const min = opt.min
    const max = opt.max
    const bytes = opt.bytes
    return bytes[Math.random() * (max - min + 1) + min | 0]
  }

  while (regenerate && tryCount-- > 0) {
    const template = format.replace(/\[([^\]]+?)\]/g, (_, group: string) => {
      const caches: Set<string> = new Set()
      const append = caches.add.bind(caches)
      const splits = group.toLowerCase().split(/\s*,\s*/g)
      const filters = splits.filter(str => /^[a-zA-Z0-9\s/|\-*?#=:;]+$/ui.test(str))
      const isRange = (str: string) => /^\s*[a-zA-Z0-9]\s*-\s*[a-zA-Z0-9]\s*$/.test(str)

      const collects = filters.reduce((caches, string) => {
        if (isRange(string.trim())) {
          const str1 = string.trim().split(/\s*-\s*/)[0]
          const str2 = string.trim().split(/\s*-\s*/)[1]
          const key1 = characters.indexOf(str1.trim())
          const key2 = characters.indexOf(str2.trim())
          const first = Math.min(key1, key2)
          const second = Math.max(key1, key2) + 1
          characters.slice(first, second).forEach(append)
        }

        if (!isRange(string.trim())) {
          append(string.trim())
        }

        return caches
      }, caches)


      const temp = Array.from(collects)
      const bytes = temp.filter(every => !!every)

      return randomizer({
        bytes,
        max: bytes.length - 1,
        min: 0
      })
    })

    const min = 0
    const max = radix - 1
    const bytes = characters.slice(byteOffset)

    unique = [...template.toLowerCase()]
      .filter(str => /^[a-zA-Z0-9\s/|\-*?#=:;]+$/ui.test(str))
      .map(v => v === random ? randomizer({ bytes, max, min }) : v)
      .join('')
      .trim()

    if (!Cacher.has(unique)) {
      regenerate = false
      appendCacher(unique)
      listenCacherHandler(new Set(Cacher))
    }
  }

  if (regenerate && isExitOnRegenerate) {
    throw new Error('[Uniquer generate unique] is Reduplicated')
  }

  if (regenerate && !isExitOnRegenerate) {
    const newOptions = reduplicateHandler!(options)
    const overOptions = { reduplicateExit: true }

    return uniquer({
      ...options,
      ...newOptions,
      ...overOptions
    })
  }

  return unique
}

Tween (动画Tween算法)

API 列表

Api ScopeApi NameApi Description
Tween*Tween动画算法

API 使用

暂无

API 源码

查看