寫出整潔的 JavaScript 程式碼 ES6 版

夜色鎮歌 發表於 2021-12-05
JavaScript ES6

好的程式碼不僅僅是可以跑起來的程式碼,更是可以被其他人輕鬆閱讀、重用和重構的程式碼,因為程式碼除了實現功能外,大部分的時間都是要被你或是團隊其他成員維護的。

寫出整潔的 JavaScript 程式碼 ES6 版

雖然本文主要專注於編寫乾淨整潔的 JavaScript ES6 程式碼,並且不與任何框架相關,但是下面將要提到的絕大多數示例也適用於其他語言,另外,下面的示例也主要是從 Robert C. Martin 的書 Clean Code 中所採納的建議,也不意味著要嚴格遵守。

變數

使用有意義的名字

變數的命名應該是描述性並且應該是有意義的,經驗法則是大多數 JavaScript 變數應該使用駝峰命名法(camelCase)。

// 錯誤 ❌
const foo = "[email protected]";
const bar = "John";
const age = 23;
const qux = true;

// 正確 ✅
const email = "[email protected]";
const firstName = "John";
const age = 23;
const isActive = true
注意,布林型別的變數名通常像是在回答問題,例如:
isActive
didSubscribe
hasLinkedAccount

避免新增不必要的上下文

在特定的物件或類中不需要加冗餘的上下文

// 錯誤 ❌
const user = {
  userId: "296e2589-7b33-400a-b762-007b730c8e6d",
  userEmail: "[email protected]",
  userFirstName: "John",
  userLastName: "Doe",
  userAge: 23,
};

user.userId;

// 正確 ✅
const user = {
  id: "296e2589-7b33-400a-b762-007b730c8e6d",
  email: "[email protected]",
  firstName: "John",
  lastName: "Doe",
  age: 23,
};

user.id;

避免硬編碼

確保宣告有意義且可搜尋的常量,而不是直接使用一個常量值,全域性變數建議使用蛇形命名法(SCREAMING_SNAKE_CASE)

// 錯誤 ❌
setTimeout(clearSessionData, 900000);

// 正確 ✅
const SESSION_DURATION_MS = 15 * 60 * 1000;

setTimeout(clearSessionData, SESSION_DURATION_MS);

函式

使用描述性的命名

函式名可以很長,長到足以描述它的作用,一般函式中都含有動詞來描述它所做的事情,但是返回布林值的函式是個例外,一般是一個回答“是”或者“否”的問題形式,另外函式命名也應該是駝峰命名法。

// 錯誤 ❌
function toggle() {
  // ...
}

function agreed(user) {
  // ...
}

// 正確 ✅
function toggleThemeSwitcher() {
  // ...
}

function didAgreeToAllTerms(user) {
  // ...
}

使用預設引數

直接使用預設值比短路語法或者在函式中加入判斷語句更加簡潔,值得注意的是,短路語法適用於所有被認為是 false 的值,例如 falsenullundefined''""0NaN,而預設引數僅替換 undefined

// 錯誤 ❌
function printAllFilesInDirectory(dir) {
  const directory = dir || "./";
  //   ...
}

// 正確 ✅
function printAllFilesInDirectory(dir = "./") {
  // ...
}

限制引數個數

這一條有爭議,函式的引數應該不多於2個,意思是函式的引數為 0 個 1 個或者 2 個,如果需要第三個引數的話說明:

  • 函式需要拆分
  • 可以把相關引數聚合成物件傳遞
// 錯誤 ❌
function sendPushNotification(title, message, image, isSilent, delayMs) {
  // ...
}

sendPushNotification("New Message", "...", "http://...", false, 1000);

// 正確 ✅
function sendPushNotification({ title, message, image, isSilent, delayMs }) {
  // ...
}

const notificationConfig = {
  title: "New Message",
  message: "...",
  image: "http://...",
  isSilent: false,
  delayMs: 1000,
};

sendPushNotification(notificationConfig);

不要在一個函式中做太多事情

原則上一個函式只做一件事,這一原則可以很好地幫助我們降低函式的體積和複雜度,也能更好的測試、除錯和重構,一個函式的程式碼行數是判斷這個函式是否做太多事情的一個指標,一般建議一個函式長度小於 20~30 行。

// 錯誤 ❌
function pingUsers(users) {
  users.forEach((user) => {
    const userRecord = database.lookup(user);
    if (!userRecord.isActive()) {
      ping(user);
    }
  });
}

// 正確 ✅
function pingInactiveUsers(users) {
  users.filter(!isUserActive).forEach(ping);
}

function isUserActive(user) {
  const userRecord = database.lookup(user);
  return userRecord.isActive();
}

避免使用 flag 變數

flag 變數意味著函式可以被進一步簡化

// 錯誤 ❌
function createFile(name, isPublic) {
  if (isPublic) {
    fs.create(`./public/${name}`);
  } else {
    fs.create(name);
  }
}

// 正確 ✅
function createFile(name) {
  fs.create(name);
}

function createPublicFile(name) {
  createFile(`./public/${name}`);
}

不要重複自己(DRY)

重複的程式碼不是一個好的訊號,你複製貼上了 N 次,下次這部分程式碼修改的時候你就要就要同時修改 N 個地方。

// 錯誤 ❌
function renderCarsList(cars) {
  cars.forEach((car) => {
    const price = car.getPrice();
    const make = car.getMake();
    const brand = car.getBrand();
    const nbOfDoors = car.getNbOfDoors();

    render({ price, make, brand, nbOfDoors });
  });
}

function renderMotorcyclesList(motorcycles) {
  motorcycles.forEach((motorcycle) => {
    const price = motorcycle.getPrice();
    const make = motorcycle.getMake();
    const brand = motorcycle.getBrand();
    const seatHeight = motorcycle.getSeatHeight();

    render({ price, make, brand, seatHeight });
  });
}

// 正確 ✅
function renderVehiclesList(vehicles) {
  vehicles.forEach((vehicle) => {
    const price = vehicle.getPrice();
    const make = vehicle.getMake();
    const brand = vehicle.getBrand();

    const data = { price, make, brand };

    switch (vehicle.type) {
      case "car":
        data.nbOfDoors = vehicle.getNbOfDoors();
        break;
      case "motorcycle":
        data.seatHeight = vehicle.getSeatHeight();
        break;
    }

    render(data);
  });
}

避免副作用

在 JavaScript 中,首選的模式應該是函式式而不是命令式,換句話說,要保證函式的純粹性,副作用可以修改共享的狀態和資源,會導致程式碼的不穩定和難以測試,排查問題也會特別棘手,所有的副作用應該集中管理。如果需要修改全域性狀態可以定義一個統一的服務去修改。

// 錯誤 ❌
let date = "21-8-2021";

function splitIntoDayMonthYear() {
  date = date.split("-");
}

splitIntoDayMonthYear();

// Another function could be expecting date as a string
console.log(date); // ['21', '8', '2021'];

// 正確 ✅
function splitIntoDayMonthYear(date) {
  return date.split("-");
}

const date = "21-8-2021";
const newDate = splitIntoDayMonthYear(date);

// Original vlaue is intact
console.log(date); // '21-8-2021';
console.log(newDate); // ['21', '8', '2021'];

另外,如果一個可變的物件作為一個函式的引數傳遞進去,返回這個引數的時候應該是這個引數的克隆物件而不是直接把這個物件修改後返回。


// 錯誤 ❌
function enrollStudentInCourse(course, student) {
  course.push({ student, enrollmentDate: Date.now() });
}

// 正確 ✅
function enrollStudentInCourse(course, student) {
  return [...course, { student, enrollmentDate: Date.now() }];
}

併發

避免使用回撥

回撥函式 太亂了,所以 ES6 給我們提供了 Promise 允許我們使用鏈式的回撥,當然 Async/Await 提供了更簡潔的方案,可以讓我們寫出更加線性的程式碼

// 錯誤 ❌
getUser(function (err, user) {
  getProfile(user, function (err, profile) {
    getAccount(profile, function (err, account) {
      getReports(account, function (err, reports) {
        sendStatistics(reports, function (err) {
          console.error(err);
        });
      });
    });
  });
});

// 正確 ✅
getUser()
  .then(getProfile)
  .then(getAccount)
  .then(getReports)
  .then(sendStatistics)
  .catch((err) => console.error(err));

// 正確 ✅✅
async function sendUserStatistics() {
  try {
    const user = await getUser();
    const profile = await getProfile(user);
    const account = await getAccount(profile);
    const reports = await getReports(account);
    return sendStatistics(reports);
  } catch (e) {
    console.error(err);
  }
}

錯誤處理

處理丟擲的異常和 rejected 的 Promise

正確的處理異常可以使我們的程式碼更加的簡裝,也會更方便的排查問題。

// 錯誤 ❌
try {
  // 可能出錯的程式碼
} catch (e) {
  console.log(e);
}

// 正確 ✅
try {
  // 可能出錯的程式碼
} catch (e) {
  // 比 console.log 更合適
  console.error(e);

  // 通知使用者
  alertUserOfError(e);

  // 通知伺服器
  reportErrorToServer(e);

  // 使用自定義的異常處理
  throw new CustomError(e);
}

註釋

只為複雜的邏輯新增註釋

不要過度的新增註釋,只需要為複雜的邏輯新增即可。

// 錯誤 ❌
function generateHash(str) {
  // 雜湊變數
  let hash = 0;

  // 獲取字串的長度
  let length = str.length;

  // 如果長度是空的就返回
  if (!length) {
    return hash;
  }

  // 遍歷字元
  for (let i = 0; i < length; i++) {
    // 獲取字元 code
    const char = str.charCodeAt(i);

    // 為 hash 賦值
    hash = (hash << 5) - hash + char;

    // 轉換為 32 位的整型
    hash &= hash;
  }
}

// 正確 ✅
function generateHash(str) {
  let hash = 0;
  let length = str.length;
  if (!length) {
    return hash;
  }

  for (let i = 0; i < length; i++) {
    const char = str.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash = hash & hash; // 轉換為 32 位的整型
  }
  return hash;
}

版本控制

完全沒有必要寫程式碼的修改歷史,版本管理(比如 git )已經幫我們做了這些事情。


// 錯誤 ❌
/**
 * 2021-7-21: Fixed corner case
 * 2021-7-15: Improved performance
 * 2021-7-10: Handled mutliple user types
 */
function generateCanonicalLink(user) {
  // const session = getUserSession(user)
  const session = user.getSession();
  // ...
}

// 正確 ✅
function generateCanonicalLink(user) {
  const session = user.getSession();
  // ...
}

本文簡短的討論了一些可以提高 ES6 程式碼可讀性的原則和方法,絕大多數原則可以應用到其他程式語言上,使用這些原則可能會比較花時間,但是長遠來看它可以保證你程式碼的可讀性可擴充套件性。