如何培養良好的程式設計實踐

付出發表於2019-03-03

花了三個星期的晚上,已經看完了《編寫可維護的JavaScript》這本書。總結如下:第一部分程式設計風格和第三部分的自動化測試在書籍中的結尾作者有將它總結整理出來,需要的可以自行去閱讀,也可以看看我之前整理的(《如何培養良好的程式設計風格》)。這本書的重點在於第二部分的程式設計實踐,也是最有營養的地方,可惜作者沒有在書中沒有去特意總結,我在這裡總結一下,以幫助大家一起提高程式碼質量。建議邊看邊敲邊感受,比單純的看文章要收穫的多。內容有點多,需要耐心耐心。

1. UI層的鬆耦合

很多設計模式是為了解決緊耦合的問題。如何做到鬆耦合,當修改一個元件而不需要更改其它地方的元件的時候,我們可以說這就是做到了鬆耦合,也是提高程式碼可維護性的關鍵所在。

1-1. 將JS從Css中抽離出來

示例程式碼

  // 不好的寫法
  .box {
    // Css表示式包裹在一個特殊的expression()函式中
    width: expression(document.body.offsetWidth + `px`)
  }
複製程式碼

推薦做法:避免使用CSS表示式(IE9以及IE9以上的瀏覽器不再支援CSS表示式)

1-2. 將CSS從JS中抽離出來

示例程式碼:

// 不好的寫法
element.style.color = `red`
element.style.cssText = `color: red; left: 10px; top: 100px;`
複製程式碼

當需要通過js來修改元素樣式的時候,通過操作CSS的className,最後在js中新增對應的類名即可。

示例程式碼:

/*定義CSS樣式*/
.reveal {
  color: red;
  left: 10px;
  top: 100px;
}
複製程式碼
// 好的寫法 - 原生寫法
element.className += `reveal`
// 好的寫法 - HTML5
element.classList.add(`reveal`)
複製程式碼

推薦做法:js不應當直接操作樣式,以便保持和CSS的鬆耦合。除了修改定位屬性的預設值,比如style.top,style.left通過js中修改預設值。

1-3. 將JS從HTML中抽離

// 不好的寫法
<button onclick="doSomething()">Click Me </button>
複製程式碼

第一個問題:在於嚴謹上來看,當按鈕上發生點選事件時,doSomething()函式必須存在。可能出現使用者點選按鈕時該函式還不存在,這時就會報JS錯誤;
第二個問題:在於可維護性來看,如果你修改了doSomething()函式名,在這個例子中,你需要同時修改HTML和JS兩部分的函式程式碼,這是典型的緊耦合的程式碼。
改進方法:
示例程式碼

function doSomething () {
 // 一些程式碼
}
var btn = document.getElementById(`action-btn`)
btn.addEventListener(`click`, doSomething, false)
複製程式碼

相容性處理
IE8以及更早的版本不支援addEventListener()函式, 因此你需要一個標準的函式將這些差異性做封裝。

示例程式碼

function addEventListernner(target, type, handler) {
 if (target.addEventListener) {
   target.addEventListener(type, handler, false)
 } else if (target.addEventListener) {
   target.addEventListener(`on` + type, handler)
 } else {
   target[`on` + type] = handler
 }
}
複製程式碼

這個函式可以在所有情形下都正常工作,我們常常像下面這樣來使用這個方法

function doSomething () {
 // 一些程式碼
}
var btn = document.getElementByid(`action-btn`)
addEventListener(btn, `click`, doSomething)
複製程式碼

推薦做法:對於”節點驅動”的的庫來說,比如JQ,推薦用事件監聽在js檔案中繫結節點同時給予對應的函式事件,不推薦直接在html檔案上繫結函式事件。

1-4. 將HTML從JS中抽離

// 不好的寫法
var div = document.getElementById(`mu-div`)
div.innerHTML = `<h3>Hello World</h3>`
複製程式碼

改進方法:

  • a.對於大量的標籤,可以採用 – 從伺服器載入

  • b.對於少量的標籤,可以採用 – 簡單的客戶端模板

  • c.複雜的客戶端模板,可以考慮如Handlebars(http://handlebarsjs.com/)所提供的解決方案,Handlebars是專為瀏覽器端JS設計的完整的客戶端模板系統。

1-5.一句話總結

HTML,CSS,JS,三者的關係應當是相互獨立分離的。如果產生交集,出現緊耦合程式碼,則違反了程式碼可維護性的原則。

2. 事件處理

情景引入

// 不好的用法
function handleClick (event) {
  var popup = document.getElementById(`popup`)
  popup.style.left = event.clientX + `px`
  popup.style.top = event.clientY + `px`
  popup.className = `reveal`
}
// 上文中的addEventListener()
addEventListener(element, `click`, handleClick)
複製程式碼

2-1. 隔離應用邏輯

上述例項程式碼的問題是事件處理程式(和使用者行為相關的)包含了應用邏輯(應用邏輯是和應用相關的功能性程式碼, 而不是和使用者行為相關的).
上述例項程式碼中,應用邏輯是在特定位置顯示一個彈出框,但是有時你需要在使用者滑鼠移至某個元素上時判斷是否顯示彈出框,或者當按下鍵盤上的某個按鍵時也彈出顯示框。
這樣多個事件的處理程式執行了同樣的應用邏輯,而你的程式碼卻被不小心複製了多份。

將應用邏輯從所有事件處理程式中抽離出來的做法是一種最佳實踐,我們將上述程式碼重寫一下如下:

// 好的寫法 -事件處理程式抽離應用邏輯
var MyApplication = {
  handleClick: function (event) {
    this.showPopup(event)
  },
  // 應用邏輯:顯示彈出框
  showPopup: function (event) {
    var popup = document.getElementById(`popup`)
    popup.style.left = event.clientX + `px`
    popup.style.top = event.clientY + `px`
    popup.className = `reveal`
  }
}
addEventListener(element, `click`, function(event) {
  MyApplication.handleClick(event)
})
複製程式碼

推薦做法: 事件處理程式抽離應用邏輯

2-2. 不要分發事件物件

在剝離出應用邏輯之後,上述程式碼還存在一個問題,即event物件被無節制分發。它從匿名函的事件處理函式傳入了MyApplication.handleClick(), 然後又傳入了MyApplication。showPopup(),
event物件上包含了很多和事件相關的額外資訊,而這段程式碼只用到了其中的兩個。
應用邏輯不應當依賴於event物件來正確完成功能。
最佳的做法是讓事件處理程式使用event物件來處理事件,然後拿到所需要的資料傳給應用邏輯。
例如:應用邏輯MyApplication。showPopup()方法只需要這兩個資料,x座標和y座標,我們將方法重寫一下如下:

// 好的寫法
var MyApplication = {
  handleClick: function (event) {
    this.showPopup(event.clientX, event.clientY)
  },
  // 應用邏輯:顯示彈出框
  showPopup: function (x, y) {
    var popup = document.getElementById(`popup`)
    popup.style.left = x + `px`
    popup.style.top = y + `px`
    popup.className = `reveal`
  }
}
addListener(element, `click`, function(event) {
  MyApplication.handleClick(event) // 可以這樣用
})
複製程式碼

在這段重寫的程式碼中MyApplication.handleClick()將x座標和y座標傳入了MyApplication。showPopup(),代替之前傳入的事件物件。這樣可以很清晰地看到MyApplication。showPopup()所期望
傳入的引數,並且在測試或程式碼的任意位置都可以很輕易地直接呼叫這段應用邏輯。比如:

// 這樣呼叫非常棒
MyApplication.showPopup(1010)
複製程式碼

推薦做法: 事件處理程式使用event物件來處理事件, 應用邏輯不應當依賴於event物件來正確完成功能,

2-3. 讓事件處理程式成為接觸到event物件的唯一的函式

事件處理函式應當在進入應用邏輯之前針對event物件執行任何必要的操作,包括阻止事件或阻止事件冒泡,都應當直接包含在事件處理程式當中。我們再次將上述程式碼重寫一下如下:

// 好的寫法
var MyApplication = {
  handleClick: function (event) {
    // 假設事件支援DOM Level2
    event.preventDefault()
    event.stopPropagation()
    // 傳入應用邏輯
    this.showPopup(event.clientX, event.clientY)
  },
  // 應用邏輯:顯示彈出框
  showPopup: function (x, y) {
    var popup = document.getElementById(`popup`)
    popup.style.left = x + `px`
    popup.style.top = y + `px`
    popup.className = `reveal`
  }
}
addEventListener(element, `click`, function(event) {
  MyApplication.handleClick(event) // 可以這樣用
})
複製程式碼

在這段程式碼中,MyApplication.handleClick是事件處理程式,因此它在將資料傳入應用邏輯之前呼叫了event.preventDefault()和event.stopPropagation(),
這清楚的展示了事件處理程式和應用邏輯之間的分工,因為應用邏輯不需要對event產生依賴,進而在很多地方都可以輕鬆地使用相同的業務邏輯,包括寫測試程式碼。

推薦做法:讓事件處理程式成為接觸到event物件的唯一的函式

2-4. 一句話總結

事件處理中的事件處理程式和應用邏輯的關係是獨立而分離的。事件處理程式負責處理event物件(不限於阻止事件或阻止事件冒泡),應用邏輯負責接收所需要的資料,不需要對event產生依賴。

3.將配置資料從程式碼中抽離出來

定義: 配置資料是在應用中寫死的值,且將來可能會被修改。

常見的配置資料有:URL,需要展現給使用者的字串,重複的值,設定(比如每頁的配置項),任何可能發生變更的值

示例程式碼

// 將配置資料抽離出來
var config = {
  MSG_INVALID_VALUE: `Invalid value`,
  URL_INVALID: `/errors/invalid.php`,
  CSS_SELECTED: `selected`
}
function validate (value) {
  if (!value) {
    alert (config.MSG_INVALID_VALUE)
    location.href = config.URL_INVALID
  }
}
function toggleSelected (element) {
  if (hasClass(element, config, CSS_SELECTED} {
    removeClass(element, config.CSS_SELECTED)
  } else {
    addClass(element, config.CSS_SELECTED)
  }
複製程式碼

在這段程式碼中,我們將配置資料儲存在了config物件中。config物件的每個屬性都儲存了一個資料片段,每個屬性名都有字首,用以表明資料的型別(MSG表示展現給使用者的資訊,URL表示網路地址,CSS表示這是一個calssName)。當然,命名約定是個人偏好。對於這段程式碼來說最重要的一點是,所有的配置資料都從函式中移除,並替換為config物件中的屬性佔位符。

4.不是你的物件不要動

請牢記,如果你的程式碼沒有建立這些物件,不要修改他們,包括原生物件(Object, Array等等),Dom物件(例如document),BOM物件(例如window),類庫的物件

4-1.原則

在面對不是我們自己擁有的物件面前,應當遵循以下三個原則

不覆蓋方法

// 不好的寫法
document.getElementById = function () {
  return null    // 引起混論
}
複製程式碼

不新增方法

Array.prototype.reverseSort = functino () {
  return this.sort().reverse()
}
複製程式碼

推薦做法: 大多數JavaScript庫有一個外掛機制,允許為程式碼庫新增一些功能。如果想修改,最佳最可維護的方式是建立一個外掛

不刪除方法

// 不好的寫法 -刪除了Dom方法
document.getElementById = null
複製程式碼

4-2. 更好的途徑 –通過繼承來擴充物件

在JavaScript中,繼承仍然有一些很大的限制。首先,還不能從DOM或BOM物件繼承。其次,由於陣列索引和length屬性之間錯綜複雜的關係,繼承自Array是不能正常工作的。

4-2-1. 基於物件的繼承,也經常叫做原型繼承,通過ES5的Object.create()方法實現

示例程式碼

var person = {
  name: `Nicholas`,
  sayName: function () {
    alert(this.name)
  }
}
var myPerson = Object.create(person)
myPerson.sayName = function () {
  alert(`Anonymous`)
}
myPerson.sayName() // 彈出 `Anonymous`   重新定義myPerson.sayName會自動切斷對person.sayName的訪問
person.sayName() // 彈出`Nicholas`
複製程式碼

Object.create()方法的第二個引數的屬性和方法將新增到新的物件中

var person = {
  name: `Nicholas`,
  sayName: function () {
    alert(this.name)
  }
}
var myPerson = Object.create(person, {
  name: {value:`Greg`}
  })
myPerson.sayName() // 彈出 `Greg`
person.sayName() // 彈出`Nicholas`
複製程式碼

一旦以這種方式建立了一個新物件,該新物件完全可以隨意修改。畢竟,你是該物件的擁有者,在自己的專案中可以任意新增方法, 覆蓋已存在的方法,甚至是刪除方法。

知識點傳送門: 關於物件更多的深淺拷貝知識點,請點選這裡自行擴充套件

4-2-2. 基於型別的繼承

繼承是依賴於原型的,通過建構函式實現

示例程式碼

function MyError (message) {
  this.message = message
}
MyError.prototype = new Error ()
複製程式碼

在上例中,MyError類繼承自Error(所謂的超類)。MyError.prototype賦值為一個Error的例項。然後,每個MyError例項從Error那裡繼承了它的屬性和方法,instanceof也能正常工作

function MyError (message) {
  this.message = message
}
MyError.prototype = new Error ()
var error = new MyError(`Something bad happened.`)
console.log(error instanceof Error) // true
console.log(error instanceof MyError) // true
複製程式碼

4-2-3.門面模式

門面模式是一種流行的設計模式,它為一個已存在的物件建立一個新的介面。你無法從DOM物件上繼承,所以唯一的能夠安全地為其新增功能的選擇就是建立一個門面。下面是一個DOM物件包裝器程式碼示例

function (element) {
  this.element = element
}
DOMWrapper.prototype.addClass = function (className) {
  element.className += `` + className
}
DOMWrapper.prototype.remove = function () {
  this.element.parentNode.removeChild(this.element)
}
// 用法
var wrapper = new DOMWrapper(document.getElementById(`my-div`))
// 新增一個className
wrapper = addClass(`selected`)
// 刪除元素
wrapper.remove()
複製程式碼

DOMWrapper型別期望傳遞給其構造器的是一個DOM元素。該元素會儲存起來以便以後引用,它還定義了一些操作該元素的方法。addClass()方法是為那些還未
實現HTML5的classList屬性的元素增加ClassName的一個簡單的方法。remove()方法封裝了從DOM中刪除一個元素的操作,遮蔽了開發者要訪問該元素父節點的需求。

4-2-4.三種型別的對比

從JavaScript的可維護性而言,門面是非常合適的方式,自己可以完全控制這些介面。你可以允許訪問任何底層物件的屬性或方法,反之亦然,也就是有效地過濾對該物件的訪問。
你也可以對已有的方法進行改造,使其更加簡單易用(上段示例程式碼就是一個案例)。底層的物件無論如何改變,只要修改門面,應用程式就能繼續正常工作。
門面實現一個特定介面,讓一個物件看上去像另一個物件,就稱作一個介面卡。門面和介面卡唯一的不同是前者建立新街口,後者實現已存在的介面。

4-3 阻止修改

ES5引入了幾個方法來防止對物件的修改。鎖定這些物件,保證任何人不能有意或無意地修改他們不想要的功能。

4-3-1. 三種鎖定修改的級別

防止擴充套件
禁止為物件`新增`屬性和方法,但已存在的屬性和方法是可以被修改或刪除

密封
類似`防止擴充套件`,而且禁止為物件`刪除`已存在的屬性和方法。

凍結
類似`密封`,而且禁止為物件`修改`已存在的屬性和方法(所有欄位均只讀)

每種鎖定的型別都有兩個方法:一個是用來實施操作,另一個用來檢測是否應用了相應的操作。

4-3-2. 應用示例程式碼

防止擴充套件

var person = {
  name: `Nicholas`
}
// 鎖定物件
Object.preventExtensions(person) // 實施可擴充套件
console.log(Object.isExtensible(person))  // false  檢測一個物件是否是可擴充套件的
person.age = 25 // 正常情況下悄悄地失敗,除非在strict模式下則會特意丟擲錯誤提示
複製程式碼

密封

var person = {
  name: `Nicholas`
}
// 鎖定物件
Object.seal(person)
console.log(Object.isExtensible(person))  // false  檢測一個物件是否是可擴充套件的
console.log(Object.isSealed(person)) // true 檢測一個物件是否是密封的
delete person.name // 正常情況下悄悄地失敗,除非在strict模式下丟擲錯誤
person.age = 25 // 同上
console.log(person)
複製程式碼

凍結

var person = {
  name: `Nicholas`
}
// 鎖定物件
Object.freeze(person)
console.log(Object.isExtensible(person))  // false  檢測一個物件是否是可擴充套件的
console.log(Object.isSealed(person)) // true 檢測一個物件是否是密封的
console.log(Object.isFrozen(person)) // true 檢測一個物件是否是凍結
person.name = `Greg` // 正常情況下悄悄地失敗,除非在strict模式下丟擲錯誤
person.age = 25 // 同上
delete person.name // 同上
console.log(person)
複製程式碼

4-3-3. 一句話總結

使用ES5中的這些方法是保證你專案不經過你同意鎖定修改的極佳的做法。如果你是一個程式碼庫的作者,很可能想鎖定核心庫某些部分來保證它們不被意外修改,或者想強迫
允許擴充的地方繼續存活著。如果你是一個應用程式的開發者,鎖定應用程式的任何不想被修改的部分。這兩種情況中,在全部定義好這些物件的功能之後,才能使用上述的方法。
一旦一個物件被鎖定了,它將無法解鎖。

5.總結

《編寫可維護的JavaScript》,第一部分的程式設計風格,給我的啟示是:我們在用Vue也好,React也好,在用框架前要多注意官方文件列出的程式設計風格,有助於我們規範程式碼結構,這是個小細節也是我們常常容易忽略的地方。第二部分程式設計實踐,HTML,JS,CSS的相互分離獨立,保持鬆耦合度;事件處理中的事件處理程式和應用邏輯的關係是獨立而分離的;將配置資料從程式碼中抽離出來;不是你的物件不要動;這些細節的改善,對於程式碼維護度的提高都是很有幫助的。至於第三部分自動化測試,講的更多的是像Ant,Ci系統工具的使用與安裝。整本書到這裡就已經結束了,以後更多的是在工作中的應用。覺得對你開發有幫助的可以點贊收藏一波,如果我哪裡寫錯了,希望能指點出來。如果你有更好的想法或者建議,可以提出來在下方評論區與我交流。大家一起進步,共同成長。感謝[鞠躬]。

6.一起交流

  • 個人的github倉庫,歡迎大家來star一下

  • 個人的微信公眾號,付出的前端路,訂閱微信公眾號yhzg_gz(點選複製,在微信中新增公眾號貼上即可)

ps: 提高自己,與異性交朋友

相關文章