沙箱模式常見於YUI3 core,它是一種採用同一構造器(Constructor)產生彼此獨立且互不干擾(self-contained)的實例對象,而從避免污染全域對象的方法。
命名空間
JavaScript本身中沒有提供命名空間機制,所以為了避免不同函數、物件以及變數名對全域空間的污染,通常的做法是為你的應用程式或函式庫建立一個唯一的全域對象,然後將所有方法與屬性加入到這個物件上。
程式碼清單1 : 傳統命名空間模式
/* BEFORE: 5 globals */ // constructors function Parent() {} function Child() {} // a variable var some_var = 1; // some objects var module1 = {}; module1.data = {a: 1, b: 2}; var module2 = {}; /* AFTER: 1 global */ // global object var MYAPP = {}; // constructors MYAPP.Parent = function() {}; MYAPP.Child = function() {}; // a variable MYAPP.some_var = 1; // an object MYAPP.modules = {}; // nested objects MYAPP.modules.module1 = {}; MYAPP.modules.module1.data = {a: 1, b: 2}; MYAPP.modules.module2 = {};
在這段程式碼中,你建立了一個全域物件MYAPP,並將其他所有物件、函數作為屬性附加到MYAPP上。
通常這是一種較好的避免命名衝突的方法,它被應用在許多專案中,但這種方法有一些缺點。
需要為所有需要加入的函數、變數加上前綴。
因為只有一個全局對象,這意味著一部分程式碼可以肆意地修改全域物件而導致其餘程式碼的被動更新。
全域建構器
你可以用一個全域建構器,而不是一個全域對象,我們給這個建構器取名為Sandbox(),你可以用這個建構器來建立對象,你也可以為建構器傳遞一個回呼函數當作參數,這個回呼函數就是你存放程式碼的獨立沙箱環境。
代碼清單2:沙箱的使用
new Sandbox(function(box){ // your code here... });
讓我們為沙箱添加點別的特性。
建立沙箱時可以不使用'new'操作符。
Sandbox()建構器接受一些額外的配置參數,這些參數定義了產生物件所需模組的名稱,我們希望程式碼更加模組化。
擁有了以上特性後,讓我們看看怎麼初始化一個物件。
程式碼清單3顯示了你可以在不需要‘new’操作符的情況下,建立一個呼叫了'ajax'和'event'模組的物件。
程式碼清單3:以陣列的形式傳遞模組名稱
Sandbox(['ajax', 'event'], function(box){ // console.log(box); });
程式碼清單4:以獨立的參數形式傳遞模組名稱
Sandbox('ajax', 'dom', function(box){ // console.log(box); });
程式碼清單5顯示了你可以把通配符'*'作為參數傳遞給建構器,這意味著呼叫所有可用的模組,為了方便起見,如果沒有向構造器傳遞任何模組名作為參數,構造器會把'*'作為預設參數傳入。
程式碼清單5:呼叫所用可用模組
Sandbox('*', function(box){ // console.log(box); }); Sandbox(function(box){ // console.log(box); });
程式碼清單6顯示你可以初始化沙箱物件多次,甚至你可以嵌套它們,而不用擔心彼此間會產生任何衝突。
程式碼清單6:嵌套的沙箱實例
Sandbox('dom', 'event', function(box){ // work with dom and event Sandbox('ajax', function(box) { // another sandboxed "box" object // this "box" is not the same as // the "box" outside this function //... // done with Ajax }); // no trace of Ajax module here });
從上面這些範例可以看出,使用沙箱模式,透過把所有程式碼邏輯包裹在一個回調函數中,你根據所需模組的不同生成不同的實例,而這些實例彼此互不干擾獨立的工作著,從而保護了全域命名空間。
現在讓我們來看看怎麼實作這個Sandbox()建構器。
加入模組
在實作主構造器之前,讓我們先看看如何在Sandbox()建構器中加入模組。
因為Sandbox()構造函數也是對象,所以你可以給它添加一個名為'modules'的屬性,這個屬性將是一個包含一組鍵值對的對象,其中每對鍵值對中Key是需要註冊的模組名,而Value則是該模組的入口函數,當建構器初始化時當前實例會作為第一個參數傳遞給入口函數,這樣入口函數就能為此實例添加額外的屬性與方法。
在程式碼清單7中,我們加入了'dom','event','ajax'模組。
程式碼清單7:註冊模組
Sandbox.modules = {}; Sandbox.modules.dom = function(box) { box.getElement = function() {}; box.getStyle = function() {}; box.foo = "bar"; }; Sandbox.modules.event = function(box) { // access to the Sandbox prototype if needed: // box.constructor.prototype.m = "mmm"; box.attachEvent = function(){}; box.dettachEvent = function(){}; }; Sandbox.modules.ajax = function(box) { box.makeRequest = function() {}; box.getResponse = function() {}; };
實作建構器
程式碼清單8描述了實作建構器的方法,其中關鍵的幾個要點:
我們檢查this是否為Sandbox的實例,若不是,證明Sandbox沒有被new操作符調用,我們將以構造器方式重新調用它。
你可以在建構器內部為this新增屬性,同樣你也可以為建構器的原型加入屬性。
模組名稱會以陣列、獨立參數、通配符‘*’等多種形式傳遞給建構器。
請注意在這個例子中我們不需要從外部文件中加載模組,但在諸如YUI3中,你可以僅僅加載基礎模組(通常被稱作種子(seed)),而其他的所有模組則會從外部文件中載入。
一旦我们知道了所需的模块,并初始化他们,这意味着调用了每个模块的入口函数。
回调函数作为参数被最后传入构造器,它将使用最新生成的实例并在最后执行。
代码清单8:实现Sandbox构造器
<script> function Sandbox() { // turning arguments into an array var args = Array.prototype.slice.call(arguments), // the last argument is the callback callback = args.pop(), // modules can be passed as an array or as individual parameters modules = (args[0] && typeof args[0] === "string") ? args : args[0], i; // make sure the function is called // as a constructor if (!(this instanceof Sandbox)) { return new Sandbox(modules, callback); } // add properties to 'this' as needed: this.a = 1; this.b = 2; // now add modules to the core 'this' object // no modules or "*" both mean "use all modules" if (!modules || modules === '*') { modules = []; for (i in Sandbox.modules) { if (Sandbox.modules.hasOwnProperty(i)) { modules.push(i); } } } // init the required modules for (i = 0; i < modules.length; i++) { Sandbox.modules[modules[i]](this); } // call the callback callback(this); } // any prototype properties as needed Sandbox.prototype = { name: "My Application", version: "1.0", getName: function() { return this.name; } }; </script>