沙盘模式可以弥补命名空间模式中的两项不足之处:
- 使用唯一全局对象作为程序的全局变量入口,使得无法在同一程序中使用两个不同版本的API,因此它们使用的是同一个唯一的全局对象名,如MYAPP;
- 较长的嵌套对象使得代码编写和解析都比较慢;
沙盘模式,正如其名字所说,提供了一个各类模块可以共同“游戏”的环境,模块之间和沙盘之间不会相互影响。
这种模式在YUI3中大量被使用。下面介绍一种沙盘模式的实现方式:
1. 全局的构造函数
在命名空间模式中只有一个全局对象;在沙盘模式中,唯一的是一个全局的构造函数:比如说,Sandbox()。Sandbox只是本例使用的名字,程序里应该使用一个有具体含义的名字。这个全局的构造函数接受一个回调函数,并为这个回调函数提供沙盘环境。
沙盘构造函数的调用大概是这样的:
new Sandbox(function (box) { // your code here... });
对象box就好像命名空间模式中的MYAPP,提供了程序库当中需要的功能。
然后再向沙盘模式加入两样技术:
使用之前提到的检查方法,保证总是用new来调用沙盘构造函数;
沙盘构造函数接受参数,来告知沙盘内应提供哪些模块的功能;
有了这两个技术,我们会检查出Sandbox()不用new来调用,并在Sandbox()内部再用new来调用它自己:保证它总是用new来调用的;并传入适当的参数来通知沙盘提供相应的功能。这样,Sanbox()的调用过程就变成了这样:
Sandbox([‘ajax‘, ‘event‘], function (box) { // console.log(box); });
也可以把要在沙盘中提供的功能模块的名字分别用两个(或多个)参数传入沙盘函数:
Sandbox(‘ajax‘, ‘dom‘, function (box) { // console.log(box); });
如果程序中有许多模块,并希望一次全部支持所有模块的功能,可以用“*”来代替所有的模块名字,或者更简单地,省略模块名的参数来表示需要所有模块:
Sandbox(‘*‘, function (box) { // console.log(box); }); Sandbox(function (box) { // console.log(box); });
沙盘模式应该足够灵活,并支持在沙盘的回调函数中再次使用沙盘函数:
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()加入静态属性来存放一些数据。
2. 定义模块
在 介绍Sandbox的实现之前,先看看如何定义模块。在沙盘模式中,全局的构造函数Sandbox是个函数对象,因此可以为它加入一个静态的属性来保存各 个模块,比如modules。modules是Sandbox的成员属性,也是一个对象,它通过内部的键值对来保存模块,键作为模块名,而值是模块的实 现。定义模块的做法是这样的:
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 () {}; };
上面的例子里,我们给modules对象加入了三个模块,分别是dom,event,以及ajax。
3. 实现Sandbox的沙盘构造函数
一个可行的Sandbox构造函数是这样的(实际上,在你的程序里可能需要把Sandbox改成有意义的名字):
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); } } } // initialize the required modules for (i = 0; i < modules.length; i += 1) { 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; } };
上面实现里的关键内容包括:
- 检查this是不是Sandbox的实例,如果不是(说明不是用new调用的Sandbox())就再次用new调用Sandbox;
- 可以用this为构造函数添加属性,也可以通过prototype来添加属性;
- 需要使用的模块可以通过模块名的数组传入,也可以使用每个参数一个模块名的方式传入,“*”匹配所有支持的模块(实际的使用中可以还需要引入定义所需模块的js文件);
- 如果所需的模块已经定义了,就初始化它,也就是执行定义模块的函数;
- Sandbox函数的最后一个参数是个回调函数,它在Sandbox函数的最后被调用,并使用Sandbox创建的对象来执行相应的代码,这些代码就像在一个沙盘中运行一样:它们得到了需要支持的模块,而且与外部代码相隔离。