JsLib/Tool.js

// ES6 方法,用来解决实际开发的 JS 问题

/**
 * 工具类
 *
 * @class Tool
 */
class Tool {
  constructor() {}

  /**
   * @description 打印开始点
   *
   * @memberof Tool
   */
  start() {
    console.log("工具类Tool start^_^_^_^_^_^_^_^_^_^")
  }

  /**
   * @description 打印结束点
   *
   * @memberof Tool
   */
  end() {
    console.log("工具类Tool end^_^_^_^_^_^_^_^_^_^")
  }

  /**
   * @description 如何检查元素是否具有指定的类?
   * @param {HTMLElement} el
   * @param {String} className
   * @return { Boolean }
   * @memberof Tool
   * @example
   * hasClass(document.getElementById('aaa'), 'a')
   */
  hasClass(el, className) {
    return el.classList.contains(className)
  }

  /**
   * @description 如何切换一个元素的类? 有类就删除,无类就添加
   * @param {HTMLElement} el
   * @param {String} className
   * @return { * }
   * @memberof Tool
   * @example
   * toggleClass(document.querySelector('p#b'), 'a')
   */
  toggleClass(el, className) {
    el.classList.toggle(className)
  }

  /**
   * @description 如何检查父元素是否包含子元素?
   * @param {HTMLElement} parent
   * @param {HTMLElement} child
   * @return { Boolean }
   * @memberof Tool
   * @example
   * elementContains(document.querySelector('head'), document.querySelector('title')) // true
   * elementContains(document.querySelector('head'), document.querySelector('body')) // false
   */
  elementContains(parent, child) {
    return parent !== child && parent.contains(child)
  }

  /**
   * @description 如何检查指定的元素在视口中是否可见?
   * @param {HTMLElement} el
   * @param {HTMLElement} partiallyVisible = false partiallyVisible是否开启全屏; 为true 需要全屏(上下左右)可以见
   * @return { Boolean }
   * @memberof Tool
   * @example
   * // 需要左右可见
   * elementIsVisibleInViewport(document.querySelector('head'), document.querySelector('title')) // true
   *
   * // 需要全屏(上下左右)可以见
   * elementIsVisibleInViewport(document.querySelector('head'), document.querySelector('body')) // false
   */
  elementIsVisibleInViewport(el, partiallyVisible = false) {
    const { top, left, bottom, right } = el.getBoundingClientRect()
    const { innerHeight, innerWidth } = window
    return partiallyVisible
      ? ((top > 0 && top < innerHeight) ||
          (bottom > 0 && bottom < innerHeight)) &&
          ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
      : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth
  }

  /**
   * @description 如何获取元素中的所有图像?
   * @param {HTMLElement} el
   * @param {Boolean} includeDuplicates = false false:去重;true:不去重
   * @return { Array } ['image1.jpg', 'image2.png', 'image1.png', '...']
   * @memberof Tool
   * @example
   * // 不去重
   * getImages(document,true) // ['image1.jpg', 'image2.png', 'image1.png', '...']
   *
   * // 去重
   * getImages(document,false) // ['image1.jpg', 'image2.png', '...']
   */
  getImages(el, includeDuplicates = false) {
    const images = [...el.getElementsByTagName("img")].map((img) =>
      img.getAttribute("src")
    )
    return includeDuplicates ? images : [...new Set(images)]
  }

  /**
   * @description 如何确定设备是移动设备还是台式机/笔记本电脑?
   * @return { String } 'Mobile' / 'Desktop'
   * @memberof Tool
   * @example
   * detectDeviceType() // "Mobile" or "Desktop"
   */
  detectDeviceType() {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
      navigator.userAgent
    )
      ? "Mobile"
      : "Desktop"
  }

  /**
   * @description 如何创建一个包含当前URL参数的对象?
   * @param { String } url
   * @return { Object } {n: 'Adam', s: 'Smith'}
   * @memberof Tool
   * @example
   * getURLParameters(tool.currentURL()) // {}
   *
   * getURLParameters('http://url.com/page?n=哈哈&s=Smith') // {n: '哈哈', s: 'Smith'}
   */
  getURLParameters(url) {
    // reduce() 对于空数组是不会执行回调函数的。

    return (url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce(
      (a, v) => (
        (a[v.slice(0, v.indexOf("="))] = v.slice(v.indexOf("=") + 1)), a
      ),
      {}
    )
  }

  /**
   * @description 如何从元素中移除事件监听器?
   * @param {HTMLElement} el
   * @param { String } evt 事件类型 如:'click'
   * @param { Function } fn 绑定函数
   * @param { Boolean } opts = false 指定移除事件句柄的阶段。true:在捕获阶段移除事件句柄;false- 默认:在冒泡阶段移除事件句柄
   * @return  { * }
   * @memberof Tool
   * @example
   * const fn = () => console.log('!');
   * document.body.addEventListener('click', fn);
   *
   * off(document.body, 'click', fn)
   */
  off(el, evt, fn, opts = false) {
    el.removeEventListener(evt, fn, opts)
  }

  /**
   * @description 如何获得给定毫秒数的可读格式?
   * @param {Number} ms 毫秒数
   * @return { String }  1000ms = 1s
   * @memberof Tool
   * @example
   * formatDuration(1001) // 1 second, 1 millisecond
   *
   * formatDuration(34325055574) // 397 days, 6 hours, 44 minutes, 15 seconds, 574 milliseconds
   */
  formatDuration(ms) {
    if (ms < 0) ms = -ms
    const time = {
      day: Math.floor(ms / 86400000),
      hour: Math.floor(ms / 3600000) % 24,
      minute: Math.floor(ms / 60000) % 60,
      second: Math.floor(ms / 1000) % 60,
      millisecond: Math.floor(ms) % 1000,
    }
    return Object.entries(time)
      .filter((val) => val[1] !== 0)
      .map(([key, val]) => `${val} ${key}${val !== 1 ? "s" : ""}`)
      .join(", ")
  }

  /**
   * @description 如何向传递的URL发出GET请求?
   * @param { String } url
   * @param {Function} callback 成功回调函数
   * @param {Function} err 失败回调函数
   * @return {*}
   * @memberof Tool
   * @example
   * httpGet('https://jsonplaceholder.typicode.com/posts/1', console.log) // {"userId": 1, "id": 1, "title": "sample title", "body": "my text"}
   */
  httpGet(url, callback, err = console.error) {
    const request = new XMLHttpRequest()
    request.open("GET", url, true)
    request.onload = () => callback(request.responseText)
    request.onerror = () => err(request)
    request.send()
  }

  /**
   * @description 如何对传递的URL发出POST请求?
   * @param { String } url
   * @param { Object } data
   * @param {Function} callback 成功回调函数
   * @param {Function} err 失败回调函数
   * @return {*}
   * @memberof Tool
   * @example
   * httpPost('https://jsonplaceholder.typicode.com/posts', data, console.log) // {"userId": 1, "id": 1, "title": "sample title", "body": "my text"}
   */
  httpPost(url, data, callback, err = console.error) {
    const request = new XMLHttpRequest()
    request.open("POST", url, true)
    request.setRequestHeader("Content-type", "application/json; charset=utf-8")
    request.onload = () => callback(request.responseText)
    request.onerror = () => err(request)
    request.send(data)
  }

  // 常用的工具函数,包含数字,字符串,数组和对象等等操作。

  /**
   * @description 金钱格式化,三位加逗号
   * @param {Number} num
   * @return {String} 5,465,615,654,465
   * @memberof Tool
   * @example
   * formatMoney('5465615654465')  // 5,465,615,654,465
   */
  formatMoney(num) {
    return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
  }

  /**
   * @description 截取字符串并加省略号
   * @param {String} str
   * @param {Number} length
   * @return {String} abcd....
   * @memberof Tool
   * @example
   * subText('fgfsd水电费水电费",5)  // fgfsd...
   */
  subText(str, length) {
    if (str.length === 0) {
      return ""
    }
    if (str.length > length) {
      return str.substr(0, length) + "..."
    } else {
      return str
    }
  }

  /**
   * @description B转换到KB,MB,GB并保留两位小数
   * @param {Number} fileSize
   * @return {String} 1254 -> 1.22KB
   * @memberof Tool
   * @example
   * formatFileSize(1254)  // 1.22KB
   */
  formatFileSize(fileSize) {
    let temp
    if (fileSize < 1024) {
      return fileSize + "B"
    } else if (fileSize < 1024 * 1024) {
      temp = fileSize / 1024
      temp = temp.toFixed(2)
      return temp + "KB"
    } else if (fileSize < 1024 * 1024 * 1024) {
      temp = fileSize / (1024 * 1024)
      temp = temp.toFixed(2)
      return temp + "MB"
    } else {
      temp = fileSize / (1024 * 1024 * 1024)
      temp = temp.toFixed(2)
      return temp + "GB"
    }
  }

  /**
   * @description Windows根据详细版本号判断当前系统名称
   * @param {String} osVersion 详细版本号
   * @return {String} 当前系统名称
   * @memberof Tool
   * @example
   * OutOsName("10.0.18362 Windows 10专业版")  // Win 10
   */
  OutOsName(osVersion) {
    if (!osVersion) {
      return
    }
    let str = osVersion.substr(0, 3)
    if (str === "5.0") {
      return "Win 2000"
    } else if (str === "5.1") {
      return "Win XP"
    } else if (str === "5.2") {
      return "Win XP64"
    } else if (str === "6.0") {
      return "Win Vista"
    } else if (str === "6.1") {
      return "Win 7"
    } else if (str === "6.2") {
      return "Win 8"
    } else if (str === "6.3") {
      return "Win 8.1"
    } else if (str === "10.") {
      return "Win 10"
    } else {
      return "Win"
    }
  }

  /**
   * @description 判断手机是Andoird还是IOS
   * @return {String}
   * @memberof Tool
   * @example
   * getOSType()
   */
  getOSType() {
    let u = navigator.userAgent,
      app = navigator.appVersion
    let isAndroid = u.indexOf("Android") > -1 || u.indexOf("Linux") > -1
    let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
    if (isIOS) {
      return "ios"
    }
    if (isAndroid) {
      return "android"
    }
    return "其它"
  }

  /**
   * @description 判断是否是移动端
   * @return {Boolean}
   * @memberof Tool
   * @example
   * isMobile()  // false || true
   */
  isMobile() {
    return "ontouchstart" in window
  }

  /**
   * @description 函数防抖
   * @param {Function} func
   * @param {Number} wait 延迟执行毫秒数
   * @param {Boolean} immediate = false true:表立即执行;false:表非立即执行
   * @return {*}
   * @memberof Tool
   * @example
   * function printWidth(){
   *      console.info(window.document.body.clientWidth);
   * }
   *
   * window.addEventListener('resize', debounce(printWidth, 900,true), false)
   */
  debounce(func, wait, immediate = false) {
    // 非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
    // 非立即执行版解说:第一开始进来,timeout为null,不用清空,走else等待一秒执行函数,在这期间,又执行这个函数,timeout不为空,清除一下定时器,又继续来等待一秒
    // 立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
    // 立即执行版解说:timeout为null,callNow为true,执行函数,无触发定时器;
    // 一秒内,又调用这个函数,清空定时器(因为timeout为true),callNow为false,不执行函数;timeout为true,开启定时器,等待一秒无人访问就将timeout设null

    let timeout
    return function () {
      let context = this
      let args = arguments //参数

      if (timeout) clearTimeout(timeout)
      // console.log(1)
      if (immediate) {
        let callNow = !timeout
        // timeout有就将其设为空
        timeout = setTimeout(() => {
          timeout = null
          // console.log(2)
        }, wait)
        //  console.log(3)
        if (callNow) func.apply(context, args)
      } else {
        timeout = setTimeout(() => {
          func.apply(context, args)
        }, wait)
      }
    }
  }

  /**
   * @description 函数节流(限制一个函数在一定时间内只能执行一次。)
   * @param {Function} func 函数
   * @param {Number} wait 延迟执行毫秒数
   * @param {Number} type = 1  1:表时间戳版;2:表定时器版
   * @return {*}
   * @memberof Tool
   * @example
   * function printHeight(){
   *      console.info(window.document.body.clientHeight);
   * }
   *
   * window.addEventListener("mousemove",throttle(printHeight,1000,2));
   */
  throttle(func, wait, type = 1) {
    let previous, timeout
    if (type === 1) {
      previous = 0
    } else if (type === 2) {
      timeout = null
    }
    return function () {
      let context = this
      let args = arguments
      if (type === 1) {
        let now = Date.now()

        if (now - previous > wait) {
          func.apply(context, args)
          previous = now
        }
      } else if (type === 2) {
        if (!timeout) {
          timeout = setTimeout(() => {
            timeout = null
            func.apply(context, args)
          }, wait)
        }
      }
    }
  }

  /**
   * @description 判断数据类型
   * @param {*} target
   * @return {String}
   * @memberof Tool
   * @example
   * type([""])  //array
   */
  type(target) {
    let ret = typeof target
    let template = {
      "[object Array]": "array",
      "[object Object]": "object",
      "[object Number]": "number - object",
      "[object Boolean]": "boolean - object",
      "[object String]": "string-object",
    }

    if (target === null) {
      return "null"
    } else if (ret == "object") {
      let str = Object.prototype.toString.call(target)
      return template[str]
    } else {
      return ret
    }
  }

  /**
   * @description 递归优化(尾递归)
   *
   * @param {Function} f 函数
   * @return {*}
   * @memberof Tool
   * @example
   * var sumTco = tco(function(x, y) {
   *      if (y > 0) {
   *          return sumTco(x + 1, y - 1)//重点在这里, 每次递归返回真正函数其实还是accumulator函数
   *      }
   *      else {
   *          //   console.log(x)
   *          return x
   *      }
   * });
   *
   * //   console.log(sumTco(1, 5));   //6    实际上现在sum函数就是accumulator函数   else那得到的
   */
  tco(f) {
    // 例子:
    // fact(6, 1) // 1 是 fact(0) 的值,我们需要手动写一下
    // fact(5, 6)
    // fact(4, 30)
    // fact(3, 120)
    // fact(2, 360)
    // fact(1, 720)
    // 720 // <= 最终的结果
    // 其实只是最后一个我们想要而已,中间那些计算的(430-434行),我们不想要保存栈里面浪费资源,尾递归就是将中间那些操作优化掉

    // 尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
    // 怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。
    // 每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行
    // 这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。
    // 其实就是递归就开了很多个栈去跑每一层,尾递归就是内层跑完,才return给下一层,永远只有一个栈在跑

    let value
    let active = false
    let accumulated = []

    return function accumulator() {
      accumulated.push(arguments) //每次将参数传入. 例如, 1 100000

      if (!active) {
        active = true
        // 出循环条件, 当最后一次返回一个数字而不是一个函数时, accmulated已经被shift(), 所以出循环
        while (accumulated.length) {
          value = f.apply(this, accumulated.shift()) //调用累加函数, 传入每次更改后的参数, 并执行
          // console.log(value)  // 会发现前面都是空的,最后一个才有值,因为前面的没必要存在栈里面,浪费资源
          // 反正这里的value都是空的,就不保存了,等到跳出while的时候,在return最后一个value就行
        }
        active = false
        return value
      }
    }
  }

  /**
   * @description 去除空格
   *
   * @param {string} str 待处理字符串
   * @param {number} [type=1] 去除空格类型 1-所有空格  2-前后空格  3-前空格 4-后空格 默认为1
   * @return {String}
   * @memberof Tool
   * @example
   * trim(" dg   g145415  44 ",1) // dgg14541544
   */
  trim(str, type = 1) {
    if (type && type !== 1 && type !== 2 && type !== 3 && type !== 4) return
    switch (type) {
      case 1:
        return str.replace(/\s/g, "")
      // return str.trim()
      case 2:
        return str.replace(/(^\s)|(\s*$)/g, "")
      case 3:
        return str.replace(/(^\s)/g, "")
      // return str.trimStart()
      case 4:
        return str.replace(/(\s$)/g, "")
      // return str.trimEnd()
      default:
        return str
    }
  }

  /**
   * @description 大小写转换
   *
   * @param {String} str 待转换的字符串
   * @param {Number} type 1-全大写 2-全小写 3-首字母大写 其他-不转换
   * @return {*}
   * @memberof Tool
   * @example
   * turnCase("asFG",1)  // ASFG
   */
  turnCase(str, type) {
    switch (type) {
      case 1:
        return str.toUpperCase()
      case 2:
        return str.toLowerCase()
      case 3:
        return str[0].toUpperCase() + str.substr(1).toLowerCase()
      default:
        return str
    }
  }

  /**
   * @description 随机16进制颜色 hexColor(方法一)
   *
   * @return {String}  16进制颜色
   * @memberof Tool
   * @example
   * hexColor()  // #000000
   */
  hexColor() {
    let str = "#"
    let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "A", "B", "C", "D", "E", "F"]
    for (let i = 0; i < 6; i++) {
      let index = Number.parseInt((Math.random() * 16).toString())
      str += arr[index]
    }
    return str
  }

  /**
   * @description 随机16进制颜色 randomHexColorCode(方法二)
   *
   * @return {String}  16进制颜色
   * @memberof Tool
   * @example
   * randomHexColorCode()  // #000000
   */
  randomHexColorCode() {
    let n = (Math.random() * 0xfffff * 1000000).toString(16)
    return "#" + n.slice(0, 6)
  }

  /**
   * @description 转义html(防XSS攻击)
   *
   * @param {String} str 待转换的字符串
   * @return {String} 转义html字符串
   * @memberof Tool
   * @example
   * escapeHTML(`
   *      <div id="app" class="a" style="height: 1500px;"></div>
   *      <P class="a" id="aaa">1</P>
   * `))
   *
   * // &lt;div id=&quot;app&quot; class=&quot;a&quot; style=&quot;height: 1500px;&quot;&gt;&lt;/div&gt;
   * // &lt;P class=&quot;a&quot; id=&quot;aaa&quot;&gt;1&lt;/P&gt;
   */
  escapeHTML(str) {
    return str.replace(
      /[&<>'"]/g,
      (tag) =>
        ({
          "&": "&amp;",
          "<": "&lt;",
          ">": "&gt;",
          "'": "&#39;",
          '"': "&quot;",
        }[tag] || tag)
    )
  }

  /**
   * @description 数字超过规定大小加上加号“+”,如数字超过99显示99+
   *
   * @param {number} val 输入的数字
   * @param {number} maxNum 数字规定界限
   * @return {String}
   * @memberof Tool
   * @example
   * outOfNum(100,99)     // 99+
   */
  outOfNum(val, maxNum) {
    val = val ? val - 0 : 0
    if (val > maxNum) {
      return `${maxNum}+`
    } else {
      return val
    }
  }

  /**
   * @description 获取当前子元素是其父元素下子元素的排位
   *
   * @param {HTMLElement} el
   * @return {Number}
   * @memberof Tool
   * @example
   * getChildInParentIndex(document.getElementById("btn"))  // 1
   */
  getChildInParentIndex(el) {
    // 小知识:先走do,index++,在去while(5) 循环五次
    // previousElementSibling 属性返回指定元素的前一个兄弟元素(相同节点树层中的前一个元素节点)

    if (!el) {
      return -1
    }

    let index = 0
    do {
      index++
    } while ((el = el.previousElementSibling))
    return index
  }

  /**
   * @description 获取元素类型
   *
   * @param {*} obj
   * @return {String}  元素类型 (number, string, object, array, null, undefined)
   * @memberof Tool
   * @example
   * dataType([])  // array
   */
  dataType(obj) {
    return Object.prototype.toString
      .call(obj)
      .replace(/^\[object (.+)\]$/, "$1")
      .toLowerCase()
  }

  /**
   * @description fade动画
   *
   * @param {HTMLElement} el
   * @param {string} [type='in'] 动画类型
   * @memberof Tool
   * @example
   * setFade(document.getElementById("btn"))
   */
  setFade(el, type = "in") {
    // 思路:其实就是让opacity从0慢慢加到1,利用requestAnimationFrame使opacity在变化的过程添加动画
    // window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
    // 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
    // 所以requestAnimationFrame会一直回调,直到opacity大于1,则停止动画
    el.style.opacity = type === "in" ? 0 : 1
    let last = +new Date()
    const tick = () => {
      const opacityValue =
        type === "in" ? (new Date() - last) / 400 : -(new Date() - last) / 400
      el.style.opacity = +el.style.opacity + opacityValue
      last = +new Date()
      if (type === "in" ? +el.style.opacity < 1 : +el.style.opacity > 0) {
        requestAnimationFrame(tick)
      }
    }
    tick()
  }

  /**
   * @description 禁止网页复制粘贴(默认都禁止)
   *
   * @param {boolean} [isStopCopy=true] 是否禁止网页复制
   * @param {boolean} [isStopPaste=true] 是否禁止网页被黏贴
   * @return {*}
   * @memberof Tool
   * @example
   * stopCopyOrPaste(true,true)
   */
  stopCopyOrPaste(isStopCopy = true, isStopPaste = true) {
    const html = document.querySelector("html")
    if (isStopCopy && isStopPaste) {
      html.oncopy = () => false
      html.onpaste = () => false
      return
    }
    if (isStopCopy) {
      html.oncopy = () => false
      return
    }
    if (isStopPaste) {
      html.onpaste = () => false
      return
    }
  }

  /**
   * @description 去除字符串中的html代码
   *
   * @param {string} [str='']
   * @return {String}
   * @memberof Tool
   * @example
   * stopCopyOrPaste('<h1>哈哈哈哈<呵呵呵</h1>')  // 呵呵呵
   */
  removeHTML(str = "") {
    return str.replace(/<[\/\!]*[^<>]*>/gi, "")
  }

  /**
   * @description 生成随机字符串
   *
   * @param {number} [length=8] 指定位数
   * @param {string} [chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'] 指定字符
   * @return {*}
   * @memberof Tool
   * @example
   * uuid()  // E10fvazi   (如果都不传,默认生成8位)
   *
   * uuid(4, "abcd")  // bccc
   */
  uuid(
    length = 8,
    chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  ) {
    let result = ""
    for (var i = length; i > 0; --i) {
      result += chars[Math.floor(Math.random() * chars.length)]
    }

    return result
  }

  /**
   * @description 保留到小数点以后n位
   *
   * @param {Number} number 目标数字
   * @param {number} [no=2] 保留到小数点以后no位
   * @return {Number}
   * @memberof Tool
   * @example
   * cutNumber(1.545454658648);   // 1.55
   * cutNumber(1.545454658648, 4);   // 1.5455
   */
  cutNumber(number, no = 2) {
    if (typeof number != "number") {
      number = Number(number)
    }
    return Number(number.toFixed(no))
  }

  /**
   * @description 计算函数执行时间
   *
   * @param {Function} callback 执行函数
   * @return {*}
   * @memberof Tool
   * @example
   * const getPow =  () => Math.pow(2, 10);
   * timeTaken(getPow);   // timeTaken: 0.010009765625 ms   // 1024
   */
  timeTaken(callback) {
    console.time("timeTaken")
    const r = callback()
    console.timeEnd("timeTaken")
    return r
  }

  /**
   * @description 创建一个发布/订阅(发布-订阅)事件集线,有emit,on和off方法。
   * @description 1. 使用Object.create(null)创建一个空的hub对象
   * @description 2. emit,根据event参数解析处理程序数组,然后.forEach()通过传入数据作为参数来运行每个处理程序。
   * @description 3. on,为事件创建一个数组(若不存在则为空数组),然后.push()将处理程序添加到该数组。
   * @description 4. off,用.findIndex()在事件数组中查找处理程序的索引,并使用.splice()删除。
   * @description 发布/订阅模式
   *
   * @return {*}
   * @memberof Tool
   * @example
   * const handler = (data) => console.log(data)
   * const hub = tool.createEventHub()
   * let increment = 0
   *
   * // 订阅,监听不同事件
   * hub.on("message", handler)
   * hub.on("message", () => console.log("Message event fired"))
   * hub.on("increment", () => console.log(increment++))
   *
   * // 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们
   * hub.emit("message", "hello world") // 打印 'hello world' 和 'Message event fired'
   * hub.emit("message", { hello: "world" }) // 打印 对象 和 'Message event fired'
   * hub.emit("increment") // increment = 1
   *
   * // 停止订阅
   * hub.off("message", handler) // 把handler函数给删除掉
   * hub.emit("message", { hello: "world" }) // Message event fired
   * // 为什么只打印一个,那是因为上面已经停止订阅了handler,自然不会打印出{ hello: "world" };
   * // message有两个订阅者,所以Message event fired还在,打印。
   */
  createEventHub() {
    return {
      hub: Object.create(null),
      emit(event, data) {
        console.log(this.hub)
        ;(this.hub[event] || []).forEach((handler) => handler(data))
      },
      on(event, handler) {
        if (!this.hub[event]) this.hub[event] = []
        this.hub[event].push(handler)
      },
      off(event, handler) {
        const i = (this.hub[event] || []).findIndex((h) => h === handler)
        if (i > -1) this.hub[event].splice(i, 1)
        if (this.hub[event].length === 0) delete this.hub[event]
      },
    }
  }

  /**
   * @description 只调用一次的函数
   *
   * @param {Function} fn 目标函数
   * @return {*}
   * @memberof Tool
   * @example
   * const startApp = function(event) {
   *   console.log(this, event); // document.body, MouseEvent
   * };
   *
   * document.body.addEventListener('click', tool.once(startApp)); // 只执行一次startApp
   */
  once(fn) {
    let called = false
    return function () {
      if (!called) {
        called = true
        fn.apply(this, arguments)
      }
    }
  }

  /**
   * @description 使用递归。
   * @description 利用Object.keys(obj)联合Array.prototype.reduce(),以每片叶子节点转换为扁平的路径节点
   * @description 如果键的值是一个对象,则函数使用调用适当的自身prefix以创建路径Object.assign()。
   * @description 否则,它将适当的前缀键值对添加到累加器对象。
   * @description prefix除非您希望每个键都有一个前缀,否则应始终省略第二个参数。
   * @description 以键的路径扁平化对象
   *
   * @param {Object} obj 目标对象
   * @param {string} [prefix=""] 扁平的路径节点(前缀)  主要是给递归用的
   * @return {Object}
   * @memberof Tool
   * @example
   * flattenObject({ a: { b: { c: 1 } }, d: 1 })  // {a.b.c: 1, d: 1}
   *
   * flattenObject({
   *   a: { b: { c: 1, c1: 2 }, b1: 5, b2: { bbb: 55 } },
   *   d: 1,
   * })  // {a.b.c: 1, a.b.c1: 2, a.b1: 5, a.b2.bbb: 55, d: 1}
   */
  flattenObject(obj, prefix = "") {
    return Object.keys(obj).reduce((acc, k) => {
      const pre = prefix.length ? prefix + "." : ""

      if (typeof obj[k] === "object") {
        Object.assign(acc, this.flattenObject(obj[k], pre + k))
      } else {
        acc[pre + k] = obj[k]
      }

      return acc
    }, {})
  }

  /**
   * @description 思路:将这种'a.b.c'转为{"a":{"b":{"c":1}}},再借助JSON.parse将字符串转成JSON对象
   * @description 那么如何转成{"a":{"b":{"c":1}}}
   * @description 1. 用split用点切割成数组,map帮每一个前面加{"x":,最后一个不加,就变成{"a":{"b":{"c":
   * @description 2. 再拼上他的值;{"a":{"b":{"c":1
   * @description 3. 那是不是缺少三个右花括号,用repeat方法复制三个花括号,即:{"a":{"b":{"c":1}}}
   * @description 4. 有字符串JSON了,用JSON.parse转成对象
   *
   * @description 用途:在做Tree组件或复杂表单时取值非常舒服。
   * @description 与上面的flattenObject方法相反,展开对象。
   * @description 以键的路径展开对象
   *
   * @param {Object} obj
   * @return {Object}
   * @memberof Tool
   * @example
   * unflattenObject({ 'a.b.c': 1, d: 1 });  // { a: { b: { c: 1 } }, d: 1 }
   */
  unflattenObject(obj) {
    return Object.keys(obj).reduce((acc, k) => {
      if (k.indexOf(".") !== -1) {
        const keys = k.split(".")

        Object.assign(
          acc,
          JSON.parse(
            "{" +
              keys
                .map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`))
                .join("") +
              obj[k] +
              "}".repeat(keys.length)
          )
        )
      } else {
        acc[k] = obj[k]
      }
      return acc
    }, {})
  }

  /**
   * @description 迭代属性并执行回调
   *
   * @param {Object} obj 目标对象
   * @param {Function} fn 执行函数
   * @return {*}
   * @memberof Tool
   * @example
   * forOwn({ foo: 'bar', a: 1 }, v => console.log(v));  // bar 1
   */
  forOwn(obj, fn) {
    return Object.keys(obj).forEach((key) => fn(obj[key], key, obj))
  }

  /**
   * @description 检查值是否为特定类型
   *
   * @param {*} type 特定类型
   * @param {*} val 值
   * @return {Boolean}
   * @memberof Tool
   * @example
   * is(Array, [1])); // true
   * is(ArrayBuffer, new ArrayBuffer())); // true
   * is(Map, new Map())); // true
   * is(RegExp, /./g)); // true
   * is(Set, new Set())); // true
   * is(WeakMap, new WeakMap())); // true
   * is(WeakSet, new WeakSet())); // true
   * is(String, '')); // true
   * is(String, new String(''))); // true
   * is(Number, 1)); // true
   * is(Number, new Number(1))); // true
   * is(Boolean, true)); // true
   * is(Boolean, new Boolean(true))); // true
   */
  is(type, val) {
    return ![, null].includes(val) && val.constructor === type
  }

  /**
   * @description constructor不能判断undefined、null,其他都可以判断
   * @description 返回值或变量的类型名
   *
   * @param {*} v 值或变量
   * @return {String}  类型名
   * @memberof Tool
   * @example
   * getType(new Set([1, 2, 3])); // set
   * getType([1, 2, 3]); // array
   * getType(function (){}); // function
   */
  getType(v) {
    return v === undefined
      ? "undefined"
      : v === null
      ? "null"
      : v.constructor.name.toLowerCase()
  }

  /**
   * @description 防XSS攻击
   * 
   * @description 转义HTML
   *
   * @param {String} str HTML字符串
   * @return {String} 转义HTML字符串
   * @memberof Tool
   * @example
   * escapeHTML('<a href="#">Me & you</a>'); // &lt;a href=&quot;#&quot;&gt;Me &amp; you&lt;/a&gt;
   */
  escapeHTML(str) {
    return str.replace(
      /[&<>'"]/g,
      (tag) =>
        ({
          "&": "&amp;",
          "<": "&lt;",
          ">": "&gt;",
          "'": "&#39;",
          '"': "&quot;",
        }[tag] || tag)
    )
  }
}

export default Tool

// 3. 第三部分:字符串:https://juejin.cn/post/6844903966526930951#heading-32