Skip to content

1.call()、apply()、bind()

call()、apply()、bind()三个函数皆为 Function 原型上的函数,都可以强制改变函数的 this 指向,即使用它们会强制改变调用函数的 this 指向为它们的第一个入参,注意:在非严格模式下指定 null 或 undefined 为 call()、apply()、bind()的第一个参数时会自动替换的函数 this 指向全局对象(浏览器环境下是 window,NodeJS 环境下是 global)。call()、apply()、bind()三者区别如下:

1.1 call()

js
// call实现
Function.prototype.myCall = function (context) {
  // (1).初始化context。如果context不为空时就指向自己本身,否则指向全局对象
  context = context || window

  /*
   * (2).挂载函数。将this挂载到context的func上,this表示调用myCall的函数,
   * 即将调用函数挂载到context.func上,在调用context.func()时由于遵循隐式this绑定原则,
   * 此时调用函数的this指向context
   */
  context.func = this

  // (3).获取调用函数参数剩余参数。获取除context以外的其他参数,myCall第二个参数是一个可变参数
  const args = Array.from(arguments).slice(1)

  // (4).获取函数执行结果
  const result = args.length ? context.func(...args) : context.func()

  // (5).删除属性,避免造成全局污染
  delete context.func

  //(6).返回函数结果
  return result
}

// 测试
const obj = {
  name: '大黄',
  age: 4,
  func: function (hobby) {
    console.log(`${this.name}今年${this.age}岁,它喜欢${hobby}`)
  },
}
// this隐式绑定,func()的this指向obj
obj.func('睡觉') // 大黄今年4岁,它喜欢睡觉

const newObj = {
  name: '小白',
  age: 3,
  hobby: '吃饭',
}
// 使用myCall()将obj.func()中的this强制指向newObj
obj.func.myCall(newObj, newObj.hobby) // 小白今年3岁,它喜欢吃饭

1.2 apply()

js
// apply实现
Function.prototype.myApply = function (context, args) {
  // (1).初始化context。如果context不为空时就指向自己本身,否则指向全局对象
  context = context || window
  /*
   * (2).挂载函数。将this挂载到context的func上,this表示调用myCall的函数,
   * 即将调用函数挂载到context.func上,在调用context.func()时由于遵循隐式this绑定原则,
   * 此时调用函数的this指向context
   */
  context.func = this
  // (3).获取函数执行结果
  const result = args.length ? context.func(...args) : context.func()
  // (4).删除属性,避免造成全局污染
  delete context.func
  // (5).返回函数结果
  return result
}

// 测试
const obj = {
  name: '大黄',
  age: 4,
  func: function (hobby) {
    console.log(`${this.name}今年${this.age}岁,它喜欢${hobby}`)
  },
}
// this隐式绑定,func()的this指向obj
obj.func('睡觉') // 大黄今年4岁,它喜欢睡觉

const newObj = {
  name: '小白',
  age: 3,
  hobby: '吃饭',
}
// 使用myApply()将obj.func()中的this强制指向newObj
obj.func.myApply(newObj, [newObj.hobby]) // 小白今年3岁,它喜欢吃饭

1.3 bind()

js
// bind实现
Function.prototype.myBind = function (context) {
  // (1).初始化context。对context进行深拷贝,防止bind执行后返回函数未执行期间,context被修改
  context = JSON.parse(JSON.stringify(context)) || window
  // (2).挂载函数。将调用函数挂载到context上。
  context.func = this
  // (3).获取调用函数除context外的参数。
  const args = Array.from(arguments).slice(1)
  // (4).返回一个新的函数
  return () => {
    // (4-1).拼接参数。将外部调用参数和返回新函数的参数进行合并
    const fnArgs = args.concat(Array.from(arguments))
    // (4-2).执行函数获取结果
    const result = fnArgs.length ? context.func(fnArgs) : context.func()
    // (4-3).删除挂载函数防止全局污染
    delete context.func
    // (4-4).返回结果
    return result
  }
}

// 测试
const obj = {
  name: '大黄',
  age: 4,
  func: function (hobby) {
    console.log(`${this.name}今年${this.age}岁,它喜欢${hobby}`)
  },
}
// this隐式绑定,func()的this指向obj
obj.func('睡觉') // 大黄今年4岁,它喜欢睡觉

const newObj = {
  name: '小白',
  age: 3,
  hobby: '吃饭',
}
// 使用myBind()将obj.func()中的this强制指向newObj,myBind返回一个函数
const bindFunc = obj.func.myBind(newObj, [newObj.hobby])
bindFunc() // 小白今年3岁,它喜欢吃饭

2.isType()

在 JavaScript 常用typeofinstanceof构造器等方式判断表达式的类型。但这三种方法具有如下缺点,这三种方式在一些特定场景下无法准确的判断表达式的类型:

  • typeof 操作符在判断 Null、Array、Object 类型时都为 object。
  • instanceof 操作符在判断 Array 和 Function 类型时都为 true。
  • constructor 方式在判断 null 或 undefined 将会报错,因为 null 和 undefined 是无效类型。
js
// typeof 例子
console.log(typeof 123) // number
console.log(typeof 123) // number
console.log(typeof 'zxp') // string
console.log(typeof true) // boolean
console.log(typeof undefined) // undefined
console.log(typeof function () {}) // function
console.log(typeof class Person {}) // function
// typeof 判断Null、Array、Object类型时都为object
console.log(typeof null) // object
console.log(typeof []) // object
console.log(typeof {}) // object

// instanceof 例子
console.log([] instanceof Array) // true
console.log([] instanceof Object) // true
console.log({} instanceof Object) // true
// instanceof 判断Array和Function类型时都为true
console.log(function () {} instanceof Function) // true
console.log(function () {} instanceof Object) // true

// constructor 例子
var a = 123,
  s = 'zxp',
  b = false,
  obj = {},
  arr = [],
  fn = function () {},
  nul = null,
  udef = undefined
console.log(a.constructor === Number) // true
console.log(s.constructor === String) // true
console.log(b.constructor === Boolean) // true
console.log(obj.constructor === Object) // true
console.log(arr.constructor === Array) // true
console.log(fn.constructor === Function) // true
console.log(nul.constructor === Object) // TypeError: Cannot read properties of null (reading 'constructor')
console.log(udef.constructor === Object) // TypeError: Cannot read properties of null (reading 'constructor')

// Object.prototype.toString.call() 例子
console.log(Object.prototype.toString.call('')) //[object String]
console.log(Object.prototype.toString.call(null)) //[object Null]
console.log(Object.prototype.toString.call([])) //[object Array]
console.log(Object.prototype.toString.call({})) //[object Object]

若要准确的判断表达式的类型推荐使用Object.prototype.toString.call(),Object.prototype.toString可以准确的获取任意对象的类型,call()是 Function 上的一个函数,用于强制改变函数中的 this 指向,使用call()可以强制改变调用函数 this 指向为call()的第一个参数

js
// 封装成一个普通函数:判断obj是否是type类型
function isType(obj, type) {
  const typeinof = Object.prototype.toString.call(obj)
  return typeinof.substring(8, typeinof.length - 1) === type
}
// 测试
console.log(isType({}, 'Object')) //true
console.log(isType([], 'Object')) //false
console.log(isType(null, 'Object')) //false

// 封装成高阶函数:判断obj是否是type类型
const isType = obj => type => Object.prototype.toString.call(obj) === `[object ${type}]`
// 测试
console.log(isType('123')('String')) //true
console.log(isType('123')('Number')) //false
console.log(isType({})('Object')) // true

3.深拷贝函数

  • 浅拷贝是指创建一个新对象,新对象具有原始对象的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。浅拷贝的实现方式有Object.assign()...扩展运算符。
  • 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大,但是拷贝前后两个对象互不影响。深拷贝的实现方式有 JSON.parse(JSON.stringify()),该方案具有如下缺点:
    • 无法拷贝 function、undefined、symbol。
    • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
    • 对于其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
    • 无法正确处理 Date 和 RegExg。
    • 无法解决循环引用。

乞丐版深拷贝

js
function deepCopy(obj) {
  // 定义一个空对象接收拷贝后的值
  let result
  // 判断obj是否是引用类型,typeof判断Object、Array结果都是"object"
  if (typeof obj === 'object') {
    // 根据obj的构造函数判断obj是否是一个数组,是数组则赋一个空数组,否则赋值一个空对象
    result = obj.constructor === Array ? [] : {}
    // 遍历obj,for in通常用于遍历对象
    for (let k in obj) {
      // 判断obj[k]是否是引用类型,如果是则递归拷贝(因为obj可能会出现对象嵌套对象的情况),否则返回obj[k]
      result[k] = typeof obj[k] === 'object' ? deepCopy(obj[k]) : obj[k]
    }
  } else {
    // 如果obj是基本类型就直接返回
    result = obj
  }
  return result
}

var user = { name: 'zxp', age: 18 }
var obj = { count: 5, user }
var newObj = deepCopy(obj)
console.log(newObj) // {"count":5,"user":{"name":"zxp","age":18}}

// 缺点:无法解决循环引用问题,递归导致超出最大调用堆栈大小
var obj1 = { count: 5, user }
obj1.obj1 = obj1
console.log(deepCopy(obj1)) // Uncaught RangeError: Maximum call stack size exceeded(未捕获范围错误:超出了最大调用堆栈大小)

进阶版深拷贝

js
const typeEnum = {
  /** 可继续遍历的数据类型 */
  mapType: '[object Map]',
  setType: '[object Set]',
  arrayType: '[object Array]',
  objectType: '[object Object]',
  argsType: '[object Arguments]',

  /** 不可继续遍历的数据类型 */
  boolType: '[object Boolean]',
  dateType: '[object Date]',
  errorType: '[object Error]',
  numberType: '[object Number]',
  stringType: '[object String]',
  regexpType: '[object RegExp]',
  symbolType: '[object Symbol]',
  funcType: '[object Function]',
}
// 获取类型
const getType = target => Object.prototype.toString.call(target)
// 获取target类型。高阶函数使函数更加简洁
const isType = target => type => Object.prototype.toString.call(target) === `[object ${type}]`
// 判断target是否是引用类型
const isObject = target => {
  const type = typeof target
  return type !== null && (type !== 'function' || type !== 'object')
}
// 初始化对象
const init = target => new target.constructor()

function forEach(array, iterator) {
  let index = -1
  const len = array.length
  while (++index < len) {
    iterator(array[index], index)
  }
  return array
}

// 克隆Symbol类型
function cloneSymbol(target) {
  return Object(Symbol.prototype.valueOf.call(target))
}
// 克隆正则
function cloneReg(target) {
  const reFlags = /\w*$/
  const result = new target.constructor(target.source, reFlags.exec(target))
  result.lastIndex = target.lastIndex
  return result
}
// 克隆函数
function cloneFunc(target) {
  // 方法主体正则
  const bodyReg = /(?<={)(.|\n)+(?=})/m
  // 方法参数正则
  const paramReg = /(?<=\().+(?=\)\s+{)/
  // 函数转字符串
  const funcString = target.toString()
  const param = paramReg.exec(funcString)
  const body = bodyReg.exec(funcString)
  if (body) {
    // 判断函数中的参数是否为空
    if (param) {
      // 获取函数的参数
      const paramArr = param[0].split(',')
      return new Function(...paramArr, body[0])
    } else {
      return new Function(body[0])
    }
  } else {
    return null
  }
  // 返回执行字符串函数的结果
  return eval(funcString)
}

// 克隆不可遍历对象
function cloneOtherType(target, type) {
  /**
   * constructor属性返回对创建此对象的数组函数的引用,例如[Function: Symbol]、
   * [Function: Object]
   */
  const Ctor = target.constructor
  switch (type) {
    case typeEnum.boolType:
    case typeEnum.numberType:
    case typeEnum.stringType:
    case typeEnum.errorType:
    case typeEnum.dateType:
      // 通过new返回一个新对象
      return new Ctor(target)
    case type.symbolType:
      return cloneSymbol(target)
    case typeEnum.regexpType:
      return cloneReg(target)
    case typeEnum.funcType:
      return cloneFunc(target)
    default:
      return null
  }
}

function clone(target, map = new WeakMap()) {
  // 判断target是否是原始类型
  if (!isObject(target)) return target

  const type = getType(target)
  let cloneTarget
  /**
   * 判断target的类型是否是可继续遍历类型,
   * 如果是不可继续遍历类型就执行cloneOtherType()
   */

  if (Object.values(typeEnum).slice(0, 5).includes(type)) {
    // 初始化cloneTarget
    cloneTarget = init(target)
  } else {
    return cloneOtherType(target, type)
  }
  // 防止循环引用
  if (map.get(target)) return target
  map.set(target, map)

  // 克隆 set
  if (type === typeEnum.setType) {
    /**
     * 当target是Set类型,cloneTarget的初始化值是Set {},
     * 循环遍历target将set中的元素递归添加到cloneTarget中
     */
    target.forEach(value => {
      cloneTarget.add(clone(value))
    })
    return cloneTarget
  }
  // 克隆 map
  if (type === typeEnum.mapType) {
    /**
     * 当target是Map类型,cloneTarget的初始化值是Map {},
     * 循环遍历target将map中的元素递归添加到cloneTarget中
     */
    target.forEach((value, key) => {
      cloneTarget.set(key, clone(value))
    })
    return cloneTarget
  }
  // 克隆数组和对象
  const keys = Object.keys(target)
  forEach(keys || target, (value, key) => {
    // 如果属性是普通类型就直接赋值给新对象,否则就递归拷贝
    cloneTarget[value] = clone(target[value], map)
  })
  return cloneTarget
}

/**  测试  **/
const obj = {
  name: 'zxp',
  sex: 'man',
  sb: Object(Symbol('asd')),
  set: new Set().add({ like: '女人' }).add({ love: 'woman' }),
  map: new Map().set('name', 'zmap'),
  arr: [1, 2, 3],
  ctx: { name: 'zzz', sex: 'woman' },
  reg: /^w/,
  date: new Date(),
  func1: function () {
    return 1
  },
}
const cloneObj = clone(obj)
console.log(cloneObj === obj) // false
cloneObj.name = 'zzzzz'
console.log(obj.name, cloneObj.name) // zxp zzzzz
console.log(cloneObj)
/**
  {
    name: 'zzzzz',
    sex: [String: 'man'],
    sb: null,
    set: Set { { like: [String: '女人'] }, { love: [String: 'woman'] } },
    map: Map { 'name' => [String: 'zmap'] },
    arr: [ [Number: 1], [Number: 2], [Number: 3] ],
    ctx: { name: [String: 'zzz'], sex: [String: 'woman'] },
    reg: /^w/,
    date: 2021-02-20T03:06:14.657Z,
    func1: [Function: anonymous]
  }
 */

4.手写防抖节流函数

“防抖(Debounce)” 和 “节流(Throttle)” 都是为了控制高频率触发事件的高阶函数,可以优化性能、减少资源浪费。

  • 防抖(Debounce):在事件频繁触发时,只在最后一次触发后的一段时间才执行回调。如果在这段时间内又触发了事件,则重新计时,常用于搜索框输入请求、窗口Resize等场景。防抖的实现也很简单,其核心利用setTimeout(定时器),定义一个定时器,在事件触发时,如果定时器存在,则清除定时器,重新计时(如果函数被多次调用,timer总是有值,因此需要先清理定时器,从而保证函数多次调用只执行最后一次)。如果定时器不存在,则创建定时器,在定时器到期后执行回调函数。
js
function debounce(fn, delay = 500) {
  let timer = null
  return function (...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}
  • 节流(Throttle):在高频触发的事件中,保证在固定时间间隔内只执行一次。常用于滚动加载(scroll)、鼠标移动(mousemove)等场景。节流的实现也很简单,其核心记录上次执行的时间,只有当时间间隔大于设定值时才允许执行。
js
function throttle(fn, delay = 500) {
  // 上次fn函数执行的时间
  let prevTime = null
  return function (...args) {
    // 当前时间
    const now = Date.now()
    // 如果上次执行时间不存在或者时间间隔大于设定值,则执行函数
    if (now - prevTime > delay) {
      // 更新上次执行时间
      prevTime = now
      fn.apply(this, args)
    }
  }
}

5.解析 URL 参数函数

5.1 通过 URLSearchParams()解析 URL 参数

js
/*
 * window.location.search可以获取查询参数(不包含?号),假设window.location.search为
 * user=z%E4%B9%98%E9%A3%8E&age=18。此种方式虽然简单但兼容较差。
 */
const urlSearchParams = new URLSearchParams(window.location.search)
// 将键值对列表转换为一个对象
const params = Object.fromEntries(urlSearchParams.entries()) // {user: 'z乘风', age: '18'}

5.2 通过 split()解析 URL 参数

js
function getParams(url) {
  const res = {}
  // 判断是否有查询参数
  if (url.includes('?')) {
    // 以?号分割得到数组最后二个元素
    const str = url.split('?')[1]
    // 分割&号得到参数数组
    const arr = str.split('&')
    arr.forEach(item => {
      const key = item.split('=')[0],
        value = item.split('=')[1]
      res[key] = decodeURIComponent(value) // 解码
    })
  }
  return res
}
const url = 'https://www.baidu.com/?user=z%E4%B9%98%E9%A3%8E&age=18'
console.log(getParams(url)) // {user: 'z乘风', age: '18'}

6.发布订阅

发布订阅模式是开发中最为常见最有精髓的设计模式,例如在 Vue2.x 中的 EventBus、$on、$emit、$off,其实现原理是在其内部维护一个事件列表,存储已订阅的事件,当订阅事件时会将事件添加到事件列表,并向对应事件添加回调函数。当发布事件后会获取事件列表中事件对应的回调函数列表,并遍历回调函数列表挨个执行回调函数。

js
class EventEmitter {
  constructor() {
    // 定义事件列表,存储事件
    this.events = {}
  }
  // 订阅事件
  on(event, callback, context) {
    // 如果事件列表不存在对应的事件则进行初始化
    if (!this.events[event]) {
      this.events[event] = []
    }
    // 向对应的事件添加回调函数
    this.events[event].push(callback)
    return this
  }
  // 发出事件
  emit(event, ...payload) {
    // 获取对应事件的回调函数列表
    const callbacks = this.events[event]
    // 循环执行回调函数列表的回调函数,并传入参数
    if (callbacks) {
      callbacks.forEach(cb => cb.apply(this, payload))
    }
    return this
  }

  // 删除订阅的事件
  off(event, callback) {
    // 如果什么都没传,重置事件列表
    if (typeof event === 'undefined') {
      this.events = {}
    }
    if (typeof event === 'string') {
      /*
       * 如果callback是函数则删除事件对应的回调函数,否则将重置该事件
       */
      this.events[event] =
        typeof callback === 'function' ? this.events[event].filter(cb => cb !== callback) : []
    }
    return this
  }
  // 只允许订阅一次事件
  once(event, callback, context) {
    // 声明一个代理回调
    const proxyCallback = (...payload) => {
      callback.apply(context, payload)
      // 回调函数执行完成之后就删除事件订阅
      this.off(event, proxyCallback)
    }
    /*
     * on将proxyCallback加入事件列表,当使用emit触发对应的事件时就会循环
     * 执行回调函数列表中的回调函数,即会执行proxyCallback,由于proxyCallback()中
     * callback()执行后,调用this.off()事件所将对应的回调函数删除,所以只保证once()的
     * callback只会执行一次。
     */
    this.on(event, proxyCallback, context)
  }
}

// 测试
const emitter = new EventEmitter()
const callback1 = (name, sex) => {
  console.log(name, sex, 'callback1')
}
const callback2 = (name, sex) => {
  console.log(name, sex, 'callback2')
}
const callback3 = (name, sex) => {
  console.log(name, sex, 'callback3')
}

emitter
  .on('event01', callback1)
  .on('event01', callback2)
  // 仅会执行一次回调
  .once('event01', callback3)
/*
 * z乘风 男 callback1
 * z乘风 男 callback2
 * z乘风 男 callback3
 */
emitter.emit('event01', 'z乘风', '男')

// 删除事件
emitter.off('event01', callback1)
emitter.emit('event01', 'z乘风', '男') // z乘风 男 callback2

7.trim()

trim()的作用是去除字符串两端的空白字符,空白字符包括所有的空白字符(space, tab, no-break space 等)以及所有行终止符字符(如 LF,CR 等)。常见实现方式如下:

  • 基于正则表达式匹配空格实现去除空格。
  • 基于字符提取法去除空格。

7.1 基于正则表达式替换实现 trim()

js
// 方式1:通过正则表达式替换实现trim()
String.prototype.trim = function () {
  /*
   * ^\s* 表示 匹配以空白字符开头的字符串,|表示或者,\s*$表示匹配以空白字符结尾的字符串,/g执行匹配模式为全局匹配
   * /^\s*|\s*$/g 表示全局匹配匹配任意个以空白字符(包括空格、制表符、换页符等等)开头,
   * 或全局匹配匹配任意个以空白字符
   */
  const reg = /^\s*|\s*$/g
  return this.replace(reg, '')
}
// 测试
console.log(' z乘风 '.trim()) // z乘风
console.log('z乘风 '.trim()) // z乘风
console.log(' z乘风'.trim()) // z乘风
console.log(' z 乘风 '.trim()) // z 乘风

7.2 字符提取法

js
// 方式2:字符提取法
String.prototype.trim = function () {
  const reg = /^\s*(.*?)\s*$/g
  this.replace(reg, '$1')
}
// 测试
console.log(' z乘风 '.trim()) // z乘风
console.log('z乘风 '.trim()) // z乘风
console.log(' z乘风'.trim()) // z乘风
console.log(' z 乘风 '.trim()) // z 乘风

8.数字千位分割

8.1 toLocaleString()实现千位分割

通过 toLocaleString()实现千位分割 toLocaleString()是 JS 内置函数,用于返回数字或字符串在特定语言环境下的表示字符串,可以通过 toLocaleString()实现数字千位分割,对于小数部分会进行四舍五入。

js
var num01 = 123456789
var num02 = 123456.4542
console.log(num01.toLocaleString()) // 123,456,789
console.log(num02.toLocaleString()) // 123,456.454 (小数部分四舍五入了)

8.2 通过字符串转为数组实现数组千位分割

原理:首先将入参分割为整数部分(即 num[0])和小数部分(即 num[1]),再将整数部分倒序排列得到字符数组,再循环整个字符数组, 每三位添加一个分隔符(即 i%3===0,而且需要排除 i===0),得到结果数组后再转为正序,如果存在小数部分则拼接小数部分,否则直接返回结果数组的字符串。

js
function numFormat(num, separator = ',') {
  // 分割小数点得到数组,num[0]表示整数部分,num[1]表示小数部分
  num = num.toString().split('.')
  // 获取整数内容并倒序排列
  const arr = num[0].split('').reverse()
  const res = []
  for (let i = 0, len = arr.length; i < len; i++) {
    // i % 3 && i !==0 表示满足千位,则添加分割符
    if (i % 3 === 0 && i !== 0) {
      res.push(separator)
    }
    // 添加元素
    res.push(arr[i])
  }
  // 翻转数组变为正序数组,由于arr是倒序数组遍历后res也是倒序
  res.reverse()

  // 如果存在小数部分则拼接小数部分,否则返回整数部分
  return num[1] ? res.join('').concat('.' + num[1]) : res.join('')
}

const a = 123456789
const b = 123456.4542
console.log(numFormat(a)) // 123,456,789
console.log(numFormat(b)) // 123,456.4542

8.3 通过 replace()+正则实现千位分割

js
function numFormat(num, separator = ',') {
  // \d+表示匹配多个数字,仅会匹配到小数点之前的内容
  return num.toString().replace(/\d+/, n => {
    // \d表示匹配数字,(?=(\d{3})+$)/g表示全局捕获以三位数字结尾的内容,$1为捕获到的内容
    return n.replace(/\d(?=(\d{3})+$)/g, function ($1) {
      return $1 + separator
    })
  })
}

const a = 123456789
const b = 12345.45562123
console.log(numFormat(a)) // 123,456,789
console.log(numFormat(b)) // 12,345.45562123

9.深度合并函数

deepmergeAll()支持数组对象深度合并,而不是覆盖。

js
const toString = Object.prototype.toString

/**
 * 判断val是否是可合并对象
 * @param {*} val 目标对象
 * @returns
 */
const isMergeableObject = val => {
  const nonNullObject = val && typeof val === 'object'
  return (
    nonNullObject &&
    toString.call(val) !== '[object RegExp]' &&
    toString.call(val) !== '[object Date]'
  )
}
/**
 * 为目标对象设置空值
 * @param {*} val 目标对象
 * @returns
 */
const emptyTarget = val => (Array.isArray(val) ? [] : {})

/**
 * 数组默认合并方法。
 * @param {*} target
 * @param {*} source
 * @param {*} optionsArgument 配置参数
 * @returns
 */
const defaultArrayMerge = (target, source, optionsArgument) => {
  let destination = target.slice()
  source.forEach(function (e, i) {
    if (typeof destination[i] === 'undefined') {
      destination[i] = cloneIfNecessary(e, optionsArgument)
    } else if (isMergeableObject(e)) {
      destination[i] = deepmerge(target[i], e, optionsArgument)
    } else if (target.indexOf(e) === -1) {
      destination.push(cloneIfNecessary(e, optionsArgument))
    }
  })
  return destination
}

function mergeObject(target, source, optionsArgument) {
  let destination = {}
  if (isMergeableObject(target)) {
    Object.keys(target).forEach(function (key) {
      destination[key] = cloneIfNecessary(target[key], optionsArgument)
    })
  }
  Object.keys(source).forEach(function (key) {
    destination[key] =
      !isMergeableObject(source[key]) || !target[key]
        ? cloneIfNecessary(source[key], optionsArgument)
        : deepmerge(target[key], source[key], optionsArgument)
  })
  return destination
}
/**
 * source是否需要克隆
 * @param {*} value
 * @param {*} optionsArgument 配置参数
 * @returns
 */
const cloneIfNecessary = (value, optionsArgument) => {
  const clone = optionsArgument && optionsArgument.clone === true
  return clone && isMergeableObject(value)
    ? deepmerge(emptyTarget(value), value, optionsArgument)
    : value
}

/**
 * 深度合并target和source
 * @param {*} target 目标对象
 * @param {*} source 合并对象
 * @param {*} optionsArgument 配置参数
 * @returns
 */
const deepmerge = (target, source, optionsArgument) => {
  const array = Array.isArray(source),
    options = optionsArgument || { arrayMerge: defaultArrayMerge },
    arrayMerge = options.arrayMerge || defaultArrayMerge
  /**
   * 如果source是数组则调用defaultArrayMerge()对数组进行合并,
   * 否则调用mergeObject()对对象进行合并
   */
  if (array) {
    return Array.isArray(target)
      ? arrayMerge(target, source, optionsArgument)
      : cloneIfNecessary(source, optionsArgument)
  } else {
    return mergeObject(target, source, optionsArgument)
  }
}

/**
 * 深度合并对象数组中的多个对象
 * @param {*} array 对象数组
 * @param {*} optionsArgument
 * @returns
 */
const deepmergeAll = (array, optionsArgument) => {
  // 数组长度为2才也允许合并
  if (!Array.isArray(array) || array.length < 2) {
    throw new Error('first argument should be an array with at least two elements')
  }
  // 循环调用deepmerge()深度合并
  return array.reduce(function (prev, next) {
    return deepmerge(prev, next, optionsArgument)
  })
}

// 测试
const result = deepmergeAll([
  { level1: { level2: { name: 'David', parts: ['head', 'shoulders'] } } },
  { level1: { level2: { face: 'meh', parts: ['knees', 'toes'] } } },
  { level1: { level2: { eyes: 'more meh', parts: ['eyes'] } } },
])
console.log(result)
/*
{
   "level1":{
      "level2":{
         "name":"David",
         "parts":[
            "head",
            "shoulders",
            "knees",
            "toes",
            "eyes"
         ],
         "face":"meh",
         "eyes":"more meh"
      }
   }
}
*/

10.模板编译功能

模板编译最常用的实现方式是通过正则表达式进行解析,当匹配指定规则时则使用数据对象替换。

js
/**
 * \s*表示以非贪婪模式匹配0个或多个空白字符,(\w+)表示匹配字母、
 * 数字、下划线并获取这一匹配
 */
const reg = /{{\s*?(\w+)\s*?}}/g;
const render = (template: string, data: Record<string, any>) => {
  return template.replace(reg, (match, key) => {
    return key && data.hasOwnProperty(key) ? data[key] : '';
  });
};
// 测试
const template = `name:{{name}},age:{{age}}`;
const data = { name: 'zchengfeng', age: 18 };
console.log(render(template, data)); // name:zchengfeng,age:18

11.手写 setTimeout()

11.1 基于 setInterval 模拟

js
function mySetTimeout(fn, delay = 0) {
  const start = Date.now()
  const timer = setInterval(() => {
    if (Date.now() - start >= delay) {
      clearInterval(timer)
      fn()
    }
  }, 1) // 每1ms检查一次
  return timer
}

function myClearTimeout(timer) {
  clearInterval(timer)
}

这种方式基于setInterval轮询实现,每次轮询检查当前时间是否超过设定值,如果超过则执行函数并清除定时器。虽然实现简单,但依赖于系统定时器,而且轮询间隔越小,性能越差。

11.2 基于 Promise + 循环事件模拟(无内置定时器)

js
function sleep(ms) {
  const start = Date.now()
  while (Date.now() - start < ms) {
    // 主动阻塞(同步等待)
  }
}

function mySetTimeout(fn, delay = 0) {
  Promise.resolve().then(() => {
    sleep(delay)
    fn()
  })
}

这种方式不依赖setTimeout 或 setInterval,只靠事件循环和微任务“耗时”,但是会阻塞主线程,一般不推荐使用。

11.3 基于 requestAnimationFrame 模拟

requestAnimationFrame是一个基于渲染帧频率的异步调度器,在下一帧渲染前执行(约16.6ms 一次),可以利用帧循环不断检查 当前时间 - 开始时间 是否超过目标 delay。当达到目标延迟时,执行回调函数 fn()。例如延迟时间为1000ms,浏览器每帧约 16.6ms,因此大约 60 帧后就能触发函数。

js
function mySetTimeout(fn, delay) {
  // 记录开始时间
  let start = performance.now()
  //  定时器对象,canceled用于取消定时器
  let timerId = { canceled: false }

  function loop(now) {
    // 检查定时器是否取消
    if (timerId.canceled) return
    // 检查是否超过延迟时间
    if (now - start >= delay) {
      fn()
    } else {
      // 继续请求下一个动画帧
      requestAnimationFrame(loop)
    }
  }
  //  初始请求动画帧
  requestAnimationFrame(loop)
  return timerId
}

function myClearTimeout(timerId) {
  timerId.canceled = true
}

这种方式基于requestAnimationFrame实现,虽然实现精度高,但只能在浏览器环境使用。

11.4 利用 Date.now() + Promise 实现异步等待

js
function mySetTimeout(fn, delay = 0) {
  // 取消状态标识
  let canceled = false
  // 记录当前时间
  const start = Date.now()

  // 检查函数
  function check() {
    // 检查取消状态
    if (canceled) return
    // 获取函数当前执行时间
    const now = Date.now()
    // 检查是否超过延迟时间
    if (now - start >= delay) {
      fn()
    } else {
      // 把 check 放入微任务队列
      Promise.resolve().then(check)
    }
  }

  Promise.resolve().then(check)

  return () => (canceled = true)
}

这种实现方式不使用 setTimeout 或 setInterval,基于事件循环 + 微任务实现异步延迟,也适用于非浏览器环境。

12.手写 setInterval()

setInterval 常用于计时场景,例如倒计时组件,由于 setInterval 的缺点,通常不会直接使用 setInterval 实现计时功能,而是借助 setTimeout 模拟 setInterval,setInterval 的缺点如下:

  • 执行时机不准:setInterval 的执行时间间隔是近似的,并不能保证在指定的时间间隔内准确执行。实际执行时间可能会受到 JavaScript 引擎当前的负载和其他因素的影响,导致执行时间不稳定。
  • 积累的延迟:如果回调函数的执行时间比设定的时间间隔更长,多个回调函数会积累在一起等待执行,导致回调函数的执行时间出现延迟。这可能导致一些计时器任务的堆积,使得回调函数的执行与预期不符
  • 容易发生内存泄漏风险。如果回调函数存在问题导致无法执行完毕,会持续占用内存资源。

12.1 使用 setTimeout 模拟 setInterval

使用 setTimeout 替代 setInterval 的优势如下:

  • 更准确的时间间隔:使用 setTimeout 可以手动控制每次回调函数的执行时间间隔,而不受 JavaScript 引擎的负载和其他因素的影响。这样可以更精确地控制回调函数的执行间隔,并减少误差。
  • 避免积累的延迟:setInterval 的一个常见问题是如果回调函数的执行时间比设定的时间间隔更长,多个回调函数会积累在一起等待执行,导致回调函数的执行时间出现延迟。使用 setTimeout 可以避免这个问题,因为每次回调函数执行完成后,手动设置下一个 setTimeout,确保下一个回调函数按照指定的时间间隔执行。
  • 动态调整时间间隔:使用 setTimeout 可以在每次回调函数执行时动态地设置下一个回调的时间间隔。这样可以根据实际需求灵活地调整时间间隔,例如根据回调函数的执行情况来调整执行频率,实现更高级的调度逻辑。
  • 可暂停和恢复:通过使用 setTimeout,可以通过清除计时器的方式实现暂停和恢复功能。可以使用 clearTimeout 清除当前计时器,从而停止回调函数的执行,并在需要时再次调用 setTimeout 来恢复执行。

使用 setTimeout 模拟 setInterval 核心在于:在回调函数内部使用递归调用 setTimeout 来实现循环执行。

js
function mySetInterval(callback, interval) {
  let timeoutId
  function intervalFn() {
    callback()
    // 递归调用intervalFn
    timeoutId = setTimeout(intervalFn, interval)
  }
  // 每次到了延迟时间就会递归调用intervalFn中的callback函数
  timeoutId = setTimeout(intervalFn, interval)
  // 返回一个函数,用于清理定时器
  return function () {
    clearTimeout(timeoutId)
  }
}

// 测试
const clearIntervalFn = mySetInterval(function () {
  console.log('Repeated execution')
}, 2000)

12.1 使用 requestAnimationFrame 实现 setInterval

requestAnimationFrame 是浏览器提供的一个用于执行动画和其他重绘操作的优化定时器函数,使用 requestAnimationFrame 来模拟 setInterval 可以提供更精确和更流畅的定时效果,并且对于可见性更好,因为它会自动暂停和恢复,以适应页面激活和非激活状态。

js
function mySetInterval(callback, interval) {
  // 获取开始时间
  let startTime = Date.now()
  // 用于跟踪经过的时间,以确定是否达到了指定的时间间隔
  let elapsed = 0
  function loop() {
    // 获取当前执行时间
    const currentTime = Date.now()
    // 计算时间差
    const deltaTime = currentTime - startTime
    // 累加时间差
    elapsed += deltaTime
    // 如果累加时间差大于执行函数间隔,则执行callback,并重置累加时间差
    if (elapsed >= interval) {
      callback()
      elapsed = 0
    }
    // 将当前执行时间作为下一次的开始时间
    startTime = currentTime
    requestAnimationFrame(loop)
  }
  // 递归调用loop
  requestAnimationFrame(loop)
}

13.once 函数

once 用于保证函数只执行一次,常用于只执行一次的初始化、订阅事件或执行一次性操作的场景(Vue 提供 once 函数的实现)。

js
// 利用闭包和高阶函数函数式编程特性实现once函数,保证once是一个纯函数,无副作用
function once(callback) {
  // 执行标志位,false表示未执行,true表示已执行
  let called
  return function (...args) {
    // 标志位为false时执行callback
    if (!called) {
      // 修改标志位状态
      called = true
      // 绑定this传入参数
      callback.apply(this, args)
    }
  }
}

// 测试
const func = once(function () {
  console.log('This function will only be called once.')
})
func() // 输出:This function will only be called once.
func() // 无输出,函数不会再次调用

14.版本号对比函数

实际项目开发中,通常需要版本管理功能以实现版本包更新功能,因此需要对比最新包版本号是否大于本地包版本号(当前版本),判断是否需要更新。

js
/**
 * 比较两个字符串版本号,version2大于version1则返回true,否则返回false
 * @param {Object} version1
 * @param {Object} version2
 */
function compareVersion(version1, version2) {
  // 边界判断
  if (typeof version1 !== 'string' || typeof version2 !== 'string') return false
  // 根据 . 号分割字符串为数组
  const v1 = version1.split('.'),
    v2 = version2.split('.')
  // 获取分割后数组的最大长度,由于两个数组长度可能会不一致,最小长度的数组元素需要补0
  for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
    const n1 = i < v1.length ? parseInt(v1[i]) : 0
    const n2 = i < v2.length ? parseInt(v2[i]) : 0
    if (n2 > n1) return true
  }
  return false
}

// 测试
compareVersion(1.0, 2.0) // false
compareVersion('0.0.2', '0.0.1') // false
compareVersion('0.0.1', '0.0.2') // true
compareVersion('0.0.1', '0.0.11') // true

15.Promise 异常处理函数

Promise虽然提供了catch方法用于处理异常,但在实际项目中,一般使用async await语法来简化异步操作,基于go语言的错误机制结合async await语法,可以返回错误和结果提升可读性。

ts
/**
 * 异步函数转换为Promise<[E, null] | [null, T]>,Promise返回数组中第一项为错误信息,第二项为结果,
 * 异步函数执行成功时,返回[null, T],异步函数执行失败时,返回[E, null]
 * @param promise 异步函数
 * @param callback 回调函数
 * @returns Promise<[E, null] | [null, T]>
 */
export async function asyncTo<T, E = Error>(
  promise: Promise<T>,
  callback?: () => void
): Promise<[E, null] | [null, T]> {
  try {
    const res = await promise
    return [null, res]
  } catch (error) {
    return [error as E, null]
  } finally {
    // 如果 callback 是异步的也等待它
    // 并且保证 finally 一定在函数返回前执行(对 await 的调用)
    if (callback) {
      callback()
    }
  }
}

惰性函数

惰性函数是一种编程优化技巧,其实现原理是利用闭包和高阶函数函数式编程特性,在第一次调用时执行函数,并将结果缓存起来,后续调用直接返回缓存结果,避免重复执行。惰性函数常用于浏览器特性检测、重量级计算缓存、环境相关的功能初始化、延迟加载的模块系统等场景。

js
function lazy(fn) {
  let result
  let initialized = false

  return function () {
    if (!initialized) {
      result = fn()
      initialized = true
    }
    return result
  }
}

惰性函数优化API兼容性检测,避免重复检测API:

js
// 检测浏览器支持的动画API
function getAnimationFrame() {
  if (typeof requestAnimationFrame !== 'undefined') {
    getAnimationFrame = () => requestAnimationFrame
  } else if (typeof webkitRequestAnimationFrame !== 'undefined') {
    getAnimationFrame = () => webkitRequestAnimationFrame
  } else if (typeof mozRequestAnimationFrame !== 'undefined') {
    getAnimationFrame = () => mozRequestAnimationFrame
  } else {
    getAnimationFrame = () => callback => setTimeout(callback, 1000 / 60)
  }

  return getAnimationFrame()
}

// 使用
const raf = getAnimationFrame()
raf(() => console.log('动画帧'))

函数式编程工具函数

compose

pipeline

柯里化函数

memo

Released under the MIT License.