五、沙箱模式
沙箱模式(sandbox pattern)解决了命名空间模式的如下几个缺点:
顾名思义,沙箱模式提供了一个可用于模拟运行的环境,且不会对其他模块和个人沙箱造成任何影响。
全局构造函数
在命名空间模式中,有一个全局对象,在沙箱模式中,则是一个全局构造函数,让我们称之为Sandbox()。可以使用该构造函数创建对象并且还可以传递回调函数,它变成了代码的隔离沙箱运行环境。
// 这样来使用沙箱
new Sandbox(function (box) {
// 你的代码写在这里
});
对象box与命名空间例子中的MYAPP是相似的,他有您所需要的所有库函数,能够使代码正常运行。
让我们向该模式添加两个新特性:
有了以上两个额外特征,让我们看一下实例化对象的代码看起来是什么样子的。
可以忽略new操作符,并且使用一些如下所示的虚构“ajax”和“event”模块来创建对象:
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) {
// 使用DOM和事件来运行
Sandbox('ajax',function (box) {
// 另一个沙箱化的“box”对象
// 这里的“box”对象与函数外部的“box”对象并不相同
// ...
// 这里用Ajax来处理
});
// 这里没有Ajax模块
});
从上面这些例子中可以看到,当使用本沙箱模式时,可以通过将代码包装到回调函数中从而保护全局命名空间。
如果需要,也可以利用函数就是对象这个事实,然后将数据存储为该Sandbox()构造函数的静态属性。
最后,可以根据所需要的模块类型创建不同的实例,并且这些实例互相独立运行。
题外话:上面的沙箱模式,是不是很像早期使用的Angular,或者一些模块化插件比如require.js等等的使用方式?
现在,我们来看看应该如何着手实现Sandbox()构造函数以及其模块,从而支持所有这些功能。
增加模块
在实现实际的构造函数之前,让我们看看如何才能够增加模块的功能。
Sandbox()构造函数也是一个对象,因此可以向它添加一个名为modules的静态属性。该属性是包含键值对的另一个对象,其中这些键是模块的名字,而值则是实现每个模块的对应函数。
Sandbox.modules = {};
Sandbox.modules.dom = function (box) {
box.getElement = function () {};
box.getStyle = function () {};
box.foo = "bar";
};
Sandbox.modules.event = function (box) {
// 如果需要,就访问Sandbox原型,如下语句:
// box.constructor.prototype.m = 'mmm';
box.attachEvent = function() {};
box.dettach = function () {};
};
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {};
box.getResponse = function () {};
};
在上面这个例子中,我们增加了DOM、event和ajax,这些都是在库或者复杂Web应用中常见的功能片段。
实现每个模块的函数可以接受当前实例box作为参数,并且可以向该实例中添加额外的属性和方法。
实现构造函数
最后,让我们来实现该Sandbox()构造函数(当然,可能希望重命名这种类型的构造函数,以便使得这些名字对于库或者应用程序来说更有字面意义):
function Sandbox() {
// 将参数转换成一个数组
var args = Array.prototype.slice.call(arguments),
// 最后一个参数是回调函数
callback = args.pop(),
// 模块可以作为一个数组传递,或作为单独的参数传递
modules = (args[0] && typeof args[0] === 'String') ? args : args[0],
i;
// 确保该函数作为构造函数被调用
if(!(this instanceof Sandbox)) {
return new Sandbox(modules,callback);
}
// 需要向this添加的属性
this.a = 1;
this.b = 2;
// 向在向该核心“this”对象添加模块
// 不指定模块名称或指定“*” 都表示“使用所有模块”
if(!modules || modules === '*') {
modules = [];
for(i in Sandbox.modules) {
if(Sandbox.modules.hasOwnProperty(u)) {
modules.push(i);
}
}
}
// 初始化所需的模块
for(i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}
// call the callback
callback(this);
}
// 需要的任何原型属性
Sandbox.prototype = {
name:"My Application",
version:"1.0",
getName: function () {
return this.name;
}
};
在以上代码实现中,其关键部分为:
六、静态成员
静态属性和方法也就是那些从一个实例到另一个实例都不会发生改变的属性和方法。
公有静态成员
JavaScript中并没有特殊的语法来表示静态成员。但是可以通过使用构造函数并且向其添加属性这种方式,从而获得与“类式”语言相同的语法,这种方式可以良好运行,这是因为构造函数与所有其它函数一样都是对象,并且它们都可以拥有属性。在前面章节中讨论的备忘模式也采用类相同的思想,即向函数中添加属性。
下面的例子定义了一个具有静态方法isShiny()的构造函数Gadget,以及一个普通的实例方法setPrice()。其中,isShiny()是一个静态方法,因为该方法并不需要特定的gadget对象就能够运行。另一方面,setPrice()方法则需要一个对象才能运行,因为gadget可被设为不同的定价:
// 构造函数
var Gadget = function () {};
// 静态方法
Gadget.isShiny = function () {
return 'you bet';
};
// 向该原型添加一个普通方法
Gadget.prototype.setPrice = function (price) {
this.price = price;
};
// 现在,让我们调用这些方法进行测试。构造函数中的静态isShiny()方法可以被直接调用,然而普通的方法则需要一个实例:
// 调用静态方法
console.log(Gadget.isShiny()); // 输出“you bet”
// 创建一个实例并调用其方法
var iphone = new Gadget();
iphone.setPrice(500);
// 试图以静态方法调用一个实例方法是无法正常运行的。同样,如果使用实例iphone对象调用静态方法也是无法正常运行的:
console.log(typeof Gadget.setPrice); // "undefined"
console.log(typeof iphone.isShiny); // "undefined"
// 有时候也可以很方便地使用静态方法与实例一起工作。这种功能比较容易实现,只需要向原型中添加一个新的方法即可,其中该新方法作为一个指向原始静态方法的外观(Facade):
Gadget.prototype.isShiny = Gadget.isShiny;
iphone.isShiny();
在这种情况下,如果在静态方法内部使用this要特别注意。当执行Gadget.isShiny()时,那么isShiny()内部的this将会指向Gadget构造函数。如果执行iphone.isShiny(),那么this将会指向iphone。
最后一个例子向您展示了如何以静态或非静态方式调用同一个方法,而在这两种场景下依赖于调用模式的不同,其表现行为略有不同。下面的instanceof函数有助于确定方法是如何被调用的。
// 构造函数
var Gadget = function (price) {
this.price = price;
};
// 静态方法
Gadget.isShiny = function () {
// 这种方法总是可以运行
var msg = "you bet";
if(this instanceof Gadget) {
// this only works if called non-statically
msg += ", if cost $" + this.price + "!";
}
return msg;
};
// 向该原型添加一个普通搞得方法
Gadget.prototype.isShiny = function () {
return Gadget.isShiny.call(this);
};
// 测试静态方法调用
console.log(Gadget.isShiny());
// 测试实例,非静态调用
var a = new Gadget('499,99');
console.log(a.isShiny());
私有静态成员
到目前为止,本章所讨论的是公有静态方法,现在让我们来看看如何实现私有静态成员。就私有静态成员而言,指的是成员具有如下属性:
下面我们看一个例子,其中counter是构造函数Gadget中的一个私有静态属性。在本章中以及存在有关私有属性的讨论,因此这一部分仍然是相同的。需要一个函数作为闭包并且包装私有成员。然后,让我们使同一个包装函数立即执行并返回一个新函数。返回的函数值分配给变量Gadget,并且成为了新的构造函数:
var Gadget = (function () {
// 静态变量/属性
var counter = 0;
// 返回
// 该构造函数新的实现
return function () {
console.log(counter += 1);
}
}());
// 新的Gadget构造函数只是简单的递增和记录私有counter成员。使用以下几个实例进行测试,可以看到counter的确是在多个实例直接共享:
var g1 = new Gadget();
var g2 = new Gadget();
var g3 = new Gadget();
由于我们对每个对象都以1为单位递增counter,这个静态属性实际上成为了对象ID标识符,它唯一标识了以Gadget构造函数创建的每个对象这种唯一标识符可能是很有用的,因此为什么不通过特权方法将其公开?下面是基于前面示例基础上的一个例子,主要增加了一个特权方法getLastId()以访问静态私有属性:
var Gadget = (function () {
// 静态变量/属性
var counter = 0,
NewGadget;
// 这将成为
// 该构造函数新的实现
NewGadget = function () {
counter += 1;
};
// 特权方法
NewGadget.prototype.getLastId = function () {
return counter;
};
// 覆盖该构造函数
return NewGadget;
}());
// 测试新的实现
var iphone = new Gadget();
console.log(iphone.getLastId());
var ipod = new Gadget();
console.log(ipod.getLastId());
var ipad = new Gadget();
console.log(ipad.getLastId());
静态属性(公有和私有)使用会带来很多便利。它们可以包含非实例相关的方法和数据,并且不会为每个实例重新创建静态属性。第7章中,当涉及单体模式时,可以看到一个使用静态属性以实现类似类的单体构造函数的例子。