JavaScript的子集和扩展


JavaScript的子集和扩展

JavaScript的子集的定义大部分都是出于安全考虑,只有使用这门语言的一个安全的子集编写脚本,才能代码执行得更安全、更稳定,比如如何更安全地执行一段由不可信第三方提供的广告代码。

JavaScript 的子集

大多数语言都会定义它们的子集,用以更安全地执行不可信的第三方代码。这里有一个很有趣的子集,定义这个子集的原因有些特殊。我们首先来看这个有趣的子集,然后再讨论安全的语言子集。

精华

Douglas Crockford曾经写过一本很薄的书《JavaScript: The Good Parts》(O'Reillyttl版社),专门介绍JavaScript中值得发扬光大的精华部分。这个语言子集的目标是简化这门语言,规避掉语言中的怪癖、缺陷部分,最终使编程更轻松、程序更健壮。Douglas Crockford是这样介绍它的动机的:

大多数编程语言都有精华部分和鸡肋部分,我发现如果只使用精华部分而避免使用鸡肋部分,我可以成为一名更好的程序员。

Crockford提炼出的子集部分不包含with和continue语句以及eval( )函数。他提倡使用函数定义表达式而不是函数定义语句来定义函数。该子集要求:循环体和条件分支都使用花括号括起来,它不允许在循环体和条件分支中只包含一条语句时省略花括号,任何语句只要不是以花括号结束都应当使用分号做结尾。

这个子集中并未包含逗号运算符、位运算符以及“++”和“--”。也不包含“==”和"!=”,因为用这两个运算符进行比较时会涉及类型转换,这里更推荐使用“===”和“!==”。

由于JavaScript并不包含块级作用域,Crockford为我们提炼出的子集部分对var语句做了限制,var语句只能出现在函数体的顶部,并要求程序员将函数内所有的变量声明写在一 条单独的var语句中,作为函数体的第一条语句。在子集中禁止使用全局变量,但这个限制只是编程约定,并不是真正的语言上的限制。

Crockford写过一个在线代码质量检测工具JSLint,可以通过 http://jslint.com 访问这个工具。这个工具提供了很多选项用来增强代码的一致性检査。除了能确保代码使用了子集推荐的特性之外,JSLint工具还对编码风格做了一些强制约定,比如合理的缩进等。

Crockford的那本书出版时,ECMAScript 5的严格模式还没有出来,然而Crockford所提取出的JavaScript “鸡肋部分”中有很大一部分在严格模式中同样做了限制。随着ECMAScript 5标准的广泛采用,JSLint工具要求在选中“The Good Parts”选项时程序中必须包含一条“use strict"指令。

子集的安全性

利用“精华部分”的一个语言子集可以设计出更具美感的程序并提升程序员的开发效率。这里将要讨论的是一个更大的子集,这个大子集的设计目的是能在一个容器或“沙箱”中更安全地运行不可信的第三方JavaScript代码。所有能破坏这个沙箱并影响全局执行环境的语言特性和API在这个安全子集中都是禁止的。每个子集都带有一个静态的检査器,可以对代码进行解析检査以确保代码是符合子集规范的。由于这个检査器的检验规则比较严格,因此有一些沙箱系统定义了范围更广、校验更松散的子集,并增加了一个代码转换的步骤,用以将针对更大子集的代码检验转换为针对更小子集的代码检验, 同时在对代码的静态分析不能确保代码安全性的情况下增加了运行时的检査。

为了让JavaScript代码静态地通过安全检査,必须移除一些JavaScript特性:

  • eval( )和Function( )构造函数在任何安全子集里都是禁止使用的,因为它们可以执行任意代码,而且JavaScript无法对这些代码做静态分析。
  • 禁止使用this关键字,因为函数(在非严格模式中)可以通过this访问全局对象。 而沙箱系统的一个重要目的就是阻止对全局对象的访问。
  • 禁止使用with语句,因为with语句增加了静态代码检査的难度。
  • 禁止使用某些全局变量。在客户端JavaScript中,浏览器窗口对象可以当做全局对象,但也具有双重身份( 这里所说的双重身份是指浏览器窗口对象除了作为普通的全局对象之外,还能通过它去操作浏览器和DOM。),因此代码中不能有对window对象的引用。同样地,客户端document对象定义了可以用来操控整个页面内容的方法。将对document的控制权交给一段不受信任的代码会有很多隐患。安全子集提供了两种不同的方法来处理类似document这类全局对象。第一种方法是,沙箱完全禁掉它们,并定义一组自定义API用以对分配给它的Web页面做有限制的访问。第二种方法,在沙箱代码所运行的“容器”内定义一个只对外提供安全的标准DOM API的“外观面板”(facade)或“document代理对象”(proxy)。
  • 禁止使用某些属性和方法,以免在沙箱中的代码拥有过多的权限。这些属性和方法包括arguments对象的两个属性caller和callee(甚至在某些子集中干脆禁止使用arguments对象)、函数的call( )和apply( )方法,以及constructor和 prototype两个属性。非标准的属性也被禁止掉了,比如_proto_。一些子集将这些不安全的属性和全局对象列进黑名单,还有一些子集提供了白名单,给出了推荐使用的安全的属性和方法。
  • 静态分析可以有效地防止带有点(.)运算符的属性存取表达式去读写特殊属性。但使用方括号口来访问属性则与此不同,因为我们无法对方括号内的字符串表达式做静态分析。基于这个原因,安全子集通常禁止使用方括号,除非方括号内是一个数字或字符串直接量。安全子集将口替换为全局函数,通过调用全局函数来査询和设置对象属性,这些函数会执行运行时检査以确保它们不会读写那些禁止访问的属性。

有一些限制,比如禁止使用eval( )和with语句,并不会给程序员带来额外负担,毕竟这些特性本来就很少在JavaScript编程中用到。另外一些限制规则,比如使用方括号对属性进行存取的限制则对开发造成诸多不便,这时就有代码转换器的用武之地了。比如,转换器会自动将使用方括号的代码转换为函数调用的代码,以便能够对它执行运行时检査。有了这种代码转换,可以安全地使用this关键字。当然,沙箱代码的运行时安全性检査和执行速度之间是一对矛盾,这里的代码转换只是一种权衡后的折中方案。

有一些安全子集已经实现了,,这里只简要地介绍一些比较重要的实现:

ADsafe

ADsafe(http://adsafe.org)是第一个正式提出的安全子集。它的提出者是Douglas Crockford (他也定义了The Good Parts子集)。ADsafe只包含静态检査,它使用 JSLint(http://jslint.org)作为检验器。这个工具禁止访问大部分的全局变量,并定 义了一个ADSAFE变量,它提供了一组可以安全使用的API,包括一些特殊的DOM方法。ADsafe并未广泛使用,但它作为一个颇具影响力的概念原型对其他安全子集有着深远的影响。

dojox.secure

受ADsafe的启发,Dojo工具包(http://dojotoolkit.org)发布了一个名为dojox.secure的子集扩展。和ADsafe一样,dojox.secure也是基于静态检査,静态检査受限于语言子集范围内。但它和ADsafe又不尽相同,它允许使用标准DOM API。同时,它包含一个用JavaScript实现的检査器。因此我们可以用它对不可信的第三方代码执行运行时前的动态检査。

Caja

Caja(Caja是西班牙语,意思是“沙盒”)是Google发布的开源安全子集。Caja定义了两个语言子集。Cajita ( “小沙盒”)是一个与ADsafe和dojox.secure类似的严格子集。Valija(“手提箱”或“行李箱”)则是一个范围更广的语言子集,更接近于ECMAScript 5的严格模式(不包含eval( ))。Caja本身也是一个编译器的名字,这个编译器可以将一段网页内容(HTML、CSS和JavaScript代码)转换为一个安全的模块,这个模块可以放心地驻留在页面中而不会对整个页面或页面上的其他模块产生影响。

Caja是OpenSocial API的一部分。比如,在门户页面 http://my.yahoo.com 中就可以看到,所有的模块都遵照Caja规范。

FBJS

FBJS是JavaScript语言的变种,这种语言被Facebook(http://facebook.com)采用, 用以在用户个人资料页嵌入不可信的第三方代码。FBJS依赖代码转换来保证代码的安全性,转换器同样提供运行时检査,以避免通过this关键字去访问全局对象, 并且对所有的顶层标识符进行重命名,给它们增加了一个标识模块的前缀,正是因为这种重命名,任何对全局变量以及其他模块的成员变量的设置或者査询操作都无法正常进行了。此外,任何对eval( )的调用也会因为eval函数名被重新命名而无法执行。FBJS模拟实现了DOMAPI的一个安全子集。

Microsoft Web Sandbox

微软的Web Sandbox(http://websandbox.livelabs.com/ )定义了JavaScript的一个更宽泛的子集,包含HTML和CSS,它的代码重写规则非常激进,有效地重新实现了一个安全的JavaScript虚拟机,针对不安全的JavaScript顶层代码进行处理。

常量和局部变量

对语言子集的讨论暂告一段落,下面开始讨论语言的扩展。在JavaScript1.5及后续版本中可以使用const关键字来定义常量。常量可以看成不可重复赋值的变量(对常量重新赋值会失败但不报错),对常量的重复声明会报错。

const pi = 3.14;	// 定义一个常量并赋值
pi = 4;	            // 任何对这个常量的重新赋值都被忽略
const pi = 4;	   // 重新声明常量会报错
var pi = 4;	       // 这里也会报错

关键字const和关键字var的行为非常类似,由于JavaScript中没有块级作用域,因此常量会被提前至函数定义的顶部。

一直以来,JavaScript中的变量缺少块级作用域的支持被普遍认为是JavaScript的短板,JavaScript 1.7针对这个缺陷增加了关键字let。关键字const一直都是JavaScript的保留字(没有使用),因此现有的代码不必作任何改动就可以增加常量,关键字let并不是保留字,JavaScript 1.7及以后的版本才能识别,需要手动加入版本号才可以。


JavaScript 版本号

有一些语言的扩展定义了新的关键字(比如let),为了让现有代码不破坏原有结构就能使用这些关键字,则需要显式指明新的语言版本以便使用新版本的语言扩展。如果你正在使用Spidermonkey或Rhino作为单独的解析器,就可以通过命令行选项指定语言版本,或者通过调用一个内置函数version( )来指定版本(显式指定的版本号是实际版本号乘以100译注5的数值,要想激活JavaScript 1.7版本则需要传入170并启用let关键字)。在Firefox中,则可以在script标签中指定语言的扩展版 本,就像这样: