前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >工具库源码解析

工具库源码解析

作者头像
chuckQu
发布2022-08-19 14:17:15
1.7K0
发布2022-08-19 14:17:15
举报
文章被收录于专栏:前端F2E

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

安装

  1. 直接下载min目录下的outils.min.js[2]使用,支持UMD通用模块规范。
代码语言:javascript
复制
  <script src="outils.min.js"></script>
  <script>
    var OS = outils.getOS()
  </script>
  1. 使用npm安装
代码语言:javascript
复制
  npm install --save-dev outils

使用

推荐按需引入。

代码语言:javascript
复制
// 只引入部分方法('outils/<方法名>')
const getOS = require('outils/getOS')
const OS = getOS()

API

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

arrayEqual

「判断两个数组是否相等。」

代码语言:javascript
复制
/**
 * @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;
}

hasClass

「判断元素是否有某个class。」

代码语言:javascript
复制
/**
 * 
 * @description 判断元素是否有某个class
 * @param {HTMLElement} ele 
 * @param {String} cls 
 * @return {Boolean}
 */
function hasClass(ele, cls) {

  // 通过正则判断元素的className属性中是否存在指定的类名
    return (new RegExp('(\\s|^)' + cls + '(\\s|$)')).test(ele.className);
}

addClass

「为元素添加class。」

代码语言:javascript
复制
/**
 * @description 为元素添加class
 * @param {HTMLElement} ele
 * @param {String} cls
 */

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

function addClass(ele, cls) {

 // 如果元素中不存在指定类名则添加
  if (!hasClass(ele, cls)) {

  // 添加方式是在元素的className属性后面追加指定类名
    // 不要忘记加上空格,因为类名以空格区分
    ele.className += " " + cls;
  }
}

removeClass

「为元素移除class。」

代码语言:javascript
复制
/**
 * 
 * @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, ' ');
    }
}

getCookie

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

「根据name读取Cookie。」

代码语言:javascript
复制
/**
 * 
 * @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()

setCookie

「设置cookie。」

代码语言:javascript
复制
/**
 * 
 * @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 操作符)会返回一个代表当前日期和时间的「字符串」

removeCookie

「根据name删除cookie。」

代码语言:javascript
复制
/**
 * 
 * @description 根据name删除cookie
 * @param  {String} name 
 */
var setCookie = require('./setCookie');

function removeCookie(name) {
    // 设置已过期,系统会立刻删除cookie
    setCookie(name, '1', -1);
}

getExplore

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

代码语言:javascript
复制
/**
 * 
 * @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()方法 ,你将会得到一个包含空字符串的数组:[""]

返回值分为两种情况:

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

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

代码语言:javascript
复制
['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]

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

代码语言:javascript
复制
JSON.stringify(s) // '["chrome/96.0.4664.110","96.0.4664.110"]'

getOS

「获取操作系统类型。」

代码语言:javascript
复制
/**
 * 
 * @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'
}

getScrollTop

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

代码语言:javascript
复制
/**
 * 
 * @description 获取滚动条距顶部的距离
 */
function getScrollTop() {

  // 优先获取document.documentElement下的属性
    return (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
}

补充知识:

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

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

setScrollTop

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

代码语言:javascript
复制
/**
 * 
 * @description 设置滚动条距顶部的距离
 * @param {Number} value
 */
function setScrollTop(value) {

  // 使用window.scrollTo滚动到距顶部x轴为0,y轴为value的距离
    window.scrollTo(0, value);
    return value;
}

补充知识:

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 (效果也是瞬间滚动)。

举个🌰:

代码语言:javascript
复制
window.scrollTo( 0, 1000 );

// 设置滚动行为改为平滑的滚动
window.scrollTo({
    top: 1000,
    behavior: "smooth"
});

offset

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

代码语言:javascript
复制
/**
 * 
 * @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、对块级元素来说,offsetTopoffsetLeftoffsetWidthoffsetHeight 描述了元素相对于 offsetParent 的边界框。

4、然而,对于可被截断到下一行的行内元素(如 「span」),offsetTopoffsetLeft 描述的是第一个边界框的位置(使用 Element.getClientRects() 来获取其宽度和高度),而 offsetWidthoffsetHeight 描述的是边界框的尺寸(使用 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}指定位置。」

代码语言:javascript
复制
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);
        });
}

windowResize

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

代码语言:javascript
复制
/**
 * 
 * @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();
  }
 });
}

throttle

「函数节流。」

代码语言:javascript
复制
/**
 * @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;

};

debounce

「函数防抖。」

代码语言:javascript
复制
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);
};

getKeyName

「根据keycode获得键名。」

代码语言:javascript
复制
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 '';
    }
};

deepClone

「深拷贝。」

代码语言:javascript
复制
/**
 * @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.");
}

isEmptyObject

「判断obj是否为空。」

代码语言:javascript
复制
/**
 * 
 * @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. Object.keys() 方法会返回一个由一个给定对象的「自身可枚举」属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
  2. Object.getOwnPropertyNames()「方法返回一个由指定对象的」所有自身属性」的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

randomColor

**随机生成颜色。**此处需要详细说明,继续往下看。

代码语言:javascript
复制
/**
 * 
 * @description 随机生成颜色
 * @return {String} 
 */
function randomColor() {
    return '#' + ('00000' + (Math.random() * 0x1000000 << 0).toString(16)).slice(-6);
}

补充知识:

  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]的随机数。」

代码语言:javascript
复制
/**
 * 
 * @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;
}

isColor

「判断是否为16进制颜色。」

代码语言:javascript
复制
/**
 * 
 * @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是重复的,不知道是刻意为之还是?

isEmail

「判断是否为邮箱地址。」

代码语言:javascript
复制
/**
 * 
 * @description   判断是否为邮箱地址
 * @param  {String}  str
 * @return {Boolean} 
 */
function isEmail(str) {

  // 判断是否为email最核心的是@和.
    return /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(str);
}

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

isIdCard

「判断是否为身份证号。」

代码语言:javascript
复制
/**
 * 
 * @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位是省市区的编号等等。

isPhoneNum

「判断是否为手机号。」

代码语言:javascript
复制
/**
 * 
 * @description   判断是否为手机号
 * @param  {String|Number} str 
 * @return {Boolean} 
 */
function isPhoneNum(str) {
    return /^(\+?0?86\-?)?1[3456789]\d{9}$/.test(str)
}

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

isUrl

「判断是否为URL地址。」

代码语言:javascript
复制
/**
 * 
 * @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);
}

「digitUppercase」

「现金额转大写。」

代码语言:javascript
复制
/**
 * 
 * @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(/^整 $/, '零元整');
};

isSupportWebP

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

代码语言:javascript
复制
/**
 * 
 * @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. HTMLCanvasElement.toDataURL() 方法返回一个包含图片展示的 data URI 。可以使用type参数(可选)指定类型,默认为image/png。第二个可选参数是图片质量,取值范围是0-1,不传或者超出取值范围,则取默认值 0.92。图片的分辨率为96dpi。返回值是包含 data URIDOMString
代码语言:javascript
复制
  document.createElement('canvas').toDataURL('image/webp')
  
  //以下为输出结果
  data:image/webp;base64,UklGRtgCAABXRUJQVlA4WAoAAAAwAAAAKwEAlQAASUNDUBgCAAAAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANkFMUEgSAAAAAQcQEREQkCT+/x9F9D/tf0MAVlA4IIAAAABwDQCdASosAZYAPm02mUmkIyKhICgAgA2JaW7hdrEbQAnsA99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfasAAD+/9YAAAAAAAAAAA==
  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」

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

代码语言:javascript
复制
/**
 * @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 '刚刚'
}

formatRemainTime

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

代码语言:javascript
复制
/**
 * 
 * @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 + "秒";
}

「isLeapYear」

「判断是否为闰年。」

代码语言:javascript
复制
/**
 * 
 * @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;
}

isSameDay

「判断是否为同一天。」

代码语言:javascript
复制
/**
 * @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;

}

timeLeft

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

代码语言:javascript
复制
/**
 * @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 }
}

monthDays

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

代码语言:javascript
复制
/** 
 * @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();
}

「parseQueryString」

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

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

「stringfyQueryString」

「对象序列化。」

代码语言:javascript
复制
/**
 * 
 * @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('&');
}

总结

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

Reference

[1]

outils: https://github.com/proYang/outils

[2]

outils.min.js: https://github.com/proYang/outils/blob/master/min/outils.min.js

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端F2E 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 安装
  • 使用
  • API
    • arrayEqual
      • hasClass
        • addClass
          • removeClass
            • getCookie
              • setCookie
                • removeCookie
                  • getExplore
                    • getOS
                      • getScrollTop
                        • setScrollTop
                          • offset
                            • scrollTo
                              • windowResize
                                • throttle
                                  • debounce
                                    • getKeyName
                                      • deepClone
                                        • isEmptyObject
                                          • randomColor
                                            • randomNum
                                              • isColor
                                                • isEmail
                                                  • isIdCard
                                                    • isPhoneNum
                                                      • isUrl
                                                        • 「digitUppercase」
                                                          • isSupportWebP
                                                            • 「formatPassTime」
                                                              • formatRemainTime
                                                                • 「isLeapYear」
                                                                  • isSameDay
                                                                    • timeLeft
                                                                      • monthDays
                                                                        • 「parseQueryString」
                                                                          • 「stringfyQueryString」
                                                                          • 总结
                                                                            • Reference
                                                                            相关产品与服务
                                                                            容器服务
                                                                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                                            领券
                                                                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档