JavaScript的子集和扩展

2021/11/15 22:39:54

本文主要是介绍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标签中指定语言的扩展版 本,就像这样:

<script type="application/javascript; version=1.8">

关键字let有4种使用方式:

  • 可以作为变量声明,和var一样;
  • 在for或for/in循环中,作为var的替代方案;
  • 在语句块中定义一个新变量并显式指定它的作用域;
  • 定义一个在表达式内部作用域中的变量,这个变量只在表达式内可用。

使用let最简单的方式就是批量替换程序中的var。通过var声明的变量在函数内都是可用的,而通过let声明的变量则只属于就近的花括号括起来的语句块(当然包括它所嵌套的语句块)。比如,如果在循环体内使用let声明变量,那么这个变量在循环体之外是不可用的,示例代码如下:

 function oddsums(n) {
     let total = 0, result=[];	// 在函数内都是有定义的
     for(let x = 1; x <= n; x++) {	// x只在循环体内有定义
          let odd = 2*x-1;	// odd只在循环体内有定义
          total += odd;
          result.push(total);
     }
     // 这里使用x或odd会导致一个引用错误
     return result; 
 }
  
 oddsums(5);    // 返回[1,4,9,16,25]

我们注意到,在这段代码中let还替代了for循环中的var。这时通过let创建的变量的作用域仅限于循环体、循环条件判断逻辑和自增操作表达式。同样,可以这样在for/in(以及for each)循环中使用let:

0 = {x:1,y:2};
for (let p in o) console.log(p);	// 输出x和 y
for each(let v in o) console.log(v); // 输出 1和2
console.log(p)	// 引用错误:p没有定义 

在声明语句中使用let和在循环初始化器中使用let,两者有着有趣的区别。对于前者来说,变量初始化表达式是在变量的作用域内计算的。但对于后者来说,变量的初始化表达式则是在变量的作用域之外计算的。当出现两个变量同名的情况时需要尤为注意:

 let x = 1;
 for(let x = x + 1; x < 5; x++)
     console.log(x);     // 输出2~4
 {                       // 开始一个新的语句块,创建新的变量作用域
     let x = x + 1;      // x没有定义,因此x+1是NaN
     console.log(x);     // 输出NaN
 }

通过var声明的变量在它们所声明的函数内始终是存在的,但直到代码执行到var语句时才初始化变量。也就是说,变量是存在的(不会抛出引用错误异常),但在var语句执行之前它的值是undefined。通过let声明变量的情况与之类似,如果在let语句之前使用这个变量(与let语句在同一个块作用域内),变量是存在的,但值是undefined。

需要注意的是,在用let声明循环变量时这个问题是不存在的,语法上是不允许在初始化之前就使用这个变量的。还有一种方法可以在let声明语句之前使用变量时避免出错,就是在一条单独的let语句(和上文所示的let声明语句不同)的代码块中既包含一组变量的声明也包含这些变量的初始化表达式。语句里的变量和初始化表达式都放在一对圆括号内,随后跟随一对花括号括起来的语句块:

let x=1, y=2;
let (x=x+1,y=x+2) {        // 注意这里的写法
     console.log(x+y);     // 输出5
};
console.log(x+y);          // 输出3

let语句中的变量初始化表达式并不是这个语句块的一部分,并且是在作用域外部解析的,理解这一点至关重要。在这段代码中,我们新建了一个新的变量X并赋值给它一个更大的值。

let关键字的最后一种用法是let语句块的一个变体,其中有一对圆括号括起来的变量列表和初始化表达式,紧跟着是一个表达式而不是一个语句块。我们把这种写法叫做let表达式,上面的代码可以写成这样:

let x=1, y=2;
console.log(let (x=x+1,y=x+2)x+y); // 输出 5

某些const和let的用法(不必是这里描述的4种形式)在将来很有可能被纳入ECMAScript标准规范中。

解构赋值

Spidermonkey 1.7实现了一种混合式赋值,我们称之为“解构赋值”(destructuring assignment)。例如,你可能在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+l
[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

解构赋值右侧的数组所包含的元素不必和左侧的变量一一对应,左侧多余的变量的赋值为undefined,而右侧多余的值则会忽略。左侧的变量列表可以包含连续的逗号用以跳过右侧对应的值。

let [x,y] = [1];         // x = 1, y = undefined
[x,y] = [1,2,3];        // x = 1, y = 2
[,x,,y] = [1,2,3,4];     // x = 2, y = 4

JavaScript并未提供将右侧的多余的值以数组的形式赋值给左侧变量的语法。比如,在这段代码的第二行,并不能将[2,3]赋值给y。

整个解构赋值运算的返回值是右侧的整个数据结构,而不是从中提取出来的某个值。因此,可以这样写“链式”解构赋值:

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];               // one=1, twoA=2, twoB=2.5

解构赋值的右侧也可以是一个对象。这种情况下,解构赋值的左侧部分也应当看起来是一个对象直接量,对象中是一个名值对的列表,名值对之间用逗号分隔,列表用花括号括起来。名值对内冒号左侧是属性名称,冒号右侧是变量名称,每一个命名属性都会从右侧对象中査找对应的赋值,每个值(或者是undefined)都会赋值给它所对应的变量。这种解构赋值很容易被搞混,因为属性名称和变量标识符通常写成一样的。在下面这个例子中,r、g和b是属性名,red、green和blue是变量名,请不要搞混:

let transparent = {r:0.0, g:0.0, b:0.0, a:1.0};      // 一个用 RGBA 值表示的颜色
let {r:red, g:green, b:blue} = transparent;          // red=0.0,green=0.0,blue=0.0

在接下来的例子中,将Math对象的全局函数复制至新的变量中,用以简化三角函数相关的代码:

// 等价于 let sin=Math.sin, cos=Math.cos, tan=Math.tan 
let {sin:sin, cos,cos, tan:tan} = Math;

就像嵌套数组可以用于解构赋值一样,嵌套对象也可以用于解构赋值,实际上,两种语 法可以合在一起使用,可以用来描述任意的数据结构,例如:

// 一个嵌套的数据结构:一个对象中包含数组,数组中又包含对象 
let data = {
    name: "destructuring assignment",
    type: "extension",
    impl: [{engine: "spidermonkey", version: 1.7},
          {engine: "rhino", version: 1.7}]
};

// 使用解构赋值从数据结构中提取4个值 
let ({name: feature, impl: [{engine: impl1, version: v1}, {engine: impl2}]}=data){ 		  	         console.log(feature);                // 输出 "destructuring assignment" 
        assignment" console. log(impl1);     // 输出 "spidermonkey"
        spidermonkey console. log(v1);       // 输出 1.7 
        console.log(impl2);                  // 输出 "rhino"
}        

需要注意的是,类似这种嵌套的解构赋值可能会让代码变得晦涩难懂。然而,有一种有趣的规律可以帮助你更好地阅读这些复杂的解构赋值。思考一下最普通的赋值(给一个变量赋值)。赋值结束后,可以将这个变量用在程序中的表达式里,这个变量的值就是刚赋的值。在解构赋值中,左侧的部分使用了类似数组直接量或对象直接量的语法。但需要注意,在解构赋值完成后,左侧部分看起来像数组直接量或对象直接量的代码是可以作为合法的数组和对象用在代码中其他位置的,所有必需的变量都已经有定义,因此可以直接将等号左侧的部分作为一个可用的数组或对象复制并粘贴到程序的其他地方。

迭代

Mozilla的JavaScript扩展引入了一些新的迭代机制,包括for/each循环和Python风格的迭代器(iterator)和生成器(generator)。下面会 介绍。

for/each 循环

for/each循环是由E4X规范(ECMAScript for XML)定义的一种新的循环语句。E4X是语言的扩展,它允许JavaScriptg序中直接出现XML标签,并添加了操作XML数据的语法和API。Web浏览器大都没有实现E4X,但是Mozilla的JavaScript 1.6(随着Firefox 1.5 发布)是支持E4X的。

for/each循环和for/in循环非常类似。但for/each并不是遍历对象的属性,而是遍历属性的值:

let 0 = {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);  //for/each: 输出 1~3

当使用数组时,for/each循环遍历循环的元素(而不是索引)。它通常按数值顺序枚举它们,但实际上这并不是标准化或必需的:

a = ['one', 'two', 'three'];
for(let p in a) console.log(p);           // Prints array indexes 0, 1, 2
for each (let v in a) console.log(v);     // Prints array elts 'one', 'two', 'three'

注意,for/each循环并不仅仅针对数组本身的元素进行遍历,它也会遍历数组中所有可枚举属性的值,包括由数组继承来的可枚举方法。因此,通常并不推荐for/each循环和数组一起使用。在ECMAScript 5之前的JavaScript版本中是可以这样用的,因为自定义属性和方法不可能设置为可枚举的。

迭代器

JavaScript 1.7为for/in循环增加了更多通用的功能。JavaScript 1.7中的循环和Python的 for/in循环非常类似,它可以遍历任何可迭代的(iterable)对象。为了便于理解,我们首先给出一些定义。

迭代器是一个对象,这个对象允许对它的值集合进行遍历,并保持任何必要的状态以便能够跟踪到当前遍历的“位置”。

迭代器必须包含next( )方法,每一次调用next( )都返回集合中的下一个值。比如下面的counter( )函数返回一个迭代器,这个迭代器每次调用next( )都会返回连续递增的整数。需要注意的是,这个函数作用域利用闭包的特性实现了计数器当前状态的保存:

// 返回迭代器的一个函数
function counter(start) {
 let nextValue = Math.round(start);      // 表示迭代器的一个私有状态
 return { next: function() { return nextValue++; }};      // 返回迭代器对象
}
let serialNumberGenerator = counter(l000);
let sn1 = serialNumberGenerator.next();	// 1000
let sn2 = serialNumberGenerator.next();	// 1001

当迭代器用于有限的集合时,当遍历完所有的值并且没有多余的值可迭代时,再调用 next( )方法会抛出 Stoplteration。StopIteration是JavaScript 1.7中的全局对象的属性。它的值是一个普通的对象(它自身没有属性),只是为了终结迭代的目的而保留的一个对象。注意,实际上,StopIteration并不是像TypeError( )和RangeError( )这样的构造函数。比如,这里实现一个rangelter( )方法,这个方法返回一个可以对某个范围的整数进行迭代的迭代器:

// 这个函数返回了一个迭代器,它可以迭代某个范围内的整数
function rangeIter(first, last) {
 let nextValue = Math.ceil(first);
 return {
    next: function() {
      if (nextValue > last) throw StopIteration;
      return nextValue++;
    }
 };
}
// 使用这个范围迭代器实现一次糟糕的迭代
let r = rangelter(1,5);    	//	获得迭代器对象
while(true) {	           // 在循环中使用它
 try {
     console.log(r.next());	   //	调用next。方法
 }
 catch(e) {
      if (e == StopIteration) break; // 抛出StopIteration时退出循环 
      else throw e;
 }
}

注意,这里的循环使用一个迭代器对象,并且显式处理StopIteration方法,这种方式非常糟糕。因此,我们并不经常直接使用迭代器对象,而是使用可迭代的对象。可迭代对象表示一组可迭代处理的值。可迭代对象必须定义一个名叫_iterator_( )的方法(开始和结尾有两条下划线),用以返回这个集合的迭代器对象。

JavaScript 1.7对for/in循环的功能进行了扩展,可以用它来遍历可迭代对象。如果关键字in右侧的值是可迭代的,那么for/in循环会自动调用它的_iterator_( )方法来获得一个迭代器对象。然后它调用迭代器的next( )方法,将返回值赋值给循环变量,随即执行循环体。for/in循环自己会处理StopIteration异常,而且处理过程对开发者是不可见的。下面的代码定义了一个range( )函数,这个函数返回一个可迭代对象(不是迭代器)用以表示某个范围内的整数。我们看到,使用迭代范围的for/in循环要比使用迭代器的while循环更加简单。

// 返回一个可迭代的对象,用以表示该范围内的一个数字
function range(min, max) {
 return {	// 返回一个表示这个范围的对象
     get min() { return min; },	      // 范围边界是固定的
     get max() { return max; },	      // 并在闭包内保存起来
     includes: function (x) {	      // 检测成员是否属于这个范围
          return min <= x && x <= max;
     },
     toString: function() {            // 以字符串形式输出这个范围
          return "[" + min + "," + max + "]"; 
     },
     _iterator_: function() {          // 范围内的整数都是可迭代的 
         let val = Math.ceil(min);     // 将当前位置保存在闭包中 
         return {                      // 返回一个迭代器对象
         next: function() {            // 返回范围内的下一个值 
             if (val > max)            // 如果到达结尾就停止 
                 throw StopIteration; 
             return val++;             // 否则返回下一个值,并自增1
         }
         };
     }
 };
}
// 这里我们对这个区间中的值进行迭代
for(let in range(1,10)) console.1og(i);        // 输出1~10之间的数字

需要注意的是,我们在创建一个可迭代的对象和它的迭代器的时候,尽管必须写一个_iterator_( )方法并抛出一个StopIteration异常,但在正常使用时并不需要我们去手动调用_iterator_ ( )方法或手动处理StopIteration异常,for/in循环会为我们处理这些逻辑。如果出于某种考虑,你想从可迭代的对象中显式获得一个迭代器对象,只需调用Iterator( )函数即可(这个函数是定义在JavaScript 1.7中的全局函)。如果这个函数的参数是一个可迭代的对象,那么它将返回这个对象的_iterator_()方法的调用结果,从而保持代码整洁干净。如果给Iterator( )函数传入第二个参数,这个参数也会参与_iterator_ ()方法的调用。

然而,引入lterator( )函数还有一个重要的目的,如果传入的对象或者数组没有定义 _iterator_( )方法,它会返回这个对象的一个可迭代的自定义迭代器。每次调用这个迭代器的next( )方法都会返回其中包含两个值的一个数组,第一个数组元素是一个属性名,第二个是命名属性的值。由于这个对象是可迭代的迭代器,因此它可以直接用于for/in循环,而不用直接调用它的next( )方法。这意味着可以将Iteratro( )函数和解构赋值一起使用,这样可以方便地对对象或数组的属性和值进行遍历:

for (let [k,v] in Iterator({a:1,b:2}))           // 对属性和值作迭代
console.log(k + "=" + v);	                     // 输出"a=1"和"b=2"

Iterator( )函数返回的迭代器还有两个重要的特性。第一,它只对自有属性进行遍历而忽略继承的属性,通常我们希望是这个样子。第二,如果给Iterator( )传入第二个参数true,返回的迭代器只对属性名进行遍历,而忽略属性值。下面这段代码展示了这两种特性:

o = {x:1, y:2}                                 // 定义一个对象,它有两个属性
Object.prototype.z = 3;                         // 所有的对象都继承了z
for(p in o) console.log(p);                     // 输出"x", "y"和"z"
for(p in Iterator(o, true)) console.log(p);     // 只输出"x"和"y"

生成器

生成器是JavaScript 1.7中的特性(是从Python中借用过来的概念),这里用到了一个新的关键字yield,使用这个关键字时代码必须显式指定JavaScript的版本1.7。关键字yield在函数内使用,用法和return类似,返回函数中的一个值。yield和return的区别在于,使用yield的函数“产生”一个可保持函数内部状态的值,这个值是可以恢复的。这种可恢复性使得yield成为编写迭代器的有力工具。生成器是一种强大的语言特性,但它初次理解起来可能有些困难,下面给出一些定义。

任何使用关键字yield的函数(哪怕yield在代码逻辑中是不可达的)都称为“生成器函数”(generator function)。生成器函数通过yield返回值。这些函数中可以使用return来终止函数的执行而不带任何返回值,但不能使用return来返回一个值。除了使用yield,对return的使用限制也使生成器函数更明显地区别于普通函数。然而和普通的函 数一样,生成器函数也通过关键字function声明,typeof运算符返回“function”,并可以从Function.prototype继承属性和方法。但对生成器函数的调用却和普通函数完全不一样,不是执行生成器函数的函数体,而是返回一个生成器对象。

生成器是一个对象,用以表示生成器函数的当前执行状态。它定义了一个next( )方法,后者可恢复生成器函数的执行,直到遇到下一条yield语句为止。这时,生成器函数中的yield语句的返回值就是生成器的next( )方法的返回值。如果生成器函数通过执行return语句或者到达函数体末尾终止,那么生成器的next( )方法将抛出一个StopIteration。

只要一个对象包含可抛出stopIteration的next( )方法,它就是一个迭代器对象。实际上,它们是可迭代的迭代器,也就是说,它们可以通过for/in循环进行遍历。下面的代码展示了如何简单地使用生成器函数以及对它所生成的返回值进行遍历:

// 针对一个整数范围定义一个生成器函数 
function range(min, max) { 
 for(let i = Math.ceil(min); i <= max; i++) yield i;
}
// 调用这个生成器函数以获得一个生成器,并对它进行遍历
for (let n in range(3,8)) console.log(n); // 输出数字 3~8

生成器函数不需要返回。实际上,最典型的例子就是用生成器来生成Fibonacci数列:

// 一个用以产生一个Fibonacci数列的生成器函数 
function fibonacci() { 
    let x = 0, y = 1;
    while(true) {
      yield y;
      [x,y] = [y,x+y];
    }
}
// 调用生成器函数以获得一个生成器
f = fibonacci();
// 将生成器当做迭代器,输出Fibonacci数列的前10个数
for(let i = 0; i < 10; i++) console.log(f.next());

生成器有时也叫做“生成器迭代器”(generator iterator),用以区分创建它的生成器函
数。


我们注意到,fibonacci( )生成器函数没有返回。因此,它所产生的生成器不会抛出StopIteration。不能把这个生成器当做一个可迭代的对象用for/in循环进行遍历,这个循环是一个无穷循环,而是把它当做一个迭代器并显式调用10次它的next( )方法来实现。这段代码运行后,生成器f依然保持着生成器函数的执行状态。如果不再使用f,则可以通过调用f.close( )方法来释放它:

f.close();

当调用了生成器的close( )方法时,和它相关的生成器函数就会终止执行,就像在函数运行挂起的位置执行一条return语句。如果当前挂起位置在一个或者多个try语句块中,那么将首先运行finally从句,再执行close( )返回。close( )没有返回值,但如果finally语句块产生了异常,这个异常则会传播给close( )。

生成器经常用来处理序列化的数据,比如元素列表、多行文本、词法分析器中的单词等。生成器可以像Unix的shell命令中的管道那样链式使用。有趣的是,这种用法中的生成器是“懒惰的”,只有在需要的时候才会从生成器(或者生成器的管道)中“取” 值,而不是一次将许多结果都计算出来。

例: 一个生成器管道
// 一个生成器,每次产生一行字符串s
// 这里没有使用s.split(),因为这样会每次都处理整个字串,并分配一个数组
// 我们希望能更"懒"一些
function eachline(s) {
let p;
while((p = s.indexOf('\n')) != -1) {
yield s.substring(O,p);
s = s.substring(p+1);
}
if (s.length > 0) yield s;
}

// 一个生成器函数,对于每个可迭代的i的每个元素x,都会产生一个f(x) 
function map(i > f) {
for(let x in i) yield f(x);
}

// 一个生成器函数,针对每个结果为true的f(x),为i生成一个元素
function select(i, f) {
for(let x in i) {
if (f(x)) yield x;
}
}

// 准备处理这个字符串
let text = " #comment \n \n hello \nworld\n quit \n unreached \n";

// 现在创建一个生成器管道来处理它
// 首先,将文本分隔成行
let lines = eachline(text);
// 然后,去掉行首和行尾的空格
let trimmed = map(lines, function(line) { return line.trim(); });
// 最后,忽略空行和注释
let nonblank = select(trimmed, function(line) {
return line.length > 0 && line[0] != "#"
});

// 现在从管道中取出经过删减和筛选后的行对其进行处理
// 直到遇到"quit"的行
for (let line in nonblank) {
if (line === "quit") break;
console.log(line);
}

生成器往往是在创建的时候初始化,传入生成器函数的值是生成器所接收的唯一输入。然而,也可以为正在执行的生成器提供更多输入。每一个生成器都有一个send( )方法,后者用来重启生成器的执行,就像next( )方法一样。和next( )不同的是,send( )可以带一个参数,这个参数的值就成为yield表达式的值(多数生成器函数是不会接收额外的输入的,关键字yield看起来像一条语句。但实际上,yield是一个表达式,是可以有值的)。除了next( )和send( )之外,还有一种方法可以重启生成器的执行,即使用throw( )。如果调用这个方法,yield表达式就将参数作为一个异常抛给throw( ),比如, 下面一段代码:

// 一个生成器函数,用以从某个初始值开始计数
// 调用生成器的send()来进行增量计算
// 调用生成器的throw("reset")来重置初始值
// 这里的代码只是示例,throw()的这种用法并不推荐
function counter(initial) { 
let nextValue = initial; 
while(true) {	 // 定义初始值
try {
    let increment = yield nextValue;     // 产生一个值并得到增量
    if (increment)                       // 如果我们传入一个增量......
        nextValue += increment;          // ......那么使用它
    else nextValue++;                    // 否则自增 1
}	             
catch (e) {	                              // 如果调用了生成器的throw(),则执行这里的逻辑
        if (e==="reset")
            nextValue = initial;
            else throw e;
        }
}
}

let c = counter(10);               // 用10来创建生成器
console.log(c.next());             // 输出10
console.log(c.send(2));            // 输出12
console.log(c.throw("reset"));     // 输出10

数组推导

JavaScript 1.7中的数组推导(array comprehension)也是从Python中借用过来的一个概念。它是一种利用另外一个数组或可迭代对象来初始化数组元素的技术。数组推导的语法是基于定义元素集合的数学模型的,也就是说,表达式和从句的写法和JavaScript程序员期望的不一致。但不必担心,因为花不了太多时间就可以掌握这种新式语法,一旦掌握它则威力无穷。

下面这段代码展示了数组推导的写法,这里用到了上文定义的range( )函数,这段代码用以初始化一个数组,数组成员是0~1如之间的偶平方数:

let evensquares = [x*x for (x in range(0,10)) if (x % 2 === 0)]

这段代码和下面这五行代码等价:

let evensquares =[];
for(x in range(0,10)) {
if (x % 2 === 0) 
evensquares.push(x*x);
}

一般来讲,数组推导的语法如下:

[expression for ( variable in object ) if ( condition )]

我们看到,数组推导包含三个部分:

  • 一个没有循环体的for/in或for/each循环。这部分推导包括一个变量(或者通过解构赋值得到的多个变量),它位于关键字in的左侧,in的右侧是一个对象(例如, 这个对象可以是一个生成器、可迭代对象或数组)。尽管这个对象后面没有循环体,这段数组推导也能正确执行迭代,并能给指定的变量赋值。注意,在变量之前没有关键字var和let,其实这里使用了隐式的let,在数组推导中的变量在方括号的外部是不可见的,也不会覆盖已有的同名变量。
  • 在执行遍历的对象之后,是圆括号中的关键字if和条件表达式,目前,这个条件表达式只是用做过滤迭代的值。每次for循环产生一个值之后会判断条件表达式。如果条件表达式返回false,则跳过这个值,这个值也不会被添加至数组当中。if从句是可选的,如果省略的话,相当于给数组推导补充一条if(true)从句。
  • 在关键字for之前是expression,可以认为这个表达式是循环体。在迭代器返回了一个值并将它赋给一个变量,且这个变量通过了conditional测试之后,将计算这个表达式,并将表达式的计算结果插入到要创建的数组中。

下面是一些具体的例子:

data = [2,3,4, -5];	                      // 一个数组
squares = [x*x for each (x in data)];     // 对每个元素求平方:[4,9,16,25]
// 如果数组元素是非负数,求它的平方根
roots = [Math.sqrt(x) for each (x in data) if (x >= 0)]
// 将一个对象的属性名放入新创建的数组中
o = {a:1, b:2, f: function(){}}
let allkeys = [p for (p in o)]
let ownkeys = [p for (p in o) if (o.hasOwnProperty(p))]
let notfuncs = [k for ([k,v] in Iterator(o)) if (typeof v !== "function")]

生成器表达式

在JavaScript 1.8中,将数组推导中的方括号替换成圆括号,它就成了一个生成器表达式。生成器表达式(generator expression)和数组推导非常类似(两者在圆括号内的语法几乎完全一样),只是它的返回值是一个生成器对象,而不是一个数组。和数组推导相比,使用生成器表达式的好处是可以惰性求值(lazy evaluation),只有在需要的时候求值而不是每次都计算求值,这种特性可以应用于潜在的无穷序列。使用生成器表达式而不用数组也有不足之处,生成器只支持对值的顺序存取而不是随机存取。和数组不同,生成器并没有索引,为了得到第n个值,必须遍历它之前的n-1个值。

map( )函数:

function map(i, f) {   // 对于i的每个元素,生成器都会生成f(x) 
     for(let x in i) yield f(x);
}

有了生成器表达式,就不必用这个map()函数了。比如,下面这段代码定义一个新的生成器h用以对每个x生成f(x),x由生成器g生成:

let h = (f(x) for (x in g));

实际上,下例中所提到的eachline( )生成器,我们可以对其进行重写,可以通过这种方式来去除空格、注释和空行:

let lines = eachline(text);
let trimmed = (l.trim() for (l in lines));
let nonblank = (l for (l in trimmed) if (l.length > 0 && l[0]!='#'));

函数简写

对于简单的函数,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);

多catch从句

在JavaScript 1.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从句中的命名参数 即是这个异常,执行到cat ch的时候会计算它的条件表达式。如果条件表达式计算结果为true,则判断当前catch从句中的逻辑,同时跳过其他的catch从句。如果catch从句中没有条件表达式,程序就会假设它包含一个if true的条件,如果它之前的catch从句都没有触发,那么这条catch语句一定会执行。如果所有的catch从句都包含条件,但没有一个条件是true,那么程序会向上抛出这个未捕获的异常。注意,因为catch从句中的条件表达式已经在圆括号内了,因此也就不必像普通的条件语句一样再给它包裹一对圆括号。

E4X: ECMAScript for XML

"ECMAScript for XML”简称E4X,是JavaScript的一个标准扩展注气它为处理XML文档定义了一系列强大的特性。Spidermonkey 1.5和Rhino 1.6已经支持E4X。由于多数浏览器厂商还未支持E4X,因此E4X被认为是一种基于Spidermonkey或Rhino引擎的服务器端技术。

E4X将XML文档(或者XML文档的元素或属性)视为一个XML对象,将XML片段(在常见的父对象中包含多个XML元素)视为一个紧密相关的XML列表对象。XML对象是一类全新的对象,E4X中定义了专门的语法来描述它(接下来会看到)。我们知道,除了函数之外所有标准的JavaScriptM象的typeofig算结果都是“object”。正如函数和原始的JavaScript对象有所区别一样,XML对象也和原始JavaScript对象不同,对它们进行typeof运算的结果是“xml”。在客户端JavaScript中,XML对象和DOM(文档对象模型)对象没有任何关系,理解这一点非常重要。E4X标准也针对XML文档元素的E4X和DOM表示方式之间的转换做了规定,这个规定是可选的,Firefox并没有实现它们之间的转换。这也是E4X更适用于服务器端编程的原因。

E4X只定义了很少的新语言语法。最显著的新语法当属将XML标签引入JavaScript语言中。可以在JavaScript代码中直接书写XML标签直接量,比如:

<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>;
}

除了使用直接量语法,我们也可以将字符串解析成XML。下面的代码为上段代码创建的节点增加了一个新元素:

pt.element += new XML('<element id="5"><name>Boron</name></element>');

当涉及XML片段的时候,使用XMLList( )替换XML( ):

pt.element += new XMLList('<element id="6"><name>Carbon</name></element>' + 
               '<element id="7"><name>Nitrogen</name></element>');

E4X提供了一些显而易见的语法用以访问所创建的XML文档的内容:

var names = pt.element.name;     // 得到所有的<name>标签的一个列表
var n = names[0];	             //"Hydrogen"(氢),name的第0个标签的内容

E4X同样为操作XML对象提供了语法支持,点点(..)运算符是“后代运算符”(descendant operator),可以用它替换普通的点(.)成员访问运算符:

// 另一种得到所有<name>标签对应列表的方法
var names2 = pt..name;

E4X甚至定义了通配符运算:

// 得到所有<element>标签的所有子节点
// 这也是得到所有<name>标签对应列表的另外一种方法
var names3 = pt.element.*;

E4X中使用字符@来区分属性名和标签名(从XPath中借用过来的语法)。比如,可以这样来获得一个属性:

// “氮”的原子序数是多少
var atomicNumber = pt.element[1].@id;

可以使用通配符来获得属性名@*

// 获得所有的<element>标签的所有属性
var atomicNums = pt.element.@*;

E4X甚至包含了一种强大且极其简洁的语法用来对列表进行过滤,过滤条件可以是任意谓词表达式:

// 对所有的<element>元素组成的一个列表进行过滤
// 过滤出那些id属性小于3的元素

var lightElements = pt.element.(@id < 3);

// 对所有的element元素组成的列表进行过滤
// 过滤出那些name以B开始的元素
// 然后得到过滤后元素的<name>标签列表
var bElementNames = pt.element.(name.charAt(0) == 'B'.name;

在E4X标准中对for/each循环有了新的定义,可以用for/each来遍历XML标签和属性列表。for/each和for/in循环非常类似,for/in循环用以遍历对象的属性名,for/each循环用以遍历对象的属性值:

// 输出元素周期表中的每个元素名
for each (var e in pt.element) {
   console.log(e.name);
}

// 输出每个元素的原子序数
for each (var n in pt.element.@*) console.log(n);

E4X表达式可以出现在赋值语句的左侧,可以用它来对已存在的标签和属性进行修改或添加新标签或属性:

// 修改氢元素的<element>标签,给它添加一个新属性
// 像下面这样添加一个子元素
//
// <element id="1" symbol="H">
//    <name>Hydrogen</name>
//    <weight>1.OO794</weight>
// </element>
//
pt.element[0].@symbol = "H";
pt.element[0].weight = 1.00794;

通过标准的delete运算符也可以方便地删除属性和标签:

delete pt.element[0].@symbol;     // 删除一个属性
delete pt..weight;	              // 删除所有的<widget>标签

我们可以通过E4X所提供的语法来进行大部分的XML操作。E4X同样定义了能够调用 XML对象的方法,例如,这里用到了insertChildBefore( )方法:

pt.insertChildBefore(pt.element[1],
  <element id="1"><name>Deuterium</name></element>);

E4X中是完全支持命名空间的,它为使用XML命名空间提供了语法支持和API支持:

// 声明默认的命名空间 
  default xml namespace = "http://www.w3.org/1999/xhtml";
// 这里也是一个包含一些svg标签的xhtml文档
d = <html>
       <body>
           This is a small red square:
           <svg xmlns="http://www.w3.org/2OOO/svg" width="10" height="10"> 
                <rect x="0" y="0" width="10” height="10" fill="red"/>
           </svg>
       </body>
// body元素和它的命名空间里的uri及其localName
var tagname = d.body.name();
var bodyns = tagname.uri;
var localname = tagname.localName;

// 选择<svg>元素需要多做一些工作,因为<svg>不在默认的命名空间中
// 因此需要为svg创建一个命名空间,并使用::运算符将命名空间添加至标签名中
var svg = new Namespace('http://www.w3.org/2000/svg');
var color = d..svg::rect.@fill // "red"


这篇关于JavaScript的子集和扩展的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程