js function 返回值的函数(js获取后端方法的返回值)

大厂技术高级前端Node进阶

回复1,加入高级Node交流群

总结了一些开发常用的 JS 小技巧,让你的代码更优雅!
js function 返回值的函数(js获取后端方法的返回值)

1. 使用 const 定义

在开发中不要过度声明变量,尽量使用表达式和链式调用形式。然后一般能用 const 就不要用 let 。这种模式写多了之后,你会发现在项目中几乎找不到几个用 let 的地方:

//badletresult=false;if(userInfo.age>30){result=true;}//goodconstresult=userInfo.age>30;

在项目中很多同事都会这样写,

handleFormChange(e){letisUpload;if(e.target.value===’upload’){isUpload=true;}else{isUpload=false;}}

但实际上 == 和 === 的表达式可以直接给变量赋值:

handleFormChange(e){constisUpload=(e.target.value===’upload’);}

如果要取反也很简单:

handleFormChange(e){constisManual=(e.target.value!==’upload’);}2. 有条件地向对象、数组添加属性1) 向对象添加属性

可以使用展开运算符来有条件地向对象中添加属性:

constcondition=true;constperson={id:1,name:”dby”,…(condition&&{age:12}),};

如果 condition 为 true ,则 { age: 16 } 会被添加到对象中;如果 condition 为 false ,相当于展开 false ,不会对对象产生任何影响

2) 向数组添加属性

这是 CRA 中 Webpack 配置的源码:

module.exports={plugins:[newHtmlWebpackPlugin(),isEnvProduction&&newMiniCssExtractPlugin(),useTypeScript&&newForkTsCheckerWebpackPlugin(),].filter(Boolean),}

上面代码中,只有 isEnvProduction 为 true 才会添加 MiniCssExtractPlugin 插件;当 isEnvProduction 为 false 则会添加 false 到数组中,所以最后使用了 filter 过滤

上面这样写,逻辑上是没有问题,但是在 TS 项目中使用的时候,类型推断会有点问题。因为存在一种布尔值添加到数组的中间状态,而 TS 只能静态分析,不能执行代码。

试问下面 arr 数组的类型:

constarr=[‘111′,true&&’222’].filter(Boolean);//string[]constarr=[‘111′,false&&’222’].filter(Boolean);//(string|boolean)[]

下面是一种类型安全的做法:

module.exports={plugins:[newHtmlWebpackPlugin(),…(isEnvProduction?[newMiniCssExtractPlugin()]:[]),…(useTypeScript?[newForkTsCheckerWebpackPlugin()]:[]),],}3. 数组小技巧1) 访问数组最后一个元素

在开发中我们常常需要访问数组最后一个元素,常规的做法是这样:

constrollbackUserList=[1,2,3,4];constlastUser=rollbackUserList[rollbackUserList.length-1];//4

上面这样写逻辑上没问题,但是我们发现当数组的变量名很长的时候,整个表达式会变得非常长,影响代码可读性。一种方法是把上述逻辑封装一下:

//将获取数组最后一个元素的逻辑封装为一个函数constgetLastEle=(arr)=>arr[arr.length-1];constlastUser=getLastEle(rollbackUserList);//4

可以看到这样语义性就比较好了。还有一种是利用 Array.prototype.slice 方法,通过传递负索引,从右往左定位到最后一个元素:

constlastUser=rollbackUserList.slice(-1)[0];//4//或者使用解构赋值const[lastUser]=rollbackUserList.slice(-1);//4

注意 slice 方法返回的是一个 新数组 而不是元素本身,需要手动从数组中取出元素,看起来不是很优雅。实际上 ES2022 专门提供了一个 Array.prototype.at 方法,用于根据给定索引获取数组元素:

constlastUser=rollbackUserList.at(-1);//4

at 方法传递索引为正时从左往右定位(这与直接通过下标访问的作用一致),索引为负时从右往左定位。在访问数组最后一个元素的场景中非常好用。从 Chrome 92 开始已经支持 at 方法,core-js 也提供了 polyfill。

developer.mozilla.org/en-US/docs/…[1]

2) 异步函数组合(异步串行调用)

我们知道,在函数式编程中有一个函数组合 compose 的技巧,可以把多个函数组合为一个函数,创建一个从右到左的数据流,右边函数执行的结果作为参数传入左边。例如:

/***给定参数:[A,B,C,D]*调用顺序:A(B(C(D())))*/constcompose=(middlewares)=>(initialValue)=>middlewares.reduceRight((accu,cur)=>cur(accu),initialValue);

从上面的代码可以看出 compose 是不能用于 Promise,因为 compose 需要将上一次调用的返回值,作为参数传入下一次调用,但 Promise 本身是一个包装过的数据结构,只有通过 then 方法才能拿到返回值。所以如果是异步的情况,我们通常会把 reduce 改成普通 FOR 循环。

如果遇到异步的情况,例如想做一系列串行的请求,是否可以更优雅呢?本人在 Vite 源码中找到了答案,在 Vite 源码中有这么一段:

awaitpostBuildHooks.reduce((queue,hook)=>queue.then(()=>hook(buildasany)),Promise.resolve();)

参考: github.com/vitejs/vite…[2]

可以看出,这里对 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)*/constasyncCompose=(middlewares)=>(initialValue)=>middlewares.reduceRight((queue,hook)=>queue.then(res=>hook(res)),Promise.resolve(initialValue));4. 解构赋值

解构赋值很方便,项目中经常会用到,可以分为以下两个场景:

对象/数组的解构;函数参数解构;

这里介绍四种常用技巧。

1) 深度解构

大部分时候我们只会解构一层,但实际上解构赋值是可以深度解构的:

letobj={name:”dby”,a:{b:1}}const{a:{b}}=obj;console.log(b);//12) 解构时使用别名

假如后端返回的对象键名不是我们想要的,可以使用别名:

constobj={//这个键名太长了,我们希望把它换掉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. 展开运算符

使用展开运算符合并两个数组或者两个对象:

consta=[1,2,3];constb=[1,5,6];//badconstc=a.concat(b);//goodconstc=[…newSet([…a,…b])];constobj1={a:1};constobj2={b:1};//badconstobj=Object.assign({},obj1,obj2);//goodconstobj={…obj1,…obj2};

这里要注意一个问题,对象和数组合并虽然看上去都是 … ,但是实际上是有区别的。

ES2015 扩展运算符只规定了在数组和函数参数中使用,但并没有规定可以在对象中使用,并且是基于 for…of 的,因此被展开的只能是数组、字符串、Set、Map 等可迭代对象,假如将普通对象展开到数组就会报错。

对象中的 … 实际上是 ES2018 中的对象展开语法,相当于 Object.assign :

babeljs.io/docs/en/bab…[3]

6. 检查属性是否存在对象中

可以使用 in 关键字检查对象中是否存在某个属性:

constperson={name:”dby”,salary:1000};console.log(‘salary’inperson);//trueconsole.log(‘age’inperson);//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”);//true

developer.mozilla.org/en-US/docs/…[4]

需要注意这个语法存在兼容性问题(Chrome > 92),不过只要正确配置 polyfill 就可以放心使用。

7. 对象的遍历

项目中很多同事都会这样写:

for(letkeyinobj){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 捕获异常:

importtofrom’await-to-js’;asyncfunctionasyncFunctionWithThrow(){const[err,user]=awaitto(UserModel.findById(1));if(!user)thrownewError(‘Usernotfound’);}

www.npmjs.com/package/awa…[5]

实际上就是在 await 前面返回的 Promise 封装了一层,提前处理异常。源码非常简单,本人自己也实现了下:

functionto(awaited){//不管是不是Promise一律转为Promiseconstp=Promise.resolve(awaited);//await-to-js采用then…catch的用法//但实际上then方法第一个回调函数里面并不包含会抛出异常的代码//因此使用then方法第二个回调函数捕获异常,不需要额外的catchreturnp.then(res=>{return[null,res];},err=>{return[err,undefined];})}2) Promise 作为状态机

看到有同事写过这样的代码:

functionvalidateUserInfo(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 其实就是一个状态机,即使不需要处理异步逻辑,我们也可以使用状态机的特性:

functionvalidateUserInfo(user){if(!userList.find(({id})=>id===user.id)){returnPromise.reject(“用户未注册”);}if(!userList.find(({username,password})=>username===user.username&&password===user.password)){returnPromise.reject(“用户名或密码错误”);}returnPromise.resolve(“登录成功”);}//使用如下validateUserInfo(userInfo).then(res=>{message.success(res);}).catch(err=>{message.error(err);})

明显这样代码就变得非常优雅了,但其实还可以更优雅。我们知道 async 函数返回值是一个 Promise 实例,因此下面两个函数是等价的:

//普通函数返回一个Promsie.resolve包裹的值constrequest=(x)=>Promise.resolve(x);//async函数返回一个值constrequest=async(x)=>x;

既然最后返回一个 Promise ,为何不直接在函数前面加 async 修饰符呢。这样成功的结果只要直接返回就行,不用 Promise.resolve 包裹:

asyncfunctionvalidateUserInfo(user){if(!userList.find(({id})=>id===user.id)){returnPromise.reject(“用户未注册”);}if(!userList.find(({username,password})=>username===user.username&&password===user.password)){returnPromise.reject(“用户名或密码错误”);}return”登录成功”;}

对 async 函数不熟悉的同学,可以参考 阮一峰 ES6 教程[6]

更进一步,由于在 Promise 内部抛出异常等同于被 reject ,因此我们可以使用 throw 语句替代 Promise.reject() :

asyncfunctionvalidateUserInfo(user){if(!userList.find(({id})=>id===user.id)){throw”用户未注册”;}if(!userList.find(({username,password})=>username===user.username&&password===user.password)){throw”用户名或密码错误”;}return”登录成功”;}

throw 语句的用法可以参考 MDN 文档[7]

3) Promise 两点使用误区

不建议在 Promise 里面使用 try…catch,这样即使 Promise 内部报错,状态仍然是 fullfilled,会进入 then 方法回调,不会进入 catch 方法回调。

functionrequest(){returnnewPromise((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 也无法捕捉到:

asyncfunctionhandleFetchUser(userList){//这里既没有使用await,也没有使用returnPromise.all(userList.map(u=>request(u)));}handleFetchUser(userList).then(res=>{//由于没有返回值,这里拿到的是undefinedconsole.log(res);}).catch(err=>{//即使handleFetchUser内部的Promisereject//async函数返回的Promise仍然是fullfilled//此时仍然会进入then方法回调,无法被catch捕捉到console.log(err);})

如果确实有这种需求,建议不要使用 async 函数,直接改用普通函数即可

10. 字符串小技巧1) 字符串不满两位补零

这个需求在开发中挺常见。例如,调用 Date api 获取到日期可能只有一位:

letdate=newDate().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->32consttimeString=`${HH}:${mm}:${ss}`;//scheme->https:////host->10.3.71.108//port->:8080constURLString=`${scheme}${host}${port}`

实际上,拼接字符串,除了使用模板字符串的方式,还可以使用数组:

consttimeString=[HH,mm,ss].join(“:”);constURLString=[scheme,host,port].join(“”);

顺便一提,本人之前维护过一个 jQuery 项目,就使用这种方式拼接 html 模板:

constdataSource=[“dby”,”dm”,”233″];consttemplate=dataSource.map(name=>`<div>${name}</div>`).join(“”);4) 判断字符串前缀、后缀

判断字符串前缀、后缀不要一言不合就使用正则表达式:

consturl=”https://bili98.cn”;constisHTTPS=/^https:\/\//.test(url);//trueconstfileName=”main.py”;constisPythonCode=/\.py$/.test(fileName);//true

推荐使用 String.prototype.startsWith 和 String.prototype.endsWith,语义性更好:

consturl=”https://bili98.cn”;constisHTTPS=url.startsWith(“https://”)//trueconstfileName=”main.py”;constisPythonCode=fileName.endsWith(“.py”);//true参考

我的代码简洁之道[8]

你会用ES6,那倒是用啊![9]

有个开发者总结这 15 优雅的 JavaScript 个技巧[10]

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

发表评论

登录后才能评论