关于JavaScript的沙箱模式

从语言学的角度上来说,允许代码无节制地使用全局变量,是最错误的选择之一。而更可怕的,就是一个变量"可能"成为全局的(在未知的时间与地点)。但是这两项,却伴随JavaScript这门语言成功地走到了现在。定州市科技工业局

也许是限于浏览器应用的规模,所以这一切还迟迟没有酿成灾难。在此之前,出现了两种解决方案。一种是ECMA在新的规范(Edition 5)中对此做出了限制,其中最重要的一条便是eval()的使用变得不再随意和无度。而另一种方案,则是相对没有那么官僚与学术的,尽管也拥有一个同样学术的名字:沙箱。

沙箱(Sandbox)并不是一个新东西,即使对于JavaScript来说,也已经存在了相当长的时间。在SpiderMonkey JS的源代码中,就明确地将一个闭包描述为一个沙箱。这包含着许多潜在的信息:它有一个初始环境,可以被重置,可以被复制,以及最重要的,在它内部的所有操作,不会影响到外部。

当然事实上远非如此。JavaScript里的闭包只是一个"貌似沙箱"的东西--仍然是出于JavaScript早期的语言规范的问题,闭包不得不允许那些"合法泄漏"给外部的东西。而对于这一切无法忍受的前端工程师们,开始寻求另外的解决之道,这其中相对较早的尝试,是基于IFRAME的实践。例如dean.edwards在2006年提出过的方案:

view source

print?

1 a_frames.document.write(
2   "<script>"+
3   "var MSIE/*@cc_on [email protected]*/;"+ // sniff
4   "parent.sandbox=MSIE?this:{eval:function(s){return eval(s)}}"+
5   "<\/script>"
6 );

显然,由于在不同的IFRAME中运行着各自的JavaScript引擎实例,所以上述的方案也意味着沙箱是"引擎"这个级别的:在任何一个沙箱中的崩溃,将导致该引擎以及对应IFRAME崩溃。但--理论上说--不会影响整个浏览器。

问题是,这并不那么理想。往往的,引擎会导致整个浏览器锁在那里,例如用alert()弹出一个对话框而又因为某种意外失去了焦点。又或者单个的IFRAME会导致全局的CPU被耗光,例如一个死循环。于是更加复杂的方案--在JavaScritp中包含一个完整的执行器--出现了。最有名的则是Narrative JavaScript,它内建了一个执行器,用于逐行地解释执行JavaScript代码,这使得它可以控制所有的代码执行序列,或者随时重置整个执行引擎--如同一个沙箱所要做的那样。

这一切或者太过依赖于环境,又或者太过复杂,但都不乏追随者。例如jsFiddle这个项目在"嵌入或装载"这样的路子上就已经有了不俗的成绩。但是,YUI在新版本中却给出了它自己的选择:以更加明确的编程约定,来实现应用级别的沙箱。这包括一个非常简单的、新的YUI语法:

view source

print?

1 YUI().use(‘dom-base‘, function(Y) {
2   // Y是一个新的沙箱
3 });

在‘dom-base‘位置上,可以是1到N个字符串,表明一个需要在沙箱中装载的模块列表。这可以是沙箱的初始列表,或者后续的callback函数(亦即是用户代码)所需依赖的模块列表。在这种实现方案中,YUI为每个沙箱维护各自的装载模块列表和上下文环境中的变量、成员。但是出于JavaScript语言自己的局限,这个沙箱依然是相当脆弱的。例如下一示例中沙箱内的代码就会污染到全局:

view source

print?

1 YUI().use(‘‘, function(Y) {
2   abc = 1234;  //<--这里可能导致一个全局变量‘abc‘被隐式地声明
3 });

同样,在上述的沙箱里也可以使用类似window、document等全局变量、修改它们的成员或无限制地调用方法(例如使用setTimeout()来创建时钟)。所以YUI的沙箱事实上是靠"规约"来约束的,而不是真正意义上的沙箱。当然,这也意味着,如果用户能按照规约来处理沙箱内的代码,那么也就能自由地享用它带来的便利:安全、移植和有效的隔离副作用。

而我们再穷究其根底,YUI沙箱的实质不过是一行:

view source

print?

1 // code from yui.js
2 //  - mod.fn(this, name)
3 mod.entryFunc(sandbox, modName);

其实际含义是:

  • mod :沙箱当前装载的模块;
  • entryFunc : 上述模块的入口函数;
  • sandbox :当前的沙箱的实例,即YUI()返回值;
  • modName:模块名

除了依赖关系(以及可能需要的异步加载)之外,YUI沙箱环境仅是用下面的代码来简单地调用callback函数:

view source

print?

1 callback(Y, response);

然而这些需求的实现并不那么复杂。首先,我们设定数据结构mod为一个对象:

view source

print?

1 { name:modName, fn: entryFunc, req: [], use: [] }

则一个环境对象env,将包括多个mod(将它们处理成对象而非数组,主要是便于使用名字来索引模块)和以及对它们进行管理操作的方法:

view source

print?

1 { mods:{}, used:{}, add:..., use:...}

最后,所谓一个沙箱sandbox,就是上述环境对象的一个实例,并在初始时sandbox.mods与sandbox.used为空。由此简单的实现为:

view source

print?

01 /**
02 * tiny sandbox framework
03 * mirror from YUI3 by aimingoo.
04 **/
05 function Sandbox() {
06   if (!(this instanceof arguments.callee)) return new arguments.callee();
07   this.mods = this.mods || {};
08   this.used = {};
09 }
10 Sandbox.prototype = {
11   add: function(modName, entryFunc, reqArr, useArr) {
12     this.mods[modName] = { fn: entryFunc, req: reqArr, use: useArr }
13   },
14   use: function() {
15     var mods = [].slice.call(arguments, 0);     // 0..length-2 is modNames
16     var callback = mods.pop();      // length-1 is callback
17     var recursive_load = function(name, mod) {
18       if (!this.used[name] && (mod=this.mods[name])) {
19         mod.req.forEach(recursive_load, this);
20         mod.fn(this, name);
21         mod.use.forEach(recursive_load, this);
22         this.used[name] = true;
23       }
24     }
25     mods.forEach(recursive_load, this);
26     callback(this);
27   }
28 }

现在我们来尝试一个与YUI类似的语法风格:

view source

print?

1 Sandbox().use(‘‘, function(){
2    alert(‘user code.‘);
3 });

或者,先向整个Sandbox环境注册一些模块(在真实的框架实现中,这一步可能是通过框架的装载器来初始化):

view source

print?

01 // for test, entry of mods
02 f1 = function() { alert(‘f1‘) };
03 f2 = function() { alert(‘f2‘) };
04 f3 = function() { alert(‘f3‘) };
05 // mods for global/common env.
06 Sandbox.prototype.mods = {
07   ‘core‘: { fn: f1, req: [], use: [] },
08   ‘oo‘:   { fn: f2, req: [‘core‘], use: [‘xml‘] },
09   ‘xml‘:  { fn: f3, req: [], use: [] }
10 }

然后再尝试在一个沙箱实例中运行代码:

view source

print?

1 // f1 -> f2 -> f3 -> user code
2 Sandbox().use(‘oo‘, function(){
3    alert(‘user code.‘);
4 });

其实即便是上述代码中用于处理模块依赖的逻辑,也并不是什么"神奇的"代码或者技巧。除开这些,这样的沙箱隔离泄露的能力还抵不过一个嵌入式DSL语言。而后者所应用的技巧很简单,看不出什么花招:

view source

print?

1 with (YUI()) this.eval("... mod_context ... ");

这样一来,在mod_context里的代码就只会在YUI()的一个实例中造成污染了。当然,仍然是源于JavaScript的限制,我们还是无法避免一个变量泄露到全局--除非,我们回到js in js这个项目,真的在环境中重新初始化一个js引擎。

从这一意义上来说,引擎级别的沙箱与操作系统的进程一样,带来的是终级的解决方案,所以Chrome、IE等等主流浏览器纷纷有了"独立进程"模式。而在这样的背景之下,试图用"框架内置沙箱"来改善ECMAScript ed3中一些设计疏失的种种努力,不过是一张张空头的支票罢了。

甚至,用这本支票签完单也未必有人会收的。

时间: 2024-12-18 00:22:01

关于JavaScript的沙箱模式的相关文章

JavaScript SandBox沙箱设计模式

沙箱模式常见于YUI3 core,它是一种采用同一构造器(Constructor)生成彼此独立且互不干扰(self-contained)的实例对象,而从避免污染全局对象的方法. 命名空间 JavaScript本身中没有提供命名空间机制,所以为了避免不同函数.对象以及变量名对全局空间的污染,通常的做法是为你的应用程序或者库创建一个唯一的全局对象,然后将所有方法与属性添加到这个对象上.缙云县外国专家局 代码清单1 : 传统命名空间模式 01 /* BEFORE: 5 globals */ 02 //

JavaScript高级---门面模式设计

门面模式 两个作用: 1.简化类的接口 2.消除类与使用它的客户代码之间的耦合 门面模式常常是开发人员最亲密的朋友.它几乎是所有javascript库的核心原则 门面模式的目的是为了让开发人员用更简单的方法调用一些相对复杂或组合的方法,主要就是简化开发的复杂性,提供一个相对容易的API去调用内部的方法供外界去使用,这样程序员开发会变得轻松些,编写一次组合代码后可以反复的去使用它,有助于节省时间和精力 注意: 不要滥用门面模式,所以使用你心仪的门面之前一定要三思而定,搞不好你就会小题大做 引入概念

JavaScript高级---组合模式设计

一.设计模式 javascript里面给我们提供了很多种设计模式: 工厂.桥.组合.门面.适配器.装饰者.享元.代理.观察者.命令.责任链 在前面我们实现了工厂模式和桥模式 工厂模式 : 核心:为了生产对象,实现解耦. 桥接模式 : (桥接模式是一种既能把两个对象连接在一起,又能避免二者间的强耦合的方法.通过“桥”把彼此联系起来,同时又允许他们各自独立变化) 主要作用:主要作用表现为将抽象与其实现隔离开来,以便二者独立化. 组合模式 : (组合模式是一种专门为创建Web上的动态用户界面而量身制定

JavaScript高级---工厂模式设计

1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <ti

Javascript备忘模式

使用备忘模式,利用了函数的自定义属性,先看一个例子 var test = function (){} test.myAttr = "attr"; 这样,就给test加上了一个自定义的属性,myAttr. 备忘模式,正式利用了这个方法,将已经运行过的结果存储起来,将函数接受到的参数作为key,将函数运行的结果作为value返回即可.代码如下 var myFunc = function (param) { if(!myFunc.cache[param]){ var result = {};

javascript 简单工厂模式

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 var Bicycle = new Interface("Bicycle",[

JavaScript中介者模式

JavaScript中介者模式 中介者模式是迎合迪米特法则的一种实现.迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另一个对象.如果对象间的耦合性太高,一个对象发生改变后,难免会影响到其他的对象.在中介者模式里,对象间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响.因此,中介者模式使各个对象得以解耦,以中介者和对象之间的一对多的关系取代了对象间的多对多的网状关系.各个对象只需关注自身功能的实现,对象之间的交互关系就交给中介者对象来实现和维护.      下面是一段使用中介者模

JavaScript的原型模式

原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象. http://www.cnblogs.com/TomXu/archive/2012/04/16/2436460.html http://www.cnblogs.com/silymer/archive/2012/11/02/2751803.html JS原型对象:所有实例都会共享它里面的属性和方法 原型(prototype),是 JavaScript 特有的一个概念,通过使用原型,JavaScrip

javascript常见编程模式举例

最近买到手了一本<javascript框架设计>,详细介绍开发js框架所用到的知识.初读一点,乐帝脆弱的理论修养就暴露无遗了,所以专门加强理论修养,重看javascript编程模式的举例.下面来介绍下js中,常见的编程模式.    1.命名空间    同其他高级语言一样,js中的命名空间概念,也是为了减少命名冲突,但js没有命名空间关键字.js实现命名空间的思路是定义一个全局变量,将此命名空间的变量和方法,定义为这个全局变量的属性. var MYAPP=MYAPP||{};//全局变量 MYA