项目中的工具---js

255 阅读10分钟

list <---> tree

  • 示例数据
 const sources = [
    { id: "1", name: '部门1', pid: "0" },
    { id: "1-1", name: '部门1的子部门1-1', pid: "1" },
    { id: "1-1-1", name: '部门1-1的子部门1-1-1', pid: "1-1" },
    { id: "1-1-2", name: '部门1-1的子部门1-1-2', pid: "1-1" },
    { id: "1-2", name: '部门1的子部门1-2', pid: "1" },
    { id: "2", name: '部门2', pid: "0" },
    { id: "2-1", name: '部门2的子部门2-1', pid: "2" },
    { id: "2-1-1", name: '部门2-1的子部门2-1-1', pid: "2-1" },
    { id: "3", name: '部门3', pid: "0" }
  ]
  listToTree(sources,{id:"id", parentId:"pid"})
  • listToTree.js
/**
 * @desc list 结构变成 tree 结构
 * @param {list} list 源数据
 * @return {trees} trees 型数据
 */
export default function listToTree (list = [], { id, parentId } = { id: "id", parentId: "parentId" }) {
  const sourceMap = new Map();
  const trees = [];
  for (const item of list) {
    item.children = [];
    sourceMap.set(item[id], item)
  }
  for (const key of sourceMap.keys()) {
    let item = sourceMap.get(key);
    if (!sourceMap.has(item[parentId])) {
      trees.push(item)
    } else {
      const parentItem = sourceMap.get(item[parentId]);
      parentItem.children.push(item)
    }
  }
  return trees
}
  • treeToList.js
/**
 * @desc tree 结构变成 list 结构
 * @param {trees} trees 源数据
 * @param {childrenKeyName} childrenKeyName
 * @return {list} list 型数据
 */
export default function treeToList(trees = [], list = [], childrenKeyName = 'children') {
    trees.forEach(item => {
      let { [childrenKeyName]: children, ...other } = item
      list.push(other)
      if (Array.isArray(children) && children.length >= 1) {
        treeToList(children, list)
      } else {
        other.leaf = true
      }
    })
    return list
  }

手动导出excel

const suffix = ".xlsx"

/**
 * 下载文件(用于下载后端返回的blob流下载)
 * @param text 文本
 * @param type 下载的文件类型
 * @param fileName 下载的文件名称
 */
 function downloadFileByBlob(text, type, fileName) {
  let url = window.URL.createObjectURL(new Blob([text], { type }));
  let aDom = document.createElement("a");
  aDom.setAttribute("href", url);
  fileName = decodeURIComponent(fileName);
  aDom.setAttribute("download", fileName);
  document.body.appendChild(aDom)
  aDom.click();
  document.body.removeChild(aDom)
  window.URL.revokeObjectURL(url);
}

/**
 * @desc 手动导出excel文件
 * @param {datas} 表体数据  格式如 [{"name": "张三","id": "A001","age": "131",}]
 * @param {header} 表头  格式如 { "name": "姓名", "age": "年龄",  "id": "编号", }  
 * @param {fileName} 导出文件名称 
 * @return {文件} 
 */
 function exportExcel({ datas = [], header = {}, fileName = "文件" } = {}) {
  let columnHtml = "";
  columnHtml += "<tr style=\"text-align: center;\">\n";
  for (let key in header) {
    columnHtml += "<td style=\"background-color:#bad5fd\">" + header[key] + "</td>\n";
  }
  columnHtml += "</tr>\n";
  let dataHtml = "";
  for (let data of datas) {
    dataHtml += "<tr style=\"text-align: center;\">\n";
    for (let key in header) {
      dataHtml += "<td>" + (data[key] ?? "") + "</td>\n";
    }
    dataHtml += "</tr>\n";
  }
  let excelHtml = "<html xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n" +
    "      xmlns:x=\"urn:schemas-microsoft-com:office:excel\"\n" +
    "<head>\n" +
    "   <xml>\n" +
    "        <x:ExcelWorkbook>\n" +
    "            <x:ExcelWorksheets>\n" +
    "                <x:ExcelWorksheet>\n" +
    "                    <x:Name></x:Name>\n" +
    "                    <x:WorksheetOptions>\n" +
    "                        <x:DisplayGridlines/>\n" +
    "                    </x:WorksheetOptions>\n" +
    "                </x:ExcelWorksheet>\n" +
    "            </x:ExcelWorksheets>\n" +
    "        </x:ExcelWorkbook>\n" +
    "   </xml>\n" +
    "   <style>td{font-family: \"宋体\";}</style>\n" +
    "</head>\n" +
    "<body>\n" +
    "<table border=\"1\">\n" +
    "    <thead>\n" +
    columnHtml +
    "    </thead>\n" +
    "    <tbody>\n" +
    dataHtml +
    "    </tbody>\n" +
    "</table>\n" +
    "</body>\n" +
    "</html>";
  downloadFileByBlob(excelHtml, "application/octet-stream", fileName + suffix);
}

对象拷贝

  • 以目标对象为主,把源对象属性拷贝到目标对象上
const sourceObj = { name: "bwf", age: 18, sex: "男", info: "哈哈" }
const targetObj = { age: "", name: "xx", id: 12 }
/**
 * @desc 以目标对象为主,把源对象属性拷贝到目标对象上(相同属性则源覆盖目标,源多余的属性不会进行拷贝,目标多余的属性不会被覆盖)
 * @param {sourceObj}  源对象
 * @param {targetObj}  目标对象 
 * @return {targetObj} 拷贝后的结果
 */
function propertyCopy(sourceObj = {}, targetObj = {}) {
    const sourceKeys = Object.keys(sourceObj)
    if (sourceKeys.length < 1) return targetObj
    for (const key in targetObj) {
        if (!sourceKeys.includes(key)) continue
        // 替换此处代码可以切换 深浅拷贝
        targetObj[key] = sourceObj[key]
    }
    return targetObj
}
// {age: 18, name: 'bwf', id: 12}
console.log('propertyCopy', propertyCopy(sourceObj, targetObj))
  • 把源对象属性按照指定规则映射到目标对象上
const sourceObj2 = { name: "bwf", age: 18, sex: "男", info: "哈哈" }
const targetObj2 = { age: "", female: "女", id: 123, userName: "", }
const map = new Map([
    ['userName', 'name'],
    ['female', 'sex'],
])

/**
 * @desc  把源对象属性按照指定规则映射到目标对象上
 * @param {sourceObj}  源对象
 * @param {targetObj}  目标对象
 * @param {map}        [targetKey, sourceKey] :[目标键名, 源键名]
 * @return {targetObj} 拷贝后的结果
 */
function convertProperty(sourceObj = {}, targetObj = {}, map = new Map()) {
    const sourceKeys = Object.keys(sourceObj)
    if (sourceKeys.length < 1) return targetObj
    for (const key in targetObj) {
        if (map.has(key)) {
            // 替换此处代码可以切换 深浅拷贝
            targetObj[key] = sourceObj[map.get(key)]
        } else {
            if (!sourceKeys.includes(key)) continue
            // 替换此处代码可以切换 深浅拷贝
            targetObj[key] = sourceObj[key]
        }
    }
    return targetObj
}
// {age: 18, female: '男', id: 123, userName: 'bwf'}
console.log('convertProperty', convertProperty(sourceObj2, targetObj2, map))

源集合 和 目标集合 是否有交集

/**
 * @desc 源集合 和 目标集合 是否有交集, true:含有交叉部分  false:无交叉部分
 * @param {sourceList}  源集合
 * @param {targetList}  目标集合
 * @return {boolean}    true:含有交叉部分  false:无交叉部分
 */
function includeList(sourceList = [], targetList = []) {
    if (sourceList.length < 1 || targetList.length < 1) return false
    for (const v of sourceList) {
        if (targetList.includes(v)) return true
    }
    return false
}
const sourceList = ['H5', 'APP']
const targetList = ['PC', 'PC_BK', 'APP', 'WEB']
// true
console.log('includeList', includeList(sourceList, targetList))

日期格式化

import dayjs from 'dayjs'

export default function debounce (val, format = 'YYYY-MM-DD HH:mm:ss') {
  if (!isNaN(val)) {
    val = parseInt(val)
  }
  return dayjs(val).format(format)
}

防抖

/**
 * @desc 防抖切面
 * @param {timeout}  默认防抖时间500毫秒
 */
window.Debounce = function (timeout = 500) {
  const instanceMap = new Map();
  return function (target, key, descriptor) {
    let original = descriptor.value;
    descriptor.value = function (...args) {
      clearTimeout(instanceMap.get(this));
      instanceMap.set(this, setTimeout(() => {
        original.apply(this, args);
        instanceMap.set(this, null);
      }, timeout));
    }
    return descriptor
  }
}

export default function debounce(func, delay) {
  let timer = null;
  return function () {
    let context = this;
    let args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      func.apply(context, args);
    }, delay);
  }
}

深拷贝

export default function deepCopy (obj){
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  let copy = Array.isArray(obj) ? [] : {};
  Object.keys(obj).forEach((key) => {
    copy[key] = deepCopy(obj[key]);
  });
  return copy;
}

脱敏

export default function desensitize (val, format = 'name'){
  let result = '';
  switch (format) {
    case 'name':
      const nameLen = val.length
      result = val[0] + '*'.repeat(nameLen - 1);
      break;
    case 'phoneNumber':
      result = val.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
      break;
    case 'idNumber':
      result = val.replace(/(\d{4})\d+(\d{4})/, '$1*********$2');
      break;
    case 'address':
      const addressLen = val.length;
      result = addressLen > 5 ? val.substring(0, 3) + '*****' + val.substring(addressLen - 2) : val;
      break;
    default:

      break;
  }
  return result;
}

处理数字成千分位 百分比


export default function numberFormat (num, format, toFixedNum = 2, ){
  const reg = /\d{1,3}(?=(\d{3})+$)/g;
  const arr = num.toFixed(toFixedNum).split('.');
  const int = arr[0];
  const decimal = arr[1];
  let result = '';
  switch (format) {
    case 'dollar':
      result = '$' + int.replace(reg, '$&,') + '.' + decimal;
      break;
    case 'percentage':
      result = int.replace(reg, '$&,') + '.' + decimal + '%';
      break;
    default:
      result = int.replace(reg, '$&,') + '.' + decimal;
      break;
  }
  return result;
}

校验器


const patterns = {
  nameCn: /^[\u4e00-\u9fa5]{2,4}$/, // 2-4个汉字,用于验证中文姓名
  nameEn: /^[A-Za-z]+([-']?[A-Za-z]+)?\s[A-Za-z]+([-']?[A-Za-z]+)?$/, //英文姓名可以包含字母、空格、连字符(-)和撇号(')等符号
  mobile: /^(13[0-9]|14[5-9]|15[0-3,5-9]|16[5,6]|17[0-8]|18[0-9]|19[1,8,9])\d{8}$/, // 用于验证手机号码
  email: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/, //用于验证电子邮箱
  chinese: /[\u4e00-\u9fa5]/, //匹配所有汉字
  geZeroInt: /^\d+$/, // 用于验证大于或等于0的整数
  int: /^-?\d+$/, // 用于验证整数,包括正整数、负整数和0
  geZero: /^\d+(\.\d+)?$/, // 用于验证大于或等于0的数字,可以是整数或小数
  idNo: /^\d{17}(\d|x)$/,// 用于验证身份证号码,支持18位身份证和17位身份证+x
  qqNo: /^[1-9]\d{4,10}$/, // 用于验证QQ号码,5-11位数字,第一位不能是0
  weNo: /^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/, // 用于验证微信号码,6-20位字符,以字母开头,允许使用数字、字母、下划线和中划线
  carNo: /^[\u4e00-\u9fa5]{1}[A-Z]{1}[A-Z_0-9]{5}$/, // 用于验证车牌号码,车牌号格式为汉字 + 大写字母 + 5位数字或字母
  creditCode: /^[0-9A-Z]{18}$/, // 用于验证统一社会信用代码,18位数字
}


/**
 * @desc 生产校验器 名称组成 `isNameCn`  `isMobile`
 */
const getValidatorsByPatterns = () => {
  const validators = {}
  for (const key of Object.keys(patterns)) {
    const PascalCaseKey = key.slice(0, 1).toUpperCase() + key.slice(1)
    validators[`is${PascalCaseKey}`] = (val) => patterns[key].test(val)
  }
  return validators
}

export default getValidatorsByPatterns()

dateHelper.js

/**
 * @desc 日期格式化等操作助手  YYYY MM DD HH:mm:ss
 * ex: new DateHelper('2022-09').getFirstDayByUnit()  new DateHelper(new Date()).format('YYYY年M月D日')  new DateHelper(new Date()).isAfter('2023-08-31 10:59:59')
 */

import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween'; // 引入相关插件
dayjs.extend(isBetween);

// 格式化字符串日期
export const formatDateStr = (dateStr, format = 'YYYY-MM-DD') => {
  return dayjs(dateStr).format(format);
}

export default class DateHelper {
  /**
   * @desc 初始化
   * @param {string | object}  dateString: 日期字符串 或者 日期对象
   */
  constructor(dateString) {
    this.date = dayjs(dateString);
  }

  /**
   * @desc 格式化时间
   * @param {string}  formatString: 需要格式化的字符串  YYYY年M月D日  YYYY-MM-DD HH:mm:ss
   * @return {string}  格式化后的时间
   */
  format(formatString) {
    return this.date.format(formatString);
  }

  /**
   * @desc 加
   * @param {number} num:步长
   * @param {string} unit:单位 millisecond second minute hour  day  week month  year
   * @param {string} formatString:'YYYY-MM-DD'
   * @return {string} 加之后的结果
   */
  add(num, unit = 'day', formatString = 'YYYY-MM-DD') {
    return this.date.add(num, unit).format(formatString);
  }

  /**
   * @desc 减
   * @param {number} num:步长
   * @param {string} unit:单位 millisecond second minute hour  day  week month  year
   * @param {string} formatString:'YYYY-MM-DD'
   * @return {string} 减之后的结果
   * @example Mon Dec 04 2023 14:34:38 GMT+0800 (中国标准时间) ===>>> 2023-11-29
   */
  subtract(num, unit = 'day', formatString = 'YYYY-MM-DD') {
    return this.date.subtract(num, unit).format(formatString);
  }

  /**
   * @desc 获取某年(某月)的第一天
   * @param {string}  unit: month  year
   * @param {string}  formatString:'YYYY-MM-DD'
   * @return {string}  某年(某月)的第一天
   * @example Mon Dec 04 2023 14:34:38 GMT+0800 (中国标准时间) ===>>> 2023-12-01
   */
  getFirstDayByUnit(unit = 'month', formatString = 'YYYY-MM-DD') {
    return this.date.startOf(unit).format(formatString);
  }

  /**
   * @desc 获取某年(某月)的最后一天
   * @param {string}  unit: month year
   * @param {string}  formatString: month year
   * @return {string} 某年(某月)的最后一天
   */
  getLastDayByUnit(unit = 'month', formatString = 'YYYY-MM-DD') {
    return this.date.endOf(unit).format(formatString);
  }

  /**
   * @desc 获取时间差
   * @param {object} otherDate:日期对象
   * @param {string} unit: month year
   * @return {number} 时间差
   */
  getDiff(otherDate, unit = 'day') {
    return this.date.diff(otherDate, unit);
  }

  /**
   * @desc 判断一个日期是否在另外一个日期之后
   * @param {object} otherDate:日期对象
   * @return {boolean} true:之后 false:之前
   */
  isAfter(otherDate) {
    return this.date.isAfter(otherDate);
  }

  /**
   * @desc 判断一个日期是否在另外一个日期之前
   * @param {object} otherDate:日期对象
   * @return {boolean} true:之前 false:之后
   */
  isBefore(otherDate) {
    return this.date.isBefore(otherDate);
  }

  /**
   * @desc 判断一个日期是否在两个日期之间
   * @param {object} a:日期对象
   * @param {object} b:日期对象
   * @return {boolean} true:是
   */
  isBetween(a, b) {
    return this.date.isBetween(a, b);
  }

  /**
   * @desc 获取一个日期是周几
   * @return {string} 周六
   */
  getDayOfWeek() {
    // const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
    const daysOfWeekChinese = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
    const dayOfWeek = this.date.day(); // 获取日期的星期几,返回值为 0(周日)、1(周一)... 5 (周五)、 6(周六)
    return daysOfWeekChinese[dayOfWeek];
  }
}

desensitizerHelper.js

export default class DesensitizerHelper {
  /**
   * @desc 姓名脱敏
   * @param {string} name
   * @return {string} 脱敏后的结果
   */
  static desensitizeName(name, defaultVal = '--') {
    if (!name) return defaultVal;
    if (name.length === 1) {
      return name;
    }
    return `${name[0]}${'*'.repeat(name.length - 1)}`;
  }

  /**
   * @desc 手机号脱敏
   * @param {string} phoneNumber
   * @return {string} 脱敏后的结果
   */
  static desensitizePhoneNumber(phoneNumber, defaultVal = '--') {
    if (!phoneNumber) return defaultVal;
    return phoneNumber.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
  }

  /**
   * @desc 邮箱脱敏
   * @param {string} email
   * @return {string} 脱敏后的结果
   */
  static desensitizeEmail(email) {
    const [username, domain] = email.split('@');
    const usernameLen = username.length;
    const maskedUsername = usernameLen <= 2 ? '*'.repeat(usernameLen) : `${username[0]}*${username[usernameLen - 1]}`;
    return `${maskedUsername}@${domain}`;
  }

  /**
   * @desc 银行卡号脱敏
   * @param {string} cardNumber
   * @return {string} 脱敏后的结果
   */
  static desensitizeBankCardNumber(cardNumber) {
    const visibleDigits = 4; // 可见的末尾几位数字
    const maskedDigits = cardNumber.length - visibleDigits;
    const maskedPart = '*'.repeat(maskedDigits);
    const visiblePart = cardNumber.slice(maskedDigits);
    return maskedPart + visiblePart;
  }

  /**
   * @desc 身份证号脱敏
   * @param {string} idNumber
   * @return {string} 脱敏后的结果
   */
  static desensitizeIDNumber(idNumber) {
    return idNumber.replace(/(\d{4})\d+(\d{4})/, '$1****$2');
  }

  /**
   * @desc 护照脱敏
   * @param {string} passportNumber
   * @return {string} 脱敏后的结果
   */
  static desensitizePassportNumber(passportNumber) {
    return passportNumber.replace(/(.{2}).+(.{2})/, '$1****$2');
  }

  /**
   * @desc 座机号脱敏
   * @param {string} landline
   * @return {string} 脱敏后的结果
   */
  static desensitizeLandlinePhoneNumber(landline) {
    return landline.replace(/(\d{3}-\d{4})(\d+)/, '$1****$2');
  }

  /**
   * @desc 地址脱敏
   * @param {string} address
   * @return {string} 脱敏后的结果
   */
  static desensitizeAddress(address) {
    const visiblePart = address.substring(0, 6);
    const maskedPart = '*'.repeat(address.length - visiblePart.length);
    return visiblePart + maskedPart;
  }
}

qsHelper

import qs from 'qs';

/**
 * @desc 从urlStr(默认地址栏)上获取?后缀的参数
 * @param {string} urlStr:url
 * @return {object}
 * @example /?age=89&name=12 ===>>> {age:89, name:12}
 */
export function getUrlParamsByUrlStr(urlStr = location.href) {
  const index = urlStr.indexOf('?');
  if (index < 0) return {};
  return qs.parse(urlStr.slice(index + 1));
}

/**
 * @desc 获取从urlStr(默认地址栏)指定key的value
 * @param {string} key:指定key
 * @return {string} value
 * @example /?age=89&name=12 ===>>> getUrlValByKey('age') ===>>>> 89
 */
export function getUrlValByKey(key, urlStr = location.href) {
  const val = getUrlParamsByUrlStr(urlStr)[key];
  return val ? decodeURI(val) : val;
}

utils

/**
 * @desc 暂停执行
 * @param {number}  ms:延迟执行的毫秒数
 */
export function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * 下载文件(用于下载后端返回的blob流下载)
 * @param {blob} blob流
 * @param {string} type 下载的文件类型
 * @param {string} fileName 下载的文件名称
 */
export function downloadFileByBlob(blob, type, fileName) {
  const url = window.URL.createObjectURL(new Blob([blob], { type }));
  const aEle = document.createElement('a');
  aEle.setAttribute('href', url);
  aEle.setAttribute('download', fileName);
  document.body.appendChild(aEle);
  aEle.click();
  document.body.removeChild(aEle);
  window.URL.revokeObjectURL(url);
}

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
export function debounce(func, wait = 300, immediate = true) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;

    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(() => {
        timeout = null;
      }, wait);
      if (callNow) func.apply(context, args);
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args);
      }, wait);
    }
  };
}

/**
 * @desc 数组重新分组
 * @param {Array}  array:原数组
 * @param {Number}  size:分片的个数
 * @return {Array} 新的二维数组
 */
export function chunkArray(array, size) {
  const result = [];
  for (let i = 0; i < array.length; i += size) {
    result.push(array.slice(i, i + size));
  }
  return result;
}

/**
 * @desc 浏览器userAgent
 * @param
 * @return
 */
export function getOs() {
  const u = navigator.userAgent;
  return {
    trident: u.indexOf('Trident') > -1, //IE内核
    presto: u.indexOf('Presto') > -1, //opera内核
    webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
    gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //火狐内核
    mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
    ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
    android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android终端
    iPhone: u.indexOf('iPhone') > -1, //是否为iPhone或者QQHD浏览器
    iPad: u.indexOf('iPad') > -1, //是否iPad
    webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
    weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增)
    qq: u.match(/\sQQ/i) == ' qq', //是否QQ
  };
}

/**
 * @desc 格式化金额
 * @param {number|string, boolean} number:传入的float数字
 * @return {string}  千分位
 * @example 123456789 ===>>> 1,234,589.00
 */
export function formatMoney(number) {
  if (Number(number).toString() !== 'NaN') {
    const numStr = Number(number).toLocaleString();
    return numStr.includes('.') ? numStr.substring(0, numStr.indexOf('.') + 3) : numStr;
  }
  // 匹配数字
  const num = number.toString().match(/^\d+(\.\d+)?/g);
  // 获取最后的 非数字的
  const suffix = number.toString().replace(/^\d+(\.\d+)?/g, '') || '';
  if (num[0]) {
    // 匹配到数字 执行
    const numStr = Number(num[0]).toLocaleString();
    return (numStr.includes('.') ? numStr.substring(0, numStr.indexOf('.') + 3) : numStr) + suffix;
  }

  return number || '--';
}

/**
 * This is just a simple version of deep copy
 * Has a lot of edge cases bug
 * If you want to use a perfect deep copy, use lodash's _.cloneDeep
 * @param {Object} source
 * @returns {Object}
 */
export function deepClone(source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments', 'deepClone');
  }
  const targetObj = source.constructor === Array ? [] : {};
  Object.keys(source).forEach(keys => {
    if (source[keys] && typeof source[keys] === 'object') {
      targetObj[keys] = deepClone(source[keys]);
    } else {
      targetObj[keys] = source[keys];
    }
  });
  return targetObj;
}