JavaScript权威指南--Javascript子集和扩展
知识要点
本章讨论javascript的子集和超集,其中子集的定义大部分处于安全考虑。只有使用这门语言的一个安全的子集编写脚本,才能让代码执行的更安全、更稳定。比如如何更安全地执行一段由不可信第三方提供的广告代码。
我们在本章对它们作必要的讲述是基于几点考虑:
- 它们的确很强大
- 它们有可能在未来成为标准
- 它们可用来写Firefox扩展插件
- 它们可用在服务器端的JavaScript编程
在简单介绍JavaScript语言的子集之后,本章后面会开始介绍语言的扩展部分。由于这些扩展毕竟不是标准,因此这里只是一个指南形式的描述。
1.javascript的子集
大多数语句都支持他们的子集,用以安全的执行不可信的第三方代码。这里有一个很有趣的子集,定义这个子集的原因有些特殊。我们首先来看这个有趣的子集,然后再讨论安全的语言子集。
1.1.精华
Douglas Crockford曾经写过一本很薄的书《JavaScript: The good parts》(O'reily 出版社)专门介绍JavaScript中值得发扬光大的精华部分。这个语言子集的目标是简化这门语言,规避掉语言中的怪癖、缺陷部分,使编程更轻松、程序更健壮。
他提炼出的子集部分不包含with和continue语句以及eval()函数。他提倡使用函数定义表达式而不是函数定义语句来定义函数。循环体和条件分支都使用花括号括起来,不允许在循环体和条件分支中只包含一条语句时省略花括号,任何语句只要不是以花括号结束都应当使用分号作结尾。
这个子集并未包含逗号运算符、位运算符以及"++"和"--"。也不包含"=="和"!=",因为会涉及到类型转换,更加推荐"==="和"!=="。
由于JavaScript并不包含块级作用域,Crockford为我们提炼出的子集部分对var语句做了限制,var语句只能出现在函数体的顶部,并要求程序员将函数内所有的变量声明写在一个单独的var语句中。子集中禁止使用全局变量,但这个限制只是编程约定,并不是真正的语言上的限制。
Crockfork写过一个在线代码质量检测工具JSLint,可以通过http://www.jslint.com/访问,这个工具提供了很多选项用来增强代码的一致性检查。除了确保代码使用了子集推荐的特性之外,还对代码风格进行了一些强制约定,比如缩进等。
在Crockford出这本书 的时候,ECMAScript5的严格模式还没有出来,所以,在ECMAScript5严格模式中,很大一部分都和它有同样的限制。随着ECMAScript5广泛采用,.jslint.工具在选中The good parts时,程序必须包含use strict代码。
1.2.子集的安全性
利用"精华部分"的 一个语言子集可以设计出更具美感的程序且提升程序员的开发效率。这里将要讨论的是一个更大的子集,这个大子集的设计目的是能在一个容器或"沙箱"中更安全地运行不可信的第三方JavaScript代码。所有能破坏这个沙箱并影响全局执行环境的语言特性和API在这个安全子集都是禁止的。每个子集都有一个静态的检查器,可以对代码进行解析检查以确保代码是符合子集规范的。
为了让javascript安全的运行,我们必须移除一些javascript特性:
- 禁止使用eval()和Function()构造函数,因为它们可以执行任意代码,而且JavaScript无法对这些代码作静态分析。
- 禁止使用this关键字,因为函数在非严格模式下可以通过this访问全局对象,这是沙箱系统阻止的
- 禁止使用with语句,因为with语句增加静态代码检查难度。
- 禁止使用某些全局变量,在客户端javascript中,浏览器窗口对象当做全局对象,也有双重身份(操作浏览器和DOM),因此代码中不能有window对象的引用。同样的,document对象定义操作整个页面的方法,将document的控制权交给一段不受信任的代码会存在隐患。安全子集提供了两种不同的方法来处理类似document这类全局对象,第一种方法是:完全禁止掉它们,并定义一组api用以分配给它的web页面做有限制的访问。第二章方法,在代码所运行的容器里定义一个只对外面提供安全标准的DOM api.
- 禁止使用某些属性和方法,这些属性和方法包括arguments对象的两个属性caller和callee(甚至在某些子集中干脆禁止使用arguments对象)、函数的call()和apply()方法、以及constructor和prototype两个属性。非标准的属性也被禁止掉。比如__proto__。
- 静态分析可以有效地防止带有点(.)运算符的属性存取表达式去读写特殊属性。禁止使用方括号。
有一些限制,比如禁止使用eval()和with语句,并不会对开发者带来额外负担,毕竟这些特性本来就很少在JavaScript编程中用到。另外一些限制规则,比如使用方括号对属性进行存取的限制则对开发造成诸多不便,这时就有代码转换器的用武之地了。比如,转换器会自动将使用方括号的代码转换为函数调用的代码,以便能够对它执行运行时检查。有了这种代码转换,我们是可以安全的使用this关键字的。
有一些安全子集已经实现了,我们只做一些简要介绍:
- ADsafe
- dojox.secure
- Caja
- FBJS
- Web Sandbox
2.常量和局部变量
语言的子集先停,进入语言的拓展
在javascript1.5后续版本中可以使用const关键字定义常量,常量重新赋值会失败但不会报错。对常量重复声明会报错。
const和var相类似,因为JavaScript没有块级作用域,所用常量被提前至函数定义的顶部。
一直以来,JavaScript的变量缺少块级作用域的支持被普遍认为是javascript的短板,javascript1.7针对这个缺陷增加了关键字let。关键字const一直都是JavaScript的保留字(没有使用),因此现有的代码不必作任何改动就可以增加常量,关键字let并不是保留字,javascript1.7及以后的版本才能识别,需要手动加入版本号才可以。let有四种使用方式:
- 可以作为变量声明,和var一样;
- for或for/in循环中,var关键字的替代方案;
- 在语句块中定义一个变量并显式指定它的作用域;
- 定义一个在表达式内部作用域中的变量,这个变量只在表达式内可见。
使用let最简单的方式就是替换程序中的var,通过var声明变量的函数内都是可用的,而通过let声明的变量则只属于就近的花括号括起来的那块语句(包括嵌套的语句)。比如在循环体内使用let声明变量,那么这个循环之外是不可用的,如下代码:
function oddsum(n) { let total = 0; let result = []; for (let x = 1; x < n; x++) { let odd = 2 * x - 1; total += odd; result.push(total); } //这里使用odd或x会导致一个引用错误 console.log(total); } oddsum(5)
我们注意到,这段代码中let还替代了for循环中的var。这时通过let创建的变量的作用域仅限于循环体、循环条件判断逻辑和自增操作表达式。同样,可以这样在for/in(以及for each,参照11.4.1)循环中使用let:
o = {x: 1,y: 2}; for (let p in o) console.log(p); //=>x和y console.log(p) //错误:p没有定义
在声明语句使用let和循环初始化使用let有着有趣的区别,对于前者来说,变量初始化表达式是在变量的作用域内计算的。但后者来说,变量的初始化表达式则是在作用域之外计算的。当出现两个同名变量时要格外注意: let x = 1; for (let x = x + 1; x < 5; x++) console.log(x) //=>2,3,4 { let x = x+1;//x没有定义,因此相加NaN console.log(x) }
上述内容有待验证,因为我执行时出错。
let x = 1, y = 2; let (x = x + 1, y = x + 2) { // 注意这里的写法 console.log(x + y); // 输出 5 }; console.log(x + y); //输出3
。。。。。。
3.解构赋值
Spidermonkey 1.7实现了一种混合式赋值,我们称之为“解构赋值”。例如,你可能在python或者ruby中接触过此概念,在解构赋值中,等号右侧是一个数组或对象(一个结构化的值),指定左侧一个或多个变量的语法和右侧数组和对象直接量的语法保持格式一致。
当发生解构赋值时,右侧的数组和对象中一个或多个的值就会被提取出来(解构),并赋值给左侧相应的变量名。除了用于常规的赋值运算符之外,解构赋值还用于初始化用var和let新声明的变量。
当和数组配合使用时,解构赋值是一种写法简单且有极其强大的功能。特别是在函数返回一组结果的时候解构赋值就显得非常有用。然而当配合对象或者嵌套对象一起使用时,解构赋值变得更加复杂且容易搞混。下面的例子展示了简单的和复杂的解构赋值:
let [x, y] = [1, 2]; // 等价于 let x=1,y=2 [x, y] = [x + 1, y + 1]; //等价于 x = x+1,y=y+1 [x, y] = [y, x]; // 交换两个变量的值 console.log([x, y]); //输出 [3,2]
当函数返回一组结果时,解构赋值方式将大大简化程序代码:
// 将 [x,y] 从笛卡尔(直角)坐标转换为 [r,theta] 极坐标 function polar(x, y) { return [Math.sqrt(x * x + y * y), Math.atan2(y, x)]; } // 将极坐标转换为笛卡尔坐标 function cartesian(r, theta) { return [r * Math.cos(theta), r * Math.sin(theta)]; } let [r, theta] = polar(1.0, 1.0); // r=Math.sqrt(2), theta=Math.PI/4 let [x, y] = cartesian(r, theta); // x=1.0, y=1.0 console.log(r);
解构赋值右侧的数组所包含的元素不必和左侧的变量一一对应,左侧多余的变量的赋值为undefined,而右侧多余的值则会忽略。左侧的变量列表可以包含连续的逗号用以跳过右侧对应的值。
let [x,y] = [1]; [x,y] = [1,2,3]; [,x,,y] = [1,2,3,4]; //x = 2
整个解构赋值运算的返回值是右侧的整个数据结构,而不是从中提取出来的某个值。因此,可以这样写“链式”解构赋值:
let first,second,all; all = [first,second] = [1,2,3,4;] //first=1,second=2,all=[1,2,3,4]
还可以用于数组嵌套,左右侧的嵌套格式应该一样:
let [one,[twoA,twoB]] = [1,[2,2.5],3]
解构赋值的右侧也可以是一个对象。名值对内冒号左侧是属性名称,冒号右侧是变量名称,每一个命名属性都会从右侧对象中查找对应的赋值,每个值(或是undefined)都会赋值给它所对应的变量。很容易搞混,因为属性名称和变量标识符通常写成一样。例如:r,g和b是属性名,red,green和blue是变量名,不要搞混:
let tran = {r:0.0,g:0.0,a:1.0}; let {r:red,g:green,b:blue} = tran; //red=0.0,....
将Math对象的全局函数复制至新的变量中,用以简化:
let {sin:sin,cos:cos,tan:tan} = Math;//等价于let sin=Math.sin,...
对象也可以嵌套,而且两种语法也可以混合使用。
4.迭代
mozilla的javascript扩展引入了一些新的迭代机制,包括for each循环和python风格的迭代器(iterator)和生成器(generator)。
4.1.for/each循环
for/each循环是由e4x规范(ecmascript for xml)定义的一种新的循环语句。e4x是语言的扩展,它允许javascript程序中直接出现xml标签,并定义了操作xml数据的语法和api。浏览器大都没有实现e4x,但是mozilla javascript 1.6(随着firefox 1.5发布)是支持e4x的。本节我们只对for/each作讲解,并不会涉及到xml对象。关于e4x的剩余内容请参照11.7节。
for each循环和for/in循环非常类似。但for each并不是遍历对象的属性,而是对属性的值作遍历(数组也一样):
let o = { one: 1, two: 2, three: 3 } for (let p in o) console.log(p); // for/in: 输出 'one', 'two', 'three' for each(let v in o) console.log(v) //在Firefox运行
注意,for/each循环并不仅仅针对数组本身的元素作遍历,它也会遍历数组中所有可枚举属性,包括继承来的可枚举的方法。因此,通常并不推荐for/each循环和数组一起使用。在ECMAScript5 之前的 javascript 版本中是可以这样用的,因为自定义属性和方法不可能设置为可枚举的。
4.2.迭代器
JavaScript1.7为for/in循环增加了更多通用的功能。可以遍历任何可迭代的对象。为了便于理解,我们首先给出一些定义。
迭代器是一个对象,这个对象允许对它的值的集合作遍历,并保持任何必要的状态以便能够跟踪到当前遍历的“位置”。
迭代器必须包含next()方法,每一次对next()调用都返回集合中的下一个值。比如下面的counter()函数返回了一个迭代器,这个迭代器每次调用next()都会返回连续递增的整数。需要注意的是,这个函数利用闭包的特性实现了计数器状态的保存:
function counter(start) { let nextValue = Math.round(start); // 表示迭代器状态的一个私有成员 return { next: function () { return nextValue++; } }; // 返回迭代器对象 } letserialNumberGenerator = counter(1000); letsn1 = serialNumberGenerator.next(); // 1000 letsn2 = serialNumberGenerator.next(); // 1001
迭代器用于有限的集合时,当所有的值都遍历完成没有多余的值可迭代时,再调用next()方法会抛出stopiteration。stopiteration是javascript 1.7中的全局对象的属性。它是一个普通的对象(它自身没有属性),只是为了终结迭代的目的而保留的一个对象。注意,实际上,stopiteration并不是像typeerror()和rangeerror()这样的构造函数。比如,这里实现了一个rangeiter()方法,这个方法返回了一个可以对某个范围的整数进行迭代的迭代器:
// 这个函数返回了一个迭代器,它可以对某个区间内的整数作迭代 function rangeIter(first, last) { let nextValue = Math.ceil(first); return { next: function () { if (nextValue > last) throw StopIteration; return nextValue++; } }; }// 使用这个迭代器实现了一个糟糕的迭代. let r = rangeIter(1, 5); // 获得迭代器对象 while (true) { // 在循环中使用它 try { console.log(r.next()); // 调用 next() 方法 } catch (e) { if (e == StopIteration) break; // 抛出 StopIteration 时退出循环 else throw e; } }
。。。。。。
5.函数简写
对于简单的函数,JavaScript 1.8引入了一种简写形式:“表达式闭包”。如果函数只包含一个表达式并返回它的值,关键字return和花括号都可以省略,并将待计算的表达式放在参数列表之后,这里有一些例子:
let succ = function(x) x + 1,yes = function() true,no = function() false;
这只是一种简单的快捷写法,用这种形式定义的函数其实和带花括号和关键字return的函数完全一样,这种快捷写法更适用于当给函数传入另一个函数的场景,比如:
data.sort(function(a, b) b - a); // 定义一个函数,用以返回数组元素的平方和 let sumOfSquares = function(data) Array.reduce(Array.map(data, function(x) x * x), function(x, y) x + y);
6.多catch从句
在javascript1.5中,try/catch语句已经可以使用多catch从句了,在catch从句的参数中加入关键字if以及一个条件判断表达式:
try { // 这里可能会抛出多种类型的异常 throw 1; } catch (e if e instanceof ReferenceError) { // 这里处理引用错误 } catch (e if e === "quit") { // 这里处理字符串是“quit”的情况 } catch (e if typeof e === "string") { // 处理其他字符串的情况 } catch (e) { // 处理余下的异常情况 } finally { // finally从句正常执行 }
当产生了一个异常时,程序将会尝试执行每一个catch从句。catch从句中的参数即是这个异常,执行到catch的时候会它的计算条件表达式。如果条件表达式计算结果为true,则执行当前catch从句中的逻辑,同时跳过其他的catch从句。如果catch从句中没有条件表达式,程序会假设它包含一个if true的条件,如果它之前的catch从句都没有被激活执行,那么这个catch中的逻辑一定会执行。如果所有的catch从句都包含条件,但没有一个条件是true,那么程序会向上抛出这个未捕获的异常。注意,因为catch从句中的条件表达式已经在括号内了,因此也就不必像普通的条件句一样再给他包裹一个括号了。
7.e4x:ECMAScript for XML
“ECMAScript for XML”简称E4X,是JavaScript的一个标准扩展(注:E4X是由ECMA-357规范定义的。可以从这里查看官方文档:http://www.ecma-international.org/publications/standards/Ecma-357.htm),它为处理XML文档定义了一系列强大的特性。Spidermonkey 1.5和Rhino 1.6已经支持E4X。由于多数浏览器厂商还未支持E4X,因此E4X被认为是一种基于Spidermonkey或Rhino引擎的服务器端技术。
E4X将XML文档(元素节点或属性)视为一个XML对象,将XML片段视为一个紧密相关的XML列表对象。本节会介绍创建和使用XML对象的一些方法。XML对象是一类全新的对象,E4X中定义了专门的语法来描述它(接下来会看到)。我们知道,除了函数之外所有标准的JavaScript对象的typeof运算结果都是“object”。正如函数和原始的JavaScript对象有所区别一样,XML对象也和原始JavaScript对象不同,对它们进行typeof运算的结果是“xml”。在客户端JavaScript中(参照第15章),XML对象和DOM(文档对象模型)对象没有任何关系,理解这一点非常重要。E4X标准也针对XML文档元素和DOM元素之间的转换作了规定,这个规定是可选的,Firefox并没有实现它们之间的转换。这也是E4X更适用于服务器端编程的原因。
本小节中我们会给出一个E4X的快速入门教程,而不会作更深入的讲解。XML对象和XML列表对象的很多方法本书中并未介绍。在参考手册部分也不会对其作讲解,如果读者希望进一步了解E4X,可以参照官方文档。
E4X只定义了很少的语言语法。最显著的当属将XML标签引入到JavaScript语言中。可以在JavaScript代码中直接书写XML标签直接量,比如:
// 创建一个XML对象 var pt = <periodictable> <element id = "1"> <name> Hydrogen </name></element> <element id = "2"> <name> Helium </name></element> <element id = "3"> <name> Lithium </name></element> </periodictable>; // 给这个表格添加一个新元素 pt.element += <element id = "4"> <name> Beryllium </name></element> ;
XML直接量语法中使用花括号作为变量输出,我们可以在XML中嵌入JavaScript表达式。例如,这里是另外一种创建XML元素的方法:
pt = <periodictable></periodictable>; // 创建一个新表格 var elements = ["Hydrogen", "Helium", "Lithium"]; // 待添加的元素 // 使用数组元素创建XML元素 for(var n = 0; n < elements.length; n++) { pt.element += <element id={n+1}><name>{elements[n]}</name></element>;
。。。。。。