New in JavaScript 1.7

来源:互联网 发布:淘宝千牛一键发货 编辑:程序博客网 时间:2024/06/05 16:36

这篇文章涵盖了 Firefox 2 中的新特性

JavaScript 1.7 介绍了一系列新的语言特性,特别是生成器、迭代器、数组领悟、let 表达式,以及解构赋值。它还引入了在JavaScript 1.6中的全部特性。

Firefox 2 Beta 1 开始提供对 JavaScript 1.7 的支持,当然也包括所有当前代码库(trunk目录)中的版本。

本文中包含的代码示例可以在 JavaScript shell 中实验。请参照 Introduction to the JavaScript shell 来学习如何安装并使用 shell。

使用 JavaScript 1.7

为了使用 JavaScript 1.7 的新特性,你需要指定 JavaScript 版本为 1.7。在HTML或者XUL代码中,这样写:

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

当使用JavaScript shell时,你需要通过在命令行添加-version 170或者使用 version() 函数,来设置版本:

 version(170);

如果用到了需要使用新关键字 "yield" 和 "let" 的特性,你就必须要指定版本为 1.7,因为已经写好的代码中可能会把那两个关键字当作变量名或者函数名来使用。如果没有涉及使用了新关键字的特性(解构赋值和数组领悟),就可以不指定 JavaScript 版本。

生成器和迭代器

每当开发那些涉及到迭代算法(如对列表的迭代或对同一数据集进行相同的演算)的代码时,通常都需要声明一些变量,并在整个计算过程中都保留它们的值。一般来说,你还需要使用一个回调函数来获得迭代算法的中间值。

生成器

下面这个迭代算法计算了斐波那契数列:

function do_callback(num) {
document.write(num + "<br>/n");
}

function fib() {
var i = 0, j = 1, n = 0;
while (n < 10) {
do_callback(i);
var t = i;
i = j;
j += t;
n++;
}
}

fib();

这段代码使用了一个回调函数,来机械地执行算法每一步迭代的操作。这里,每一个斐波那契数都被简单地打印在控制台上。

使用生成器和迭代器,可以提供一个更新更好的方法。现在就让我们来看看使用了生成器的斐波那契数列:

function fib() {
var i = 0, j = 1;
while (true) {
yield i;
var t = i;
i = j;
j += t;
}
}

var g = fib();
for (var i = 0; i < 10; i++) {
document.write(g.next() + "<br>/n");
}

包含 yield 关键字的函数,就是一个生成器。当你调用它时,它的真正参数就会绑定到实际的变量上,但它自身其实并不做实际的计算,而是返回一个生成器-迭代器(generator-iterator)。每调用一次该生成器-迭代器的 next() 方法就再执行一次迭代算法。yield 关键字指定了每一步的值,就像生成器-迭代器版的 return 一样,指出了算法的每次迭代之间的边界。你每调用一次 next(),生成器代码就从 yield 以下的语句处恢复。

你可以重复调用一个生成器-迭代器的 next() 方法来使它转动,直到到达你期望的结果条件。在这个例子中,我们只需不停地调用 g.next(),就可以获得任意多的斐波那契数,直到满足我们所需要的结果个数。

从指定位置恢复生成器-迭代器

一个生成器在被调用其 next() 方法启动之后,你可以使用 send() 方法传递一个指定的的值,作为上一个 yield 的结果。这个生成器将返回后续 yield 的操作数。

你无法强制生成器从某个位置启动,在你 send() 一个指定的值给生成器之前,你必须先以 next() 启动它。

注意: 有意思的是,调用 send(undefined) 与调用 next() 是等价的。不过,用任何除 undefined 之外的值调用 send() 启动一个刚刚创建的生成器将引发 TypeError 异常。
生成器的异常

通过调用生成器的 throw() 方法并传递异常值,你可以强制生成器抛出异常。此异常将从该生成器当前挂起的上下文中被抛出,就好像在当前被挂起的 yield 中插入了一个 throw value 语句。

如果在抛出异常的过程中没有遇到 yield,那么该异常将被通过调用 throw() 传播,且后续的 next() 调用将导致 StopIteration 异常抛出。

关闭生成器

生成器的 close() 方法能强行关闭生成器自身。关闭生成器的效果如下:

  1. 运行这个生成器函数中活动的 finally 语句。
  2. 一旦 finally 语句抛出任何除 StopIteration 之外的异常,该异常被传播给 close() 方法的调用者。
  3. 该生成器终结。
生成器示例

这段代码执行了一个每 100 次循环 yield 一次的生成器。

var gen = generator();

function driveGenerator() {
if (gen.next()) {
window.setTimeout(driveGenerator, 0);
} else {
gen.close();
}
}

function generator() {
while (i < something) {
/** 工作 **/

++i;
/** 100 循环 yield 一次 **/
if ((i % 100) == 0) {
yield true;
}
}

yield false;
}

迭代器

迭代器可以让你迭代数据,是一种特殊的对象。

在常规的用法中,迭代器对象是“不可见”的,你不需要显示地操作它们。取而代之,使用 JavaScript 的 for...in  for each...in 语句 自然地在对象的键、值之上循环。

var objectWithIterator = getObjectSomehow();

for (var i in objectWithIterator)
{
document.write(objectWithIterator[i] + "<br>/n");
}

如果你想实现你自己的迭代器对象,或对直接操作迭代器有另外的需求,你就需要了解 next 方法、StopIteration 异常和 __iterator__ 方法了。

你可以通过调用 Iterator(对象名) 来为一个对象创建迭代器。调用一个对象的 __iterator__ 方法时,需要查找它的迭代器;当这个方法不存在时,创建一个默认的迭代器。默认迭代器会根据传统的 for...in for each...in 模型产出该对象的属性。也可以提供一个自定义的迭代器,只需重载 __iterator__ 方法,使它返回你自定义的迭代器的一个实例即可。你可以使用 Iterator(对象) 从脚本中获取一个对象的迭代器,以避免直接访问 __iterator__ 的属性,因为只有前者才对数组起作用。

一旦你拥有了一个迭代器,你就能够轻松地获取对象中的下一项了,调用该迭代器的 next() 方法即可。对象中没有余下的数据时,抛出 StopIteration 异常。

这里有一个直接迭代器操作的简单示例:

var obj = {name:"Jack Bauer", username:"JackB", id:12345, agency:"CTU", region:"Los Angeles"}; 

var it = Iterator(obj);

try {
while (true) {
print(it.next() + "/n");
}
} catch (err if err instanceof StopIteration) {
print("End of record./n");
} catch (err) {
print("Unknown error: " + err.description + "/n");
}

该程序的输出如下:

name,Jack Bauer
username,JackB
id,12345
agency,CTU
region,Los Angeles
End of record.

你可以有选择性地指定创建迭代器的第二个参数,它是一个指示你调用其 next() 方法时是否需要返回 keys 的布尔值。这个参数被作为第一个参数传入给用户定义的 __iterator__ 函数。在上面的例子中,把 var it = Iterator(obj); 改为 var it = Iterator(obj, true); 后的输出如下:

name
username
id
agency
region
End of record.

对于每一种情况,返回数据的实际顺序只基于实现,数据的顺序是不能保证的。

迭代器是一种扫描对象中的数据的简便的方式。被扫描的对象的内容中可能包含你没有注意到的数据,所以当你需要保护那些你的应用程序并不期望的数据时,迭代器会显得非常有用。

数组领悟

数组领悟是一种对生成器的运用,展示了一种初始化数组的强大而方便的方式。例如:

function range(begin, end) {
for (let i = begin; i < end; ++i) {
yield i;
}
}

range() 是一个返回从 begin  end 之间值的生成器。知道了这些,我们就可以这样使用它:

var ten_squares = [i * i for each (i in range(0, 10))];

这段代码预初始化了一个新数组 ten_squares,包含了范围在 0..9 之间的值的平方数。

在初始化数字时你可以使用任何限制条件。如果你想要初始化一个包含 0 到 20 之间偶数的数组,你可以使用下面的代码:

var evens = [i for each (i in range(0, 21)) if (i % 2 == 0)];

在 JavaScript 1.7 之前,这个工作必须要写出类似这样的代码:

var evens = [];
for (var i=0; i <= 20; i++) {
if (i % 2 == 0)
evens.push(i);
}

一旦你熟悉了数组领悟,你就会发现它不但紧凑,而且易于阅读。

作用域规则

数组领悟被暗含的块包围着,包含了方括号内所有的东西,类似隐含的 let 声明。

更多细节。

let 的块作用域

有好几种使用 let 来管理数据和函数的块作用域的方式:

  • let 语句 提供了一种在块作用域中联系值与变量、常量、函数的途径,且不会影响到块之外的同名变量。
  • let 表达式 使你能在仅有一个表达式所限定的作用域内建立变量。
  • let 定义 在一个块中,定义变量、常量和函数,并把它们的作用域限制在块内。从语法上看,let 定义 很类似 var
  • 你也可以使用 let 建立只存在于 for 循环上下文中的变量。

let 语句

let 语句为变量、常量和函数提供了本地作用域,其工作是把零个或多个变量绑定到单个代码块的词法作用域上,这个过程与块语句完全相同。特别要注意的是,在 let 语句内部使用 var 声明的变量,其作用域和它在 let 语句外被声明时相同,这样的变量仍然具有函数作用域。

例如:

var x = 5;
var y = 0;

let (x = x+10, y = 12) {
print(x+y + "/n");
}

print((x + y) + "/n");

这段程序的输出是:

27
5

此规则对任何 JavaScript 中的代码块适用。使用 let 声明,可以给代码块建立属于它自己的本地变量。

注意: 使用 let 语句这个语法时,跟在 let 后面的括号是必需的,丢掉它们将导致一个语法错误。
作用域规则

 let 定义的变量的作用域是 let 块本身,覆盖了任何 let 块内部的块,除非这些内部块定义了同名的变量。

let 表达式

你可以使用 let 建立作用域仅在一个表达式中的变量。

var x = 5;
var y = 0;
document.write( let(x = x + 10, y = 12) x+y + "<br>/n");
document.write(x+y + "<br>/n");

输出如下:

27
5

在这个例子中,对 x 的值 x+10 的绑定和对 y 的值 12 的绑定的作用域被限制于表达式 x+y

作用域规则

let 表达式:

let (声明) 表达式

 表达式 周围创建了一个隐含的块。

let 定义

let 关键字也可被用于在块中定义变量、常量和函数。

if (x > y)
{
let gamma = 12.7 + y;
i = gamma * x;
}

使用内部函数时,let 语句,let 表达式和 let 定义常常能使代码更加清晰。

var list = document.getElementById("list");
for (var i = 1; i <= 5; i++)
{
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
let j = i;
item.onclick = function (ev) {
alert("Item " + j + " is clicked.");
};
list.appendChild(item);
}

上面的例子能像我们所期望的那样工作,因为五个匿名函数的实例引用了变量 j 的不同实例。请注意,如果你把 let 换成 var,或者移除变量 j 并仅在内部函数中使用变量 i,这段代码就不能正常工作了。

作用域规则

 let 定义的变量的作用域覆盖了它们被定义的块,已经任何没有重定义它们的子块。从这一点来看,let 的效果非常类似 var。主要的区别在于用 var 定义的变量的作用域是整个闭合函数:

  function varTest() {
var x = 31;
if (true) {
var x = 71; // 同一个变量!
alert(x); // 71
}
alert(x); // 71
}

function letTest() {
let x = 31;
if (true) {
let x = 71; // 不是一个变量
alert(x); // 71
}
alert(x); // 31
}

= 右侧的表达式处于块的内部。这与 let-表达式和 let-语句的作用域不同:

  function letTests() {
let x = 10;

// let-语句
let (x = x + 20) {
alert(x); // 30
}

// let-表达式
alert(let (x = x + 20) x); // 30

// let-定义
{
let x = x + 20; // 这里的 x 的求值结果为 undefined
alert(x); // undefined + 20 ==> NaN
}
}

在程序和类中,let 不创建 global 对象上的属性,这与 var 的做法不同。但 let 会在一个隐含的块中创建属性,使其它上下文中的语句可以被求值。这就意味着,let 不会覆写前面用 var 定义的变量。例如:

var x = 'global';
let x = 42;
document.write(this.x + "<br>/n");

这段代码将输出 "global" 而不是 "42"。

隐含的块指的不是那种被括弧括起的块,它只由 JavaScript 引擎隐式创建。

 eval() 执行的 let  var 不同,它不会在变量对象(活动对象或在最内层绑定的组成部分)上创建属性。为使其它上下文中的语句可以被求值,它会在一个隐含的块中创建属性,这是 eval() 对于程序的操作与前述规则共同作用的结果。

换句话说,当你使用 eval() 执行代码时,这段代码就被当作一段独立程序处理,拥有自己的包围其代码的隐含的块。

for 循环中的 let-界定变量

你可以使用 let 关键字在 for 循环的作用域内绑定本地变量,和使用 var 类似。

** 添加对象 **
var i=0;
for ( let i=i ; i < 10 ; i++ )
document.write(i + "<br>/n");

for ( let [name,value] in obj )
document.write("Name: " + name + ", Value: " + value + "<br>/n");
作用域规则
for (let 表达式1; 表达式2; 表达式3) 语句

在这个例子中,表达式1表达式2表达式3语句都被闭合在一个隐含的块中,其中包含了用 let expr1 声明的本地变量。上面例子中的第一个循环示范了这个用法。

for (let 表达式1 in 表达式2) 语句
for each(let 表达式1 in 表达式2) 语句

在这两个例子中,都存在包含了每个 statement 的隐含的块。上面例子中的第二个循环展示了这里的第一种用法。

解构赋值

解构赋值使用了一种反映数组常量和对象常量结构的语法,使得从数组或对象中抽取数据成为可能。

通过使用对象常量和数组常量表达式,你可以轻松地按需创建成包的数据,然后以任意你想要的方式使用它们。你甚至可以从函数中返回它们。

而你使用解构赋值最重要的一件事就是,你可以从一个语句中读取整个结构。在这一节的余下的部分中会给出很多例子,你将看到你可以使用的很多有趣的功能。

解构赋值这种能力与 Perl,Python 等语言所显示出的特性很类似。

示例

用例子就可以很好地诠释解构赋值,所以这里几乎没有什么需要你通过阅读来学习的内容。

避免临时变量

你可以用解构赋值来交换两个变量的值。例如:

var a = 1;
var b = 3;

[a, b] = [b, a];

这段代码执行之后,b 为 1,a 为 3。不使用解构赋值的话,这个工作就需要一个临时变量。(当然了,在某些低级语言中,可以使用 XOR-交换技巧)

与此类似,解构赋值也可以被用于三个或更多变量之间的轮换:

var a = 'o';
var b = "<span style='color:green;'>o</span>";
var c = 'o';
var d = 'o';
var e = 'o';
var f = "<span style='color:blue;'>o</span>";
var g = 'o';
var h = 'o';

for (lp=0;lp<40;lp++)
{[a, b, c, d, e, f, g, h] = [b, c, d, e, f, g, h, a];
document.write(a+''+b+''+c+''+d+''+e+''+f+''+g+''+h+''+"<br />");}

这段代码执行之后,将显示一个可视化的、彩色的变量轮换过程。

回到我们上文中的菲波那契数生成器的例子,我们可以使用一个成组赋值语句来计算 "i" 和 "j" 的新值,从而消除临时变量 "t"。

function fib() {
var i = 0, j = 1;
while (true) {
yield i;
[i, j] = [j, i + j];
}
}

var g = fib();
for (let i = 0; i < 10; i++)
print(g.next());
多值返回

有了解构赋值之后,函数就可以返回多个值了。虽然在以前也可以从函数中返回数组,但解构赋值为此增加了极大的便利。

function f() {
return [1, 2];
}

正如你所看到的,用方括号括住所有要返回的值,使它们看上去像是数组,这就是多值返回。在上面的例子中,f() 返回 [1, 2] 作为其输出。

var a, b;
[a, b] = f();
document.write ("A is " + a + " B is " + b + "<br>/n");

命令 [a, b] = f() 使用函数结果给方括号中的变量依次赋值。a 被设为 1,b 被设为 2。

你也可以把这些返回值恢复成一个数组:

var a = f();
document.write ("A is " + a);

在这个例子中,a 是一个包含值 1 和 2 的数组。

在对象中循环

你也可以使用解构赋值把数据从对象中倾倒出来:

let obj = { width: 3, length: 1.5, color: "orange" };

for (let [name, value] in Iterator(obj)) {
document.write ("Name: " + name + ", Value: " + value + "<br>/n");
}

这段代码循环了对象 obj 中的所有键值对,并显示出它们的名字和值。这个例子的输出如下:

Name: width, Value: 3
Name: length, Value: 1.5
Name: color, Value: orange

在 JavaScript 1.7 中,obj 周围的 Iterator() 不是必须的,不过 JavaScript 1.8 就需要了,这样可以允许对数组的解构赋值(见 bug 366941 )。

在对象组成的数组中循环

你可以循环一个由对象组成的数组,并倾倒出每个对象中你感兴趣的域。

var people = [
{
name: "Mike Smith",
family: {
mother: "Jane Smith",
father: "Harry Smith",
sister: "Samantha Smith"
},
age: 35
},
{
name: "Tom Jones",
family: {
mother: "Norah Jones",
father: "Richard Jones",
brother: "Howard Jones"
},
age: 25
}
];

for each (let {name: n, family: { father: f } } in people) {
document.write ("Name: " + n + ", Father: " + f + "<br>/n");
}

这段代码用 name 域给 n 赋值,用 family.father 域给 f 赋值,然后打印它们。这个过程被作用于 people 数组中的每一个对象。输出如下:

Name: Mike Smith, Father: Harry Smith
Name: Tom Jones, Father: Richard Jones
忽略某些返回值

你也可以忽略掉那些你不感兴趣的返回值:

function f() {
return [1, 2, 3];
}

var [a, , b] = f();
document.write ("A is " + a + " B is " + b + "<br>/n");

这段代码运行之后,a 为 1,b 为 3,2 这个值被忽略了。你也可以用这种方式忽略多个,甚至是全部返回值。例如:

[,,,] = f();
从正则表达式匹配结果中倾倒值

当正则表达式的 exec() 方法找到一个匹配时,它会返回一个数组,第一项是目标字符串中被匹配的完整部分,后面是被正则表达式中被括号括起的组所匹配的数段字符串。解构赋值使你能轻松地倾倒出该数组中的各个部分,并忽略掉其中不需要的匹配。

// 一个简单的正则表达式,用来匹配 http / https / ftp 型的 URL
var parsedURL = /^(/w+)/:////([^//]+)//(.*)$/.exec(url);
if (!parsedURL)
return null;
var [, protocol, fullhost, fullpath] = parsedURL;