玩转JS之ES新特性及实用小技巧
String.prototype.padStart -- es2017
把指定字符串填充到字符串头部,返回新字符串。
语法
str.padStart(targetLength [, padString]
targetLength
当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
padString
可选
填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断。此参数的默认值为 " "
示例
'abc'.padStart(10); // " abc"
'abc'.padStart(10, "foo"); // "foofoofabc"
'abc'.padStart(6,"123465"); // "123abc"
'abc'.padStart(8, "0"); // "00000abc"
'abc'.padStart(1); // "abc"
应用场景
日期格式化
const now = new Date()
const year = now.getFullYear()
// 月份和日期 如果是一位前面给它填充一个0
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = (now.getDate()).toString().padStart(2, '0')
console.log(year, month, day)
console.log( `${year}-${month}-${day}` ) //输出今天的日期 2022-03-23
数字替换(手机号,银行卡号等)
const tel = '18781268679'
const newTel = tel.slice(-4).padStart(tel.length, '*')
console.log(newTel) // *******5678
String.prototype.padEnd -- es2017
语法
同上
示例
'abc'.padEnd(10); // "abc "
'abc'.padEnd(10, "foo"); // "abcfoofoof"
'abc'.padEnd(6, "123456"); // "abc123"
'abc'.padEnd(1); // "abc"
应用场景
时间戳补零
在JS前端我们处理时间戳的时候单位是ms毫秒,但是后端返回的时间戳则不一定是毫秒,可能只有10位,以s秒为单位。所以,我们在前端处理这个时间戳的时候,保险起见,要先做一个13位的补全,保证单位是毫秒。
for await of -- es2017
异步迭代器(for-await-of
):循环等待每个Promise
对象变为resolved
状态才进入下一步。
for of
同步运行
function TimeOut(time){
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(time)
}, time)
})
}
async function test() {
let arr = [TimeOut(2000), TimeOut(1000), TimeOut(3000)]
for (let item of arr) {
console.log(Date.now(),item.then(console.log))
}
}
test()
发现输出结果并不是我们想要的,接着我们使用 for await of
function TimeOut(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(time)
}, time)
})
}
async function test() {
let arr = [TimeOut(2000), TimeOut(1000), TimeOut(3000)]
for await (let item of arr) {
console.log(Date.now(), item)
}
}
test()
// 1647999860945 2000
// 1647999860945 1000
// 1647999861945 3000
Promise.prototype.finally() -- es2018
在promise执行结束时,无论结果是fulfilled或者是rejected,在执行then()和catch()后,都会执行finally指定的回调函数。这为指定执行完promise后,无论结果是fulfilled还是rejected都需要执行的代码提供了一种方式,避免同样的语句需要在then()和catch()中各写一次的情况。
应用场景
loading 关闭
Object.fromEntries() -- es2019
方法 Object.fromEntries() 把键值对列表转换为一个对象,这个方法是和 Object.entries() 相对的。
应用场景
过滤
// course表示所有课程,想请求课程分数大于80的课程组成的对象:
const course = {
math: 70,
english: 85,
chinese: 90
}
const res = Object.entries(course).filter(([key, val]) => val > 80)
console.log(res) // [ [ 'english', 85 ], [ 'chinese', 90 ] ]
console.log(Object.fromEntries(res)) // { english: 85, chinese: 90 }
url的search参数转换
// let url = "https://www.baidu.com?name=jimmy&age=18&height=1.88"
// queryString 为 window.location.search
const queryString = "?name=jimmy&age=18&height=1.88";
const queryParams = new URLSearchParams(queryString);
const paramObj = Object.fromEntries(queryParams);
console.log(paramObj); // { name: 'jimmy', age: '18', height: '1.88' }
Array.prototype.flat() -- es2019
flat()
方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
语法
var newArray = arr.flat([depth])
depth
可选
指定要提取嵌套数组的结构深度,默认值为 1。
示例
扁平化嵌套数组(以往我们扁平化数组都是通过 reduce
实现)
// Before
function flatten(arr) {
return arr.reduce((prev, curr) => prev.concat(Array.isArray(curr) ? flatten(curr) : curr), []);
}
const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
//使用 Infinity,可展开任意深度的嵌套数组
const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
reduce代替map和filter
const _arr = [0, 1, 2];
// map
const arr = _arr.map(v => v * 2);
const arr = _arr.reduce((t, v) => [...t, v * 2], []);
// arr => [0, 2, 4]
// filter
const arr = _arr.filter(v => v > 0);
const arr = _arr.reduce((t, v) => v > 0 ? [...t, v] : t, []);
// arr => [1, 2]
// map和filter
const arr = _arr.map(v => v * 2).filter(v => v > 2);
const arr = _arr.reduce((t, v) => {
v = v * 2;
v > 2 && t.push(v);
return t;
}, []);
// arr => [4]
空值合并运算符(Nullish coalescing Operator)-- es2020
空值合并操作符( ??
)是一个逻辑操作符,当左侧的操作数为 null
或者undefined
时,返回其右侧操作数,否则返回左侧操作数。
注意点
将 ??
直接与 AND(&&
)和 OR(||
)操作符组合使用是不可取的。
null || undefined ?? "foo"; // 抛出 SyntaxError
true || undefined ?? "foo"; // 抛出 SyntaxError
示例
const foo = undefined ?? "foo"
const bar = null ?? "bar"
console.log(foo) // foo
console.log(bar) // bar
const foo = "" ?? 'default string';
const foo2 = "" || 'default string';
console.log(foo); // ""
console.log(foo2); // "default string"
const baz = 0 ?? 42;
const baz2 = 0 || 42;
console.log(baz); // 0
console.log(baz2); // 42
可选链 Optional chaining -- es2020
可选链操作符( ?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于 .
链式操作符,不同之处在于,在引用为 null
或者 undefined
的情况下不会引起错误,该表达式短路返回值是 undefined
。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined
。
示例
const user = {
address: {
street: 'xx街道',
getNum() {
return '80号'
}
}
}
// 在之前的语法中,想获取到深层属性或方法,不得不做前置校验,否则很容易命中 Uncaught TypeError: Cannot read property... 这种错误,这极有可能让整个应用挂掉。
const street = user && user.address && user.address.street
const num = user && user.address && user.address.getNum && user.address.getNum()
console.log(street, num)
// 用了 Optional Chaining ,上面代码会变成
const street2 = user?.address?.street
const num2 = user?.address?.getNum?.()
console.log(street2, num2)
与空值合并操作符一起使用
let customer = {
name: "jimmy",
details: { age: 18 }
};
let customerCity = customer?.city ?? "成都";
console.log(customerCity); // "成都"
注意点
可选链不能用于赋值
let object = {};
object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment
globalThis -- es2020
在以前,从不同的 JavaScript 环境中获取全局对象需要不同的语句。在 Web 中,可以通过 window
、self
取到全局对象,在 Node.js 中,它们都无法获取,必须使用 global
。
现在globalThis
提供了一个标准的方式来获取不同环境下的全局 this
对象(也就是全局对象自身)。不像 window
或者 self
这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用 globalThis
,不必担心它的运行环境。
为便于记忆,你只需要记住,全局作用域中的 this
就是globalThis
。以后就用globalThis
就行了。
pc_platform
对 globalThis
兼容处理
Promise.allSettled() -- es2020
我们都知道 Promise.all()
具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常(reject),所有任务都会挂掉,Promise直接进入reject 状态。
场景:现在页面上有三个请求,分别请求不同的数据,如果一个接口服务异常,整个都是失败的,都无法渲染出数据
我们需要一种机制,如果并发任务中,无论一个任务正常或者异常,都会返回对应的的状态,这就是Promise.allSettled
的作用
const promise1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("promise1");
// reject("error promise1 ");
}, 3000);
});
};
const promise2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("promise2");
// reject("error promise2 ");
}, 1000);
});
};
const promise3 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve("promise3");
reject("error promise3 ");
}, 2000);
});
};
// Promise.all 会走到catch里面
Promise.all([promise1(), promise2(), promise3()])
.then((res) => {
console.log(res);
})
.catch((error) => {
console.log("error", error); // error promise3
});
// Promise.allSettled 不管有没有错误,三个的状态都会返回
Promise.allSettled([promise1(), promise2(), promise3()])
.then((res) => {
console.log(res);
// 打印结果
// [
// {status: 'fulfilled', value: 'promise1'},
// {status: 'fulfilled',value: 'promise2'},
// {status: 'rejected', reason: 'error promise3 '}
// ]
})
.catch((error) => {
console.log("error", error);
});
Promise.any -- (实验性)
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。
只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态
const promise1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("promise1");
// reject("error promise1 ");
}, 3000);
});
};
const promise2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("promise2");
// reject("error promise2 ");
}, 1000);
});
};
const promise3 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("promise3");
// reject("error promise3 ");
}, 2000);
});
};
Promise.any([promise1(), promise2(), promise3()])
.then((first) => {
// 只要有一个请求成功 就会返回第一个请求成功的
console.log(first); // 会返回promise2
})
.catch((error) => {
// 所有三个全部请求失败 才会来到这里
console.log("error", error);
});
优雅处理 async/await 异常
正常情况async/await
使用起来确实方便、简洁,也避免了出现回调地狱的情况
但我们使用 async/await
时候往往需要使用try catch
包裹避免 Promise.reject()
中断后续代码执行
/**
* @description: 对awaited异步进行包装,异常处理 (使用async/await时避免try/catch进行处理)
* @param {*} awaited 需要进行异常处理的请求 (一般情况下是一个Promise)
* @return {Promise} 返回一个Promise,在调用的地方通过async/await 解构
* 使用:let [err, result] = await this.wrappedAwait(awaited), 正常请求时result为接口返回的数据,异常时err为错误信息
*/
function wrappedAwait(awaited) {
let p = Promise.resolve(awaited); // 非Promise则转为Promise
return p.then(
res => [null, res],
err => [err, null]
);
}
async function test() {
const [err, res] = await wrappedAwait({ userId: 123 });
console.log('__err__', err);
// __err__ null
console.log('__res__', res);
// __res__ {userId: 123}
}
async function test2() {
const [err, res] = await wrappedAwait(Promise.reject('a error'));
console.log('__err__', err);
// __res__ a error
console.log('__res__', res);
// __res__ null
}
test()
test2()
逻辑运算符和赋值表达式(&&=,||=,??=)-- es2020
&&=
逻辑与赋值 x &&= y
等效于:x && (x = y);
let a = 1;
let b = 0;
a &&= 2;
console.log(a); // 2
b &&= 2;
console.log(b); // 0
||=
逻辑或赋值(x ||= y
)运算仅在 x
为false时赋值。x ||= y
等同于:x || (x = y);
const a = { duration: 50, title: '' };
a.duration ||= 10;
console.log(a.duration); // 50
a.title ||= 'title is empty';
console.log(a.title); // "title is empty"
??=
逻辑空赋值运算符 (x ??= y
) 仅在 x
是 null
或 undefined
时对其赋值。
x ??= y
等价于:x ?? (x = y);
function config(options) {
options.duration ??= 100;
options.speed ??= 25;
return options;
}
config({ duration: 125 }); // { duration: 125, speed: 25 }
config({}); // { duration: 100, speed: 25 }
Array.prototype.at -- es2022
Array.prototype.at()
接收一个正整数或者负整数作为参数,表示获取指定位置的成员
参数正数就表示顺数第几个,负数表示倒数第几个,这可以很方便的某个数组末尾的元素
var arr = [1, 2, 3, 4, 5]
// 以前获取最后一位
console.log(arr[arr.length-1]) //5
// 简化后
console.log(arr.at(-1)) // 5
forEach如何跳出循环?
forEach的的回调函数形成了一个作用域,在里面使用return
并不会跳出,只会被当做continue
利用 try catch
function getItemById(arr, id) {
let item = null;
try {
arr.forEach(function (curItem, i) {
if (curItem.id === id) {
item = curItem;
throw Error();
}
})
} catch (e) {
}
return item;
}
数组映射
我们最常用的数组映射方法是 Array.map
,另一种方法就是Array.from
const cities = [
{ name: 'Paris', visited: 'no' },
{ name: 'Lyon', visited: 'no' },
{ name: 'Marseille', visited: 'yes' },
{ name: 'Rome', visited: 'yes' },
{ name: 'Milan', visited: 'no' },
{ name: 'Palermo', visited: 'yes' },
{ name: 'Genoa', visited: 'yes' },
{ name: 'Berlin', visited: 'no' },
{ name: 'Hamburg', visited: 'yes' },
{ name: 'New York', visited: 'yes' }
];
const cityNames = Array.from(cities, ({ name}) => name);
console.log(cityNames);
// 输出 ["Paris", "Lyon", "Marseille", "Rome", "Milan", "Palermo", "Genoa", "Berlin", "Hamburg", "New York"]
解构原始数据
const rawUser = {
name: 'Jona',
surname: 'Doe',
email: 'john@doe.com',
displayName: 'SuperCoolJohn',
joined: '2021-03-31',
image: 'path-to-the-image',
followers: 45
}
let user = {}, userDetails = {};
({ name: user.name, surname: user.surname, ...userDetails } = rawUser);
console.log(user);
// 输出 { name: "John", surname: "Doe" }
console.log(userDetails);
// 输出 { email: "john@doe.com", displayName: "SuperCoolJohn", joined: "2016-05-05", image: "path-to-the-image", followers: 45 }
快速创建一个m * n的二维数组
// 方式1:利用new Array和map方法
function makeMatrix1(m, n) {
return new Array(m).fill(0).map(() => new Array(n).fill(0));
}
// 方式2:Array.from可以在创建数组时传入一个控制函数,对每一项进行处理,其返回值即为每一项的值(相当于一个map方法)
function makeMatrix2(m, n) {
return Array.from(new Array(m), () => new Array(n).fill(0));
}
对象属性链式get
场景
一个obj对象嵌套了很多层, 给定一个字符串key = 'first.second.third'
, 根据key得到最终的obj.first.second.third
的值
const obj = {
first: {
second: {
third: 'message'
}
}
};
const getResultValue = (obj, key) => key.split('.').reduce((o, i) => {
if (o) return o[i];
}, obj);
const key = 'first.second.third';
const result = getResultValue(obj, key);
console.log(result);
// message
奇偶性判断 -- 按位与(&)
- 按位与运算是把两个操作数转换成二进制再逐位比较,当前位都为1结果为1,否则为0。
- 而所有数字转化为二进制的奇偶性就只用看末尾,奇数尾数为1,偶数尾数为0
const a = 2
a & 1 ? alert('a是奇数!') : alert('a是偶数!')
数字 -1 的判断 -- 按位取反(~)
判断一个数是否为-1是我们经常遇到的,String.indexOf()
在查找字符串的时候没有找到会返回-1,Arroy.findIndex()
方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1,很多程序,插件,框架错误状态值默认返回-1,在位运算中,~-1===0
的。
const str = 'sjoeai'
// 常规
if ( str.indexOf('a') != -1 ) {
alert('a在字符串str中');
}
// Better ~取反
if ( ~str.indexOf('a') ) {
alert('a在字符串str中');
}
数字取整 -- (~~)
let num = 13.14;
// parseInt()
console.log(parseInt(num)); // 参数如果为NaN、null、undefined等等会得到NaN
// Math.floor()
console.log(Math.floor(num)); // 参数如果为字符串、NaN、undefined等等会得到NaN,参数为null结果为0
// ~~
console.log(~~num); // 非数字都会转化为0
生成随机字符串
方式1 (wxt-utils
里的随机生成字符串就是类似于此方法)
/**
* @description: 生成随机字符串
* @param {Number} len 想要生成的随机字符串长度
* @param {Boolean} isPlainNumber 随机字符串是否纯数字
* @return {String} 要输出的字符串
*/
function randomHandler(len = 16, isPlainNumber = false) {
let chars = isPlainNumber ? '1234567890' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
let result = '';
for (let i = 0; i < len; i++) {
let currIndex = ~~(Math.random() * chars.length);
result += chars[currIndex];
}
return result;
}
console.log(randomHandler(20));
方式2
利用Math.random()
和toString()
方法巧妙生成随机字符串
Math.random()
可以生成一个[0, 1)区间的随机数toString(radix)
方法可以将数字转化为radix
进制的字符串
console.log(Math.random()) // 0.4219865731472241
console.log(Math.random()) // 0.6486352330721274
console.log(Math.random()) // 0.862253082562344
console.log(Math.random().toString()) // 0.8898541967454725
console.log(Math.random().toString()) // 0.21295312937219646
console.log(Math.random().toString(36).substring(2)) // ni6869kv5
console.log(Math.random().toString(36).substring(2)) // 1cs693i4bd1
console.log(Math.random().toString(36).substring(2)) // bo740ginr0w
发现了一点小瑕疵:Math.random()
生成的小数点后面的数字长度是不固定的
那就稍微优化一下:如果一次random达不到所需的目标长度,那就random多次后拼接起来呗
/**
* @description: 生成随机字符串
* @param {Number} len 想要生成的随机字符串长度
* @param {Boolean} isPlainNumber 随机字符串是否纯数字
* @return {String} 要输出的字符串
*/
function randomHandler(len = 16, isPlainNumber = false) {
let result = '';
// 如果要求纯数字,则转化为10进制,否则转化为36进制 (26个字母+10个数字)
let radix = isPlainNumber ? 10 : 36;
// 使用substring(2) 保留小数位,去掉整数和小数点
let getOnce = () => Math.random().toString(radix).substring(2);
while(result.length < len) {
result += getOnce();
}
return result.substring(0, len);
}
console.log(randomHandler(20));
发现利用Math.random().toString(16)
貌似直接就可以实现一个常见的小功能:生成随机颜色
function getRandomColor() {
return `#${Math.random().toString(16).substr(2, 6)}`
}
console.log(getRandomColor())
// #a8119b
交换两个变量
let x = 1;
let y = 2;
let temp = x;
x = y;
y = temp;
// Better
[x, y] = [y, x];
处理多个条件
const conditions = ["Condition 2","Condition String2"];
someFunction(str){
return str.includes("someValue1") || str.includes("someValue2")
}
// Better
someFunction(str){
const conditions = ["someValue1","someValue2"];
return conditions.some(condition=>str.includes(condition));
}
动态键值对 替换 switch 或 if else
const UserRole = {
ADMIN: "Admin",
GENERAL_USER: "GeneralUser",
SUPER_ADMIN: "SuperAdmin",
};
function getRoute(userRole = "default role"){
switch(userRole){
case UserRole.ADMIN:
return "/admin"
case UserRole.GENERAL_USER:
return "/GENERAL_USER"
case UserRole.SUPER_ADMIN:
return "/superadmin"
default:
return "/Default path"
}
}
console.log(getRoute(UserRole.ADMIN)) // return "/admin"
console.log(getRoute("Anything")) // return Default path
console.log(getRoute()) // return Default path
console.log(getRoute(null)) // return Default path
// Better
const appRoute = {
[UserRole.ADMIN]: "/admin",
[UserRole.GENERAL_USER]: "/user",
[UserRole.SUPER_ADMIN]: "/superadmin"
};
function getRoute(userRole = "default role"){
return appRoute[userRole] || "Default path";
}