1. 有条件地向对象、数组添加属性
1) 向对象添加属性
可以使用展开运算符来有条件地向对象中添加属性:
const condition = true;const person = { id: 1, name: "dby", ...(condition && { age: 12 }),};假如 condition 为 true ,则 { age: 16 } 会被添加到对象中;假如 condition 为 false ,相当于展开 false ,不会对对象产生任何影响
2) 向数组添加属性
这是 CRA 中 Webpack 设置的源码:
module.exports = { plugins: [ new HtmlWebpackPlugin(), isEnvProduction && new MiniCssExtractPlugin(), useTypeScript && new ForkTsCheckerWebpackPlugin(), ].filter(Boolean),}上面代码中,只有 isEnvProduction 为 true 才会添加 MiniCssExtractPlugin 插件;当 isEnvProduction 为 false 则会添加 false 到数组中,以是末了使用了 filter 过滤
上面如许写,逻辑上是没有问题,但是在 TS 项目中使用的时间,范例推断会有点问题。由于存在一种布尔值添加到数组的中心状态,而 TS 只能静态分析,不能实行代码。
试问下面 arr 数组的范例:
const arr = ['111', true && '222'].filter(Boolean); // string[]const arr = ['111', false && '222'].filter(Boolean); // (string | boolean)[]下面是一种范例安全的做法:
module.exports = { plugins: [ new HtmlWebpackPlugin(), ...(isEnvProduction ? [new MiniCssExtractPlugin()] : []), ...(useTypeScript ? [new ForkTsCheckerWebpackPlugin()] : []), ],}3. 数组小本领
1) 访问数组末了一个元素
在开发中我们常常须要访问数组末了一个元素,通例的做法是如许:
const rollbackUserList = [1, 2, 3, 4];const lastUser = rollbackUserList[rollbackUserList.length - 1]; // 4上面如许写逻辑上没问题,但是我们发现当数组的变量名很长的时间,整个表达式会变得非常长,影响代码可读性。一种方法是把上述逻辑封装一下:
// 将获取数组末了一个元素的逻辑封装为一个函数const getLastEle = (arr) => arr[arr.length - 1];const lastUser = getLastEle(rollbackUserList); // 4可以看到如许语义性就比力好了。尚有一种是使用 Array.prototype.slice 方法,通过通报负索引,从右往左定位到末了一个元素:
const lastUser = rollbackUserList.slice(-1)[0]; // 4// 大概使用解构赋值const [lastUser] = rollbackUserList.slice(-1); // 4注意 slice 方法返回的是一个 新数组 而不是元素本身,须要手动从数组中取出元素,看起来不是很优雅。现实上 ES2022 专门提供了一个 Array.prototype.at 方法,用于根据给定索引获取数组元素:
const lastUser = rollbackUserList.at(-1); // 4at 方法通报索引为正时从左往右定位(这与直接通过下标访问的作用同等),索引为负时从右往左定位。在访问数组末了一个元素的场景中非常好用。从 Chrome 92 开始已经支持 at 方法,core-js 也提供了 polyfill。
developer.mozilla.org/en-US/docs/…
2) 异步函数组合(异步串行调用)
我们知道,在函数式编程中有一个函数组合 compose 的本领,可以把多个函数组合为一个函数,创建一个从右到左的数据流,右边函数实行的效果作为参数传入左边。比方:
/** * 给定参数:[A, B, C, D] * 调用次序:A(B(C(D()))) */const compose = (middlewares) => (initialValue) => middlewares.reduceRight( (accu, cur) => cur(accu), initialValue);从上面的代码可以看出 compose 是不能用于 Promise,由于 compose 须要将上一次调用的返回值,作为参数传入下一次调用,但 Promise 本身是一个包装过的数据布局,只有通过 then 方法才华拿到返回值。以是假如是异步的情况,我们通常会把 reduce 改成平常 FOR 循环。
假如碰到异步的情况,比方想做一系列串行的哀求,是否可以更优雅呢?本人在 Vite 源码中找到了答案,在 Vite 源码中有这么一段:
await postBuildHooks.reduce( (queue, hook) => queue.then(() => hook(build as any)), Promise.resolve();)参考: github.com/vitejs/vite…
可以看出,这里对 reducer 函数举行了包装,将 hook 的实行放到 then 方法回调中,如许就可以保证调用次序。按照这个思绪,我们对上面 compose 方法稍微改动一下,就可以得到一个 asyncCompose:
/** * 给定参数:[A, B, C, D] * 调用次序:Promise.resolve().then(res => D(res)).then(res => C(res)).then(res => B(res)).then(res => A(res)) * 以上只是为了让各人看清楚,简化之后如下:Promise.resolve().then(D).then(C).then(B).then(A) */const asyncCompose = (middlewares) => (initialValue) => middlewares.reduceRight( (queue, hook) => queue.then(res => hook(res)), Promise.resolve(initialValue));4. 解构赋值
解构赋值很方便,项目中常常会用到,可以分为以下两个场景:
这里先容四种常用本领。
1) 深度解构
大部门时间我们只会解构一层,但现实上解构赋值是可以深度解构的:
let obj = { name: "dby", a: { b: 1 }}const { a: { b } } = obj;console.log(b); // 12) 解构时使用别名
假如后端返回的对象键名不是我们想要的,可以使用别名:
const obj = { // 这个键名太长了,我们渴望把它换掉 aaa_bbb_ccc: { name: "dby", age: 12, sex: true }}const { aaa_bbb_ccc: user } = obj;console.log(user); // { name: "dby", age: 12, sex: true }3) 解构时使用默认值
对象的解构可以使用默认值,默认值见效的条件是,对象的属性值严格即是 undefined :
fetchUserInfo() .then(({ aaa_bbb_ccc: user = {} }) => { // ... })以上三个特性可以结合使用
4) 使用短路克制报错
解构赋值固然好用,但是要注意解构的对象不能为 undefined 、null ,否则会报错,故要给被解构的对象一个默认值:
const {a,b,c,d,e} = obj || {};5. 展开运算符
使用展开运算符归并两个数组大概两个对象:
const a = [1,2,3];const b = [1,5,6];// badconst c = a.concat(b);// goodconst c = [...new Set([...a,...b])];const obj1 = { a:1 };const obj2 = { b:1 };// badconst obj = Object.assign({}, obj1, obj2);// goodconst obj = { ...obj1, ...obj2 };这里要注意一个问题,对象和数组归并固然看上去都是 ... ,但是现实上是有区别的。
ES2015 扩展运算符只规定了在数组和函数参数中使用,但并没有规定可以在对象中使用,而且是基于 for...of 的,因此被展开的只能是数组、字符串、Set、Map 等可迭代对象,假如将平常对象展开到数组就会报错。
对象中的 ... 现实上是 ES2018 中的对象展开语法,相当于 Object.assign :
babeljs.io/docs/en/bab…
6. 查抄属性是否存在对象中
可以使用 in 关键字查抄对象中是否存在某个属性:
const person = { name: "dby", salary: 1000 };console.log('salary' in person); // trueconsole.log('age' in person); // false但是 in 关键字实在并不安全,会把原型上的属性也包罗进去,比方:
"hasOwnProperty" in {}; // true"toString" in {}; // true以是保举使用下面的方法举行判断:
Object.prototype.hasOwnProperty.call(person, "salary"); // true不外上面如许的问题就是太长了,每次使用都要如许写很贫困。ECMA 有一个新的提案 Object.hasOwn() ,相当于 Object.prototype.hasOwnProperty.call() 的别名:
Object.hasOwn(person, "salary"); // truedeveloper.mozilla.org/en-US/docs/…
须要注意这个语法存在兼容性问题(Chrome > 92),不外只要精确设置 polyfill 就可以放心使用。
7. 对象的遍历
项目中很多同事都会如许写:
for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { // ... }}吐槽:用 Object.keys 大概 Object.entries 转成数组就可以用数组方法遍历了,而且遍历的是自身属性,不会遍历到原型上。
Object.keys(obj).forEach(key => { // ...})Object.entries(obj).forEach(([key, val]) => { // ...})举个例子,将对象的 key、value 拼接为查询字符串:
const _stringify = (obj) => Object.entries(obj).map(([key, val]) => `${key}=${val}`).join("&");_stringify({ a: 1, b: 2, c: "2333"});// 'a=1&b=2&c=2333'反驳:偶然候不想遍历整个对象,数组方法不能用 break 停止循环呀。
吐槽:看来你对数组方法把握还是不够彻底,使用 find 方法找到符合条件的项就不会继承遍历。
Object.keys(obj).find(key => key === "name");总结:在开发中只管不要写 for 循环,无论数组和对象。对象就通过 Object.keys 、Object.values 、Object.entries 转为数组举行遍历。如许可以写成 JS 表达式,充实使用函数式编程。
8. 使用 includes 简化 if 判断
在项目中常常看到如许的代码:
if (a === 1 || a === 2 || a === 3 || a === 4) { // ...}可以使用 includes 简化代码:
if ([1, 2, 3, 4].includes(a)) { // ...}9. Promise 三点总结
1) async/await 优雅非常处置惩罚
在 async 函数中,只要此中某个 Promise 报错,整个 async 函数的实行就停止了,因此非常处置惩罚非常紧张。但现实上 async 函数的非常处置惩罚非常贫困,很多同事都不乐意写。有没有一种简单的方法呢?看到一个 await-to-js 的 npm 包,可以优雅处置惩罚 async 函数的非常,不须要手动添加 try...catch 捕捉非常:
import to from 'await-to-js';async function asyncFunctionWithThrow() { const [err, user] = await to(UserModel.findById(1)); if (!user) throw new Error('User not found');}www.npmjs.com/package/awa…
现实上就是在 await 前面返回的 Promise 封装了一层,提前处置惩罚非常。源码非常简单,本人本身也实现了下:
function to(awaited) { // 不管是不是 Promise 划一转为 Promise const p = Promise.resolve(awaited); // await-to-js 接纳 then...catch 的用法 // 但现实上 then 方法第一个回调函数内里并不包罗会抛出非常的代码 // 因此使用 then 方法第二个回调函数捕捉非常,不须要额外的 catch return p.then( res => { return [null, res]; }, err => { return [err, undefined]; } )}2) Promise 作为状态机
看到有同事写过如许的代码:
function validateUserInfo(user) { if (!userList.find(({ id }) => id === user.id)) { return { code: -1, message: "用户未注册" } } if ( !userList.find( ({ username, password }) => username === user.username && password === user.password ) ) { return { code: -1, message: "用户名或暗码错误" } } return { code: 0, message: "登录乐成" }}观察发现这边实在就是两个状态,然后还须要一个字段提示操纵效果。这种情况下我们可以使用 Promise 。有人说为啥咧,显着没有异步逻辑啊。我们知道,Promise 实在就是一个状态机,即使不须要处置惩罚异步逻辑,我们也可以使用状态机的特性:
function validateUserInfo(user) { if (!userList.find(({ id }) => id === user.id)) { return Promise.reject("用户未注册"); } if ( !userList.find( ({ username, password }) => username === user.username && password === user.password ) ) { return Promise.reject("用户名或暗码错误"); } return Promise.resolve("登录乐成");}// 使用如下validateUserInfo(userInfo) .then(res => { message.success(res); }) .catch(err => { message.error(err); })显着如许代码就变得非常优雅了,但实在还可以更优雅。我们知道 async 函数返回值是一个 Promise 实例,因此下面两个函数是等价的:
// 平常函数返回一个 Promsie.resolve 包裹的值const request = (x) => Promise.resolve(x);// async 函数返回一个值const request = async (x) => x;既然末了返回一个 Promise ,为何不直接在函数前面加 async 修饰符呢。如许乐成的效果只要直接返回就行,不消 Promise.resolve 包裹:
async function validateUserInfo(user) { if (!userList.find(({ id }) => id === user.id)) { return Promise.reject("用户未注册"); } if ( !userList.find( ({ username, password }) => username === user.username && password === user.password ) ) { return Promise.reject("用户名或暗码错误"); } return "登录乐成";}对 async 函数不认识的同砚,可以参考 阮一峰 ES6 教程
更进一步,由于在 Promise 内部抛出非常等同于被 reject ,因此我们可以使用 throw 语句更换 Promise.reject() :
async function validateUserInfo(user) { if (!userList.find(({ id }) => id === user.id)) { throw "用户未注册"; } if ( !userList.find( ({ username, password }) => username === user.username && password === user.password ) ) { throw "用户名或暗码错误"; } return "登录乐成";}throw 语句的用法可以参考 MDN 文档
3) Promise 两点使用误区
不发起在 Promise 内里使用 try...catch,如许即使 Promise 内部报错,状态仍然是 fullfilled,会进入 then 方法回调,不会进入 catch 方法回调。
function request() { return new Promise((resolve, reject) => { try { // ... resolve("ok"); } catch(e) { console.log(e); } })}request() .then(res => { console.log("哀求效果:", res); }) .catch(err => { // 由于在 Promise 中使用了 try...catch // 因此即使 Promise 内部报错,也不会被 catch 捕捉到 console.log(err); })Promise 内部的非常,老诚实实往外抛就行,让 catch 方法来处置惩罚,符合单一职责原则
不发起在 async 函数中,既倒霉用 await,也倒霉用 return,如许就算内部的 Promise reject 也无法捕捉到:
async function handleFetchUser(userList) { // 这里既没有使用 await,也没有使用 return Promise.all(userList.map(u => request(u)));}handleFetchUser(userList) .then(res => { // 由于没有返回值,这里拿到的是 undefined console.log(res); }) .catch(err => { // 即使 handleFetchUser 内部的 Promise reject // async 函数返回的 Promise 仍然是 fullfilled // 此时仍然会进入 then 方法回调,无法被 catch 捕捉到 console.log(err); })假如确实有这种需求,发起不要使用 async 函数,直接改用平常函数即可
10. 字符串小本领
1) 字符串不满两位补零
这个需求在开发中挺常见。比方,调用 Date api 获取到日期大概只有一位:
let date = new Date().getDate(); // 3通例做法:
if (data.toString().length == 1) { date = `0${date}`;}使用 String.prototype.slice :
// 不管几位,都在前面拼接一个 0 ,然后截取末了两位date = `0${date}`.slice(-2);使用 String.prototype.padStart :
// 当字符串长度小于第一个参数值,就在前面补第二个参数date = `${date}`.padStart(2, 0);2) 千分位分隔符
实现如下的需求:
如许看起来非常符合 (?=p) 的规律,p 可以表现每三个数字,要添加逗号所处的位置正好是 (?=p) 匹配出来的位置。
第一步,先实行把末了一个逗号弄出来:
"300000000".replace(/(?=\d{3}$)/, ",")// '300000,000'第二步,把全部逗号都弄出来:
"300000000".replace(/(?=(\d{3})+$)/g, ",")// ',300,000,000'使用括号把一个 p 模式变成一个团体
第三步,去掉首位的逗号:
"300000000".replace(/(?!^)(?=(\d{3})+$)/g, ",")// '300,000,000'3) 借用数组拼接字符串
很多同砚都知道 模板字符串 可以很方便地举行字符串拼接,但是须要拼接较多参数的时间,如许就显得比力贫困:
// HH -> 23// mm -> 58// ss -> 32const timeString = `${HH} {mm} {ss}`;// scheme -> https://// host -> 10.3.71.108// port -> :8080const URLString = `${scheme}${host}${port}`现实上,拼接字符串,除了使用模板字符串的方式,还可以使用数组:
const timeString = [HH, mm, ss].join(":");const URLString = [scheme, host, port].join("");趁便一提,本人之前维护过一个 jQuery 项目,就使用这种方式拼接 html 模板:
const dataSource = ["dby", "dm", "233"];const template = dataSource.map(name => `<div>${name}</div>`).join("");4) 判断字符串前缀、后缀
判断字符串前缀、后缀不要一言不合就使用正则表达式:
const url = "https://bili98.cn";const isHTTPS = /^https:\/\//.test(url); // trueconst fileName = "main.py";const isPythonCode = /\.py$/.test(fileName); // true保举使用 String.prototype.startsWith 和 String.prototype.endsWith,语义性更好:
const url = "https://bili98.cn";const isHTTPS = url.startsWith("https://") // trueconst fileName = "main.py";const isPythonCode = fileName.endsWith(".py"); // true转载:https://juejin.cn/post/7021047385337888775 |