重构 JavaScript 以提高性能和可读性(附示例!)
最近,我写了一篇关于如何编写快速 JavaScript 的文章。其中一些示例将速度发挥到了极致,虽然速度很快,但代价是完全无法维护。在速度和可理解性之间有一个平衡点,而优秀的代码就存在于此。
我们将根据我遇到的实际示例重构一些代码片段。有时我需要在提交 PR 之前对自己的代码进行此类重构。有时,我会在开始一个故事或修复 bug 时对现有代码进行小规模重构,以便更容易实现我的更改。
场景 1
我们是一个 URL 缩短网站,类似于 TinyURL。我们接受一个长 URL,并返回一个短 URL,以便将访问者重定向到该长 URL。我们有两个函数。
// Unrefactored code
const URLstore = [];
function makeShort(URL) {
const rndName = Math.random().toString(36).substring(2);
URLstore.push({[rndName]: URL});
return rndName;
}
function getLong(shortURL) {
for (let i = 0; i < URLstore.length; i++) {
if (URLstore[i].hasOwnProperty(shortURL) !== false) {
return URLstore[i][shortURL];
}
}
}
问题:如果getLong
使用 store 中不存在的短 URL 调用 会发生什么?由于没有明确返回任何内容,所以undefined
会返回。由于我们不确定如何处理这个问题,所以我们明确地抛出一个错误,以便在开发过程中发现问题。
从性能角度来看,如果你经常迭代平面数组,尤其是它是程序的核心部分,请务必小心。此处的重构旨在更改 的数据结构URLstore
。
目前,每个 URL 对象都存储在一个数组中。我们将其可视化为一行存储桶。当我们想要将短 URL 转换为长 URL 时,平均需要检查一半的存储桶才能找到正确的短 URL。如果我们有数千个存储桶,并且每秒执行数百次这样的操作,该怎么办?
答案是使用某种形式的哈希函数,Map 和 Set在底层使用了这种函数。哈希函数用于将给定的键映射到哈希表中的某个位置。下面,当我们将短 URL 放入存储区时makeShort
,以及在取出存储区时,都会发生这种情况getLong
。根据您测量运行时间的方式,结果是平均而言,无论存储区总数有多少个,我们只需要检查一个存储区!
// Refactored code
const URLstore = new Map(); // Change this to a Map
function makeShort(URL) {
const rndName = Math.random().toString(36).substring(2);
// Place the short URL into the Map as the key with the long URL as the value
URLstore.set(rndName, URL);
return rndName;
}
function getLong(shortURL) {
// Leave the function early to avoid an unnecessary else statement
if (URLstore.has(shortURL) === false) {
throw 'Not in URLstore!';
}
return URLstore.get(shortURL); // Get the long URL out of the Map
}
对于这些例子,我们假设随机函数不会发生冲突。“克隆 TinyURL”是一个常见的系统设计问题,而且非常有趣。如果随机函数真的发生冲突怎么办?很容易就能补充一些关于扩展和冗余的补充说明。
场景 2
我们是一个社交媒体网站,用户 URL 是随机生成的。我们不会使用随机乱码,而是使用friendly-words
Glitch 团队开发的软件包。他们会用这个软件包来为您最近创建的项目生成随机名称!
// Unrefactored code
const friendlyWords = require('friendly-words');
function randomPredicate() {
const choice = Math.floor(Math.random() * friendlyWords.predicates.length);
return friendlyWords.predicates[choice];
}
function randomObject() {
const choice = Math.floor(Math.random() * friendlyWords.objects.length);
return friendlyWords.objects[choice];
}
async function createUser(email) {
const user = { email: email };
user.url = randomPredicate() + randomObject() + randomObject();
await db.insert(user, 'Users')
sendWelcomeEmail(user);
}
人们常说一个函数应该只做一件事。这里,createUser
它做的就是一件事……有点像。它创建了一个用户。然而,如果我们展望未来,这个函数很有可能(如果我们的业务成功的话)会变得非常庞大。所以,让我们先把它拆开来看。
你可能也注意到了,我们的随机函数中有一些重复的逻辑。该friendly-worlds
包还提供了“团队”和“集合”的列表。我们没法为每个选项都写一个函数。让我们写一个函数,它接受一个友好列表作为参数。
// Refactored code
const friendlyWords = require('friendly-words');
const generateURL = user => {
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
user.url = `${pick(friendlyWords.predicates)}-${pick(friendlyWords.objects)}` +
`-${pick(friendlyWords.objects)}`; // This line would've been too long for linters!
};
async function createUser(email) {
const user = { email: email };
// The URL-creation algorithm isn't important to this function so let's abstract it away
generateURL(user);
await db.insert(user, 'Users')
sendWelcomeEmail(user);
}
我们分离了一些逻辑,减少了代码行数。我们内联了一个名为 的函数pick
,它接受一个长度为 1 及以上的数组,并返回一个随机数,然后我们使用模板字面量来构建 URL。
策略
这里有一些简单易行的方法,可以让代码更易于阅读。代码整洁没有绝对的标准——总会有例外的情况!
从函数中提前返回:
function showProfile(user) {
if (user.authenticated === true) {
// ..
}
}
// Refactor into ->
function showProfile(user) {
// People often inline such checks
if (user.authenticated === false) { return; }
// Stay at the function indentation level, plus less brackets
}
缓存变量,以便函数可以像句子一样被读取:
function searchGroups(name) {
for (let i = 0; i < continents.length; i++) {
for (let j = 0; j < continents[i].length; j++) {
for (let k = 0; k < continents[i][j].tags.length; k++) {
if (continents[i][j].tags[k] === name) {
return continents[i][j].id;
}
}
}
}
}
// Refactor into ->
function searchGroups(name) {
for (let i = 0; i < continents.length; i++) {
const group = continents[i]; // This code becomes self-documenting
for (let j = 0; j < group.length; j++) {
const tags = group[j].tags;
for (let k = 0; k < tags.length; k++) {
if (tags[k] === name) {
return group[j].id; // The core of this nasty loop is clearer to read
}
}
}
}
}
在实现您自己的功能之前,请检查 Web API:
function cacheBust(url) {
return url.includes('?') === true ?
`${url}&time=${Date.now()}` :
`${url}?time=${Date.now()}`
}
// Refactor into ->
function cacheBust(url) {
// This throws an error on invalid URL which stops undefined behaviour
const urlObj = new URL(url);
urlObj.searchParams.append('time', Date.now); // Easier to skim read
return url.toString();
}
第一次就把代码写好至关重要,因为在很多企业中,重构的价值并不大。或者至少,很难让利益相关者相信,如果代码库无人照管,最终会导致生产力停滞。
与 150 多名订阅我的关于编程和个人成长的新闻通讯的人一起!
我发布有关科技的推文@healeycodes。
文章来源:https://dev.to/healeycodes/refactoring-javascript-for-performance-and-readability-with-examples-1hec