目录
# 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.7976931348623157e10308, // 浮点数的最大值拷贝后变成null
infinityMin: -1.7976931348623157e10308, // 浮点数的最小值拷贝后变成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);
# 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 = [{ stockId: '立减券' }]; // 本次要发放的福利券
// 用户已领取的福利券,不再重复发放
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]
# 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);
此处不推荐使用对象字面量存储数据,阮一峰老师的建议很有道理。
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要 key: value 的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
尤其公司有代码复杂度要求的同学,一定要学会以上两种方法。
# 12. 更可靠的判断数据类型
typeof 检测一些基本的数据类型,正则、{}、[]、null 输出结果为 object
console.log(typeof /\d/); //object
console.log(typeof {}); //object
console.log(typeof []); //object
console.log(typeof null); //object
console.log(typeof 123); //number
console.log(typeof true); //boolean
console.log(typeof function() {}); //function
console.log(typeof undefined); //undefined
A instanceof B 判断 a 的构造器是否为 b
function b() {}
let a = new b();
console.log(a instanceof b); //true
console.log(b instanceof Object); //true
let arr = [1, 2, 3, 4];
console.log(arr instanceof Array); //true
以上两种方法有有其局限性,推荐使用更可靠的判断数据类型方法
function judgeDataType(val, type) {
const dataType = Object.prototype.toString
.call(val)
.replace(/\[object (\w+)\]/, '$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} 格式化结果;异常情况下返回空串
*/
const formatDateTime = (date, fmt) => {
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return '';
let dateStr = fmt || 'yyyy-MM-dd hh:mm:ss';
// 处理年
if (/(y+)/.test(dateStr)) {
// RegExp.$1代表括号里匹配到的字符串
// '2022'.substr(2) -> '22'
// 如yyyy年MM月dd日处理为2022年,yy年处理为22年
dateStr = dateStr.replace(RegExp.$1, `${dateObj.getFullYear()}`.substr(4 - RegExp.$1.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)) {
if (new RegExp(`(${key})`).test(dateStr)) {
// RegExp.$1 -> 'M'或'MM' 如果是M的话直接返回月份值如5
// 如果是MM要将5前面补0,变成'05',其他同理
dateStr = dateStr.replace(RegExp.$1, RegExp.$1.length === 1 ? value : `${value}`.padStart(2, '0'));
}
}
return dateStr;
};
日常开发中使用类似上面的一些小技巧方法可以大大提高开发效率,使你的代码更优雅。
