JavaScript編碼風格約定

第一部分 編程風格


    每個人都有自己獨特的編程風格,有情有獨鍾亦或恨之入骨的,每個人都希望用自己的方式制定規範來實施。這些應當被歸爲個人編程嗜好。我們需要的是在編程過程中儘早的確定統一的編程風格。

    在團隊開發中,編程風格一致性變得尤爲重要,因爲:
    1 任何開發者都不在乎某個文件的作者是誰,也沒有必要花費額外的精力去理解代碼的邏輯並且重新排版,因爲所有代碼的排版風格都是非常一致的,因爲風格不一致會導致我們打開代碼文件的第一件事情不是立即工作,而是進行縮進的排版整理。
    2 能很容易的辨別出有問題的代碼並且發現錯誤,如果所有的代碼風格很像,當看到一段與衆不同的代碼時,很肯能問題就出在這裏。

    JSLint 和 JSHint 是兩個檢查編程風格的工作。不僅找出代碼中潛在的錯誤,還能對潛在的風格問題給予提示警告。
    “程序是寫給人讀的,只是偶爾讓計算機執行一下” - Donald Knuth

第一章 基本的格式化


    編程風格的核心就是基本的格式化規範,這些規範決定着如何編寫高水準的代碼。

    1.1 縮進層級

    所有語言都是如此,都會討論如何對代碼縮進。
    if(wl && wl.length){
    for(var i=0; i<wl.length; i++){
    p = wl[i];
        type=Y.Lang.type(wl[i]);
    ...
    }
    }
    
    稍微編寫過程序的都不會使用上面的格式,害人害己。
    if(wl && wl.length){
        for(var i=0; i<wl.length; i++){
            p = wl[i];
            type=Y.Lang.type(wl[i]);
            ...
        }
    }

    代碼應該如何統一縮進其實一直沒有統一的共識。1.使用製表符縮進 。 2.使用空格縮進。
    jQuery 明確規定使用製表符縮進。
    Dauglas Crockford 的 JavaScript 規定使用4個空格縮進。
    Google 的 JavaScript規定使用2個空格縮進。
    Dojo 編程風格指南使用製表符縮進。

    尼古拉斯(作者)建議4個空格,或在編輯器中設置一個 製表符 替換爲 4個空格。爲一個縮進層。

    1.2 語句結尾

    強制規定,語句結束插入分號。
    function(){
        return
        {
            ...
        }
    }
    上面代碼就會引起問題。

    1.3 行的長度

    很少有JS的規範提及到行的長度的風格的,很多語言都建議行的字數不超過 80 字,因爲很早以前的編輯器,這是最大的行長度。
    Java語言規範中 源碼單行長不超過 70 字,文檔中單行長不超過 80 字。
    Android開發者規範中規定不超過 100 字。
    非官方的Ruby規定不操過 80 字。
    Python編程規範規定單行不超過 79 字。

    JavaScript 在 Crockford 代碼規範中規定爲 80 字符。

    1.4 換行

    當一行字字符數超過了最大限制的時候就需要換行。通常會在運算符之後換行,下一行會增加2個層級的縮進。
    callFunction(document,"aaaaaaaaaaaaaaa","bbbbbbbbbbbbbbbb","cccccccccccccccccccc",
            "bbbbbbbbbbbbbbbbbbb");//超出首字母c的2個層級
    需要注意的是規範爲在運算符之後換行,以及增加2個層級的縮進。

    當然如果是賦值的話就保持賦值運算符對其。
    var result = "aaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbb" + "ccccccccccccccccccc" +
                        "ddddddddddddddddddddddddd";
    

    1.5 空行

    空行常常被忽略,但是空行也是提高代碼可讀性的強大武器。
    if(wl && wl.length){

        for(var i=0; i<wl.length; i++){

            p = wl[i];
            type=Y.Lang.type(wl[i]);
            
            if(a){
                ...
            }
        }
    }

    空行一般來說不是很好在規範中規定,
    1.方法之間
    2.方法中局部變量可第一條語句之間。
    3.多行或者單行註釋之前。
    4.在方法內的邏輯片段之前。
    目前沒有規範對空行進行明確的規定。

    1.6 命名

    "計算機科學只存在兩個難題:緩存失效和命名" - Phil Karlton
    只要寫代碼就會涉及到變量和函數,所以變量和函數的命名對於增強代碼的可讀性至關重要。
    JavaScripe 遵循 ECMA ,即駝峯式大小寫。
    var myName;
    var anotherName;
    google Dojo等規範都規定使用小駝峯命名法。,當然還有一種曾經風光一時的 匈牙利命名法。

    1.6.1 變量和函數

    變量名總是應該遵循駝峯大小寫命名法,並且命名的前綴應當是名詞,使得可以將變量和函數區分開。
    // 以下OK
    var count = 10;
    var myName = "Ni";
    var found = true;

    // 以下就需要重新命名
    var getCount = 10;
    var isFound = true;

    //函數應當以動詞開頭
    var getName  = function(){
        return myName;
    };

    還有一點示範
    can        函數返回布爾值
    has        函數返回布爾值
    is           函數返回布爾值
    get        函數返回非布爾值
    set        函數用來保存值

    1.6.2 常量

    JS中沒有常量的概念。來自C語言的約定,常量使用大寫字母並且使用下劃線命名。
    var MAX_COUNT = 10;
    var URL = "http://www.guokr.com";
    google Dojo 等編程風格指南都規定如此。

    1.6.3 構造函數

    JS中的構造函數只不過是冠以 new 運算符的函數而已。採用大駝峯命名法。
    function Person( name ){
        this.name = name;
    }

    1.7 直接量

    JS中的原始值:字符串、數字、布爾值、null和underfined。

    1.7.1 字符串

    JS中的字符串使用雙引號和單引號是完全相同的。對於規範只需要統一即可。
    jQuery 和 Crockford規範爲雙引號來括住字符串,google規範爲單引號括住字符串。

    1.7.2 數字

    JS中整數和浮點數都存儲爲相同的數據類型。
    不推薦使用 10. 和 .1 的寫法。會產生歧義,是否是我們漏掉了數字。

    1.7.3 null

    null 常會與 undefined 搞混淆。以下場景才應當使用null:
    1. 用來初始化變量,該變量可能被賦值爲對象。
    2. 用來和一個已經初始化的變量進行比較,這個變量可以是非對象。
    3. 當函數期望的參數是對象,用作參數傳入。
    4. 當函數的返回值期望是對象時,用作返回值傳出。
    以下場景不應當使用 null:
    1. 不要使用null來檢測是否傳入了參數。
    2. 不要用null來檢測一個未初始化的變量。

    var person = null;
    
    function getPerson(){
        if( condition ){
            return new Person("ABC");
        }else{
            return null;
        }
    }
    對於 null 的理解最好是當做對象的佔位符使用,主流的規範沒有提及。

    1.7.4 undefined

    由於 
    var person;
    typeof person; //  undefined
    typeof foo;   // undefined
    以及
    null == undefined
    所以,禁止使用特殊值 undefined 可以有效地確保只在一種情況下 typeof 才返回 undefined。

    1.7.5 對象直接量 與 數組直接量(字面量)

    創建對象與數組最流行的方法爲對象字面量和數組字面量,比相應的構造函數式創建更高效直接。


第二章 註釋

    

    2.1 單行註釋

    三種使用方法:
    1. 獨佔一行,用來解釋下一行,並且這行註釋之前總有一行空行,縮進的層級與下一行代碼保持一致。
    2.在代碼行的尾部註釋。代碼與註釋之間至少有一個縮進。註釋包括之前本行的代碼不應該超過最大字符數限制。
    3.註釋掉大塊代碼。

    // 好的註釋方法
    if( condition ){
        
        // 解釋下一行代碼
        func();
    }

    // 不好的註釋方法
    if( condition ){
        // 解釋下一行代碼
        func();
    }

    if( condition ){
    // 解釋下一行代碼
        func();
    }

    2.2 多行註釋

    偏向使用 Java 風格的註釋
    /*
     * 這裏是註釋
     * 這裏是註釋
     * 這裏是註釋
     */
    縮進的格式與單行註釋表示一致。

    2.3 使用註釋

    當代碼不夠清晰的時候使用註釋,當代碼很明瞭的時候不應當添加註釋。
    並且註釋不應該只是解釋變量名稱或者函數名稱式的那種廢話。
    當代碼很難理解或者代碼可能被誤認爲錯誤的代碼的時候,需要添加註釋。

    2.4 文檔註釋

    技術角度來說,文檔註釋並不是 JS 的組成部分,但是是一種普遍的實踐。
    1.應當對所有的方法和可能的返回值添加描述。
    2.對自定義的構造函數類型和期望的參數添加描述。
    3.對於包含對象或者方法的對象進行描述。

第三章 語句和表達式


    JS中,諸如 if 和 for 之類的規定不管是單行代碼還是多行代碼,均使用 { } 
    // 好的寫法
    if( contidion ){
        foo();
    }
    
    if( conidion ){
        foo();
        foo();
    }

    // 不好的寫法
     if( contidion ){ foo(); }
    
     if( contidion )  foo(); 

    所有的語句塊都應當使用 { }, if , for , while ,do while , try catch finnally 。

    3.1 花括號對齊方式

    有兩種對齊方式
    if( conidion ){
        foo();
        foo();
    }
    和
    if( conidion )
    {
        foo();
        foo();
    }
    很顯然由於第二種又時候會會讓瀏覽器執行代碼的時候產生不一樣的解析,所以使用第一種花括號對齊方式。
    

    3.2 塊語句間隔

    語句塊間隔有3中形式:
    1.語句名 圓括號 左花括號間沒有空格間隔。
    2.左圓括號之前 右圓括號之後添加一個空格。
    3.左圓括號 右圓括號前後都添加一個空格。
    作者推薦一致使用第二種形式。

    3.3 switch 語句

    JS中任何表達式都可以合法的用於case從句中,其他語言必須使用原始值和常量。
    

    3.3.1 縮進

    JS的switch縮進一致是有爭議的話題。
    switch (condition) {
        case "a":
            //代碼
            break;

        case "b":
            //代碼
            break;

        default :
            // 代碼
    }
    每個case相對於switch都有一個縮進層。
    第二個case開始之前都有一個空行。
    另一種 Crockford 和Dojo編程風格爲
    switch (condition) {
    case "a":
        //代碼
        break;
    case "b":
        //代碼
        break;
    default :
        // 代碼
    }

    3.3.2 case語句的連續執行(貫穿)

    switch (condition) {
        case "a":
        case "b":
            //代碼
            break;

        default :
            // 代碼
    }
    Douglas 在 Crockford中禁止出現貫穿現象, 但是Nichlas認爲只要程序邏輯很清楚,貫穿完全是一個可以接受的編程方式。Jquery 編程風格中也是允許的。

    3.3.3 default

    是否在任何時候都需要 default。

    switch (condition) {
        case "a":
        case "b":
            //代碼
            break;

       //沒有 default :
    }
    更傾向於沒有默認行爲並且在註釋中寫入省略 default。

    3.4 with

    with 語句可以更改包含上下文解析變量的方式。通過with可以使用局部變量的方式和函數的形式來訪問特定對象中的屬性和方法。可以縮短代碼的長度。
    但是是的JS引擎與壓縮工具無法壓縮。嚴格模式中 with 是禁止的。 基本所有的規範中都禁止使用 with。

    3.5 for 循環

    對於for循環中的 break 和 return 會改變循環的方向,儘量避免使用,但是不禁止。

    3.6 for-in 循環 

    for-in循環建議與 hasOwnProperty 一起檢測屬性,除非你需要檢查原型。
    for-in是用來遍歷對象的,不要用其來遍歷數組。


第四章 變量,函數和運算符


    JS編程的本質是編寫一個個函數來完成任務。在函數內部,變量和運算符可以通過移動操作字節來使某個事件發生。通過書寫格式化來減少複雜度,增強可讀性顯得十分重要。
    

    4.1 變量聲明

    變量聲明是通過 var 語句來完成的。變量聲明不管是在何處聲明,所有的 var 語句都會提前到代碼頂部執行。
    變量聲明提升意味着:在函數任何地方定義變量和在函數頂部定義變量是完全一樣的。所以有了一種風格就是將所有變量聲明集中在函數頂部而不是散落在代碼各個地方進行聲明。
    由而產生了一種單一的 var 風格聲明。

    function doSomeThing (item){
        var i, len, value = 10, result = value+10 ;
        
        for(i=10, len=item.length; i<len; i++){
            do(item[i]);
        }
    }

    尼古拉斯推薦 合併var聲明,可以清晰看到所有變量名 以及縮小代碼量。並且建議每個變量都獨佔一行,統一縮進。
    function doSomeThing (item){
        var i,
              len,
              value = 10,
              result = value+10 ;
        
        for(i=10, len=item.length; i<len; i++){
            do(item[i]);
        }
    }
    記得有位牛人博客曾經指出一些較爲牛逼的建議,其中一條就是攻擊單一 var 聲明。 認爲比如 for 循環中的() 中 i 就應該在使用的時候聲明邏輯纔可以更清晰,
    而很多代碼量是很大的,在前面聲明之後有可能會忘了一些變量名的本意。

    4.2 函數聲明

    和變量聲明一樣,函數聲明也會被JS引擎提升。所以在函數聲明之前與函數聲明之後調用函數都是一樣的。
    推薦總是先聲明函數,再使用函數。Crockford規範還推薦在函數內部的函數應當緊跟着變量聲明之後聲明。

    function doSomeThing (item){
        var i,
              len,
              value = 10,
              result = value+10 ;
        
        function do(item){
            ...
        }
        
        for(i=10, len=item.length; i<len; i++){
            do(item[i]);
        }
    }
    當函數在聲明之前使用的話,JSLint和JSHint都會給出警告。
    函數聲明不應當出現在語句塊之內,會有兼容方面的問題導致各個瀏覽器解析不一致。
    if(condition){
        function doThing(){
            ...
        }
    } else {
        function doThing(){
            ...
        }
    }
    該種場景目前屬於ECMA的灰色地帶,應當避免。也是google規範中禁止的。

    4.3 函數調用隔離

    一般情況下函數調用的寫法是,在函數名和左括號之間沒有空格,爲了將他與塊語句區分開來。
    // good
    doThing(item);

    // not good
    doThing (item);

    //用來作比較的語句應當加入空格
    while (condition){
        // code
    }

    對於jQuery的風格就是 在內部參數的前後加入空格,是個代碼比較透氣,增強可讀性和讀代碼的舒適度。
    doThing( item );
    當然不是所有的情況都應當如此,對於jQuery其他情況如下
    doThing(function() {});
    doThing({ item: item });
    doThing([ item ]);
    doThing("Hi !");

    4.4 立即調用的函數

    可能省略圓括號其效果是一樣的,但是會帶來可讀性上的降低。
    // 好的寫法
    var value = (function(){
        
        //函數體

        return {
            message: "Hi !"
        }
    }());

    // 不好的寫法
    var value = function(){
        
        //函數體

        return {
            message: "Hi !"
        }
    }();

    4.5 嚴格模式

    ECMA5 中引入的嚴格模式,可以通過這種方式來謹慎的解析 JS 代碼,以減少錯誤。
    "use strict";
    遵循ECMA5的瀏覽器會將其識別爲指令,使用嚴格模式進行解析代碼,而不支持的瀏覽器只會當做普通代碼塊。
    不推薦將嚴格模式使用在全局之中,可能產生的問題就是,如果我有11個文件需要做合併處理,那麼當有一個文件是使用的全局的嚴格模式,那麼合併後的文件整體就會使用嚴格模式,也行我其餘的10個文件不遵循嚴格模式呢?
    function sayHi(){
        "use strict";
    
        return "Hi !";
        
    }
    在JSLint與JSHint中,也會對出現在函數體之外的嚴格迷模式聲明發出警告。當然,推薦使用嚴格模式。

    4.6 相等

    JS具有強制類型轉換的機制,所以判斷相等的操作還是很微妙的
    5 == "5" // true
    25 == "0x19" // true
    2 == true // false

    對於對象,則會首先調用對象的 valueOf() 方法,得到原始類型值比較,如果沒有定義則調用 toString()。
    var object ={
        toString: function(){
            return "0x19";
        }
    }
    object == 25; // true
    由於強類型轉換的情況,所以需要使用 === 和 !== 來處理比較的情況。而不推薦使用 == 和 != 。
    

    4.6.1 eval()

    eval() , Function構造函數 , setTimeout() , setInterval() 都是可以講一段字符串作爲代碼來執行。
    執行的代碼可能產生變量名污染和安全性的問題。
    當然也不完全封殺 eval 因爲對於一些特殊情況還是需要使用 eval 來處理代碼的,一般只是用在沒有別的方法來處理的時候才使用。

    4.6.2 原始包裝類型

    對於 字符串構造函數, 函數構造函數等,一律使用字面量形式,不適用構造函數形式。對於正則表達式,在有些情況下比如動態創建正則的時候還是需要使用的。 

第二部分 編程實踐


    構建軟件設計的方法有兩種:一種是把軟件做的很簡單以至於明顯找不到缺陷,另一種是把它做的很複雜以至於找不到明顯的缺陷。
                                                                                                                                      - C.A.R.Hoare  80年圖靈獎獲得者
    上一部分編程風格只關心代碼的呈現,而這部分編程實踐則關心編碼的結果。引導開發者以某種方式進行編碼,設計模式是編程實現的組成部分,專用於解決和軟件組織的相關特定問題。
    

    第五章 UI層的鬆散耦合


    在實際的場景中,css 與 js 應該爲兄弟關係,而不是依賴關係。
    需要增加分層的合理性和消除依賴。

    5.1 什麼是鬆耦合

    很多的設計模式就是爲了解決緊耦合的問題。如果2個組件耦合太緊密,就說明一個組件和另一個組件直接相關。
    比如有一個名爲 error 的className,貫穿整個站點。有一天覺得他命名不合理需要更改的時候,有可能我們需要做更改上百個使用到它的組件,也有可能只需要更改一個組件。
    你能做到只修改一個組件而不是說修改 N 個組件,那麼就做到了鬆耦合。對於一些大型系統來說這是至關重要的。

    5.2 將 JS 從 CSS中抽離

    IE 8 中可以使用 css 表達式(CSS expression)。允許將JS代碼直接插到 css 中。
    .box{
        width: expression(document.body.offsetWidth + "px");
    }
    這是絕對不建議這麼做的。 當然 IE 9 已經不再支持了。

    5.3 將 CSS 從 JS 中抽離

    兩門語言相互協作的很不錯,通過 JS 控制樣式最流行的一種做法就是 直接修改 DOM 的 style 屬性
    element.style.color = "#FFF";
    element.style.display= "block"
    有些還使用 cssText 來進行對 style 的賦值。
    以上是不建議 在各個方面都不如以下方式:
    將需要改變的樣式放入新的 css 選擇器中,需要更新樣式的時候只需要加入或者替換該 classname即可。
    JS不應當直接操作樣式,以便與 css保持鬆耦合。

    有一種情況是可以的,就是將頁面元素重新定位的時候,比如一些動畫變幻等效果。

    5.4 將 JS 從 HTML 中分離

    在學習 JS 之初,經常將事件綁定放入頁面的 HTML 中。
    <button οnclick="doSomething()" id="action-btn">Click Me</button>
    首先,是需要 doSomething 函數已存在,就需要將 JS 代碼放置在 HTML 之前。
    在次,對於可維護性方面有很大的弊端。
    所以應當使用事件處理程序的綁定機制。addEventListener removeEventListener。當然一些JS庫將其封裝的更好。

    5.5 將 HTML 從 JS 中抽離

    正如將 JS 從 HTML 中抽離一樣,最好也將 HTML 從 JS 中抽離。因爲在我們對文本或者頁面結構進行調試的時候我們更希望直接面對的是 HTML 而不是一些 JS 語句動態的修改結構。
    在 JS 中使用最多的情況就是給 innerHTML 賦值。
    var div = document.getElementById("myDiv");
    div.innerHTML = "<p>abc</p><p>efg</p>";

    將HTML嵌入JS中是非常不好的實踐,原因有:
    1.大大增加了跟蹤文本和結構性問題的複雜度。
    2.加大了代碼的維護成本。
    3.相對於只修改 HTML,當JS和HTML混淆在一起時,修改代碼更容易引發其他問題。

    將HTML從JS抽離
    1.從服務器加載。將模板放置於原創服務器。使用Ajax方式加載模板文件。
    2.簡單的客戶端模板。帶有一些替換標籤的片段,<li><a href="%s">%s</a></li> 然後通過函數將模板和數據結合。
    3.複雜的客戶端模板。目前一些較爲健全的開源模板庫,可以很好地幫助實現所需要的較爲大的項目需求。


第六章 避免使用全局變量


    JS執行環節有很多獨特之處相對於其他語言來說,如全局變量和函數的使用。
    JS本身的初始執行環境就是有多種多樣的全局變量所定義的,這些全局變量在環境創始之初就存在了。
    全局對象是一個神祕的對象,表示腳本的最外層上下文。

    瀏覽器中 window對象往往重載並等於全局對象,因此任何在全局對象中聲明的變量或者函數都爲window對象的屬性。
    而不需要顯式的將這些掛在到 window 對象上。
    var color = "red";
    function getColor(){
        alert(color);
    }

    color === window.color;
    getColor === window.getColor;

6.1 全局變量帶來的問題

    隨着代碼的增長,全局變量毫無疑問的會導致一些非常重要的可維護性問題。

6.1.1 命名衝突

    當全局變量和全局函數越來越多的時候,發生命名衝突概率就會越來越大。各種變量,函數將會被重置,那麼很多各種各樣的BUG就會隨之而來。
    比如:
    function getColor(){
        alert(color);
    }
    中的 color 由於依賴與全局的變量,那麼這種依賴關係將很難被追蹤到,我在不小心重置了 color 後不知道有多少像該函數一樣的函數進行了全局依賴的行爲。

    接下來,全局變量與一些瀏覽器內置API衝突的風險。

6.1.2 代碼的脆弱性

    一個依賴於全局變量的函數即是深耦合於上下文環境之中。環境的變化就可能會導致函數的失效。
    但是當 color 被作爲參數傳遞進函數的話,那麼情況就大大不一樣了。
    function getColor( color ){
        alert(color);
    }
    不在依賴全局變量,並且從與上下文的深耦合之中脫離出來。所以當定義函數儘可能的將數據置於局部變量之中,任何外部數據都應當以參數的形式傳遞進入函數,保證函數與其外部環境隔離開,不至於形成深度耦合的關係。

6.1.3 難以測試

    任何依賴於全局變量才能正常工作的函數,只有爲其重新創建完整的全局變量才能正確的測試,然後,就木有然後了。
    保證函數不多全局變量有依賴,將大大增強代碼的可測試性,當然不包括JS中原生的對象,如Date,Array等。

6.2 意外的全局變量

    JS 中有很多陷阱會使我們一不小心就創建了全局變量,如:
    function doSomething(){
        var count = 10;
              title = "abcdefg";

        var a = b = 0;
    }
    對於意外的全局變量一些工具,比如JSLint和JSHint就可以起到作用了,因爲意外創建全局變量並不會引起JS引擎的報錯,有時候很難察覺到,而這些工具就是我們很好的預防,消除一些意外創建的情況。還有嚴格模式下也會報錯來提醒程序猿。

6.3 單全局變量方式

    YUI 引入 唯一 YUI全局變量。
    jQuery 引入 $ 和 jQuery 全局變量。
    Dojo 引入 dojo 全局變量。
    Closure 引入 goog 全局變量。
    單全局變量意味着創建一個唯一的全局對象名,將所有你的功能代碼掛在到該對象上,都成爲該對象的屬性,從而不創建其他的全局變量。
    
    function Book( title ){
        this.title = title;
        this.page = 1;
    }
    Book.prototype.turnPage = function( desc_num ){
        this.page += desc_num;
    }
    var chapter1 = new Book("one");
    var chapter2 = new Book("two");
    var chapter3 = new Book("three");
    
    會有好多全局變量,那麼:
    var MainJS = {};
     MainJS.Book = function( title ){
        this.title = title;
        this.page = 1;
    }
    MainJS.Book.prototype.turnPage = function( desc_num ){
        this.page += desc_num;
    }
     MainJS. chapter1 = new MainJS.Book("one");
     MainJS. chapter2 = new MainJS.Book("two");
     MainJS. chapter3 = new MainJS.Book("three");
    其實,很簡單,但是帶來的效果確是非常不錯的。

6.3.1 命名空間

    JS中的命名空間,其實質就是不斷的往一個定義的全局對象中,合理有規則的塞東西。
    var MyGlobal = {
        namespace : function( ns ){
            var parts = ns.split("."),
                  object = this,
                  i, len;
            
            if( parts[0] === "MyGlobal" ){
                parts = parts.slice(1);
            }

            for( i = 0,len = parts.length; i<len; i++ ){

                if( !object[parts[i]] ){
                    object[parts[i]] = {};
                }
                object = object[parts[i]];
            }

            return object;
        }
    }

    然後可以自由的創建命名空間了,
    MyGlobal.namespace("aa.bb.cc.dd");
    MyGlobal.namespace("MyGlobal.aa.bb.cc.dd");

6.3.2 模塊

    另外一種基於單全局變量的擴充方法是使用模塊。
    模塊是一種通用的功能片段,並不創建全局變量和命名空間,而是將這些代碼存放在一個表示執行一個任務或者發佈一個藉口的單函數中。兩種流行的模塊是 YUI模塊 和 異步模塊定義(AMD)。

    YUI模塊
    是使用YUI JS類庫創建新模塊的一種模式,寫法:
    YUI.add("module-name", function(Y){
        // 模塊正文
    }, "version", { requires: ["depend1", "depend2"]  });

    異步模塊定義 AMD
    define( "module-name", ["depend1", "depend2"] , function(d1, d2){
        // 模塊正文
    });

6.4 零全局變量

    JS代碼注入到頁面的時候可以實現不創建全局變量。當然使用的場景不會非常多。在一段完全獨立的代碼,或者代碼非常小且不提供任何接口的時候。
    (function(win){
        // 代碼 不暴露任何接口
    }( window ));
    只要代碼需要被其他的代碼所依賴的時候,就不可以使用零全局變量方式。在對於代碼塊的代碼合併時候有挺大作用。


第七章 事件處理


    事件處理在JS中是至關重要的,影響着網站的各個方面。所有的JS代碼均通過事件綁定到UI上,所以大多前端工程師需要花費很多的事件來編寫和修改事件處理程序。
    大多事件處理程序相關代碼和事件環境緊緊偶合在一起,導致了可維護性很糟糕。

7.1 典型用法

    開發人員基本都瞭解,事件觸發的時候事件對象event會作爲回調函數參數傳入事件處理程序之中。event對象中含有的大量信息基本上我們只會用到很小一部分。
    // 不好的例子
    function handleClick( event ){
        var popup = document.getElementById( "popup" );
        popup.style.left = event.clientX + "px";
        popup.style.top = event.clientY + "px";
        popup.className = "reveal";
    }
    addListener(element, "click", handleClick);
    這是比較普遍的做法,實際上並不是很好,具有侷限性。

7.2 規則1:隔離應用邏輯

    上一段代碼中,第一個問題是事件處理程序中包含了應用邏輯(application logic),應用邏輯是與應用相關的功能性代碼,而不是用戶行爲相關的。
    將應用邏輯從所有的事件處理程序中抽離出來是一種最佳實踐。因爲很有可能在之後的某段代碼中我們會使用到同一段邏輯,抽離就降低了代碼的耦合度,增強了可讀性和維護成本。
    如:
    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";
        }
    }

    addListener(element, "click", function(event){
        MyApplication.handleClick( event );
    });
    這樣就將應用邏輯從事件處理程序中剝離,如果在某些地方又要使用該邏輯就可以很方便的使用方法。

7.3 不要分發事件對象

    再撥離了應用邏輯之後上段代碼又引發了一個問題,event對象被無節制的分發,所以,應用邏輯不應當依賴於event對象。
    1.方法接口沒有表明那些數據是必要的。
    2.影響測試方面效率。
    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";
        }
    }
    這樣就很不錯了,再加上進入程序時,阻止默認事件和事件冒泡即可。
    清楚地展示了事件處理程序和應用邏輯之間的分工。應用邏輯也不需要對event產生任何依賴,進而很多地方都可以使用相同的應用邏輯

第八章 避免空比較


    JS中經常會有變量與null比較。來判斷變量是否被賦予合理的值。

    var Controller = {
        process : function( items ){
            if( items !== null ){
                items.sort();
                item.forEach(function( item ){
                    // ... Code
                });
            }
        } 
    }

    很明顯我們本意是需要檢測,傳入的參數是否爲數組,但是僅僅和null進行判斷並不能夠提供足夠的信息來保證後續代碼執行的安全性。還好,靈活的JS提供了多種檢測的方式。

    8.1 檢測原始值

    5種原始類型 字符串、數字、布爾值、null和undefined。
    typeof 可以檢測各個原始值的類型,並且返回相應類型字符串。而 typeof null 會返回 "object"。

    if( typeof str === "string" ){
        // ... Code
    }

    需要注意的是,typeof 一個未聲明的變量也不會報錯,會返回 "undefined" ,所以也需要注意該種情況,對於檢測 null,則直接與 null 進行比較即可。

    var element = document.getElementById("my-div");
    if( element === null ){
        element.className = "abc";
    }

    8.2 檢測引用值

    對於檢測引用值,typeof會力不從心,基本都會返回"object",那麼對於引用值檢測最好的方法是使用 instanceof。

    if( value instanceof Date ){
        // ... Code
    }
    if( value instanceof RegExp ){
        // ... Code
    }
    if( value instanceof Error){
        // ... Code
    }

    instanceof 還有個特性就是,它不只是檢測構造這個對象的構造器,還檢測原型鏈。

    var now = new Date();
    now instanceof Date;
    now instanceof Object;
    均爲 true。由於這個原因也使得使用 instanceof 來檢測引用值也不是最佳的方法。

    對於自定義的構造函數來說,instancdof 檢測是唯一的方法。當然需要注意一下 跨幀 使用時的問題。

    8.2.1 檢測函數

    JS中同樣存在 Function 構造函數,每個函數均是其實例。
    myFunc instanceof Function
    當然也是不適用與跨幀的情況。

    還有一個好的寫法就是使用 typeof
    typeof myFunc === "function"
    但是對於 IE8以及以下瀏覽器來說,
    typeof document.getElementById 會返回 "object".( 還有幾個其他的DOM操作 )。

    8.2.2 檢測數組

    JS最古老的跨域問題在於 幀 frame 之間來回傳遞數組。每個幀有自己的 數組構造函數,所以在一個幀中的數組實例是不被另外幀所承認的。
    優雅的解決方案:
    Object.prototype.toString.call( value ) === "[object Array]"
    對於檢測數組的需求 ES5 中引入了 Array.isArray() 方法來檢測。 

    優雅方案
    function idArray( value ){
        if( typeof Array.isArray === "function" ){
            return Array.isArray( value );
        }else{
            return Object.prototype.toString.call( value ) === "[object Array]";
        }
    }

    8.3 檢測屬性

    很多時候我們會使用與 null  undfined 對比來判斷是否屬性存在。
    if( obj[prop] ){  }
    if( obj[prop] != null ){  }
    if( obj[prop] != undefined){  }
    這樣會導致代碼有漏洞,不能將所有的情況都覆蓋到。

    對於屬性的檢測,最好的方法是使用 in,in運算符僅僅會簡單的判斷 屬性 是否存在,而不會去讀屬性的值。當然 in是會在原型鏈中進行查找的。
    如果只是希望檢測實例屬性,那麼使用 hasOwnPrototype() 進行檢測。
    注意,在IE8以及之前瀏覽器,DOM元素並非繼承自 Object 所以沒有該方法。
    最後,不管什麼時候需要檢測屬性是否存在的時候,請使用 in 或者 hasOwnProperty()。


第九章 將配置數據從代碼中分離出來


    代碼就是一些計算機運行的指令,當我們傳遞數據進入計算機的時候,指令對數據進行操作產生結果。
    那麼當我們在修改一些數據問題的時候就會帶來一些源代碼引入BUG的風險,所以對於應用來說,應當將一些關鍵數據從主要的源碼中抽離出來,這樣就可以是我們在修改數據或者源碼的時候更放心。
    

    9.1 什麼是配置數據

    配置數據就是我們在應用中寫死(hardcoded)的值。
    // 將配置數據藏於源碼之中

    function validate( value ){
        if( !value ){
            alert("Invalid value");
            location.href = "/errors/invalid.html";
        }
    }

    上面的函數具有2個配置數據片段, 字符串 "Invalid value" 和 URL地址 "/errors/invalid.html"。
    之所以將其認爲是配置數據是因爲它們都寫死在代碼裏,並且在將來可能需要修改。下面是一些配置數據的例子:
    1. URL
    2. 需要展示給用戶看的字符串
    3. 重複的值
    4. 設置的值,一些配置選項
    5.任何可能發生變化的值

    我們需要記住,配置數據隨時都有可能做修改,我們不希望因爲有人需要修改頁面的提示信息而需要修改JS源碼。

    9.2 抽離配置數據

    其實,抽離配置數據非常簡單,只需要創建一個管理整體的配置數據的對象即可。
    var config = {
        MSG_INVALID_VALUE : "Invalid value",
        URL_INVALID : "/errors/invalid.html"
    }
    再在需要使用配置數據的地方使用即可。
    注意統一在配置數據對象中的命名。將配置數據提取出來之後任何人需要做修改都不會引起一些代碼的邏輯錯誤問題,對於很大的系統,可以將配置數據專門使用其他單獨文件,使其於代碼隔離。

    9.3 保存配置數據

    配置數據最好放在單獨的文件之中,以便清晰地分隔數據和應用邏輯。一種值得嘗試的方法是將這些配置數據放於非JS的文件中。
    1. 使用各種流行的屬性文件來存放。(如 Java屬性文件)
    2. JSONP,將JSON結構使用函數調用包裝起來。
    3. 純JS對象
    還有一些專門可以管理配置文件的庫可以使用。

第十章 拋出自定義錯誤


    編程語言具有“創建”錯誤的能力,在JS中拋出錯誤是一門藝術。再合適的時間拋出錯誤可以大大減少我們調試的事件,對代碼的滿意度也將急劇提升。

    10.1 錯誤的本質

    當某些非期望的事情發生的時候,程序就會要發一個錯誤。這也許是傳入了非法的值,或許是遇到無效的操作符等等。
    編程語言定義了一組基本的規則,當偏離了這些規則的時候將導致錯誤,只有當錯誤被拋出來時,我們纔能有地方入手解決,如果錯誤無聲無息,那麼解決的代價可想而知。所以錯誤時開發者的朋友而非敵人。
    JS錯誤消息信息稀少,並且在一些老版本的IE中更是令人深痛惡絕。所以在代碼中的特殊之處有計劃的拋出一個錯誤總比在所有地方有可能拋出錯誤簡單得多。

    10.2 在 JS 中拋出錯誤

    毫無疑問,在 JS 中拋出錯誤比任何語言中做同樣的事情更有價值,這要歸結於Web端調試的複雜性。可以使用 throw 操作符,將提供一個對象作爲錯誤拋出,Error對象時最常用的。
    throw new Error("bad happened");
    還有種方式就是直接拋出字符串
    throw "bad happened";
    但會有兼容性的問題,所以不建議使用。

    10.3 拋出錯誤的好處

    可以看一下一下兩個函數的優略:
    function getDivs( element ){
        return element.getElementByTagName("div");
    }

    function getDivs( element ){
        if( element && element.getElementsByTagName ){
            return element.getElementByTagName("div");
        } else {
            throw new Error("getDivs() : Argument must be a DOM element");
        }
    }
    雖然chrome等控制檯以及對於錯誤的拋出做的非常好了,但是也還是有一些瀏覽器在一些特殊情況下會起到很大作用。

    10.4 何時拋出錯誤

    // 不好的寫法 檢測太多
    function addClass(element, className){
        if( !element || typeof element.className != "string" ){
            throw new Error("addClass() : First argument must be a DOM element");
        }
        if( typeof element.className != "string" ){
            throw new Error("addClass() : First argument must be a DOM element");
        }
        element.className +=" " + className;
    }
    原本只是需要加一個class 但是加了那麼多的拋出錯誤,就會適得其反。會引起過度殺傷。
    // 好的寫法
    function addClass(element, className){
        if( !element || typeof element.className != "string" ){
            throw new Error("addClass() : First argument must be a DOM element");
        }
        element.className +=" " + className;
    }

    1. 當修復了一個難以調試的錯誤的時候,嘗試增加一兩個自定義錯誤,當再次發生錯誤的時候有助於解決問題。
    2. 如果正在編寫代碼,思考一下我希望哪些事情不會發生,如果發生將引發較大問題,在這個某些問題上拋出錯誤。
    3. 如果在編寫別人的代碼,思考下他們的使用方式在特定的情況下拋出錯誤。
    這些我們所做的都不是爲了防止錯誤,而是在有錯誤 時候更快更容易的找到錯誤的原因。

    10.5 try-catch 語句

    try-catch 可以在瀏覽器拋出錯誤之前解析,將可能引發錯誤的代碼放在 try 塊中,將處理錯誤的代碼放在 catch 中。當然還可以加入 finally 塊。
    在catch中不要爲空,應該總要寫點聲明老處理錯誤,不然就會依舊不知道錯誤在哪裏。

    10.6 錯誤的類型

    Error : 所有錯誤的基本類型,引擎不會拋出此類型。
    EvalError : 通過 eval() 函數執行的代碼發生錯誤。
    RangeError : 一個數字超出邊界時拋出,試圖創建一個長度爲-20的數組。
    ReferenceError : 期望對象不存在,例如試圖在null上調用函數。
    SyntaxError :  語法上的錯誤。
    TypeError : 變量不是期望類型的時候。
    URIError : 給一些內置函數傳入非法URL時拋出的錯誤。


第十一章 不是你的對象不要動

    
    JS獨特之處就是任何東西的都不是神聖不可侵犯的,默認情況下,你可以修改任何你能觸及到的對象。那麼,在一個大型項目之中,對象的修改變成了一個大問題。

    11.1 什麼是你的

    當你的代碼創建了這些對象,那麼你擁有這些對象。維護是你的責任。牢記,如果你的代碼沒有創建這些對象,那麼不要修改它們。
    1. 原生對象。    2. DOM對象    3. 瀏覽器對象模式對象(BOM)。    4. 類庫對象。

    11.2 原則

    在JS中,我們將已經存在的對象視爲一種背景,在此之上開發代碼,應該將JS對象當做一個實用的工具函數來看待。
    不覆蓋方法,不增加方法,不刪除方法。

    11.2.1 不覆蓋方法

    JS有史以來最糟糕的實踐就是 覆蓋一個非自己擁有的對象。(作者說在 Yahoo 便遇到了很多此類問題) 。
    神聖的 document.getElementById 都可以被輕易覆蓋。

    11.2.2 不新增方法

    幾乎無法阻止你向任何對象新添方法,爲對非自己的對象添加新方法是一個大問題,命名衝突。雖然對象目前可能沒有該方法,但是不能保證未來也沒有。更糟糕的是,一旦未來的原生方法和你的方法行爲不一致,那麼你整個代碼將陷入維護的噩夢。
    Prototype庫就是一個不好的例子,很隨意的修改了大量的原生對象和方法。導致在JS的歷史上遇到很多問題。
    在小於1.6的版本中,Prototype定義了一個document.getElementsByClassName() 方法,當然在很早以前這完全沒有問題,因爲本身還沒有該方法,隨後當在H5中官方定義後,其團隊不得不加上防守性的代碼
    if( !document.getElementsByClassName ){
        document.getElementsByClassName = function(){
            // 非原生實現
        }
    }
    然而,之前的Prototype所使用的方法結果是可以使用 each的,H5定義的可沒有 each。所以結果可想而知。

    11.2.3 不刪除方法

    刪除JS方法其實和新增一個方法一樣簡單,當然覆蓋也算刪除的一種。不管方法有多糟糕,我們都不容易知道是否真的沒有代碼使用它,很多庫都如此,即使很糟糕,在版本更新中也不會立即刪除掉。

    11.3 更好的途徑

    修改非自己擁有的對象是解決某些問題的很好的方案。可能有一些方法,所謂的設計模式,不直接修改這些對象而是擴展這些對象。
    最受歡迎的對象擴充方式就是繼承。一種類型的對象如果已經做到了你想要的大多數工作,那麼繼承,再加一些新的功能即可。JS中有兩種基本形式:基於對象的繼承和基於類型的繼承。

    11.3.1 基於對象的繼承

    基於對象的繼承也就是原型繼承,即ES5中的 Object.create() 方法。
    var person = {
        name : "Nicholas",
        sayName : function(){
            alert( this.name );
        }
    }
    var ni = Object.create( person );
    當然可以擴展對象
    var ni = Object.create( person, {
        name : {
            value : "Greg"
        }
    } );
    一旦以這種方式創建了對象,那麼你可以隨意修改新對象,畢竟你是對象的擁有者。

    11.3.2 基於類型的繼承

    基於類型的繼承是通過構造函數而非對象
    function MyError( message ){
        this.message = message;
    }
    MyError.prototype = new Error();

    11.3.3 門面模式

    門面模式是一種流行的設計模式,他爲一個已經存在的對象創建新的接口,門面模式是一個全新的對象,背後是一個已經存在的對象在工作。如果你的用例中無法使用繼承滿足需要,可以使用門面模式。
    funtion DOMWrapper( element ){
        this.element = element;
    }
    DOMWrapper.prototype.addClass( className ){
        this.element.classmName += " " + className; 
    }

    var wrapper = new DOMWrapper( document.getElementBuId("myDiv") );
    wrapper.addClass("selected");
    
    從JS的可維護性來說,門面模式是非常合適的方式,你可以完全控制這些接口。你可以允許訪問任何底層對象的方法或者屬性,反之也可以有效地過濾對象的訪問。
    門面實現一個特定的接口,讓一個對象看上去像另一個對象就是適配器。兩者的差別就是前者創建接口,後者實現已經存在的接口。

    11.4 關於Polyfill的註解

    隨着ES5和H5的特性在各個瀏覽器中實現,JS polyfills(shims)開始就行起來,polyfill是指一種功能的模擬,這些功能在新版本的瀏覽器中已經實現,然後用自定義的方式使其在老版本中兼容實現。

    11.5 阻止修改

    在ES5中已經引入了幾個方法來防止對對象的修改。
    有三種級別: 防止擴展, 密封, 凍結。
    var person = {
        name : "Nicholas"
    }
    Object.preventExtension(person);
    person.age = 25;
    就會悄悄的失敗,如果是在 strict 的情況下會報錯。


第十二章 瀏覽器嗅探

    瀏覽器嗅探始終是web領域的一個熱門話題。
    

    12.1 User-Agent 檢測

    用戶代理檢測是更具客戶端瀏覽器的 user-agent 字符串進行檢測,但是最大的問題就是解析 user-agent 並不是很容易,有些瀏覽器爲了保證兼容性,會複製另一個瀏覽器的 user-agent 字符串,每當一個新的瀏覽器出來的時候我們都需要對 user-agent 檢測的代碼進行重新修正。意味着我們無法預期的出了問題,只能等待問題出現後再進行處理。
    如果你選用了 user-agent 檢測,那麼最安全的方式就是隻去檢測就瀏覽器,比如只針對 IE8及以下瀏覽器等。而不要判斷比如 IE 的整個系列等。

    12.2 特性檢測

    特性檢測的原理就是爲特定瀏覽器的各個特性進行檢測,並當特性存在時處理問題。
    比如早期的根據ID獲取元素的特性檢測

    function getById( id ){

        var element = null;

        if( document.getElementById ){
            element = document.getElementById( id );
        }else if( document.all ){
            element = document.all[id];
        }else if( document.layers ){
            element = document.layers[id];
        }
        
        return element;
    }

    特性檢測的重要組成部分
    1. 探測標準方法
    2. 探測不同瀏覽器的特點方法
    3. 當被檢測方法不存在的時候提供合乎邏輯的備用方案。

    12.3 避免特性推斷

    特性推斷是更具一個特性的存在,推斷另外一個特性是否存在。“如果他看起來像鴨子,就必定會像鴨子一樣嘎嘎叫”

    12.4 避免瀏覽器推斷

    12.5 如何取捨

    特性推斷和瀏覽器推斷是非常糟糕的做法,應當不惜一切代價避免他,純粹的使用特性檢測是一種很好的方式,幾乎在任何情況下都是你想要的結果。
    對於 user-agent 檢測,從來都不禁止他,因爲總有非常適合的場景需要使用他。


發佈了7 篇原創文章 · 獲贊 9 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章