重构 JavaScript 以提高性能和可读性(附示例!)

2025-06-07

重构 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-wordsGlitch 团队开发的软件包。他们会用这个软件包来为您最近创建的项目生成随机名称!

// 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
PREV
如果我希望我的网站能够存在 100 年怎么办?
NEXT
我的第一个 Golang 程序