實(shí)例詳解你不知道的十個(gè) JS 小技巧
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
原文鏈接: https://juejin.cn/post/7021047385337888775
我是加菲貓?? ,如果你喜歡我的文章,歡迎點(diǎn)贊分享哦
總結(jié)了一些開(kāi)發(fā)常用的 JS 小技巧,讓你的代碼更優(yōu)雅!
1. 使用 const 定義
在開(kāi)發(fā)中不要過(guò)度聲明變量,盡量使用表達(dá)式和鏈?zhǔn)秸{(diào)用形式。然后一般能用 const 就不要用 let 。這種模式寫(xiě)多了之后,你會(huì)發(fā)現(xiàn)在項(xiàng)目中幾乎找不到幾個(gè)用 let 的地方:
// bad
let result = false;
if (userInfo.age > 30) {
result = true;
}
// good
const result = userInfo.age > 30;
在項(xiàng)目中很多同事都會(huì)這樣寫(xiě),
handleFormChange(e) {
let isUpload;
if (e.target.value === 'upload') {
isUpload = true;
} else {
isUpload = false;
}
}
但實(shí)際上 == 和 === 的表達(dá)式可以直接給變量賦值:
handleFormChange(e) {
const isUpload = (e.target.value === 'upload');
}
如果要取反也很簡(jiǎn)單:
handleFormChange(e) {
const isManual = (e.target.value !== 'upload');
}
2. 有條件地向?qū)ο蟆?shù)組添加屬性
1) 向?qū)ο筇砑訉傩?/span>
可以使用展開(kāi)運(yùn)算符來(lái)有條件地向?qū)ο笾刑砑訉傩裕?/p>
const condition = true;
const person = {
id: 1,
name: "dby",
...(condition && { age: 12 }),
};
如果
condition為true,則{ age: 16 }會(huì)被添加到對(duì)象中;如果condition為false,相當(dāng)于展開(kāi)false,不會(huì)對(duì)對(duì)象產(chǎn)生任何影響
2) 向數(shù)組添加屬性
這是 CRA 中 Webpack 配置的源碼:
module.exports = {
plugins: [
new HtmlWebpackPlugin(),
isEnvProduction &&
new MiniCssExtractPlugin(),
useTypeScript &&
new ForkTsCheckerWebpackPlugin(),
].filter(Boolean),
}
上面代碼中,只有
isEnvProduction為true才會(huì)添加MiniCssExtractPlugin插件;當(dāng)isEnvProduction為false則會(huì)添加false到數(shù)組中,所以最后使用了filter過(guò)濾
上面這樣寫(xiě),邏輯上是沒(méi)有問(wèn)題,但是在 TS 項(xiàng)目中使用的時(shí)候,類型推斷會(huì)有點(diǎn)問(wèn)題。因?yàn)榇嬖谝环N布爾值添加到數(shù)組的中間狀態(tài),而 TS 只能靜態(tài)分析,不能執(zhí)行代碼。
試問(wèn)下面 arr 數(shù)組的類型:
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. 數(shù)組小技巧
1) 訪問(wèn)數(shù)組最后一個(gè)元素
在開(kāi)發(fā)中我們常常需要訪問(wèn)數(shù)組最后一個(gè)元素,常規(guī)的做法是這樣:
const rollbackUserList = [1, 2, 3, 4];
const lastUser = rollbackUserList[rollbackUserList.length - 1]; // 4
上面這樣寫(xiě)邏輯上沒(méi)問(wèn)題,但是我們發(fā)現(xiàn)當(dāng)數(shù)組的變量名很長(zhǎng)的時(shí)候,整個(gè)表達(dá)式會(huì)變得非常長(zhǎng),影響代碼可讀性。一種方法是把上述邏輯封裝一下:
// 將獲取數(shù)組最后一個(gè)元素的邏輯封裝為一個(gè)函數(shù)
const getLastEle = (arr) => arr[arr.length - 1];
const lastUser = getLastEle(rollbackUserList); // 4
可以看到這樣語(yǔ)義性就比較好了。還有一種是利用 Array.prototype.slice 方法,通過(guò)傳遞負(fù)索引,從右往左定位到最后一個(gè)元素:
const lastUser = rollbackUserList.slice(-1)[0]; // 4
// 或者使用解構(gòu)賦值
const [lastUser] = rollbackUserList.slice(-1); // 4
注意 slice 方法返回的是一個(gè) 新數(shù)組 而不是元素本身,需要手動(dòng)從數(shù)組中取出元素,看起來(lái)不是很優(yōu)雅。實(shí)際上 ES2022 專門(mén)提供了一個(gè) Array.prototype.at 方法,用于根據(jù)給定索引獲取數(shù)組元素:
const lastUser = rollbackUserList.at(-1); // 4
at方法傳遞索引為正時(shí)從左往右定位(這與直接通過(guò)下標(biāo)訪問(wèn)的作用一致),索引為負(fù)時(shí)從右往左定位。在訪問(wèn)數(shù)組最后一個(gè)元素的場(chǎng)景中非常好用。從 Chrome 92 開(kāi)始已經(jīng)支持at方法,core-js也提供了 polyfill。developer.mozilla.org/en-US/docs/…[1]
2) 異步函數(shù)組合(異步串行調(diào)用)
我們知道,在函數(shù)式編程中有一個(gè)函數(shù)組合 compose 的技巧,可以把多個(gè)函數(shù)組合為一個(gè)函數(shù),創(chuàng)建一個(gè)從右到左的數(shù)據(jù)流,右邊函數(shù)執(zhí)行的結(jié)果作為參數(shù)傳入左邊。例如:
/**
* 給定參數(shù):[A, B, C, D]
* 調(diào)用順序:A(B(C(D())))
*/
const compose = (middlewares) => (initialValue) => middlewares.reduceRight(
(accu, cur) => cur(accu),
initialValue
);
從上面的代碼可以看出 compose 是不能用于 Promise,因?yàn)?compose 需要將上一次調(diào)用的返回值,作為參數(shù)傳入下一次調(diào)用,但 Promise 本身是一個(gè)包裝過(guò)的數(shù)據(jù)結(jié)構(gòu),只有通過(guò) then 方法才能拿到返回值。所以如果是異步的情況,我們通常會(huì)把 reduce 改成普通 FOR 循環(huán)。
如果遇到異步的情況,例如想做一系列串行的請(qǐng)求,是否可以更優(yōu)雅呢?本人在 Vite 源碼中找到了答案,在 Vite 源碼中有這么一段:
await postBuildHooks.reduce(
(queue, hook) => queue.then(() => hook(build as any)),
Promise.resolve();
)
參考: github.com/vitejs/vite…[2]
可以看出,這里對(duì) reducer 函數(shù)進(jìn)行了包裝,將 hook 的執(zhí)行放到 then 方法回調(diào)中,這樣就可以保證調(diào)用順序。按照這個(gè)思路,我們對(duì)上面 compose 方法稍微改動(dòng)一下,就可以得到一個(gè) asyncCompose:
/**
* 給定參數(shù):[A, B, C, D]
* 調(diào)用順序:Promise.resolve().then(res => D(res)).then(res => C(res)).then(res => B(res)).then(res => A(res))
* 以上只是為了讓大家看清楚,簡(jiǎn)化之后如下: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. 解構(gòu)賦值
解構(gòu)賦值很方便,項(xiàng)目中經(jīng)常會(huì)用到,可以分為以下兩個(gè)場(chǎng)景:
對(duì)象/數(shù)組的解構(gòu); 函數(shù)參數(shù)解構(gòu);
這里介紹四種常用技巧。
1) 深度解構(gòu)
大部分時(shí)候我們只會(huì)解構(gòu)一層,但實(shí)際上解構(gòu)賦值是可以深度解構(gòu)的:
let obj = {
name: "dby",
a: {
b: 1
}
}
const { a: { b } } = obj;
console.log(b); // 1
2) 解構(gòu)時(shí)使用別名
假如后端返回的對(duì)象鍵名不是我們想要的,可以使用別名:
const obj = {
// 這個(gè)鍵名太長(zhǎng)了,我們希望把它換掉
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) 解構(gòu)時(shí)使用默認(rèn)值
對(duì)象的解構(gòu)可以使用默認(rèn)值,默認(rèn)值生效的條件是,對(duì)象的屬性值嚴(yán)格等于 undefined :
fetchUserInfo()
.then(({ aaa_bbb_ccc: user = {} }) => {
// ...
})
以上三個(gè)特性可以結(jié)合使用
4) 使用短路避免報(bào)錯(cuò)
解構(gòu)賦值雖然好用,但是要注意解構(gòu)的對(duì)象不能為 undefined 、null ,否則會(huì)報(bào)錯(cuò),故要給被解構(gòu)的對(duì)象一個(gè)默認(rèn)值:
const {a,b,c,d,e} = obj || {};
5. 展開(kāi)運(yùn)算符
使用展開(kāi)運(yùn)算符合并兩個(gè)數(shù)組或者兩個(gè)對(duì)象:
const a = [1,2,3];
const b = [1,5,6];
// bad
const c = a.concat(b);
// good
const c = [...new Set([...a,...b])];
const obj1 = { a:1 };
const obj2 = { b:1 };
// bad
const obj = Object.assign({}, obj1, obj2);
// good
const obj = { ...obj1, ...obj2 };
這里要注意一個(gè)問(wèn)題,對(duì)象和數(shù)組合并雖然看上去都是 ... ,但是實(shí)際上是有區(qū)別的。
ES2015 擴(kuò)展運(yùn)算符只規(guī)定了在數(shù)組和函數(shù)參數(shù)中使用,但并沒(méi)有規(guī)定可以在對(duì)象中使用,并且是基于 for...of 的,因此被展開(kāi)的只能是數(shù)組、字符串、Set、Map 等可迭代對(duì)象,假如將普通對(duì)象展開(kāi)到數(shù)組就會(huì)報(bào)錯(cuò)。
對(duì)象中的 ... 實(shí)際上是 ES2018 中的對(duì)象展開(kāi)語(yǔ)法,相當(dāng)于 Object.assign :
babeljs.io/docs/en/bab…[3]
6. 檢查屬性是否存在對(duì)象中
可以使用 in 關(guān)鍵字檢查對(duì)象中是否存在某個(gè)屬性:
const person = { name: "dby", salary: 1000 };
console.log('salary' in person); // true
console.log('age' in person); // false
但是 in 關(guān)鍵字其實(shí)并不安全,會(huì)把原型上的屬性也包括進(jìn)去,例如:
"hasOwnProperty" in {}; // true
"toString" in {}; // true
所以推薦使用下面的方法進(jìn)行判斷:
Object.prototype.hasOwnProperty.call(person, "salary"); // true
不過(guò)上面這樣的問(wèn)題就是太長(zhǎng)了,每次使用都要這樣寫(xiě)很麻煩。ECMA 有一個(gè)新的提案 Object.hasOwn() ,相當(dāng)于 Object.prototype.hasOwnProperty.call() 的別名:
Object.hasOwn(person, "salary"); // true
developer.mozilla.org/en-US/docs/…[4]
需要注意這個(gè)語(yǔ)法存在兼容性問(wèn)題(Chrome > 92),不過(guò)只要正確配置 polyfill 就可以放心使用。
7. 對(duì)象的遍歷
項(xiàng)目中很多同事都會(huì)這樣寫(xiě):
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// ...
}
}
吐槽:用 Object.keys 或者 Object.entries 轉(zhuǎn)成數(shù)組就可以用數(shù)組方法遍歷了,而且遍歷的是自身屬性,不會(huì)遍歷到原型上。
Object.keys(obj).forEach(key => {
// ...
})
Object.entries(obj).forEach(([key, val]) => {
// ...
})
舉個(gè)例子,將對(duì)象的 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'
反駁:有時(shí)候不想遍歷整個(gè)對(duì)象,數(shù)組方法不能用 break 終止循環(huán)呀。
吐槽:看來(lái)你對(duì)數(shù)組方法掌握還是不夠徹底,使用 find 方法找到符合條件的項(xiàng)就不會(huì)繼續(xù)遍歷。
Object.keys(obj).find(key => key === "name");
總結(jié):在開(kāi)發(fā)中盡量不要寫(xiě) for 循環(huán),無(wú)論數(shù)組和對(duì)象。對(duì)象就通過(guò)
Object.keys、Object.values、Object.entries轉(zhuǎn)為數(shù)組進(jìn)行遍歷。這樣可以寫(xiě)成 JS 表達(dá)式,充分利用函數(shù)式編程。
8. 使用 includes 簡(jiǎn)化 if 判斷
在項(xiàng)目中經(jīng)常看到這樣的代碼:
if (a === 1 || a === 2 || a === 3 || a === 4) {
// ...
}
可以使用 includes 簡(jiǎn)化代碼:
if ([1, 2, 3, 4].includes(a)) {
// ...
}
9. Promise 三點(diǎn)總結(jié)
1) async/await 優(yōu)雅異常處理
在 async 函數(shù)中,只要其中某個(gè) Promise 報(bào)錯(cuò),整個(gè) async 函數(shù)的執(zhí)行就中斷了,因此異常處理非常重要。但實(shí)際上 async 函數(shù)的異常處理非常麻煩,很多同事都不愿意寫(xiě)。有沒(méi)有一種簡(jiǎn)單的方法呢?看到一個(gè) await-to-js 的 npm 包,可以優(yōu)雅處理 async 函數(shù)的異常,不需要手動(dòng)添加 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…[5]
實(shí)際上就是在 await 前面返回的 Promise 封裝了一層,提前處理異常。源碼非常簡(jiǎn)單,本人自己也實(shí)現(xiàn)了下:
function to(awaited) {
// 不管是不是 Promise 一律轉(zhuǎn)為 Promise
const p = Promise.resolve(awaited);
// await-to-js 采用 then...catch 的用法
// 但實(shí)際上 then 方法第一個(gè)回調(diào)函數(shù)里面并不包含會(huì)拋出異常的代碼
// 因此使用 then 方法第二個(gè)回調(diào)函數(shù)捕獲異常,不需要額外的 catch
return p.then(
res => {
return [null, res];
},
err => {
return [err, undefined];
}
)
}
2) Promise 作為狀態(tài)機(jī)
看到有同事寫(xiě)過(guò)這樣的代碼:
function validateUserInfo(user) {
if (!userList.find(({ id }) => id === user.id)) {
return {
code: -1,
message: "用戶未注冊(cè)"
}
}
if (
!userList.find(
({ username, password }) =>
username === user.username &&
password === user.password
)
) {
return {
code: -1,
message: "用戶名或密碼錯(cuò)誤"
}
}
return {
code: 0,
message: "登錄成功"
}
}
觀察發(fā)現(xiàn)這邊其實(shí)就是兩個(gè)狀態(tài),然后還需要一個(gè)字段提示操作結(jié)果。這種情況下我們可以使用 Promise 。有人說(shuō)為啥咧,明明沒(méi)有異步邏輯啊。我們知道,Promise 其實(shí)就是一個(gè)狀態(tài)機(jī),即使不需要處理異步邏輯,我們也可以使用狀態(tài)機(jī)的特性:
function validateUserInfo(user) {
if (!userList.find(({ id }) => id === user.id)) {
return Promise.reject("用戶未注冊(cè)");
}
if (
!userList.find(
({ username, password }) =>
username === user.username &&
password === user.password
)
) {
return Promise.reject("用戶名或密碼錯(cuò)誤");
}
return Promise.resolve("登錄成功");
}
// 使用如下
validateUserInfo(userInfo)
.then(res => {
message.success(res);
})
.catch(err => {
message.error(err);
})
明顯這樣代碼就變得非常優(yōu)雅了,但其實(shí)還可以更優(yōu)雅。我們知道 async 函數(shù)返回值是一個(gè) Promise 實(shí)例,因此下面兩個(gè)函數(shù)是等價(jià)的:
// 普通函數(shù)返回一個(gè) Promsie.resolve 包裹的值
const request = (x) => Promise.resolve(x);
// async 函數(shù)返回一個(gè)值
const request = async (x) => x;
既然最后返回一個(gè) Promise ,為何不直接在函數(shù)前面加 async 修飾符呢。這樣成功的結(jié)果只要直接返回就行,不用 Promise.resolve 包裹:
async function validateUserInfo(user) {
if (!userList.find(({ id }) => id === user.id)) {
return Promise.reject("用戶未注冊(cè)");
}
if (
!userList.find(
({ username, password }) =>
username === user.username &&
password === user.password
)
) {
return Promise.reject("用戶名或密碼錯(cuò)誤");
}
return "登錄成功";
}
對(duì)
async函數(shù)不熟悉的同學(xué),可以參考 阮一峰 ES6 教程[6]
更進(jìn)一步,由于在 Promise 內(nèi)部拋出異常等同于被 reject ,因此我們可以使用 throw 語(yǔ)句替代 Promise.reject() :
async function validateUserInfo(user) {
if (!userList.find(({ id }) => id === user.id)) {
throw "用戶未注冊(cè)";
}
if (
!userList.find(
({ username, password }) =>
username === user.username &&
password === user.password
)
) {
throw "用戶名或密碼錯(cuò)誤";
}
return "登錄成功";
}
throw語(yǔ)句的用法可以參考 MDN 文檔[7]
3) Promise 兩點(diǎn)使用誤區(qū)
不建議在 Promise 里面使用 try...catch,這樣即使 Promise 內(nèi)部報(bào)錯(cuò),狀態(tài)仍然是 fullfilled,會(huì)進(jìn)入 then 方法回調(diào),不會(huì)進(jìn)入 catch 方法回調(diào)。
function request() {
return new Promise((resolve, reject) => {
try {
// ...
resolve("ok");
} catch(e) {
console.log(e);
}
})
}
request()
.then(res => {
console.log("請(qǐng)求結(jié)果:", res);
})
.catch(err => {
// 由于在 Promise 中使用了 try...catch
// 因此即使 Promise 內(nèi)部報(bào)錯(cuò),也不會(huì)被 catch 捕捉到
console.log(err);
})
Promise內(nèi)部的異常,老老實(shí)實(shí)往外拋就行,讓catch方法來(lái)處理,符合單一職責(zé)原則
不建議在 async 函數(shù)中,既不使用 await,也不使用 return,這樣就算內(nèi)部的 Promise reject 也無(wú)法捕捉到:
async function handleFetchUser(userList) {
// 這里既沒(méi)有使用 await,也沒(méi)有使用 return
Promise.all(userList.map(u => request(u)));
}
handleFetchUser(userList)
.then(res => {
// 由于沒(méi)有返回值,這里拿到的是 undefined
console.log(res);
})
.catch(err => {
// 即使 handleFetchUser 內(nèi)部的 Promise reject
// async 函數(shù)返回的 Promise 仍然是 fullfilled
// 此時(shí)仍然會(huì)進(jìn)入 then 方法回調(diào),無(wú)法被 catch 捕捉到
console.log(err);
})
如果確實(shí)有這種需求,建議不要使用
async函數(shù),直接改用普通函數(shù)即可
10. 字符串小技巧
1) 字符串不滿兩位補(bǔ)零
這個(gè)需求在開(kāi)發(fā)中挺常見(jiàn)。例如,調(diào)用 Date api 獲取到日期可能只有一位:
let date = new Date().getDate(); // 3
常規(guī)做法:
if (data.toString().length == 1) {
date = `0${date}`;
}
使用 String.prototype.slice :
// 不管幾位,都在前面拼接一個(gè) 0 ,然后截取最后兩位
date = `0${date}`.slice(-2);
使用 String.prototype.padStart :
// 當(dāng)字符串長(zhǎng)度小于第一個(gè)參數(shù)值,就在前面補(bǔ)第二個(gè)參數(shù)
date = `${date}`.padStart(2, 0);
2) 千分位分隔符
實(shí)現(xiàn)如下的需求:
從后往前每三個(gè)數(shù)字前加一個(gè)逗號(hào) 開(kāi)頭不能加逗號(hào)
這樣看起來(lái)非常符合 (?=p) 的規(guī)律,p 可以表示每三個(gè)數(shù)字,要添加逗號(hào)所處的位置正好是 (?=p) 匹配出來(lái)的位置。
第一步,先嘗試把最后一個(gè)逗號(hào)弄出來(lái):
"300000000".replace(/(?=\d{3}$)/, ",")
// '300000,000'
第二步,把所有逗號(hào)都弄出來(lái):
"300000000".replace(/(?=(\d{3})+$)/g, ",")
// ',300,000,000'
使用括號(hào)把一個(gè)
p模式變成一個(gè)整體
第三步,去掉首位的逗號(hào):
"300000000".replace(/(?!^)(?=(\d{3})+$)/g, ",")
// '300,000,000'
3) 借用數(shù)組拼接字符串
很多同學(xué)都知道 模板字符串 可以很方便地進(jìn)行字符串拼接,但是需要拼接較多參數(shù)的時(shí)候,這樣就顯得比較麻煩:
// HH -> 23
// mm -> 58
// ss -> 32
const timeString = `${HH}:${mm}:${ss}`;
// scheme -> https://
// host -> 10.3.71.108
// port -> :8080
const URLString = `${scheme}${host}${port}`
實(shí)際上,拼接字符串,除了使用模板字符串的方式,還可以使用數(shù)組:
const timeString = [HH, mm, ss].join(":");
const URLString = [scheme, host, port].join("");
順便一提,本人之前維護(hù)過(guò)一個(gè) jQuery 項(xiàng)目,就使用這種方式拼接 html 模板:
const dataSource = ["dby", "dm", "233"];
const template = dataSource.map(name => `<div>${name}</div>`).join("");
4) 判斷字符串前綴、后綴
判斷字符串前綴、后綴不要一言不合就使用正則表達(dá)式:
const url = "https://bili98.cn";
const isHTTPS = /^https:\/\//.test(url); // true
const fileName = "main.py";
const isPythonCode = /\.py$/.test(fileName); // true
推薦使用 String.prototype.startsWith 和 String.prototype.endsWith,語(yǔ)義性更好:
const url = "https://bili98.cn";
const isHTTPS = url.startsWith("https://") // true
const fileName = "main.py";
const isPythonCode = fileName.endsWith(".py"); // true
參考
我的代碼簡(jiǎn)潔之道[8]
你會(huì)用ES6,那倒是用啊![9]
有個(gè)開(kāi)發(fā)者總結(jié)這 15 優(yōu)雅的 JavaScript 個(gè)技巧[10]
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺(jué)得這篇內(nèi)容對(duì)你有幫助,我想請(qǐng)你幫我2個(gè)小忙:
1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客 www.inode.club 讓我們一起成長(zhǎng) 點(diǎn)贊和在看就是最大的支持
