##一.UI 層的鬆耦合
鬆耦合就是要求各層遵循「最少知識原則」,或者說是各層各司其職,不要越權:
-
HTML:結構層
-
CSS:表現層
-
JS:行為層
對於各層的職能,有一句比較貼切的解釋:HTML 是名詞(n),CSS 是形容詞(adj)和副詞(adv),JS 是動詞
因為三層聯繫緊密,實際應用中很容易越權:
###1.從 css 中分離 js
盡量不要用 css 表達式,如果非要用也應該把相應的代碼放在 hack 中,便於維護
###2.從 js 中分離 css
不要用 ele.style.attr 及 ele.cssText,應該用操作類名代替
###3.從 html 中分離 js
不要用 onclick 等屬性直接指定事件處理函數,應該用添加事件處理器方式代替
一般不要用<script>標籤直接嵌入 js 代碼,盡量放在外部 js 文件中。當然,對於功能單一且不需要復用的代碼可以直接嵌在 HTML 中,例如表單驗證代碼
###4.從 js 中分離 html
不要直接用 innerHTML 硬編碼,可以用以下 3 種方式代替:
-
用 Ajax 從服務端獲取 HTML 串,避免硬編碼
-
用簡單的客戶端模版,有 2 種實現方式:
-
用註釋攜帶模式串
-
用 script 標籤攜帶模式串,把 type 屬性設置為瀏覽器無法識別的值,還應該給 script 標籤設置 id 屬性以便於獲取模式串,例如:
<script type="text/x-my-template" id="list-item"> <li><a href="%s">%s</li> </script>推薦使用這種方式,因為更容易獲取模式串
-
-
用複雜的客戶端模版,比如 jade、ejs
P.S.因為 html 與 css 的解耦與 js 編程沒有關係,所以書中也沒有相應內容
##二.少用全局變量
###1.全局變量帶來的麻煩
-
命名衝突
-
代碼不健壯,函數需要的所有外部數據都應該用參數傳進來,而不要用全局變量傳參
-
難以測試,需要重建整個全局環境
###2.隱式全局變量
不要用隱式全局變量方式聲明全局變量,建議所有變量聲明都帶上var關鍵字
為了避免隱式全局變量,還應該開啟嚴格模式("use strict";),[IE10+] 支援
###3.單全局變量方式
-
用命名空間,提供一個避免命名空間衝突的方法:
// 頂級命名空間 var app = { /* * 創建/獲取子命名空間,支援鏈式調用 */ namespace: function(ns) { var parts = ns.split("."), object = this, i, len; for (i = 0, len = parts.length; i < len; i++) { if (!object[parts[i]]) { object[parts[i]] = {}; } object = object[parts[i]]; } return object; // 支援鏈式調用 } } // 測試 app.namespace("Consts").homepage = "http://ayqy.net/"; app.namespace("Consts").author = "ayqy"; // http://ayqy.net/, ayqy alert(app.Consts.homepage + ", " + app.Consts.author); -
模組化
AMD/CMD,一點擴展知識如下:
CommonJS 是一套理論規範(比如 js 的理論規範是 ES),而 SeaJS, RequireJS 都是對 CommonJS 的 Modules 部分的具體實現
CommonJS 是面向瀏覽器外 (server 端) 的 js 制定的,所以是同步模組加載,SeaJS 是 CommonJS 的一個實現,而 RequireJS 雖然也是對 CommonJS 的一個實現,但它是異步模組加載,算是更貼近瀏覽器的單線程環境
總結:CommonJS 的 Modules 部分提出了模組化代碼管理的理論,為了讓 js 可以模組化加載,而 RequireJS, SeaJS 等各種實現可以稱為模組化腳本加載器
- CMD:Common 模組定義,例如 SeaJS
- AMD:異步模組定義,例如 RequireJS
都是用來定義代碼模組的一套規範,便於模組化加載腳本,提高響應速度
CMD 與 AMD 的區別:
-
CMD 依賴就近。便於使用,在模組內部可以隨用隨取,不需要提前聲明依賴項,所以性能方面存在些許降低(需要遍歷整個模組尋找依賴項目)
-
AMD 依賴前置。必須嚴格聲明依賴項,對於邏輯內部的依賴項(軟依賴),以異步加載,回調處理的方式解決
###4.零全局變量方式
用 IIFE(匿名函數立即執行)實現,針對不需要復用的功能模組可以用 IIFE 完全消除全局變量,所以一般 IIFE 都是用來輔助命名空間/模組化方式的
##三.事件處理
###1.典型用法(不好)
// 事件處理器
function handleClick(event) {
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "display";
}
// 添加事件處理器
ele.addEventListener("click", handleClick);
###2.分離應用邏輯(稍好一點)
var app = {
// 事件處理
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 = "display";
}
};
// 添加事件處理器
// P.S.事件處理器是一個方法聲明,而不是方法調用,無法傳參,所以需要多一層匿名函數
ele.addEventListener("click", function() {
app.handleClick(event);
});
###3.不要傳遞事件對象(最好)
var app = {
// 事件處理
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 = x + "px";
popup.className = "display";
}
};
// 添加事件處理器
ele.addEventListener("click", function() {
app.handleClick(event);
});
「不要傳遞事件對象」是一條優化原則,在[JS 高程的優化部分](http://ayqy.net/blog/JS 學習筆記 12_優化/) 也有提到過,但本書給出了詳細理由
直接傳遞事件對象存在以下缺點:
-
接口定義不明確,參數作用未知
-
難以測試(重建一個 event 對象?)
##四.少與 null 比較
###1.檢測基本值
用 typeof 檢測,但要注意typeof null返回object,這不太科學,因為 js 認為 null 是一個空對象的引用
但用 === null 檢測 DOM 元素是合理的,因為 null 是 document.getXXByXXX 的可能輸出之一
###2.檢測引用值
instanceof 並不能準確地檢測子類型,而且不要用它檢測 fun 和 arr,因為不能跨 frame
- 檢測 fun
用 typeof 檢測一般方法;用 in 檢測 DOM 方法
- 檢測 arr
用Object.prototype.toString.call(arr) === "[Object Array]"檢測
注意:ES5 有原生的 Array.isArray() 方法,[IE9+] 支援
###3.檢測屬性
用 in 配合 hasOwnProperty() 檢測
注意:[IE8-] 的 DOM 元素不支援 hasOwnProperty(),用的時候要先檢測
##五.分離配置數據
###1.配置數據有哪些?
-
硬編碼的值
-
將來可能會變的值
比如:
-
URL
-
需要顯示給用戶的字符串
-
重複使用的唯一值
-
設置(例如每頁顯示多少列表項)
-
可能會變的任何值(不好維護的東西都算配置數據)
###2.分離配置數據
先從應用邏輯中分離出配置數據,最簡單的可以把所有配置數據分級存放在自定義的 config 對象中
###3.存儲配置數據
可以用 js 文件存儲配置數據,便於加載,但配置數據要嚴格符合 js 語法,容易出錯。作者建議把配置數據存儲為格式簡單的屬性文件再用工具轉換為 JSON/JSONP/js 格式文件用於加載,作者推薦一個自己寫的工具 props2js
##六.拋出自定義錯誤
###1.Error 的本質
用來標誌出乎意料的東西,避免靜默失敗,便於調試
###2.用 js 拋出錯誤
不要拋其它類型,要拋 Error 對象,因為兼容性,例如:
throw "error: invalid args"; // 有些瀏覽器不顯示該字符串
###3.拋出錯誤的優點
精確定位錯誤,建議錯誤信息格式:函數名 + 錯誤原因
###4.何時該拋出錯誤
只拋常用方法(工具方法)中的錯誤,一般原則:
-
修復了奇葩 bug 之後,應該添幾個自定義錯誤,防止錯誤再次發生
-
如果寫代碼的時候覺得某些點可能引起大麻煩,就應該拋出自定義錯誤
-
如果代碼是寫給別人用的,應該想想別人使用的時候可能遇到哪些問題,應該在自定義錯誤中給出提示
###5.try-catch 語句
finally 不常用,因為會影響 catch 中的 return
不要留空的 catch 塊,靜默失敗可能會把問題變得更麻煩
###6.幾種錯誤類型
可以拋出原生的幾種錯誤類型實例,配合 instanceof 做針對性錯誤處理
P.S.關於具體的幾種錯誤類型以及錯誤處理的更多信息,請查看 [黯羽輕揚:JS 學習筆記 8_錯誤處理](http://ayqy.net/blog/JS 學習筆記 8_錯誤處理/)
##七.尊重對象所有權
###1.哪些對象不屬於我們?
-
原生對象(Object、Array 等等)
-
DOM 對象(比如 document)
-
BOM 對象(比如 window)
-
庫對象(比如 JQuery)
###2.具體原則
-
不要重寫方法
-
不要添新方法,非要改庫功能的話,可以給庫開發插件
-
不要刪除方法,不想用的不要刪除,標明過時即可
注意:delete 對原型屬性無效,只對實例屬性有用
function Fun() {
this.attr1 = 1; // 實例屬性
}
Fun.prototype.attr2 = 2; // 原型屬性
// 測試
var obj = new Fun();
alert(obj.attr1 + ", " + obj.attr2); // 1, 2
delete obj.attr1;
delete obj.attr2;
alert(obj.attr1 + ", " + obj.attr2); // undefined, 2
delete Fun.prototype.attr2;
alert(obj.attr1 + ", " + obj.attr2); // undefined, undefined
###3.更好的方式
- 基於對象的繼承
也就是 clone 一個新對象,新對象是屬於自己的,可以隨便改
- 基於類型的繼承
注意:不要繼承 DOM/BOM/Array,因為支持性不好
P.S.關於對象繼承/類型繼承的具體實現,請查看 [黯羽輕揚:重新理解 JS 的 6 種繼承方式](http://ayqy.net/blog/重新理解 JS 的 6 種繼承方式/)
- 外觀模式
其實就是組合,因為繼承/組合都是實現代碼復用的方式,關於外觀模式的更多信息請查看 黯羽輕揚:設計模式之外觀模式(Facade Pattern)
一點題外話:facade 與 adapter 的區別是前者創建新接口,後者只是實現了已存在的接口,作者一針見血
###4.polyfill 的優缺點
polyfill 是「在舊版瀏覽器上複製標準 API 的 JavaScript 補充」。「標準 API」指的是 HTML5 技術或功能,例如 Canvas。「JavaScript 補充」指的是可以動態地加載 JavaScript 代碼或庫,在不支援這些標準 API 的瀏覽器中模擬它們。因為 polyfill 模擬標準 API,所以能夠以一種面向所有瀏覽器未來的方式針對這些 API 進行開發,最終目標是:一旦對這些 API 的支援變成絕對大多數,則可以方便地去掉 polyfill,無需做任何額外工作。
關於 polyfill 的更多信息請查看 博客園:[譯]shim 和 polyfill 有什麼區別?
優點:不需要的時候很容易去掉
缺點:如果 polyfill 的實現與標準不完全一致就麻煩了
建議不要用 polyfill,應該用原生方法 + 外觀模式來代替,這樣更靈活
###5.防篡改
應該開嚴格模式,因為非嚴格模式下靜默失敗難以調試
##八.瀏覽器檢測
###1.UA(User Agent)檢測
非要 UA 檢測的話,盡量向後檢測而不是向前,因為後面的不會再變了
P.S.作者認為不用管 UA 會不會變,理由是會設置 UA 的用戶應該也知道這樣做的後果
###2.特性檢測
特性檢測的一般格式如下:
-
嘗試標準方式
-
嘗試特定瀏覽器的實現方式
-
不支援的話要給出邏輯反饋(比如 return null)
例如:
function setAnimation (callback) {
// 1.嘗試標準方式
if (window.requestAnimationFrame) { // standard
return requestAnimationFrame(callback);
}
// 2.嘗試���定瀏覽器的實現方式
else if (window.mozRequestAnimationFrame) { // Firefox
return mozRequestAnimationFrame(callback);
}
else if (window.webkitRequestAnimationFrame) { // WebKit
return webkitRequestAnimationFrame(callback);
}
else if (window.oRequestAnimationFrame) { // Opera
return window.oRequestAnimationFrame(callback);
}
else if (window.msRequestAnimationFrame) { // IE
return window.msRequestAnimationFrame(callback);
}
// 3.不支援的邏輯反饋
else {
return setTimeout(callback, 0);
}
}
###3.杜絕特性推斷
不能根據一個特性推斷另一個特性,因為長得像鴨子的東西不一定也像鴨子一樣呱呱叫
###4.杜絕瀏覽器推斷
不要根據特性去推斷瀏覽器,比如典型的:
if (document.all) { // IE
// ...
}
這樣做是不對的,不應該嘗試用特徵去描述一個東西,很容易因為條件過少或者過多導致描述不準確
###5.到底應該用哪一個?
盡量用直接特性檢測,不行才用 UA 檢測。至於推斷,就根本不考慮了,沒有任何理由去用推斷
###參考資料
- 《Maintainable JavaScript》

暫無評論,快來發表你的看法吧