# 工具库源码解析

在日常开发中,我们会经常遇到转换固定格式或者生成指定格式的需求,比如生成随机颜色,判断是否为邮箱、身份证等等。那么今天就来分析一下github 的开源库outils

# 安装

  1. 直接下载min目录下的outils.min.js使用,支持 UMD 通用模块规范。
<script src="outils.min.js"></script>
<script>
  var OS = outils.getOS();
</script>
1
2
3
4
  1. 使用 npm 安装
npm install --save-dev outils
1

# 使用

推荐按需引入。

// 只引入部分方法('outils/<方法名>')
const getOS = require("outils/getOS");
const OS = getOS();
1
2
3

# API

接下来就逐一进行 API 的解读与源码分析。

# arrayEqual

判断两个数组是否相等。

/**
 * @description 判断两个数组是否相等
 * @param {Array} arr1
 * @param {Array} arr2
 * @return {Boolean}
 */
function arrayEqual(arr1, arr2) {
  // 如果两个数组是同一引用,则意味着相等
  if (arr1 === arr2) return true;

  // 如果数组长度不同,则不相等
  if (arr1.length !== arr2.length) return false;

  // 如果数组的某一项不同,则不相等
  for (var i = 0; i < arr1.length; ++i) {
    if (arr1[i] !== arr2[i]) return false;
  }

  // 否则意味着相等
  return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# hasClass

判断元素是否有某个class

/**
 *
 * @description 判断元素是否有某个class
 * @param {HTMLElement} ele
 * @param {String} cls
 * @return {Boolean}
 */
function hasClass(ele, cls) {
  // 通过正则判断元素的className属性中是否存在指定的类名
  return new RegExp("(\\s|^)" + cls + "(\\s|$)").test(ele.className);
}
1
2
3
4
5
6
7
8
9
10
11

# addClass

为元素添加class

/**
 * @description 为元素添加class
 * @param {HTMLElement} ele
 * @param {String} cls
 */

var hasClass = require("./hasClass");

function addClass(ele, cls) {
  // 如果元素中不存在指定类名则添加
  if (!hasClass(ele, cls)) {
    // 添加方式是在元素的className属性后面追加指定类名
    // 不要忘记加上空格,因为类名以空格区分
    ele.className += " " + cls;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# removeClass

为元素移除class

/**
 *
 * @description 为元素移除class
 * @param {HTMLElement} ele
 * @param {String} cls
 */

var hasClass = require("./hasClass");

function removeClass(ele, cls) {
  // 如果元素存在指定类名,方可移除
  if (hasClass(ele, cls)) {
    // 声明获取类名的正则
    var reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");

    // 使用字符串的replace方法将类名替换为空格字符串
    // 最终的className可能会有多个空格,HTML规范接受多个空格
    ele.className = ele.className.replace(reg, " ");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# getCookie

下面三个方法涉及到操作cookie,关于cookie的知识可以另起一篇文章了,我们下回接说。本文先讲解方法。

根据name读取Cookie

/**
 *
 * @description 根据name读取cookie
 * @param  {String} name
 * @return {String}
 */
function getCookie(name) {
  // 获取cookie去除空格后,通过';'分割为键值对(key=value)数组
  var arr = document.cookie.replace(/\s/g, "").split(";");
  for (var i = 0; i < arr.length; i++) {
    // 分割键值对(key=value)数组。第一项为key,第二项为value
    var tempArr = arr[i].split("=");

    // 如果指定name与key相同则返回指定value。这里没有使用恒等,应该是为了兼容隐式转换
    if (tempArr[0] == name) {
      // cookie的value会进行编码,因此需要调用API进行解码并返回
      return decodeURIComponent(tempArr[1]);
    }
  }
  return "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

tips:

encodeURI()不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而encodeURIComponent()则会对它发现的任何非标准字符进行编码(替换所有非字母数字字符)。因此使用后者可以编码更多的字符,防止 XSS 攻击。同样的,解码就需要使用对应的方法decodeURIComponent()

# setCookie

设置cookie

/**
 *
 * @description  设置Cookie
 * @param {String} name
 * @param {String} value
 * @param {Number} days
 */
function setCookie(name, value, days) {
  var date = new Date();

  // 根据当地时间获取指定日期,加上days参数后,再更改日期。
  date.setDate(date.getDate() + days);

  // 设置指定的key=value,并且加上过期时间
  document.cookie = name + "=" + value + ";expires=" + date;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

补充几个知识点:

1、Date.prototype.getDate() 返回当地时间下指定月份的日期。比如今天是2022-01-02,该方法会返回数字2 。返回值是介于1-31的整数。

2、Date.prototype.setDate() 更改当地时间下指定月份的日期。接收的参数是月份日期的整数值。

3、设置cookie可以直接赋值给document.cookie ,此时会新增一条cookie,而不是覆盖已有的。设置过期时间的方法就是增加expires属性。该属性的值就是世界标准时间(UTC)

4、JavaScript 的时间由**世界标准时间(UTC)**1970 年 1 月 1 日开始,用毫秒计时,一天由 86,400,000 毫秒组成。Date  对象的范围是 -100,000,000 天至 100,000,000 天(等效的毫秒值)。

5、以一个函数的形式来调用  Date  对象(即不使用  new  操作符)会返回一个代表当前日期和时间的字符串

# removeCookie

根据name删除cookie

/**
 *
 * @description 根据name删除cookie
 * @param  {String} name
 */
var setCookie = require("./setCookie");

function removeCookie(name) {
  // 设置已过期,系统会立刻删除cookie
  setCookie(name, "1", -1);
}
1
2
3
4
5
6
7
8
9
10
11

# getExplore

获取浏览器的类型和版本。

/**
 *
 * @description 获取浏览器类型和版本
 * @return {String}
 */
function getExplore() {
  var sys = {},
    ua = navigator.userAgent.toLowerCase(),
    s;

  // 通过正则匹配用户代理字符串,来存储具体的浏览器类型和版本
  (s = ua.match(/rv:([\d.]+)\) like gecko/))
    ? (sys.ie = s[1])
    : (s = ua.match(/msie ([\d\.]+)/))
    ? (sys.ie = s[1])
    : (s = ua.match(/edge\/([\d\.]+)/))
    ? (sys.edge = s[1])
    : (s = ua.match(/firefox\/([\d\.]+)/))
    ? (sys.firefox = s[1])
    : (s = ua.match(/(?:opera|opr).([\d\.]+)/))
    ? (sys.opera = s[1])
    : (s = ua.match(/chrome\/([\d\.]+)/))
    ? (sys.chrome = s[1])
    : (s = ua.match(/version\/([\d\.]+).*safari/))
    ? (sys.safari = s[1])
    : 0;
  // 根据关系进行判断
  if (sys.ie) return "IE: " + sys.ie;
  if (sys.edge) return "EDGE: " + sys.edge;
  if (sys.firefox) return "Firefox: " + sys.firefox;
  if (sys.chrome) return "Chrome: " + sys.chrome;
  if (sys.opera) return "Opera: " + sys.opera;
  if (sys.safari) return "Safari: " + sys.safari;
  return "Unkonwn";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

补充知识:

String.prototype.match() 方法检索返回一个字符串匹配正则表达式的结果。其中,参数为正则表达式对象

如果传入一个非正则表达式对象,则会隐式地使用  new RegExp(obj)  将其转换为一个  RegExp 。如果你没有给出任何参数并直接使用match()方法  ,你将会得到一个包含空字符串的数组:[""]

返回值分为两种情况:

  • 如果使用 g 标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
  • 如果未使用 g 标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。

上述示例中没有使用全局标志,因此返回第一个完整匹配及其捕获组。打印一下s看看:

['chrome/96.0.4664.110', '96.0.4664.110', index: 87, input: 'mozilla/5.0 (macintosh; intel mac os x 10_14_6) applewebkit/537.36 (khtml, like gecko) chrome/96.0.4664.110 safari/537.36', groups: undefined]
1

返回结果是一个数组。其中有三个附加属性:

  • groups: 一个捕获组数组 或  undefined(如果没有定义命名捕获组)。
  • index: 匹配的结果的开始位置。
  • input: 搜索的字符串。

我们想要得到的浏览器版本就存在于返回结果的第二项。这也是为什么源码里面赋值使用的s[1]

还有一个有意思的点,当尝试将返回结果进行字符串化,发现结果会省略附加属性。只保留了索引是数字的值。

JSON.stringify(s); // '["chrome/96.0.4664.110","96.0.4664.110"]'
1

# getOS

获取操作系统类型。

/**
 *
 * @description 获取操作系统类型
 * @return {String}
 */
function getOS() {
  // 获取navigator相关属性,并进行类型保护
  var userAgent =
    ("navigator" in window &&
      "userAgent" in navigator &&
      navigator.userAgent.toLowerCase()) ||
    "";
  var vendor =
    ("navigator" in window &&
      "vendor" in navigator &&
      navigator.vendor.toLowerCase()) ||
    "";
  var appVersion =
    ("navigator" in window &&
      "appVersion" in navigator &&
      navigator.appVersion.toLowerCase()) ||
    "";

  // 通过正则来判断相关操作系统类型,并返回相关字符串
  if (
    /iphone/i.test(userAgent) ||
    /ipad/i.test(userAgent) ||
    /ipod/i.test(userAgent)
  )
    return "ios";
  if (/android/i.test(userAgent)) return "android";
  if (/win/i.test(appVersion) && /phone/i.test(userAgent))
    return "windowsPhone";
  if (/mac/i.test(appVersion)) return "MacOSX";
  if (/win/i.test(appVersion)) return "windows";
  if (/linux/i.test(appVersion)) return "linux";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# getScrollTop

获取滚动条距顶部的距离。

/**
 *
 * @description 获取滚动条距顶部的距离
 */
function getScrollTop() {
  // 优先获取document.documentElement下的属性
  return (
    (document.documentElement && document.documentElement.scrollTop) ||
    document.body.scrollTop
  );
}
1
2
3
4
5
6
7
8
9
10
11

补充知识:

Document.documentElement  是一个会返回文档对象(document)的根元素只读属性(如HTML文档的<html>元素)。

对于任何非空 HTML 文档,调用  document.documentElement  总是会返回一个<html>元素,且它一定是该文档的根元素。借助这个只读属性,能方便地获取到任意文档的根元素

# setScrollTop

设置滚动条距顶部的距离。

/**
 *
 * @description 设置滚动条距顶部的距离
 * @param {Number} value
 */
function setScrollTop(value) {
  // 使用window.scrollTo滚动到距顶部x轴为0,y轴为value的距离
  window.scrollTo(0, value);
  return value;
}
1
2
3
4
5
6
7
8
9
10

补充知识:

1、Window.scrollTo() 表示滚动到文档中的某个坐标。

2、接受两种类型的参数:

  1. window.scrollTo(x-coord,y-coord) 。其中,第一个参数表示横轴坐标,第二个参数表示纵轴坐标。
  2. window.scrollTo(options) 。其中,options是一个包含三个属性的对象。
    1. top 等同于y-coord
    2. left等同于x-coord
    3. behavior **类型String,表示滚动行为,支持参数 smooth(平滑滚动),instant(瞬间滚动),默认值auto (效果也是瞬间滚动)。

举个 🌰:

window.scrollTo(0, 1000);

// 设置滚动行为改为平滑的滚动
window.scrollTo({
  top: 1000,
  behavior: "smooth",
});
1
2
3
4
5
6
7

# offset

获取元素距离文档(document)的位置。

/**
 *
 * @description  获取一个元素的距离文档(document)的位置,类似jQ中的offset()
 * @param {HTMLElement} ele
 * @returns { {left: number, top: number} }
 */
function offset(ele) {
    var pos = {
        left: 0,
        top: 0
    };

		// 循环获取元素的offsetLeft和offsetTop属性,并累加到pos中
    while (ele) {
        pos.left += ele.offsetLeft;
        pos.top += ele.offsetTop;

				// ele指向该元素的定位元素或者最近的 table,td,th,body元素
				**//** 其中body元素的offsetParent属性为null
        ele = ele.offsetParent;
    };
    return pos;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

补充知识:

1、HTMLElement.offsetLeft  是一个只读属性,返回当前元素左上角相对于  HTMLElement.offsetParent  节点的左边界偏移的像素值。

2、HTMLElement.offsetTop ****同理。

3、对块级元素来说,offsetTopoffsetLeftoffsetWidth  及  offsetHeight  描述了元素相对于  offsetParent  的边界框。

4、然而,对于可被截断到下一行的行内元素(如  span),offsetTop  和  offsetLeft  描述的是第一个边界框的位置(使用  Element.getClientRects()  来获取其宽度和高度),而  offsetWidth  和  offsetHeight  描述的是边界框的尺寸(使用  Element.getBoundingClientRect  来获取其位置)。因此,使用  offsetLeft、offsetTop、offsetWidthoffsetHeight  来对应 left、top、width 和 height 的一个盒子将不会是文本容器 span 的盒子边界。

5、HTMLElement.offsetParent 是一个只读属性,返回一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的  table,td,th,body元素。当元素的  style.display  设置为 "none" 时,offsetParent  返回  null

# scrollTo

在 ${duration}时间内,滚动条平滑滚动到 ${to}指定位置。

var getScrollTop = require("./getScrollTop");
var setScrollTop = require("./setScrollTop");

// 使用该API来逐帧滚动,达到平滑的效果
// 做了兼容性处理,如果浏览器不支持则退化为宏任务定时器触发
var requestAnimFrame = (function () {
  return (
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function (callback) {
      window.setTimeout(callback, 1000 / 60);
    }
  );
})();
/**
 *
 * @description  在 ${duration}时间内,滚动条平滑滚动到 ${to}指定位置
 * @param {Number} to
 * @param {Number} duration
 */
function scrollTo(to, duration) {
  // 如果时间小于0,则直接滚动到指定位置
  // 亦是递归终止条件
  if (duration < 0) {
    setScrollTop(to);
    return;
  }

  // 计算指定位置和当前位置的差值,可能为负数
  var diff = to - getScrollTop();

  // 如果指定位置就是当前位置,则返回
  if (diff === 0) return;

  // 指定滚动的步长
  var step = (diff / duration) * 10;
  requestAnimFrame(function () {
    // 如果步长绝对值大于差值绝对值,那么则直接滚动差值的距离,并返回
    // 这里使用绝对值是因为差值可能为负数
    if (Math.abs(step) > Math.abs(diff)) {
      setScrollTop(getScrollTop() + diff);
      return;
    }

    // 根据步长逐帧滚动
    setScrollTop(getScrollTop() + step);

    // 判断临界值条件,达到以下条件则意味着已经滚动到指定位置
    // 如果超过指定位置也就是getScrollTop()大于或者小于to时,会在下一帧走到上面return的条件
    if (
      (diff > 0 && getScrollTop() >= to) ||
      (diff < 0 && getScrollTop() <= to)
    ) {
      return;
    }

    // 递归调用,时间减去16ms是因为一帧为1000 / 60 等于 16.67ms
    scrollTo(to, duration - 16);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# windowResize

H5 软键盘缩回、弹起回调。

/**
 *
 * @description H5软键盘缩回、弹起回调
 * 当软件键盘弹起会改变当前 window.innerHeight,监听这个值变化
 * @param {Function} downCb 当软键盘弹起后,缩回的回调
 * @param {Function} upCb 当软键盘弹起的回调
 */

function windowResize(downCb, upCb) {
  var clientHeight = window.innerHeight;

  // 兼容处理参数,防止传入非函数
  downCb = typeof downCb === "function" ? downCb : function () {};
  upCb = typeof upCb === "function" ? upCb : function () {};

  // 监听resize事件
  window.addEventListener("resize", () => {
    var height = window.innerHeight;

    // resize事件触发后,如果两次innerHeight相等,则意味着软键盘将要收缩
    if (height === clientHeight) {
      downCb();
    }

    // innerHeight变小意味着软键盘将要弹起
    if (height < clientHeight) {
      upCb();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# throttle

函数节流。

/**
 * @description  函数节流。
 * 适用于限制`resize`和`scroll`等函数的调用频率
 *
 * @param  {Number}    delay          0 或者更大的毫秒数。 对于事件回调,大约100或250毫秒(或更高)的延迟是最有用的。
 * @param  {Boolean}   noTrailing     可选,默认为false。
 *                                    如果noTrailing为true,当节流函数被调用,每过`delay`毫秒`callback`也将执行一次。
 *                                    如果noTrailing为false或者未传入,`callback`将在最后一次调用节流函数后再执行一次.
 *                                    (延迟`delay`毫秒之后,节流函数没有被调用,内部计数器会复位)
 * @param  {Function}  callback       延迟毫秒后执行的函数。`this`上下文和所有参数都是按原样传递的,
 *                                    执行去节流功能时,调用`callback`。
 * @param  {Boolean}   debounceMode   如果`debounceMode`为true,`clear`在`delay`ms后执行。
 *                                    如果debounceMode是false,`callback`在`delay` ms之后执行。
 *
 * @return {Function}  新的节流函数
 */
module.exports = function throttle(delay, noTrailing, callback, debounceMode) {
  // After wrapper has stopped being called, this timeout ensures that
  // `callback` is executed at the proper times in `throttle` and `end`
  // debounce modes.
  var timeoutID;

  // Keep track of the last time `callback` was executed.
  var lastExec = 0;

  // `noTrailing` defaults to falsy.
  if (typeof noTrailing !== "boolean") {
    debounceMode = callback;
    callback = noTrailing;
    noTrailing = undefined;
  }

  // The `wrapper` function encapsulates all of the throttling / debouncing
  // functionality and when executed will limit the rate at which `callback`
  // is executed.
  function wrapper() {
    var self = this;
    var elapsed = Number(new Date()) - lastExec;
    var args = arguments;

    // Execute `callback` and update the `lastExec` timestamp.
    function exec() {
      lastExec = Number(new Date());
      callback.apply(self, args);
    }

    // If `debounceMode` is true (at begin) this is used to clear the flag
    // to allow future `callback` executions.
    function clear() {
      timeoutID = undefined;
    }

    if (debounceMode && !timeoutID) {
      // Since `wrapper` is being called for the first time and
      // `debounceMode` is true (at begin), execute `callback`.
      exec();
    }

    // Clear any existing timeout.
    if (timeoutID) {
      clearTimeout(timeoutID);
    }

    if (debounceMode === undefined && elapsed > delay) {
      // In throttle mode, if `delay` time has been exceeded, execute
      // `callback`.
      exec();
    } else if (noTrailing !== true) {
      // In trailing throttle mode, since `delay` time has not been
      // exceeded, schedule `callback` to execute `delay` ms after most
      // recent execution.
      //
      // If `debounceMode` is true (at begin), schedule `clear` to execute
      // after `delay` ms.
      //
      // If `debounceMode` is false (at end), schedule `callback` to
      // execute after `delay` ms.
      timeoutID = setTimeout(
        debounceMode ? clear : exec,
        debounceMode === undefined ? delay - elapsed : delay
      );
    }
  }

  // Return the wrapper function.
  return wrapper;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

# debounce

函数防抖。

var throttle = require("./throttle");

/**
 * @description 函数防抖 
 * 与throttle不同的是,debounce保证一个函数在多少毫秒内不再被触发,只会执行一次,
 * 要么在第一次调用return的防抖函数时执行,要么在延迟指定毫秒后调用。
 * @example 适用场景:如在线编辑的自动存储防抖。
 * @param  {Number}   delay         0或者更大的毫秒数。 对于事件回调,大约100或250毫秒(或更高)的延迟是最有用的。
 * @param  {Boolean}  atBegin       可选,默认为false。
 *                                  如果`atBegin`为false或未传入,回调函数则在第一次调用return的防抖函数后延迟指定毫秒调用。
                                    如果`atBegin`为true,回调函数则在第一次调用return的防抖函数时直接执行
 * @param  {Function} callback      延迟毫秒后执行的函数。`this`上下文和所有参数都是按原样传递的,
 *                                  执行去抖动功能时,,调用`callback`。
 *
 * @return {Function} 新的防抖函数。
 */
function debounce(delay, atBegin, callback) {
  return callback === undefined
    ? throttle(delay, atBegin, false)
    : throttle(delay, callback, atBegin !== false);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# getKeyName

根据keycode获得键名。

var keyCodeMap = {
  8: "Backspace",
  9: "Tab",
  13: "Enter",
  16: "Shift",
  17: "Ctrl",
  18: "Alt",
  19: "Pause",
  20: "Caps Lock",
  27: "Escape",
  32: "Space",
  33: "Page Up",
  34: "Page Down",
  35: "End",
  36: "Home",
  37: "Left",
  38: "Up",
  39: "Right",
  40: "Down",
  42: "Print Screen",
  45: "Insert",
  46: "Delete",

  48: "0",
  49: "1",
  50: "2",
  51: "3",
  52: "4",
  53: "5",
  54: "6",
  55: "7",
  56: "8",
  57: "9",

  65: "A",
  66: "B",
  67: "C",
  68: "D",
  69: "E",
  70: "F",
  71: "G",
  72: "H",
  73: "I",
  74: "J",
  75: "K",
  76: "L",
  77: "M",
  78: "N",
  79: "O",
  80: "P",
  81: "Q",
  82: "R",
  83: "S",
  84: "T",
  85: "U",
  86: "V",
  87: "W",
  88: "X",
  89: "Y",
  90: "Z",

  91: "Windows",
  93: "Right Click",

  96: "Numpad 0",
  97: "Numpad 1",
  98: "Numpad 2",
  99: "Numpad 3",
  100: "Numpad 4",
  101: "Numpad 5",
  102: "Numpad 6",
  103: "Numpad 7",
  104: "Numpad 8",
  105: "Numpad 9",
  106: "Numpad *",
  107: "Numpad +",
  109: "Numpad -",
  110: "Numpad .",
  111: "Numpad /",

  112: "F1",
  113: "F2",
  114: "F3",
  115: "F4",
  116: "F5",
  117: "F6",
  118: "F7",
  119: "F8",
  120: "F9",
  121: "F10",
  122: "F11",
  123: "F12",

  144: "Num Lock",
  145: "Scroll Lock",
  182: "My Computer",
  183: "My Calculator",
  186: ";",
  187: "=",
  188: ",",
  189: "-",
  190: ".",
  191: "/",
  192: "`",
  219: "[",
  220: "\\",
  221: "]",
  222: "'",
};
/**
 * @description 根据keycode获得键名
 * @param  {Number} keycode
 * @return {String}
 */
function getKeyName(keycode) {
  // 如果keycode存在于维护的散列表里,则返回对应的值
  if (keyCodeMap[keycode]) {
    return keyCodeMap[keycode];

    // 否则打印错误,并返回空字符串
  } else {
    console.log("Unknow Key(Key Code:" + keycode + ")");
    return "";
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

# deepClone

深拷贝。

/**
 * @description 深拷贝,支持常见类型
 * @param {Any} values
 * @return {Any}
 */
function deepClone(values) {
  var copy;

  // 处理null、undefined以及原始类型的情况,直接返回
  if (null == values || "object" != typeof values) return values;

  // 处理Date,先获取时间,再设置时间并返回
  if (values instanceof Date) {
    copy = new Date();
    copy.setTime(values.getTime());
    return copy;
  }

  // 处理数组,循环每一项,同时递归调用深拷贝函数并赋值
  if (values instanceof Array) {
    copy = [];
    for (var i = 0, len = values.length; i < len; i++) {
      copy[i] = deepClone(values[i]);
    }
    return copy;
  }

  // 处理对象,循环每一项,如果是自有属性就递归调用深拷贝函数并赋值
  if (values instanceof Object) {
    copy = {};
    for (var attr in values) {
      if (values.hasOwnProperty(attr)) copy[attr] = deepClone(values[attr]);
    }
    return copy;
  }

  // 异常处理
  throw new Error("Unable to copy values! Its type isn't supported.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# isEmptyObject

判断obj是否为空。

/**
 *
 * @description   判断`obj`是否为空
 * @param  {Object} obj
 * @return {Boolean}
 */
function isEmptyObject(obj) {
  // 如果值为falsy、类型不为`object`、是数组则返回false
  if (!obj || typeof obj !== "object" || Array.isArray(obj)) return false;

  // 如果对象没有自身可枚举的键,则返回true,否则返回false
  return !Object.keys(obj).length;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

补充知识:

  1. Object.keys()  方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
  2. Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。

# randomColor

随机生成颜色。

/**
 *
 * @description 随机生成颜色
 * @return {String}
 */
function randomColor() {
  return (
    "#" + ("00000" + ((Math.random() * 0x1000000) << 0).toString(16)).slice(-6)
  );
}
1
2
3
4
5
6
7
8
9
10

补充知识:

  1. 根据运算符优先级,乘(*)的优先级大于按位左移(<<),由于生成的随机颜色是 16 进制的 6 位数,因此将 16 进制的 7 位数与伪随机相乘,生成的 16 进制刚好是 6 位。

  2. 生成随机数后进行按位左移 0 位,目的是进行取整。左移操作符  (<<)  将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零。通过位运算进行取整的方式有如下几种:

    方式 1:与 0 进行或运算 x|0

    方式 2:与-1 进行与运算x&-1

    方式 3:与 0 异或 x^0

    方式 4:双非运算 ~~x

    方式 5:与同一个数两次异或 x^a^a

    方式 6:左移 0 位 x<<0

    方式 7:有符号右移 0 位 x>>0

    方式 8:对正数无符号右移 0 位 x>>>0

    方式 9:无符号右移 0 位再与 0 进行或运算 x>>>0|0

    方式 10:无符号右移 0 位再与 0 进行异或运算 x>>>0^0

  3. Math.random()  函数返回一个浮点数,  伪随机数在范围从0到小于1([0,1))。如果碰巧随机出 0,那么需要补齐 6 位。所以需要使用’00000’添加前缀。这里还设计到隐式转换,最终转换为了字符串。

  4. Number.prototype.toString()方法返回指定  Number  对象的字符串表示形式。其中接收一个参数,参数表示指定要用于数字到字符串的转换的基数(从 2 到 36)。如果未指定参数,则默认值为 10。由于使用伪随机与0x1000000 相乘并取整返回的是 10 进制,因此需要通过toString(16) 转换为 16 进制。

  5. String.prototype.slice()方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。该方法接收两个参数。

    1. beginIndex。 从该索引(以 0 为基数)处开始提取原字符串中的字符。如果值为负数,会被当做  strLength + beginIndex  看待。其中strLength 为字符串长度。
    2. endIndex。 该参数可选。在该索引(以 0 为基数)处结束提取字符串。如果省略该参数,slice()  会一直提取到字符串末尾。如果该参数为负数,则被看作是 strLength + endIndex
  6. 回到上述源码,整体来看最终结果会有两种:

    1. Math.random() 为 0 时,('00000' + (Math.random() * 0x1000000 << 0).toString(16)) 返回 '000000' 。通过slice(-6) ,按照截取规则,相当于slice(6 + (-6)),也就是slice(0),那么最终就是截取整个字符串。
    2. Math.random() 不为 0 时,('00000' + (Math.random() * 0x1000000 << 0).toString(16)) 返回 '00000bd4f6c' (随机) 。按照截取规则,相当于slice(11 + (-6)) ,也就是slice(5) ,最终截取就是从第 5 个索引开始截取,直到最后( 'bd4f6c')。
    3. 最后一步拼接#,并返回。

# randomNum

生成指定范围[min, max]的随机数。

/**
 *
 * @description 生成指定范围[min, max]的随机数
 * @param  {Number} min
 * @param  {Number} max
 * @return {Number}
 */
function randomNum(min, max) {
  // 向上取整最小范围
  min = Math.ceil(min);

  // 向下取整最大范围
  max = Math.floor(max);

  // 由于伪随机数无法取1,因此需要再加一进行随机,方可随机到max
  // 向下取整最大值,并与最小值相加就是最后的随机值
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# isColor

判断是否为 16 进制颜色。

/**
 *
 * @description 判断是否为16进制颜色,rgb 或 rgba
 * @param  {String}  str
 * @return {Boolean}
 */
function isColor(str) {
  // 匹配形如 #fff、#fffffff、rgb(255,255,255)、rgba(255,255,255,1.0)
  return /^(#([0-9a-fA-F]{3}){1,2}|[rR][gG][Bb](\((\s*(2[0-4]\d|25[0-5]|[01]?\d{1,2} "0-9a-fA-F]{3}){1,2}|[rR][gG][Bb")\s*,){2}\s*(2[0-4]\d|25[0-5]|[01]?\d{1,2})\s*\)|[Aa]\((\s*(2[0-4]\d|25[0-5]|[01]?\d{1,2})\s*,){3}\s*([01]|0\.\d+)\s*\)))$/.test(
    str
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

通过上图可以精确的看到正则匹配的流程。不过看上去组 5 和组 6 是重复的,不知道是刻意为之还是?

# isEmail

判断是否为邮箱地址。

/**
 *
 * @description   判断是否为邮箱地址
 * @param  {String}  str
 * @return {Boolean}
 */
function isEmail(str) {
  // 判断是否为email最核心的是@和.
  return /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(str);
}
1
2
3
4
5
6
7
8
9
10

从上图也可以看出,@.都是必需的。

# isIdCard

判断是否为身份证号。

/**
 *
 * @description  判断是否为身份证号
 * @param  {String|Number} str
 * @return {Boolean}
 */
function isIdCard(str) {
  return /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/.test(
    str
  );
}
1
2
3
4
5
6
7
8
9
10
11

该方法没有很好的判断年份以及省市区编号。如果想要更加精确,需要根据身份证的组成单独进行判断,比如前 6 位是省市区的编号等等。

# isPhoneNum

判断是否为手机号。

/**
 *
 * @description   判断是否为手机号
 * @param  {String|Number} str
 * @return {Boolean}
 */
function isPhoneNum(str) {
  return /^(\+?0?86\-?)?1[3456789]\d{9}$/.test(str);
}
1
2
3
4
5
6
7
8
9

从上图可以看出,该正则考虑到了+86 国家编号的情况。然后手机号的第二位包括了[3456789]

# isUrl

判断是否为 URL 地址。

/**
 *
 * @description   判断是否为URL地址
 * @param  {String} str
 * @return {Boolean}
 */
function isUrl(str) {
  return /[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/i.test(
    str
  );
}
1
2
3
4
5
6
7
8
9
10
11

# digitUppercase

现金额转大写。

/**
 *
 * @description   现金额转大写
 * @param  {Number} n
 * @return {String}
 */
function digitUppercase(n) {
  var fraction = ["角", "分"];
  var digit = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"];
  var unit = [
    ["元", "万", "亿"],
    ["", "拾", "佰", "仟"],
  ];

  // n为负数则是欠
  var head = n < 0 ? "欠" : "";

  // 处理了n为负数的情况,此处直接取绝对值
  n = Math.abs(n);
  var s = "";

  // 处理小数点的情况
  for (var i = 0; i < fraction.length; i++) {
    s += (
      digit[Math.floor(n * 10 * Math.pow(10, i)) % 10] + fraction[i]
    ).replace(/零./, "");
  }

  // 如果处理完小数点依旧为空,则意味着是整数
  s = s || "整";

  // 对值进行向下取整
  n = Math.floor(n);

  // 从低位到高位进行拼接,并且去除多余的零
  for (var i = 0; i < unit[0].length && n > 0; i++) {
    var p = "";
    for (var j = 0; j < unit[1].length && n > 0; j++) {
      p = digit[n % 10] + unit[1][j] + p;
      n = Math.floor(n / 10);
    }
    s = p.replace(/(零.)*零 $/, "").replace(/^$/, "零") + unit[0][i] + s;
  }

  // 进行整体的拼接,并调整描述
  return (
    head +
    s
      .replace(/(零.)*零元/, "元")
      .replace(/(零.)+/g, "零")
      .replace(/^整 $/, "零元整")
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# isSupportWebP

判断浏览器是否支持 webP 格式图片。

/**
 *
 * @description 判断浏览器是否支持webP格式图片
 * @return {Boolean}
 */
function isSupportWebP() {
  // array.map方法是ES5支持的方法,如果浏览器不支持该方法,则不可能支持webP
  // 创建canvas并设置包含图片展示的data url,如果支持webP,则返回的字符串开头便是data:image/webp
  return (
    !![].map &&
    document
      .createElement("canvas")
      .toDataURL("image/webp")
      .indexOf("data:image/webp") == 0
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

补充知识:

  1. HTMLCanvasElement.toDataURL()  方法返回一个包含图片展示的  data URI 。可以使用type参数(可选)指定类型,默认为image/png。第二个可选参数是图片质量,取值范围是0-1,不传或者超出取值范围,则取默认值  0.92。图片的分辨率为96dpi。返回值是包含  data URIDOMString
  document.createElement('canvas').toDataURL('image/webp')

  //以下为输出结果
  data:image/webp;base64,UklGRtgCAABXRUJQVlA4WAoAAAAwAAAAKwEAlQAASUNDUBgCAAAAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANkFMUEgSAAAAAQcQEREQkCT+/x9F9D/tf0MAVlA4IIAAAABwDQCdASosAZYAPm02mUmkIyKhICgAgA2JaW7hdrEbQAnsA99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfasAAD+/9YAAAAAAAAAAA==
1
2
3
4
  1. 如果浏览器支持webP ,那么data URI 则会以data:image/webp开头。通过字符串的原型方法indexOf('data:image/webp') == 0 就可以判断是否支持。
  2. Data URLs 即前缀为  data:  协议的 URL,其允许内容创建者向文档中嵌入小文件Data URLs 由四个部分组成:前缀(data:)、指示数据类型的MIME类型、如果非文本则为可选的base64标记、数据本身。如果MIME类型被省略,则默认值为  text/plain;charset=US-ASCII 。注意四个部分之间的分隔符,分别是:;,

# formatPassTime

格式化指定时间距现在的已过时间。

/**
 * @description   格式化 ${startTime}距现在的已过时间
 * @param  {Date} startTime
 * @return {String}
 */
function formatPassTime(startTime) {
  // 获取当前时间,返回的是毫秒数
  var currentTime = Date.parse(new Date()),
    // 计算时间间隔
    time = currentTime - startTime,
    // 分别转换年、月、日、小时、分钟
    day = parseInt(time / (1000 * 60 * 60 * 24)),
    hour = parseInt(time / (1000 * 60 * 60)),
    min = parseInt(time / (1000 * 60)),
    month = parseInt(day / 30),
    year = parseInt(month / 12);

  // 按顺序判断年、月、日、小时、分钟,如果存在则拼接相应描述并返回
  if (year) return year + "年前";
  if (month) return month + "个月前";
  if (day) return day + "天前";
  if (hour) return hour + "小时前";
  if (min) return min + "分钟前";
  // 如果时间间隔小于分钟级别,则直接返回刚刚
  else return "刚刚";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# formatRemainTime

格式化现在距离指定时间的剩余时间。

/**
 *
 * @description   格式化现在距 ${endTime}的剩余时间
 * @param  {Date} endTime
 * @return {String}
 */
function formatRemainTime(endTime) {
  var startDate = new Date(); //开始时间
  var endDate = new Date(endTime); //结束时间
  var t = endDate.getTime() - startDate.getTime(); //时间差
  var d = 0,
    h = 0,
    m = 0,
    s = 0;

  // 如果时间间隔大于等于0,则进行转换,否则直接返回都是0的数据
  if (t >= 0) {
    // 计算天数并向下取整
    d = Math.floor(t / 1000 / 3600 / 24);

    // 小时、分钟、秒分别通过取余和向下取整来拿到数据
    h = Math.floor((t / 1000 / 60 / 60) % 24);
    m = Math.floor((t / 1000 / 60) % 60);
    s = Math.floor((t / 1000) % 60);
  }

  // 返回拼接的剩余时间
  return d + "天 " + h + "小时 " + m + "分钟 " + s + "秒";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# isLeapYear

判断是否为闰年。

/**
 *
 * @description 是否为闰年
 * @param {Number} year
 * @returns {Boolean}
 */

function isLeapYear(year) {
  // 闰年的判断依据是年份为4的倍数,同时年份不是100的倍数或者是400的倍数
  if (0 === year % 4 && (year % 100 !== 0 || year % 400 === 0)) {
    return true;
  }
  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# isSameDay

判断是否为同一天。

/**
 * @description   判断是否为同一天
 * @param  {Date} date1
 * @param  {Date} date2 可选/默认值:当天
 * @return {Boolean}
 */
function isSameDay(date1, date2) {
  if (!date2) {
    date2 = new Date();
  }

  // 分别获取两个参数的年月日
  var date1_year = date1.getFullYear(),
    date1_month = date1.getMonth() + 1,
    date1_date = date1.getDate();
  var date2_year = date2.getFullYear(),
    date2_month = date2.getMonth() + 1,
    date2_date = date2.getDate();

  // 比较日月年是否相等,如果都相等,则为同一天
  // 从后往前比较可以提前退出,防止无效判断
  return (
    date1_date === date2_date &&
    date1_month === date2_month &&
    date1_year === date2_year
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# timeLeft

计算开始时间到结束时间的剩余时间。

/**
 * @description ${startTime - endTime}的剩余时间,startTime大于endTime时,均返回0
 * @param { Date | String } startTime
 * @param { Date | String } endTime
 * @returns { Object } { d, h, m, s } 天 时 分 秒
 */
function timeLeft(startTime, endTime) {
  // 参数缺失则直接返回
  if (!startTime || !endTime) {
    return;
  }

  // 如果参数是Date实例则直接使用,否则转换为Date实例
  // 将时间格式的-转换为/,是为了适配ios
  var startDate, endDate;
  if (startTime instanceof Date) {
    startDate = startTime;
  } else {
    startDate = new Date(startTime.replace(/-/g, "/")); //开始时间
  }
  if (endTime instanceof Date) {
    endDate = endTime;
  } else {
    endDate = new Date(endTime.replace(/-/g, "/")); //结束时间
  }

  // 计算得到天、时、分、秒
  var t = endDate.getTime() - startDate.getTime();
  var d = 0,
    h = 0,
    m = 0,
    s = 0;
  if (t >= 0) {
    d = Math.floor(t / 1000 / 3600 / 24);
    h = Math.floor((t / 1000 / 60 / 60) % 24);
    m = Math.floor((t / 1000 / 60) % 60);
    s = Math.floor((t / 1000) % 60);
  }
  return { d, h, m, s };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# monthDays

获取指定日期月份的总天数。

/**
 * @description 获取指定日期月份的总天数
 * @param {Date} time
 * @return {Number}
 */
function monthDays(time) {
  time = new Date(time);

  // 获取指定日期的年份,返回4位数
  var year = time.getFullYear();

  // 获取指定日期的月份,范围为0-11,因此需要+1
  var month = time.getMonth() + 1;

  // 通过形如new Date(year, monthIndex, day)来创建Date实例
  // 然后获取指定年月的总天数,返回1-31
  return new Date(year, month, 0).getDate();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# parseQueryString

**url参数转对象。**首先需要明确的是,url参数是?后面的形如key1=value1&key2=value2的值。

/**
 *
 * @description   url参数转对象
 * @param  {String} url  default: window.location.href
 * @return {Object}
 */
function parseQueryString(url) {
  // 如果url为falsy,则使用window.location.href
  url = !url ? window.location.href : url;

  // 如果不存在?,意味着没有查询参数,直接返回空对象
  if (url.indexOf("?") === -1) {
    return {};
  }

  // 如果url以?开头,则截取第一个字符串到最后;
  // 否则找到最后的?,并截取?下一个字符串到最后
  var search =
    url[0] === "?" ? url.substr(1) : url.substring(url.lastIndexOf("?") + 1);

  // 如果?后面为空,也意味着没有查询参数,返回空对象
  if (search === "") {
    return {};
  }

  // 分割为key=value组成的数组
  search = search.split("&");
  var query = {};
  for (var i = 0; i < search.length; i++) {
    // 分割为[key, value]
    var pair = search[i].split("=");

    // 解码相应的值,并赋值给query对象
    // value为falsy,则默认为空字符串
    query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
  }

  // 最后返回query对象
  return query;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

补充知识:

  1. substr()  方法返回一个字符串中从指定位置开始到指定字符数的字符。该参数接收两个参数。
    1. start参数。开始提取字符的位置。如果为负值,则被看作  strLength + start,其中 strLength  为字符串的长度。
    2. length 参数(可选)。提取的字符数。
    3. 避免使用该方法,使用substring()代替。上述示例完全可以进行代替。
  2. **substring()**方法返回一个字符串在开始索引到结束索引之间的一个子集, 或从开始索引直到字符串的末尾的一个子集。该参数接收两个参数。
    1. indexStart 。需要截取的第一个字符的索引,该索引位置的字符作为返回的字符串的首字母。
    2. indexEnd 。可选。一个 0 到字符串长度之间的整数,以该数字为索引的字符不包含在截取的字符串内。

# stringfyQueryString

对象序列化。

/**
 *
 * @description   对象序列化
 * @param  {Object} obj
 * @return {String}
 */
function stringfyQueryString(obj) {
  // 特殊值处理
  if (!obj) return "";
  var pairs = [];

  for (var key in obj) {
    var value = obj[key];

    // 如果对象的值为数组,则处理成形如key[i]=value
    // 同时进行编码处理
    if (value instanceof Array) {
      for (var i = 0; i < value.length; ++i) {
        pairs.push(
          encodeURIComponent(key + "[" + i + "]") +
            "=" +
            encodeURIComponent(value[i])
        );
      }
      continue;
    }

    // 对象的值不是数组,则处理成key=value
    // 同时进行编码处理
    pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]));
  }

  // 使用&拼接并返回
  return pairs.join("&");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 总结

通过分析工具库,可以方便我们创建出自己的工具库,提高开发效率。以上就是所有工具库方法的解读,如有错误之处,请大家多多指教。