花了三個星期的晚上,已經看完了《編寫可維護的JavaScript》這本書。總結以下:第一部分編程風格和第三部分的自動化測試在書籍中的結尾做者有將它總結整理出來,須要的能夠自行去閱讀,也能夠看看我以前整理的(《如何培養良好的編程風格》)。這本書的重點在於第二部分的編程實踐,也是最有養分的地方,惋惜做者沒有在書中沒有去特地總結,我在這裏總結一下,以幫助你們一塊兒提升代碼質量。建議邊看邊敲邊感覺,比單純的看文章要收穫的多。內容有點多,須要耐心耐心。php
不少設計模式是爲了解決緊耦合的問題。如何作到鬆耦合,當修改一個組件而不須要更改其它地方的組件的時候,咱們能夠說這就是作到了鬆耦合,也是提升代碼可維護性的關鍵所在。css
示例代碼html
// 很差的寫法
.box {
// Css表達式包裹在一個特殊的expression()函數中
width: expression(document.body.offsetWidth + 'px')
}
複製代碼
推薦作法:避免使用CSS表達式(IE9以及IE9以上的瀏覽器再也不支持CSS表達式)前端
示例代碼:git
// 很差的寫法
element.style.color = 'red'
element.style.cssText = 'color: red; left: 10px; top: 100px;'
複製代碼
當須要經過js來修改元素樣式的時候,經過操做CSS的className,最後在js中添加對應的類名便可。github
示例代碼:express
/*定義CSS樣式*/
.reveal {
color: red;
left: 10px;
top: 100px;
}
複製代碼
// 好的寫法 - 原生寫法
element.className += 'reveal'
// 好的寫法 - HTML5
element.classList.add('reveal')
複製代碼
推薦作法:js不該當直接操做樣式,以便保持和CSS的鬆耦合。除了修改定位屬性的默認值,好比style.top,style.left經過js中修改默認值。編程
// 很差的寫法
<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文件上綁定函數事件。
// 很差的寫法
var div = document.getElementById('mu-div')
div.innerHTML = '<h3>Hello World</h3>'
複製代碼
改進方法:
a.對於大量的標籤,能夠採用 - 從服務器加載
b.對於少許的標籤,能夠採用 - 簡單的客戶端模板
c.複雜的客戶端模板,能夠考慮如Handlebars(http://handlebarsjs.com/)所提供的解決方案,Handlebars是專爲瀏覽器端JS設計的完整的客戶端模板系統。
HTML,CSS,JS,三者的關係應當是相互獨立分離的。若是產生交集,出現緊耦合代碼,則違反了代碼可維護性的原則。
情景引入
// 很差的用法
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)
複製代碼
上述實例代碼的問題是事件處理程序(和用戶行爲相關的)包含了應用邏輯(應用邏輯是和應用相關的功能性代碼, 而不是和用戶行爲相關的). 上述實例代碼中,應用邏輯是在特定位置顯示一個彈出框,可是有時你須要在用戶鼠標移至某個元素上時判斷是否顯示彈出框,或者當按下鍵盤上的某個按鍵時也彈出顯示框。 這樣多個事件的處理程序執行了一樣的應用邏輯,而你的代碼卻被不當心複製了多份。
將應用邏輯從全部事件處理程序中抽離出來的作法是一種最佳實踐,咱們將上述代碼重寫一下以下:
// 好的寫法 -事件處理程序抽離應用邏輯
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)
})
複製代碼
推薦作法: 事件處理程序抽離應用邏輯
在剝離出應用邏輯以後,上述代碼還存在一個問題,即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(10, 10)
複製代碼
推薦作法: 事件處理程序使用event對象來處理事件, 應用邏輯不該當依賴於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對象的惟一的函數
事件處理中的事件處理程序和應用邏輯的關係是獨立而分離的。事件處理程序負責處理event對象(不限於阻止事件或阻止事件冒泡),應用邏輯負責接收所須要的數據,不須要對event產生依賴。
定義: 配置數據是在應用中寫死的值,且未來可能會被修改。
常見的配置數據有: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對象中的屬性佔位符。
請牢記,若是你的代碼沒有建立這些對象,不要修改他們,包括原生對象(Object, Array等等),Dom對象(例如document),BOM對象(例如window),類庫的對象
在面對不是咱們本身擁有的對象面前,應當遵循如下三個原則
不覆蓋方法
// 很差的寫法
document.getElementById = function () {
return null // 引發混論
}
複製代碼
不新增方法
Array.prototype.reverseSort = functino () {
return this.sort().reverse()
}
複製代碼
推薦作法: 大多數JavaScript庫有一個插件機制,容許爲代碼庫新增一些功能。若是想修改,最佳最可維護的方式是建立一個插件
不刪除方法
// 很差的寫法 -刪除了Dom方法
document.getElementById = null
複製代碼
在JavaScript中,繼承仍然有一些很大的限制。首先,還不能從DOM或BOM對象繼承。其次,因爲數組索引和length屬性之間錯綜複雜的關係,繼承自Array是不能正常工做的。
示例代碼
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'
複製代碼
一旦以這種方式建立了一個新對象,該新對象徹底能夠隨意修改。畢竟,你是該對象的擁有者,在本身的項目中能夠任意新增方法, 覆蓋已存在的方法,甚至是刪除方法。
知識點傳送門: 關於對象更多的深淺拷貝知識點,請點擊這裏自行擴展
繼承是依賴於原型的,經過構造函數實現
示例代碼
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
複製代碼
門面模式是一種流行的設計模式,它爲一個已存在的對象建立一個新的接口。你沒法從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中刪除一個元素的操做,屏蔽了開發者要訪問該元素父節點的需求。
從JavaScript的可維護性而言,門面是很是合適的方式,本身能夠徹底控制這些接口。你能夠容許訪問任何底層對象的屬性或方法,反之亦然,也就是有效地過濾對該對象的訪問。 你也能夠對已有的方法進行改造,使其更加簡單易用(上段示例代碼就是一個案例)。底層的對象不管如何改變,只要修改門面,應用程序就能繼續正常工做。 門面實現一個特定接口,讓一個對象看上去像另外一個對象,就稱做一個適配器。門面和適配器惟一的不一樣是前者建立新街口,後者實現已存在的接口。
ES5引入了幾個方法來防止對對象的修改。鎖定這些對象,保證任何人不能有意或無心地修改他們不想要的功能。
防止擴展 禁止爲對象'添加'屬性和方法,但已存在的屬性和方法是能夠被修改或刪除
密封 相似'防止擴展',並且禁止爲對象'刪除'已存在的屬性和方法。
凍結 相似'密封',並且禁止爲對象'修改'已存在的屬性和方法(全部字段均只讀)
每種鎖定的類型都有兩個方法:一個是用來實施操做,另外一個用來檢測是否應用了相應的操做。
防止擴展
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)
複製代碼
使用ES5中的這些方法是保證你項目不通過你贊成鎖定修改的極佳的作法。若是你是一個代碼庫的做者,極可能想鎖定核心庫某些部分來保證它們不被意外修改,或者想強迫 容許拓展的地方繼續存活着。若是你是一個應用程序的開發者,鎖定應用程序的任何不想被修改的部分。這兩種狀況中,在所有定義好這些對象的功能以後,才能使用上述的方法。 一旦一個對象被鎖定了,它將沒法解鎖。
《編寫可維護的JavaScript》,第一部分的編程風格,給個人啓示是:咱們在用Vue也好,React也好,在用框架前要多注意官方文檔列出的編程風格,有助於咱們規範代碼結構,這是個小細節也是咱們經常容易忽略的地方。第二部分編程實踐,HTML,JS,CSS的相互分離獨立,保持鬆耦合度;事件處理中的事件處理程序和應用邏輯的關係是獨立而分離的;將配置數據從代碼中抽離出來;不是你的對象不要動;這些細節的改善,對於代碼維護度的提升都是頗有幫助的。至於第三部分自動化測試,講的更多的是像Ant,Ci系統工具的使用與安裝。整本書到這裏就已經結束了,之後更多的是在工做中的應用。以爲對你開發有幫助的能夠點贊收藏一波,若是我哪裏寫錯了,但願能指點出來。若是你有更好的想法或者建議,能夠提出來在下方評論區與我交流。你們一塊兒進步,共同成長。感謝[鞠躬]。
我的的github倉庫,歡迎你們來star一下
我的的微信公衆號,付出的前端路,訂閱微信公衆號yhzg_gz(點擊複製,在微信中添加公衆號粘貼便可)
ps: 提升本身,與異性交朋友