JavaScript 中的名稱空間

chengkang發表於2016-06-26

全域性變數應該由有系統範圍相關性的物件們保留,並且它們的命名應該避免含糊並儘量減少命名衝突的風險。在實踐中,這意味著你應該避免建立全域性物件,除非它們是絕對必須的。

不過,恩,這些你早都知道了……

所以你對此是怎麼做的?傳統方法告訴我們,最好的消除全域性策略是建立少數作為潛在模組和子系統的實際名稱空間的全域性物件。我將探索幾種有關名稱空間的方式,並以我基於 James Edwards 最近的一篇文章得到的一個優雅、安全和靈活的解決方案結束。

靜態名稱空間

我用靜態名稱空間作為那些名稱空間標籤實際上硬編碼的解決方案的涵蓋性術語。是的,你可以將一個名稱空間重新分配給另一個,不過新的名稱空間將會引用和舊的那一個同樣的物件。

1.通過直接分配

最基礎的方法。這樣非常冗長,並且如果你還想重新命名這些名稱空間,你就有得活兒幹了。不過它是安全和清楚明白的。

var myApp = {}
myApp.id = 0;
myApp.next = function() {
    return myApp.id++;  
}
myApp.reset = function() {
    myApp.id = 0;   
}
window.console && console.log(
    myApp.next(),
    myApp.next(),
    myApp.reset(),
    myApp.next()
); //0, 1, undefined, 0

你也可以通過使用this引用兄弟屬性來使將來的維護更輕鬆一些,不過這有一點冒險因為沒有什麼能阻止你的那些名稱空間裡的方法被重新分配。

var myApp = {}
myApp.id = 0;
myApp.next = function() {
    return this.id++;   
}
myApp.reset = function() {
    this.id = 0;    
}
myApp.next(); //0
myApp.next(); //1
var getNextId = myApp.next;
getNextId(); //NaN whoops!

2.使用物件字面量

現在我們只需要引用名稱空間名一次,因此之後改變名字更簡單了一些(假設你還沒反覆引用這個名稱空間)。仍有一個危險是this的值可能會丟擲一個『驚喜』 – 不過假設在一個物件字面結構裡定義的物件不會被重新分配相對安全一點。

var myApp = {
    id: 0,
    next: function() {
        return this.id++;   
    },
    reset: function() {
        this.id = 0;    
    }
}
window.console && console.log(
    myApp.next(),
    myApp.next(),
    myApp.reset(),
    myApp.next()
) //0, 1, undefined, 0

3.模組模式

我發現自己最近用模組模式更多。邏輯被一個方法包裝從全域性域隔離開了(通常是自呼叫的),它返回一個代表這個模組公開介面的物件。通過立即呼叫這個方法並分配結果給一個名稱空間變數,我們就鎖住了這個命名變數中模組的 API。此外,任何沒有包括在返回值中的變數將永遠保持私有,只對引用他們的公開方法可見。

var myApp = (function() {
    var id= 0;
    return {
        next: function() {
            return id++;    
        },
        reset: function() {
            id = 0;     
        }
    };  
})();   
window.console && console.log(
    myApp.next(),
    myApp.next(),
    myApp.reset(),
    myApp.next()
) //0, 1, undefined, 0

如上物件字面量例子,名稱空間名字可以輕易更換,不過還有額外優勢:物件字面量是四班的 – 它全是關於屬性分配,沒有支援邏輯的空間。此外,所有屬性必須被初始化,並且屬性值無法輕易跨物件引用(因此,比如,內部閉包就不可能使用了)。模組模式沒有任何上述約束,並且給我們額外的隱私福利。

動態名稱空間

我們也可以將這一節稱為名稱空間注入。名稱空間由一個直接引用方法包裝內部的代理代表 – 這意味著我們不再需要打包分配給名稱空間的返回值。這讓名稱空間定義變得更靈活並且讓擁有多個存在於獨立名稱空間中(或者甚至在全域性上下文中)的模組的獨立例項。動態名稱空間支援模組模式的全部特徵並附加直觀和可讀性強的優勢。

4.提供名稱空間引數

在這裡我們只是將名稱空間作為引數傳給自呼叫方法。變數id是私有的,因為他並沒有被分配給context

var myApp = {};
(function(context) { 
    var id = 0;
    context.next = function() {
        return id++;    
    };
    context.reset = function() {
        id = 0;     
    }
})(myApp);  
window.console && console.log(
    myApp.next(),
    myApp.next(),
    myApp.reset(),
    myApp.next()
) //0, 1, undefined, 0

我們甚至可以把context設定給全域性物件(通過一個字的改變!)。這是庫主們的巨大財富 – 他們可以將他們的特性包裝在一個自呼叫函式中,然後讓使用者來決定它們是不是全域性的(John Resig 在他寫 JQuery 時就是一個這個理論的早期採用者)。

var myApp = {};
(function(context) { 
    var id = 0;
    context.next = function() {
        return id++;    
    };
    context.reset = function() {
        id = 0;     
    }
})(this);   
window.console && console.log(
    next(),
    next(),
    reset(),
    next()
) //0, 1, undefined, 0

5.用this作為名稱空間代理

James Edwads 最近釋出的一篇文章激起了我的興趣。《My Favorite JavaScript Design Patter》 顯然被很多評論者誤解了,他們認為他可能也是藉助於模組模式。這篇文章宣傳了多種技術(可能導致了讀者的迷惑),但是在它的核心部分是一點我已經修改並呈現為一個名稱空間工具的很天才的東西。

這個模式的美就在於它僅僅是按照這個語言被設計的方式使用 – 不多不少、不投機也不取巧。此外因為名稱空間是通過this關鍵字(它在給定的執行上下文中是不變的)注入的,它不可能被意外修改。

var myApp = {};
(function() {
    var id = 0;

    this.next = function() {
        return id++;    
    };

    this.reset = function() {
        id = 0;     
    }
}).apply(myApp);    

window.console && console.log(
    myApp.next(),
    myApp.next(),
    myApp.reset(),
    myApp.next()
); //0, 1, undefined, 0

更棒的是,apply(以及call) API 提供了與上下文和引數天然的隔離 – 因此給模組建立者傳遞附加引數非常乾淨。下面的例子表明了這一點,並且展示瞭如何獨立於多個名稱空間來執行模組。

var subsys1 = {}, subsys2 = {};
var nextIdMod = function(startId) {
    var id = startId || 0;
    this.next = function() {
        return id++;    
    };
    this.reset = function() {
        id = 0;     
    }
};
nextIdMod.call(subsys1);    
nextIdMod.call(subsys2,1000);   
window.console && console.log(
    subsys1.next(),
    subsys1.next(),
    subsys2.next(),
    subsys1.reset(),
    subsys2.next(),
    subsys1.next()
) //0, 1, 1000, undefined, 1001, 0

當然如果我們如果我們需要一個全域性 id 生成器,非常簡單……

nextIdMod();    
window.console && console.log(
    next(),
    next(),
    reset(),
    next()
) //0, 1, undefined, 0

這個我們作為例子使用的 id 生成器工具並沒有表現出這個模式的全部潛力。通過包裹一整個庫和使用this關鍵字作為名稱空間的替身,我們使得使用者在任何他們選擇的上下文中執行這個庫很輕鬆(包括全域性上下文)。

//library code
var protoQueryMooJo = function() {  
    //everything
}
//user code
var thirdParty = {};
protoQueryMooJo.apply(thirdParty);

其他的考慮

我希望避免名稱空間巢狀。它們很難追蹤(對人和電腦都是)並且它們會讓你的程式碼因為一些亂七八糟的東西變得很多。如 Peter Michaux 指出的,深度巢狀的名稱空間可能是那些檢視重新建立他們熟悉和熱愛的長包鏈的老派 Java 開發者的遺產。

通過 .js 檔案來固定一個單獨的名稱空間也是可以的(雖然只能通過名稱空間注入或者直接分配每一個變數),不過你應該對依賴謹慎些。此外將名稱空間繫結到檔案上可以幫助讀者更輕易弄清整個程式碼。

因為 JavaScript 並沒有正式的名稱空間結構,所以有很多自然形成的方法。這個調查只詳細說明了其中的一部分,可能有更好的技術我沒有發現。我很樂意知道它們。

相關文章