在日常开发中,我们会经常遇到转换固定格式或者生成指定格式的需求,比如生成随机颜色,判断是否为邮箱、身份证等等。那么今天就来分析一下github
的开源库outils[1]。
min
目录下的outils.min.js[2]使用,支持UMD通用模块规范。 <script src="outils.min.js"></script>
<script>
var OS = outils.getOS()
</script>
npm install --save-dev outils
推荐按需引入。
// 只引入部分方法('outils/<方法名>')
const getOS = require('outils/getOS')
const OS = getOS()
接下来就逐一进行API的解读与源码分析。
「判断两个数组是否相等。」
/**
* @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;
}
「判断元素是否有某个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);
}
「为元素添加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;
}
}
「为元素移除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, ' ');
}
}
下面三个方法涉及到操作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 '';
}
「tips:」
encodeURI()
不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而encodeURIComponent()
则会对它发现的「任何非标准字符」进行编码(替换所有非字母数字字符)。因此使用后者可以编码更多的字符,防止XSS攻击。同样的,解码就需要使用对应的方法decodeURIComponent()
。
「设置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、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
操作符)会返回一个代表当前日期和时间的「字符串」。
「根据name
删除cookie
。」
/**
*
* @description 根据name删除cookie
* @param {String} name
*/
var setCookie = require('./setCookie');
function removeCookie(name) {
// 设置已过期,系统会立刻删除cookie
setCookie(name, '1', -1);
}
「获取浏览器的类型和版本。」
/**
*
* @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'
}
补充知识:
String.prototype.match()
方法检索返回一个字符串匹配正则表达式的结果。其中,参数为「正则表达式对象」。
如果传入一个非正则表达式对象,则会隐式地使用 new RegExp(obj)
将其转换为一个 RegExp
。如果你没有给出任何参数并直接使用match()
方法 ,你将会得到一个包含空字符串的数组:[""]
。
返回值分为两种情况:
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"]'
「获取操作系统类型。」
/**
*
* @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'
}
「获取滚动条距顶部的距离。」
/**
*
* @description 获取滚动条距顶部的距离
*/
function getScrollTop() {
// 优先获取document.documentElement下的属性
return (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
}
补充知识:
「Document.documentElement
」 是一个会返回文档对象(document
)的「根元素」的「只读」属性(如HTML
文档的<html>
元素)。
对于任何非空 HTML
文档,调用 document.documentElement
总是会返回一个<html>
元素,且它一定是该文档的根元素。借助这个只读属性,能方便地获取到任意文档的根元素
。
「设置滚动条距顶部的距离。」
/**
*
* @description 设置滚动条距顶部的距离
* @param {Number} value
*/
function setScrollTop(value) {
// 使用window.scrollTo滚动到距顶部x轴为0,y轴为value的距离
window.scrollTo(0, value);
return value;
}
补充知识:
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"
});
「获取元素距离文档(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、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
。
「在
{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);
});
}
「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();
}
});
}
「函数节流。」
/**
* @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;
};
「函数防抖。」
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);
};
「根据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 '';
}
};
「深拷贝。」
/**
* @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.");
}
「判断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
}
补充知识:
Object.keys()
」 方法会返回一个由一个给定对象的「自身可枚举」属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。Object.getOwnPropertyNames()
「方法返回一个由指定对象的」所有自身属性」的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。**随机生成颜色。**此处需要详细说明,继续往下看。
/**
*
* @description 随机生成颜色
* @return {String}
*/
function randomColor() {
return '#' + ('00000' + (Math.random() * 0x1000000 << 0).toString(16)).slice(-6);
}
补充知识:
*
)的优先级大于按位左移(<<
),由于生成的随机颜色是16进制的6位数,因此将16进制的7位数与伪随机相乘,生成的16进制刚好是6位。<<
)」 将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零。通过位运算进行取整的方式有如下几种:
方式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()
方法提取某个字符串的一部分,并「返回一个新的字符串」,且「不会改动原字符串」。该方法接收两个参数。strLength + beginIndex
看待。其中strLength
为字符串长度。slice()
会一直提取到字符串末尾。如果该参数为负数,则被看作是 strLength + endIndex
。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'
)。#
,并返回。「生成指定范围[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;
}
「判断是否为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);
}
通过上图可以精确的看到正则匹配的流程。不过看上去组5和组6是重复的,不知道是刻意为之还是?
「判断是否为邮箱地址。」
/**
*
* @description 判断是否为邮箱地址
* @param {String} str
* @return {Boolean}
*/
function isEmail(str) {
// 判断是否为email最核心的是@和.
return /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(str);
}
从上图也可以看出,@
和.
都是必需的。
「判断是否为身份证号。」
/**
*
* @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)
}
该方法没有很好的判断年份以及省市区编号。如果想要更加精确,需要根据身份证的组成单独进行判断,比如前6位是省市区的编号等等。
「判断是否为手机号。」
/**
*
* @description 判断是否为手机号
* @param {String|Number} str
* @return {Boolean}
*/
function isPhoneNum(str) {
return /^(\+?0?86\-?)?1[3456789]\d{9}$/.test(str)
}
从上图可以看出,该正则考虑到了+86
国家编号的情况。然后手机号的第二位包括了[3456789]
。
「判断是否为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);
}
「现金额转大写。」
/**
*
* @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(/^整 $/, '零元整');
};
「判断浏览器是否支持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;
}
补充知识:
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==
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
。注意四个部分之间的分隔符,分别是:
、;
、,
。「格式化指定时间距现在的已过时间。」
/**
* @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 '刚刚'
}
「格式化现在距离指定时间的剩余时间。」
/**
*
* @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 + "秒";
}
「判断是否为闰年。」
/**
*
* @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;
}
「判断是否为同一天。」
/**
* @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;
}
「计算开始时间到结束时间的剩余时间。」
/**
* @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 }
}
「获取指定日期月份的总天数。」
/**
* @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();
}
**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;
}
补充知识:
substr()
」 方法返回一个字符串中从指定位置开始到指定字符数的字符。该参数接收两个参数。start
参数。开始提取字符的位置。如果为负值,则被看作 strLength + start,其中
strLength
为字符串的长度。length
参数(可选)。提取的字符数。substring()
代替。上述示例完全可以进行代替。substring()
**方法返回一个字符串在开始索引到结束索引之间的一个子集, 或从开始索引直到字符串的末尾的一个子集。该参数接收两个参数。indexStart
。需要截取的第一个字符的索引,该索引位置的字符作为返回的字符串的首字母。indexEnd
。可选。一个 0 到字符串长度之间的整数,以该数字为索引的字符「不包含」在截取的字符串内。「对象序列化。」
/**
*
* @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]
outils: https://github.com/proYang/outils
[2]
outils.min.js: https://github.com/proYang/outils/blob/master/min/outils.min.js