如何寫出優雅耐看的JavaScript程式碼

發表於2019-09-20

前言

在我們平時的工作開發中,大多數都是大人協同開發的公共專案;在我們平時開發中程式碼codeing的時候我們考慮程式碼的可讀性複用性擴充套件性

乾淨的程式碼,既在質量上較為可靠,也為後期維護、升級奠定了良好基礎。

我們從以下幾個方面進行探討:

變數

1、變數命名

一般我們在定義變數是要使用有意義的詞彙命令,要做到見面知義

//bad code
const yyyymmdstr = moment().format('YYYY/MM/DD');
//better code
const currentDate = moment().format('YYYY/MM/DD');

2、可描述

通過一個變數生成了一個新變數,也需要為這個新變數命名,也就是說每個變數當你看到他第一眼你就知道他是幹什麼的。

//bad code
const ADDRESS = 'One Infinite Loop, Cupertino 95014';
const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], ADDRESS.match(CITY_ZIP_CODE_REGEX)[2]);

//better code
const ADDRESS = 'One Infinite Loop, Cupertino 95014';
const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || [];
saveCityZipCode(city, zipCode);

3、形參命名

在for、forEach、map的迴圈中我們在命名時要直接

//bad code
const locations = ['Austin', 'New York', 'San Francisco'];
locations.map((l) => {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  // 需要看其他程式碼才能確定 'l' 是幹什麼的。
  dispatch(l);
});

//better code
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  dispatch(location);
});

4、避免無意義的字首

例如我們只建立一個物件是,沒有必要再把每個物件的屬性上再加上物件名

//bad code
const car = {
  carMake: 'Honda',
  carModel: 'Accord',
  carColor: 'Blue'
};

function paintCar(car) {
  car.carColor = 'Red';
}

//better code
const car = {
  make: 'Honda',
  model: 'Accord',
  color: 'Blue'
};

function paintCar(car) {
  car.color = 'Red';
}

5、預設值

//bad code
function createMicrobrewery(name) {
  const breweryName = name || 'Hipster Brew Co.';
  // ...
}

//better code
function createMicrobrewery(name = 'Hipster Brew Co.') {
  // ...
}

函式

1、引數

一般引數多的話要使用ES6的解構傳參的方式

//bad code
function createMenu(title, body, buttonText, cancellable) {
  // ...
}

//better code
function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}

//better code
createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
});

2、單一化處理

一個方法裡面最好只做一件事,不要過多的處理,這樣程式碼的可讀性非常高

//bad code
function emailClients(clients) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

//better code
function emailActiveClients(clients) {
  clients
    .filter(isActiveClient)
    .forEach(email);
}
function isActiveClient(client) {
  const clientRecord = database.lookup(client);    
  return clientRecord.isActive();
}

3、物件設定預設屬性

//bad code
const menuConfig = {
  title: null,
  body: 'Bar',
  buttonText: null,
  cancellable: true
};
function createMenu(config) {
  config.title = config.title || 'Foo';
  config.body = config.body || 'Bar';
  config.buttonText = config.buttonText || 'Baz';
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);


//better code
const menuConfig = {
  title: 'Order',
  // 'body' key 缺失
  buttonText: 'Send',
  cancellable: true
};

function createMenu(config) {
  config = Object.assign({
    title: 'Foo',
    body: 'Bar',
    buttonText: 'Baz',
    cancellable: true
  }, config);

  // config 就變成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
  // ...
}

createMenu(menuConfig);

4、避免副作用

函式接收一個值返回一個新值,除此之外的行為我們都稱之為副作用,比如修改全域性變數、對檔案進行 IO 操作等。

當函式確實需要副作用時,比如對檔案進行 IO 操作時,請不要用多個函式/類進行檔案操作,有且僅用一個函式/類來處理。也就是說副作用需要在唯一的地方處理。

副作用的三大天坑:隨意修改可變資料型別、隨意分享沒有資料結構的狀態、沒有在統一地方處理副作用。

//bad code
// 全域性變數被一個函式引用
// 現在這個變數從字串變成了陣列,如果有其他的函式引用,會發生無法預見的錯誤。
var name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
  name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];


//better code
var name = 'Ryan McDermott';
var newName = splitIntoFirstAndLastName(name)

function splitIntoFirstAndLastName(name) {
  return name.split(' ');
}

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];

在 JavaScript 中,基本型別通過賦值傳遞,物件和陣列通過引用傳遞。以引用傳遞為例:

假如我們寫一個購物車,通過 addItemToCart()方法新增商品到購物車,修改 購物車陣列。此時呼叫 purchase()方法購買,由於引用傳遞,獲取的 購物車陣列正好是最新的資料。

看起來沒問題對不對?

如果當使用者點選購買時,網路出現故障, purchase()方法一直在重複呼叫,與此同時使用者又新增了新的商品,這時網路又恢復了。那麼 purchase()方法獲取到 購物車陣列就是錯誤的。

為了避免這種問題,我們需要在每次新增商品時,克隆 購物車陣列並返回新的陣列。

//bad code
const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
};

//better code
const addItemToCart = (cart, item) => {
  return [...cart, {item, date: Date.now()}]
};

5、全域性方法

在 JavaScript 中,永遠不要汙染全域性,會在生產環境中產生難以預料的 bug。舉個例子,比如你在 Array.prototype上新增一個 diff方法來判斷兩個陣列的不同。而你同事也打算做類似的事情,不過他的 diff方法是用來判斷兩個陣列首位元素的不同。很明顯你們方法會產生衝突,遇到這類問題我們可以用 ES2015/ES6 的語法來對 Array進行擴充套件。

//bad code
Array.prototype.diff = function diff(comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter(elem => !hash.has(elem));
};

//better code
class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter(elem => !hash.has(elem));        
  }
}

6、避免型別檢查

JavaScript 是無型別的,意味著你可以傳任意型別引數,這種自由度很容易讓人困擾,不自覺的就會去檢查型別。仔細想想是你真的需要檢查型別還是你的 API 設計有問題?

//bad code
function travelToTexas(vehicle) {
  if (vehicle instanceof Bicycle) {
    vehicle.pedal(this.currentLocation, new Location('texas'));
  } else if (vehicle instanceof Car) {
    vehicle.drive(this.currentLocation, new Location('texas'));
  }
}

//better code
function travelToTexas(vehicle) {
  vehicle.move(this.currentLocation, new Location('texas'));
}

如果你需要做靜態型別檢查,比如字串、整數等,推薦使用 TypeScript,不然你的程式碼會變得又臭又長。

//bad code
function combine(val1, val2) {
  if (typeof val1 === 'number' && typeof val2 === 'number' ||
      typeof val1 === 'string' && typeof val2 === 'string') {
    return val1 + val2;
  }

  throw new Error('Must be of type String or Number');
}

//better code
function combine(val1, val2) {
  return val1 + val2;
}

複雜條件判斷

我們編寫js程式碼時經常遇到複雜邏輯判斷的情況,通常大家可以用if/else或者switch來實現多個條件判斷,但這樣會有個問題,隨著邏輯複雜度的增加,程式碼中的if/else/switch會變得越來越臃腫,越來越看不懂,那麼如何更優雅的寫判斷邏輯

1、if/else

點選列表按鈕事件

/**
 * 按鈕點選事件
 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 商品售罄 4 開團成功 5 系統取消
 */
const onButtonClick = (status)=>{
  if(status == 1){
    sendLog('processing')
    jumpTo('IndexPage')
  }else if(status == 2){
    sendLog('fail')
    jumpTo('FailPage')
  }else if(status == 3){
    sendLog('fail')
    jumpTo('FailPage')
  }else if(status == 4){
    sendLog('success')
    jumpTo('SuccessPage')
  }else if(status == 5){
    sendLog('cancel')
    jumpTo('CancelPage')
  }else {
    sendLog('other')
    jumpTo('Index')
  }
}

從上面我們可以看到的是通過不同的狀態來做不同的事情,程式碼看起來非常不好看,大家可以很輕易的提出這段程式碼的改寫方案,switch出場:

2、switch/case

/**
 * 按鈕點選事件
 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 商品售罄 4 開團成功 5 系統取消
 */
const onButtonClick = (status)=>{
  switch (status){
    case 1:
      sendLog('processing')
      jumpTo('IndexPage')
      break
    case 2:
    case 3:
      sendLog('fail')
      jumpTo('FailPage')
      break  
    case 4:
      sendLog('success')
      jumpTo('SuccessPage')
      break
    case 5:
      sendLog('cancel')
      jumpTo('CancelPage')
      break
    default:
      sendLog('other')
      jumpTo('Index')
      break
  }
}

這樣看起來比if/else清晰多了,細心的同學也發現了小技巧,case 2和case 3邏輯一樣的時候,可以省去執行語句和break,則case 2的情況自動執行case 3的邏輯。

3、存放到Object

將判斷條件作為物件的屬性名,將處理邏輯作為物件的屬性值,在按鈕點選的時候,通過物件屬性查詢的方式來進行邏輯判斷,這種寫法特別適合一元條件判斷的情況。

const actions = {
  '1': ['processing','IndexPage'],
  '2': ['fail','FailPage'],
  '3': ['fail','FailPage'],
  '4': ['success','SuccessPage'],
  '5': ['cancel','CancelPage'],
  'default': ['other','Index'],
}
/**
 * 按鈕點選事件
 * @param {number} status 活動狀態:1開團進行中 2開團失敗 3 商品售罄 4 開團成功 5 系統取消
 */
const onButtonClick = (status)=>{
  let action = actions[status] || actions['default'],
      logName = action[0],
      pageName = action[1]
  sendLog(logName)
  jumpTo(pageName)
}

4、存放到Map

const actions = new Map([
  [1, ['processing','IndexPage']],
  [2, ['fail','FailPage']],
  [3, ['fail','FailPage']],
  [4, ['success','SuccessPage']],
  [5, ['cancel','CancelPage']],
  ['default', ['other','Index']]
])
/**
 * 按鈕點選事件
 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 商品售罄 4 開團成功 5 系統取消
 */
const onButtonClick = (status)=>{
  let action = actions.get(status) || actions.get('default')
  sendLog(action[0])
  jumpTo(action[1])
}

這樣寫用到了es6裡的Map物件,是不是更爽了?Map物件和Object物件有什麼區別呢?

  1. 一個物件通常都有自己的原型,所以一個物件總有一個"prototype"鍵。
  2. 一個物件的鍵只能是字串或者Symbols,但一個Map的鍵可以是任意值。
  3. 你可以通過size屬性很容易地得到一個Map的鍵值對個數,而物件的鍵值對個數只能手動確認。

程式碼風格

常量大寫

//bad code
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

class animal {}
class Alpaca {}

//better code
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

class Animal {}
class Alpaca {}

先宣告後呼叫

//bad code
class PerformanceReview {
  constructor(employee) {
    this.employee = employee;
  }

  lookupPeers() {
    return db.lookup(this.employee, 'peers');
  }

  lookupManager() {
    return db.lookup(this.employee, 'manager');
  }

  getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
  }

  getManagerReview() {
    const manager = this.lookupManager();
  }

  getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.perfReview();

//better code
class PerformanceReview {
  constructor(employee) {
    this.employee = employee;
  }

  perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
  }

  getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  lookupPeers() {
    return db.lookup(this.employee, 'peers');
  }

  getManagerReview() {
    const manager = this.lookupManager();
  }

  lookupManager() {
    return db.lookup(this.employee, 'manager');
  }

  getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.perfReview();

相關文章