优雅提效的高级js开发技巧(持更)

6,149 阅读9分钟

本文所有代码示例均为es6+写法,强烈建议大家使用es6+语法开发,不太了解的新同学一定去学习一遍,推荐阮一峰老师的ES入门教程ES7+语法可以看我的另一篇文章ES7+新特性及其兼容性一览

1. 简单数组去重

扩展运算符 + set 去重

const arr = ['a','b','c','a','b'];
const newArr = [...new Set(arr)];

Array.from + Set 去重

const arr = ['a','b','c','a','b'];
const newArr = Array.from(new Set(arr));

2. 数组深拷贝

简单数组拷贝,只能拷贝一层,不适用于对象数组

 const arr = [1, 2, 3];

 const arr1 = arr.slice(0); // slice

 const arr2 = arr.concat(); // concat

 const arr3 = [...arr]; // 扩展运算符

 const arr4 = Array.from(arr); // from

使用JSON

const arr = [{age: 1}, {age: 2}, {age: 3}];
const newArr = JSON.parse(JSON.stringify(arr));

但是此种方式并不可靠,以下几种情况需要注意:

const obj = {
  nan: NaN, // NaN拷贝后变成null
  infinityMax:1.7976931348623157E+10308,  // 浮点数的最大值拷贝后变成null
  infinityMin:-1.7976931348623157E+10308, // 浮点数的最小值拷贝后变成null
  undef: undefined, // 拷贝后直接丢失
  fun: () => 'func', // 拷贝后直接丢失
  date:new Date, // 时间类型拷贝后会被变成字符串类型数据
}

这不能怪JSON,因为人家本意就不是做深拷贝的,只是开发者的一厢情愿。

JSON.stringify 的目的是将数据转成 json 格式,而 json 有自己的规范,并不是 js 专属,它是跨语言的,各种语言都用统一的 json 格式来通信,所以只能支持各种语言常用的数据类型。

更可靠一些的,使用下面方法实现:

/**
* 深拷贝
* @param {Object|Array} target  拷贝对象
* @returns {Object|Array} result 拷贝结果
*/
  function deepCopy(target) {
    if (Array.isArray(target)) { // 处理数组
      return target.map(item => deepCopy(item));
    } 

    if (Object.prototype.toString.call(target)=== '[object Object]') { // 处理对象
       // 先将对象转为二维数组,再将二维数组转回对象(这个过程还是浅拷贝)
       // 所以使用map方法将二维数组里的元素进行深拷贝完了再转回对象
       return Object.fromEntries(Object.entries(target).map(([k, v]) => [k, deepCopy(v)]));
    }
    return target; // 深拷贝要处理的就是引用类型内部属性会变化的情况,像正则、Error、函数、Date这种一般不会发生变化的直接返回原数据就可以
  }

上面的方法已经足够覆盖你的大多数场景了。 但是它也不是完美的,例如Map,Set等还是浅拷贝,而且存在循环引用会导致栈溢出的问题,戳这里去看更好的深拷贝方案,深拷贝渐进式解决方案

3. 数组合并

扩展运算符,不改变原数组

const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [...arr1, ...arr2];

扩展运算符,改变原数组

const arr1 = [1, 2];
const arr2 = [3, 4];
arr1.push(...arr2);

concat 方法

const arr1 = [1, 2];
const arr2 = [3, 4];
const newArr = arr1.concat(arr2);

apply 方法

const arr1 = [1, 2];
const arr2 = [3, 4];
arr1.push.apply(arr1, arr2);

综合性能对比: push() > concat() > […array1,…array2], 扩展运算符基本会慢10倍以上,不推荐使用。

4. 对象数组去重(重复键)

一般情况下对象数组中的对象都有其唯一键,所以不需判断所有属性都相等。

arrDistinctByKey(arr, key){
  const temp = new Map(); // 使用Map可以比对多种类型,不限于字符串
  return arr.filter((item) => !temp.has(item[key]) && temp.set(item[key], true));
}

// 常用于过滤列表
const list = [{id: 1, name: 'xiaoming'}, {id: 1, name: 'xiaoming'}, {id: 2, name: 'xiaoliang'}];
const newArr = arrDistinctByKey(list, 'id');
// newArr: [{id: 1, name: 'xiaoming'}, {id: 2, name: 'xiaoliang'}]

5. 对象数组取交集(相同键)

arrIntersectionByKey(arr1, arr2, key) {
  const arr2Keys = arr2.map(item => item[key]);
  return arr1.filter(item => arr2Keys.includes(item[key]));
}

// 例如找出用户已领券中包含的本次即将发放的立减券,弹窗告知用户已领取其中的某些券
const receivedCoupons = [{ name: '立减券' },{ name: '折扣券' }]; // 已领的券
const welfareCoupons = [{ name: '立减券' }]; // 本次要发放的福利券
// 用户已领取的福利券,不再重复发放
arrIntersectionByKey(receivedCoupons,welfareCoupons, 'name');
// [{ name: '立减券' }]

6. 使用flat和flatMap方法处理嵌套数组

拿筛选组件举例:

// 选项组数组,这是一个筛选组件的数据
const optionsGroup = [
  {
    groupTitle: '资源类型',
    groupId: 'current',
    mode: 'multi',
    options: [
      { text: '直流桩', value: [1, 3], active: true },
      { text: '交流桩', value: [2, 4, 5], active: false },
    ],
  },
  {
    groupTitle: '通勤方式',
    groupId: 'commute',
    mode: 'multi',  // 多选
    options: [
      { text: '公交', value: 0, active: true },
      { text: '地铁', value: 1, active: true },
      { text: '驾车', value: 1, active: false },
    ],
  },
];

尝试将上面数组处理为下面这样的数据结构,可以先自己试一下

[
  { text: '公交', value: 0, active: true, groupId: 'commute' },
  ...
],

再看一下使用flatMap后的代码,怎么样,有没有被惊艳到。

// 3行代码搞定
// 先在options里面添加上groupId
// flatMap会将options数组即[ [{ text: '公交', value: 0, active: true, groupId: 'commute' }], ...]变成[{ text: '公交', value: 0, active: true, groupId: 'commute' }, ...]
const activated = optionsGroup
        .flatMap(item => item.options.map(option => ({ ...option, groupId: item.groupId })))
        .filter(item => item.active);

flatMap相当于在map的功能基础上,加上了flat方法的效果,flat方法的效果是将数组降维一层,可参考 ES7+新特性及其兼容性一览

7. 快速创建一个指定长度的数组并填充内容

const array = new Array(100).fill('');
// (100) ['', '', ..., '']
const array = Array.from(new Array(100).keys());
  // (100) [0, 1, ..., 98, 99]
const array = Array.from({length: 100}, (v,i) => i);
  // (100) [0, 1, ..., 98, 99]

此方法用于测试或mock数据非常方便

Array.from()方法:

  Array.from(arrayLike[, mapFn[, thisArg]])

参数

  • arrayLike

    想要转换成数组的类数组对象或可迭代对象。

    类数组对象: (拥有一个 length 属性和若干索引属性的任意对象)

  • mapFn 可选

    如果指定了该参数,新数组中的每个元素会执行该回调函数,相当于map函数

  • thisArg 可选

    可选参数,执行回调函数 mapFn 时 this 对象。

8. 利用数组交换值

[a, b] = [b, a];

9. 在数组中每隔n个元素插入一个新元素

const n = 2;
const list = Array.from(new Array(10).keys()); // mock数据
const addElement = {a: 1}; // 新增元素

// 优化for循环
for (let i = 0, len = list.length; i < Math.floor(len / n); i++) {
  // i: 0; 1; 2; 3; 4; 5
  // (i + 1) * n) + i: 2; 5; 8; 11; 14
  list.splice(((i + 1) * n) + i, 0, addElement);  // 相应位置上添加元素
}
console.log(list); // [0, 1, {…}, 2, 3, {…}, 4, 5, {…}, 6, 7, {…}, 8, 9, {…}]

有更优雅的写法,欢迎交流。

10. 替代短路或,使用includes,该方式也是可避免代码复杂度过高的有效方法之一

if(from === 'a' || from === 'b' || from === 'c'){}

if(['a', 'b', 'c'].includes(from)) {}

11. 使用Map代替switch或多个if判断,该方式也是可避免代码复杂度过高的有效方法之一


function getStatusText(status) {
  switch (status) {
    case 1:
      return '待发货';
    case 2:
      return '已发货';
    case 3:
      return '已完成';
    default:
      return '';
  }
}

// 使用Map替代
const statusMap = new Map()
    .set(1, '待发货')
    .set(2, '已发货')
    .set(3, '已完成');
// 或
const statusMap = new Map([
  [1, '待发货'],
  [2, '已发货'],
  [3, '已完成'],
]); // 这种写法的内部执行的算法实际上也是循环执行set,与上面自己写set其实是一样的
const statusText = statusMap.get(status);

// 其实还有更简单的,直接用数组下标映射
const statusText = ['待发货', '已发货', '已完成'][status - 1];

此处不推荐使用对象字面量存储数据,阮一峰老师的建议很有道理。

注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。

尤其公司有代码复杂度要求的同学,一定要学会以上两种方法。

12. 更可靠的判断数据类型

typeof 可以检测一些基本的数据类型,正则、Date、{}、[]、null 等 输出结果为都为 object

 console.log(typeof /\d/); //object
 console.log(typeof new Date()); //object
 console.log(typeof {}); //object
 console.log(typeof []); //object
 console.log(typeof (null)); //object
 console.log(typeof new Map()); //object
 console.log(typeof 123); //number
 console.log(typeof true); //boolean
 console.log(typeof function () {}); //function
 console.log(typeof (undefined)); //undefined

instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

 function b(){}
 let a = new b;
 console.log(a instanceof b); // a的原型链上存在b的constructor.prototype 所以为true
 console.log(b instanceof Object); //true

 let arr = [1,2,3,4];
 console.log(arr instanceof Array); //true

 new Date instanceof Object; // true
 new Date instanceof Date; // true

以上两种方法有有其局限性,推荐使用更可靠的判断数据类型方法

function judgeDataType(val, type) {
  const dataType = Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
  return type ? dataType === type : dataType;
}
console.log(judgeDataType("young")); // "string"
console.log(judgeDataType(20190214)); // "number"
console.log(judgeDataType(true)); // "boolean"
console.log(judgeDataType([], "array")); // true
console.log(judgeDataType({}, "array")); // false

13. 检查是否为空对象

Object.keys({}).length === 0  

JSON.stringify({}) === '{}';

14. 使用Proxy在对象执行某些操作前拦截做一些事情

常用于更改原生API的行为,如数组方法或框架提供的方法等:

  // 如拦截微信小程序框架的showToast方法
  const toastProxy = new Proxy(wx.showToast, {
    apply: (target, ctx, argArr) => {
      // 在方法被调用时更改传入参数
      const newArgArr = argArr;
      newArgArr[0].title = `温馨提示:${newArgArr[0].title}`;
      return Reflect.apply(target, ctx, newArgArr);  // 执行目标对象原始行为
    },
  });

  toastProxy({ title: '您输入的名称过长' });

这只是一个小应用,实际上它可以做很多大事,例如埋点上报等,用法大家自己扩展,要了解这个思路,遇到某些情况时能够想起来用Proxy解决问题。

可以拦截的操作:

const proxy = new Proxy(target, handler);
handler.get // 拦截取值
handler.set // 拦截赋值
handler.has // 拦截in运算符 如'nickname' in user
handler.apply // 拦截方法被调用
handler.construct // 拦截 Proxy 实例作为构造函数调用的操作,比如:new proxy(...args)
handler.defineProperty // 拦截
// Object.defineProperty(proxy, propKey, propDesc)
// Object.defineProperties(proxy, propDescs)
handler.deleteProperty  // 拦截删除属性 delete proxy[propKey]
handler.ownKeys // 拦截
// Object.getOwnPropertyNames(proxy)
// Object.getOwnPropertySymbols(proxy)
// Object.keys(proxy)
// for...in 循环
handler.isExtensible // 拦截 Object.isExtensible(proxy),
handler.preventExtensions // 拦截 Object.preventExtensions(proxy)
handler.getPrototypeOf  // 拦截 Object.getPrototypeOf(proxy)
handler.setPrototypeOf // 拦截 Object.setPrototypeOf(proxy, proto)
handler.getOwnPropertyDescriptor  // 拦截 Object.getOwnPropertyDescriptor(proxy, propKey)

15. 格式化时间(加强版)

/**
 * 格式化时间对象
 * @param {Date|Number} date  Date对象或时间戳
 * @param {String}      fmt 目标格式,如:YYYY年MM月dd日,MM/DD/YYYY,YYYYMMdd,YYYY-MM-DD HH:mm:ss等
 * @returns {String}    格式化结果;异常情况下返回空串
 */
export const formatDateTime = (date: Date | number, fmt = 'YYYY-MM-DD HH:mm:ss'): string => {
  const dateObj = date instanceof Date ? date : new Date(date);
  if (isNaN(dateObj.getTime())) return '';

  let dateStr = fmt;
  const resultYear = dateStr.match(/(Y+)/)?.[1]; // 如果环境不支持新语法(可选链),改一下 && 的形式即可
  // 处理年
  if (resultYear) {
    // resultYear是括号里匹配到的字符串
    // '2022'.substr(2) -> '22'
    // 如YYYY年MM月dd日处理为2022年,yy年处理为22年
    dateStr = dateStr.replace(resultYear, `${dateObj.getFullYear()}`.substring(4 - resultYear.length));
  }

  // 处理月份、日、小时、分、秒、毫秒
  const obj = {
    'M+': dateObj.getMonth() + 1, // 月份
    'D+': dateObj.getDate(), // 日
    'H+': dateObj.getHours(), // 小时
    'm+': dateObj.getMinutes(), // 分
    's+': dateObj.getSeconds(), // 秒
    S: dateObj.getMilliseconds(), // 毫秒
  };

  for (const [key, value] of Object.entries(obj)) {
    const result = dateStr.match(new RegExp(`(${key})`))?.[1]; // 如果环境不支持新语法(可选链),改一下 && 的形式即可
    if (result) {
      // result -> 'M'或'MM' 如果是M的话直接返回月份值如5
      // 如果是MM要将5前面补0,变成'05',其他同理
      dateStr = dateStr.replace(result, result.length === 1 ? `${value}` : `${value}`.padStart(2, '0'));
    }
  }

  return dateStr;
};



16. 序列(range)生成器


const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));

range(0, 4, 1);
// [0, 1, 2, 3, 4]

// 范围1..10,步进2
range(1, 10, 2);
// [1, 3, 5, 7, 9]

17. 快速处理富文本字符串

去掉富文本中html标签

'<p>我是中国人</p>'.replace(/<[^>]*>/g, '') // 我是中国人

使用 decode 将 HTML 实体解码为原始字符,还原为可读的文本。

HTML 实体的格式是 & 后跟一个实体名称或者实体的十进制或十六进制编码,然后以 ; 结尾。例如:

  • &lt; 表示小于号 <
  • &gt; 表示大于号 >
  • &amp; 表示 & 符号 &
  • &nbsp; 表示非断行空格

import { decode } from 'html-entities';

decode('&lt;小于号'); // <小于号 

18. 判断某个时间是否是当天

我们经常遇到一些需要判断当天的场景,例如,某个弹窗每天展示一次,可以使用以下方法快速判断是否是当天

new Date(targetTime).toDateString() === new Date().toDateString() // 相等则为同一天

19. 在 ESModule中使用 __filename 和 __dirname

ES 模块中没有 __dirname__filename 这两个全局变量。相反,ES 模块使用动态的 importimport.meta 语法来处理模块路径。

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

20. 用css写三角箭头

// 箭头向左
    &::before {
        position: absolute;
        top: 0;
        left: -10px; /* 调整箭头的位置 */
        width: 0;
        height: 0;
        border-top: 12px solid transparent; /* 上边框 */
        border-right: 15px solid #fdf1e7; /* 右边框,颜色与背景色相同 */
        border-bottom: 12px solid transparent; /* 下边框 */
        content: "";
    }

& 表示父元素,例如一个容器

21. 监听点击页面上的任意位置做一些操作

假设要实现点击页面其他任意位置关闭 一个 tip 组件,具体做法如下:

  1. 在 Vue 组件中监听整个页面的点击事件。
  2. 如果点击事件发生在 .doubt-tip 容器之外的任何地方,就将 showDoubtTip 设置为 false,以关闭提示框。
<template>
  <div>
    <button @click="showDoubtTip = true">显示提示</button>

    <!-- 根据 showDoubtTip 的值来显示/隐藏提示 -->
    <div class="doubt-tip" v-if="showDoubtTip">
      这是提示内容
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      showDoubtTip: false, // 初始化为 false,不显示提示
    };
  },
  mounted() {
    // 在组件挂载后监听整个页面的点击事件
    document.addEventListener('click', this.handleClickOutside);
  },
  beforeDestroy() {
    // 在组件销毁前移除点击事件监听器,以防止内存泄漏
    document.removeEventListener('click', this.handleClickOutside);
  },
  methods: {
    handleClickOutside(event) {
      // 点击事件发生在 .doubt-tip 容器之外时,关闭提示
      const doubtTip = this.$el.querySelector('.doubt-tip');
      if (doubtTip && !doubtTip.contains(event.target)) {
        this.showDoubtTip = false;
      }
    },
  },
};
</script>

22. 从给定数组中随机获取指定数量的元素

/**
 * 从给定数组中随机获取指定数量的元素。
 * @param {any[]} array - 源数组
 * @param {number} count - 要获取的元素数量
 * @returns {any[]} - 随机获取的元素数组
 */
function getRandomElements(array: any[], count: number) {
  const randomElements: any[] = [];

  // 从数组中随机取出元素
  while (randomElements.length < count && array.length > 0) {
    const randomIndex = Math.floor(Math.random() * array.length);
    const element = array[randomIndex];
    randomElements.push(element);
  }

  return randomElements;
}

23. url 参数获取或更改

/**
 * 更新 URL 中的参数值,如不存在则新增
 *
 * @param {string} url - 原始的 URL 地址。
 * @param {string} param - 要更新或添加的参数名称。
 * @param {string} value - 新的参数值。
 * @returns {string} - 包含更新后参数的新 URL。
 */
export function updateURLParameter(url: string, param: string, value: string) {
  const urlObject = new URL(url);
  const searchParams = new URLSearchParams(urlObject.search);

  if (searchParams.has(param)) {
    searchParams.set(param, value); // 替换已存在的参数值
  } else {
    searchParams.append(param, value); // 添加新的参数
  }

  urlObject.search = searchParams.toString();

  return urlObject.toString();
}

/**
 * 获取url参数
 *
 * @export
 * @param {string} key - key
 * @returns {string}
 */
export function getUrlParam(key: string) {
  const searchParams = new URLSearchParams(window.location.search);
  return searchParams.get(key);
}

24. 从链接下载文件

    const fileUrl = https://....;
    const xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';

    xhr.onload = () => {
        const reader = new FileReader();
        reader.onloadend = () => {
            const link = document.createElement('a');
            link.href = reader.result;
            link.download = `${name}.pdf`; // 设置默认文件名
            link.style.display = 'none';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        };

        reader.readAsDataURL(xhr.response);
    };

    xhr.open('GET', fileUrl);
    xhr.send();

25. 按钮呼吸效果

@keyframes breathe {
  0% { transform: scale(1); }
  50% { transform: scale(1.1); }
  100% { transform: scale(1); }
}

.breathe {
  animation: breathe 1s infinite; /* 1s 表示动画周期,infinite 表示无限循环 */
}

大家日常开发中使用类似上面的一些小技巧方法可以大大提高开发效率,使你的代码更优雅。