编写更简洁的 JavaScript 代码的 8 种技巧
结论
我相信成为一名软件工程师就像成为一名超级英雄!能力越大,责任越大。虽然编写代码是软件工程师不可或缺的一部分,就像估算、头脑风暴、编写单元测试一样,编写干净的代码也同样重要。
在本文中,我们将介绍 8 种不同的技巧,帮助您编写更简洁的 JavaScript 代码。
如果您更喜欢观看视频,请查看:
视频教程
现在让我们逐一讨论每种技术。
纯函数
纯函数是指给定相同输入时始终返回相同输出的函数。它不依赖于除提供的输入之外的任何外部变量,也不会影响/改变任何外部变量。
纯函数使测试变得非常容易,因为它们使测试变得非常简单,因为您可以随时存根/模拟输入并测试您的预期值。让我们看下面的例子
let name = "Peter Parker";
const splitName = () => {
name = name.split(' ');
}
name = splitName();
console.log(name); // outputs [ 'Peter', 'Parker' ]
上面的代码看似合理,其实不然(哈哈)。这是因为该splitName
函数依赖于一个名为 的外部变量name
,如果其他人开始更改此变量,该函数splitName
就会提供不同的输出。这使其成为非纯函数,因为我们仍然会调用它,splitName()
但输出会有所不同。
让我们将其更改为纯函数,看看它会是什么样子:
let name = "Peter Parker";
const splitName = (nameString) => {
return nameString.split(' ');
}
name = splitName(name);
console.log(name); // outputs [ 'Peter', 'Parker' ]
通过上述更改,splitName
现在是一个纯函数,因为:
- 它仅依赖于输入(输入
nameString
)。 - 它不会改变/重新分配任何外部变量
更少或命名的参数
使用函数时,我们经常会用到位置参数,这些参数必须在函数声明时提供。例如,在 call 中,如果不提供and ,arithmaticOp(num1, num2, operator)
我们就无法提供argument 。虽然这个例子没问题,但对于许多函数来说,这会成为一个问题。 请考虑以下示例:operator
num1
num2
const createButton = (title, color, disabled, padding, margin, border, shadow) => {
console.log(title, color, disabled, padding, margin, border, shadow)
}
查看上面的代码,您已经可以看到,如果我们想在调用 + 时使任何参数成为可选参数(使用默认值)createButton
,那将是一场灾难,可能看起来像这样:
createButton('John Wick', undefined /* optional color */, true ,'2px....', undefined /* optional margin*/);
你会发现上面的语句看起来一点也不干净。而且,从函数调用语句中很难看出哪个形参对应函数的哪个实参。所以我们可以遵循以下做法:
- 如果我们有 2 个或更少的参数,我们可以将它们保留为位置参数
- 否则,我们提供一个带有键值对的对象
让我们将这个技术与上面的例子一起使用,看看它是什么样子的:
const createButton = ({title, color, disabled, padding, margin, border, shadow}) => {
console.log(title, color, disabled, padding, margin, border, shadow)
}
createButton({
title: 'John Wick',
disabled: true,
shadow: '2px....'
});
注意,现在调用函数的语句createButton
简洁多了。而且我们可以轻松地看到键值对中哪个值对应着函数的参数。太棒了!🎉
对象/数组解构
考虑以下 JavaScript 示例,其中我们从对象中获取一些属性并分配给它们各自的变量:
const user = {
name: 'Muhammad Ahsan',
email: 'hi@codewithahsan.dev',
designation: 'Software Architect',
loves: 'The Code With Ahsan Community'
}
const name = user.name;
const email = user.email;
const loves = user.loves;
在上面的例子中,user.*
多次使用这种符号实在是太麻烦了。这时,对象解构就派上用场了。我们可以使用对象解构将上面的例子修改如下:
const user = {
name: 'Muhammad Ahsan',
email: 'hi@codewithahsan.dev',
designation: 'Software Architect',
loves: 'The Code With Ahsan Community'
}
const {name, email, loves} = user;
瞧!好多了,对吧?我们再来看一个例子:
const getDetails = () => {
return ['Muhammad Ahsan', 'hi@codewithahsan.dev', 'Some Street', 'Some City', 'Some Zip', 'Some Country'];
}
const details = getDetails();
const uName = details[0];
const uEmail = details[1];
const uAddress = `${details[2]}, ${details[3]}, ${details[4]}, ${details[5]}`;
const uFirstName = uName.split(' ')[0];
const uLastName = uName.split(' ')[1];
唉。我甚至讨厌上面这个例子的代码🤣。但不得不这么做。你可以看到代码看起来非常奇怪,而且很难阅读。我们可以使用数组解构来让它更简洁一些,如下所示:
const getDetails = () => {
return ['Muhammad Ahsan', 'hi@codewithahsan.dev', 'Some Street', 'Some City', 'Some Zip', 'Some Country'];
}
const [uName, uEmail, ...uAddressArr] = getDetails();
const uAddress = uAddressArr.join(', ');
const [uFirstName, uLastName] = uName.split('');
console.log({
uFirstName,
uLastName,
uEmail,
uAddress
});
你可以看到这有多干净🤩
避免使用硬编码值
这个问题我经常在审核的 Pull Request 中要求修改,但最终却无法解决。我们来看一个例子:
/**
* Some huge code
*
*
*
*
*
*/
setInterval(() => {
// do something
}, 86400000);
// WHAT IS THIS 86400000 ??? 🤔
看代码的人肯定不知道这个数字代表什么,怎么计算的,以及背后的业务逻辑是什么。与其硬编码这个值,不如创建一个常量,如下所示:
const DAY_IN_MILLISECONDS = 3600 * 24 * 1000; // 86400000
setInterval(() => {
// do something
}, DAY_IN_MILLISECONDS);
// now this makes sense
让我们考虑另一个例子:
const createUser = (name, designation, type) => {
console.log({name, designation, type});
}
createUser('Muhammad Ahsan', 'Software Architect', '1');
// WHAT IS this '1'? 🤔
看一下方法的调用createUser
。阅读代码的人很难理解这'1'
代表什么。也就是说,type
这代表什么用户。因此,与其'1'
在这里硬编码值,不如创建一个与用户类型对应的对象映射,如下所示:
const USER_TYPES = {
REGULAR_EMPLOYEE: '1'
}
const createUser = (name, designation, type) => {
console.log({name, designation, type});
}
createUser('Muhammad Ahsan', 'Software Architect', USER_TYPES.REGULAR_EMPLOYEE);
// smoooooooth 😎
避免使用简写变量名
简写变量在需要的地方才有意义。比如,如果你有像x
和这样的位置坐标y
,那就没问题了。但是,如果我们创建像p
、t
这样的变量c
而没有上下文,那么这样的代码就很难阅读、跟踪和维护。例如,请看这个例子:
const t = 25;
let users = ['Muhammad Ahsan', 'Darainn Mukarram'];
users = users.map((user) => {
/**
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
return {
...user,
tax: user.salary * t / 100 // WHAT IS `t` again? 🤔
}
})
上面的例子表明,现在开发者/读者必须一直向上滚动页面或直接跳到定义部分才能理解这个变量的含义。因此,这不是干净的代码😠。这也被称为“思维导图”,只有作者自己才知道变量的含义。因此,与其使用简写变量名,不如使用如下的正式名称:
const taxFactor = 25;
let users = ['Muhammad Ahsan', 'Darainn Mukarram'];
users = users.map((user) => {
// some code
return {
...user,
tax: user.salary * taxFactor / 100
}
})
现在这变得更有意义了。
使用 Object.assign() 设置默认 Object 值
在某些情况下,你可能希望从另一个对象创建一个新对象,并在源对象没有默认值时提供一些默认值。请考虑以下示例:
const createButton = ({title, color, disabled, padding}) => {
const button = {};
button.color = color || '#333';
button.disabled = disabled || false;
button.title = title || '';
button.padding = padding || 0;
return button;
}
const buttonConfig = {
title: 'Click me!',
disabled: true
}
const newButton = createButton(buttonConfig);
console.log('newButton', newButton)
我们不需要做所有这些,而是可以使用Object.assign()
以下方法覆盖源对象提供的默认属性:
const createButton = (config) => {
return {
...{
color: '#dcdcdc',
disabled: false,
title: '',
padding: 0
},
...config
};
}
const buttonConfig = {
title: 'Click me!',
disabled: true
}
const newButton = createButton(buttonConfig);
console.log('newButton', newButton)
使用方法链(特别是对于类)
如果我们知道类/对象的用户会同时使用多个函数,那么方法链就非常有用。你可能在 moment.js 之类的库中见过这种方法。我们来看一个例子:
class Player {
constructor (name, score, position) {
this.position = position;
this.score = score;
this.name = name;
}
setName(name) {
this.name = name;
}
setPosition(position) {
this.position = position;
}
setScore(score) {
this.score = score;
}
}
const player = new Player();
player.setScore(0);
player.setName('Ahsan');
player..setPosition([2, 0]);
console.log(player)
在上面的代码中,您可以看到我们需要为玩家调用一系列函数。如果您的对象/类需要这样做,请使用方法链。您只需从要链接的函数中返回对象的实例即可。您可以按如下方式修改上述示例以实现此目的:
class Player {
constructor (name, score, position) {
this.position = position;
this.score = score;
this.name = name;
}
setName(name) {
this.name = name;
return this; // <-- THIS
}
setPosition(position) {
this.position = position;
return this; // <-- THIS
}
setScore(score) {
this.score = score;
return this; // <-- THIS
}
}
const player = new Player();
player.setScore(0).setName('Ahsan').setPosition([2, 0]);
// SUPER COOL 😎
console.log(player)
使用 Promises 而不是回调
Promise 让我们的生活变得更轻松。几年前,我们遇到了一种叫做回调地狱的东西,它让代码变得非常难以阅读。它看起来像这样:
即使我使用的库有回调函数,我也会尝试添加一个包装器来保证回调函数的有效性(是的,现在有了个术语)。让我们考虑以下示例:
const getSocials = (callback) => {
setTimeout(() => {
callback({socials: {youtube: 'youtube.com/CodeWithAhsan', twitter: '@codewith_ahsan'}});
}, 1500);
}
const getBooks = (callback) => {
setTimeout(() => {
callback({books: ['Angular Cookbook']});
}, 1500);
}
const getDesignation = (callback) => {
setTimeout(() => {
callback({designation: 'Software Architect'});
}, 1500);
}
const getUser = (callback) => {
setTimeout(() => {
callback({user: 'Ahsan'});
}, 1500);
}
getUser(({user}) => {
console.log('user retrieved', user)
getDesignation(({designation}) => {
console.log('designation retrieved', designation)
getBooks(({books}) => {
console.log('books retrieved', books)
getSocials(({socials}) => {
console.log('socials retrieved', socials)
})
})
})
})
上面代码中的所有函数都是异步的,它们会在 1.5 秒后返回数据。现在,如果涉及到 15 个不同的函数,想象一下它会是什么样子。可能就像我上面分享的图片一样😅。为了避免这种回调地狱,我们可以将函数 Promises 化,并使用 Promise,如下所示,以提高可读性:
const getSocials = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({socials: {youtube: 'youtube.com/CodeWithAhsan', twitter: '@codewith_ahsan'}});
}, 1500);
})
}
const getBooks = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({books: ['Angular Cookbook']});
}, 1500);
})
}
const getDesignation = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({designation: 'Software Architect'});
}, 1500);
})
}
const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({user: 'Ahsan'});
}, 1500);
})
}
getUser()
.then(({user}) => {
console.log('user retrieved', user);
return getDesignation();
})
.then(({designation}) => {
console.log('designation retrieved', designation)
return getBooks();
})
.then(({books}) => {
console.log('books retrieved', books);
return getSocials();
})
.then(({socials}) => {
console.log('socials retrieved', socials)
})
您可以看到,代码现在可读性更强了,因为所有.then()
语句都缩进,并显示了每一步检索的数据.then()
。我们可以很容易地使用此语法查看步骤,因为每次.then()
调用都会返回下一个函数调用及其承诺。
现在我们可以更上一层楼,让代码更具可读性。怎么做呢?通过使用async await
。我们将修改代码如下以实现此目的:
const getSocials = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({socials: {youtube: 'youtube.com/CodeWithAhsan', twitter: '@codewith_ahsan'}});
}, 1500);
})
}
const getBooks = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({books: ['Angular Cookbook']});
}, 1500);
})
}
const getDesignation = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({designation: 'Software Architect'});
}, 1500);
})
}
const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({user: 'Ahsan'});
}, 1500);
})
}
const performTasks = async () => {
const {user} = await getUser();
console.log('user retrieved', user);
const {designation} = await getDesignation();
console.log('designation retrieved', designation);
const {books} = await getBooks();
console.log('books retrieved', books);
const {socials} = await getSocials();
console.log('socials retrieved', socials);
}
请注意,我们将代码包裹在函数内部,正如您所见,函数本身performTasks()
就是一个函数。在函数内部,我们使用关键字进行每个函数调用,这实际上会等待函数的 Promise 被解析后再执行下一行代码。使用这种语法,我们的代码看起来就像是同步的,但实际上是异步的。而且我们的代码更加简洁 🙂async
async
await
结论
希望你喜欢这篇文章。如果喜欢,记得点赞和收藏。欢迎关注我的YouTube 频道,了解更多精彩内容。如果你热爱冒险,并有兴趣将你的 #Angular 技能提升到一个新的高度,不妨看看我的Angular Cookbook。
文章来源:https://dev.to/codewithahsan/8-techniques-to-write-cleaner-javascript-code-369e