# 工具库源码解析
在日常开发中,我们会经常遇到转换固定格式或者生成指定格式的需求,比如生成随机颜色,判断是否为邮箱、身份证等等。那么今天就来分析一下github
的开源库outils (opens new window)。
# 安装
- 直接下载
min
目录下的outils.min.js (opens new window)使用,支持 UMD 通用模块规范。
<script src="outils.min.js"></script>
<script>
var OS = outils.getOS();
</script>
2
3
4
- 使用 npm 安装
npm install --save-dev outils
# 使用
推荐按需引入。
// 只引入部分方法('outils/<方法名>')
const getOS = require("outils/getOS");
const OS = getOS();
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;
}
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);
}
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;
}
}
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, " ");
}
}
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 "";
}
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;
}
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);
}
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";
}
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]
返回结果是一个数组。其中有三个附加属性:
groups
: 一个捕获组数组 或undefined
(如果没有定义命名捕获组)。index
: 匹配的结果的开始位置。input
: 搜索的字符串。
我们想要得到的浏览器版本就存在于返回结果的第二项。这也是为什么源码里面赋值使用的s[1]
。
还有一个有意思的点,当尝试将返回结果进行字符串化,发现结果会省略附加属性。只保留了索引是数字的值。
JSON.stringify(s); // '["chrome/96.0.4664.110","96.0.4664.110"]'
# 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";
}
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
);
}
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;
}
2
3
4
5
6
7
8
9
10
补充知识:
1、Window.scrollTo()
表示滚动到文档中的某个坐标。
2、接受两种类型的参数:
window.scrollTo(x-coord,y-coord)
。其中,第一个参数表示横轴坐标,第二个参数表示纵轴坐标。window.scrollTo(options)
。其中,options
是一个包含三个属性的对象。top
等同于y-coord
。left
等同于x-coord
。behavior
**类型String
,表示滚动行为,支持参数smooth
(平滑滚动),instant
(瞬间滚动),默认值auto
(效果也是瞬间滚动)。
举个 🌰:
window.scrollTo(0, 1000);
// 设置滚动行为改为平滑的滚动
window.scrollTo({
top: 1000,
behavior: "smooth",
});
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;
}
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、对块级元素来说,offsetTop
、offsetLeft
、offsetWidth
及 offsetHeight
描述了元素相对于 offsetParent
的边界框。
4、然而,对于可被截断到下一行的行内元素(如 span),offsetTop
和 offsetLeft
描述的是第一个边界框的位置(使用 Element.getClientRects()
来获取其宽度和高度),而 offsetWidth
和 offsetHeight
描述的是边界框的尺寸(使用 Element.getBoundingClientRect
来获取其位置)。因此,使用 offsetLeft、offsetTop、offsetWidth
、offsetHeight
来对应 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);
});
}
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();
}
});
}
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;
};
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);
}
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 "";
}
}
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.");
}
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
补充知识:
Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。Object.getOwnPropertyNames()
方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
# randomColor
随机生成颜色。
/**
*
* @description 随机生成颜色
* @return {String}
*/
function randomColor() {
return (
"#" + ("00000" + ((Math.random() * 0x1000000) << 0).toString(16)).slice(-6)
);
}
2
3
4
5
6
7
8
9
10
补充知识:
根据运算符优先级,乘(
*
)的优先级大于按位左移(<<
),由于生成的随机颜色是 16 进制的 6 位数,因此将 16 进制的 7 位数与伪随机相乘,生成的 16 进制刚好是 6 位。生成随机数后进行按位左移 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
Math.random()
函数返回一个浮点数, 伪随机数在范围从0到小于1([0,1))。如果碰巧随机出 0,那么需要补齐 6 位。所以需要使用’00000’
添加前缀。这里还设计到隐式转换,最终转换为了字符串。Number.prototype.toString()
方法返回指定Number
对象的字符串表示形式。其中接收一个参数,参数表示指定要用于数字到字符串的转换的基数(从 2 到 36)。如果未指定参数,则默认值为 10。由于使用伪随机与0x1000000
相乘并取整返回的是 10 进制,因此需要通过toString(16)
转换为 16 进制。String.prototype.slice()
方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。该方法接收两个参数。- beginIndex。 从该索引(以 0 为基数)处开始提取原字符串中的字符。如果值为负数,会被当做
strLength + beginIndex
看待。其中strLength
为字符串长度。 - endIndex。 该参数可选。在该索引(以 0 为基数)处结束提取字符串。如果省略该参数,
slice()
会一直提取到字符串末尾。如果该参数为负数,则被看作是strLength + endIndex
。
- beginIndex。 从该索引(以 0 为基数)处开始提取原字符串中的字符。如果值为负数,会被当做
回到上述源码,整体来看最终结果会有两种:
- 当
Math.random()
为 0 时,('00000' + (Math.random() * 0x1000000 << 0).toString(16))
返回'000000'
。通过slice(-6)
,按照截取规则,相当于slice(6 + (-6))
,也就是slice(0)
,那么最终就是截取整个字符串。 - 当
Math.random()
不为 0 时,('00000' + (Math.random() * 0x1000000 << 0).toString(16))
返回'00000bd4f6c'
(随机) 。按照截取规则,相当于slice(11 + (-6))
,也就是slice(5)
,最终截取就是从第 5 个索引开始截取,直到最后('bd4f6c'
)。 - 最后一步拼接
#
,并返回。
- 当
# 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;
}
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
);
}
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);
}
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
);
}
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);
}
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
);
}
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(/^整 $/, "零元整")
);
}
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
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
补充知识:
HTMLCanvasElement.toDataURL()
方法返回一个包含图片展示的data URI
。可以使用type
参数(可选)指定类型,默认为image/png
。第二个可选参数是图片质量,取值范围是0-1
,不传或者超出取值范围,则取默认值0.92
。图片的分辨率为96dpi
。返回值是包含data URI
的DOMString
。
document.createElement('canvas').toDataURL('image/webp')
//以下为输出结果
data:image/webp;base64,UklGRtgCAABXRUJQVlA4WAoAAAAwAAAAKwEAlQAASUNDUBgCAAAAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANkFMUEgSAAAAAQcQEREQkCT+/x9F9D/tf0MAVlA4IIAAAABwDQCdASosAZYAPm02mUmkIyKhICgAgA2JaW7hdrEbQAnsA99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfasAAD+/9YAAAAAAAAAAA==
2
3
4
- 如果浏览器支持
webP
,那么data URI
则会以data:image/webp
开头。通过字符串的原型方法indexOf('data:image/webp') == 0
就可以判断是否支持。 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 "刚刚";
}
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 + "秒";
}
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;
}
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
);
}
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 };
}
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();
}
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;
}
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
补充知识:
substr()
方法返回一个字符串中从指定位置开始到指定字符数的字符。该参数接收两个参数。start
参数。开始提取字符的位置。如果为负值,则被看作strLength + start,其中
strLength
为字符串的长度。length
参数(可选)。提取的字符数。- 避免使用该方法,使用
substring()
代替。上述示例完全可以进行代替。
- **
substring()
**方法返回一个字符串在开始索引到结束索引之间的一个子集, 或从开始索引直到字符串的末尾的一个子集。该参数接收两个参数。indexStart
。需要截取的第一个字符的索引,该索引位置的字符作为返回的字符串的首字母。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("&");
}
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
# 总结
通过分析工具库,可以方便我们创建出自己的工具库,提高开发效率。以上就是所有工具库方法的解读,如有错误之处,请大家多多指教。