本章讨论javascript的集和超集,其中子集的定义大部分处于安全考虑。只有使用这门语言的一个安全的子集编写脚本,才能让代码执行的更安全、更稳定。ECMScript3标准是1999年版本的,10年后,ECMAScript5规范的更新,由于ECMAScript标准规范是允许对其做任何扩充的,伴随着Mozilla项目的改进,Firefox1.0、1.5、2.3.和3.5版本中分别发布了javascript1.5、1.6、1.7、1.8、1.81版本,这些javascript的扩展版本已经融入到ECMAScript5中,还有很多特性是非标准的,但这些特性很有可能在将来融入到ECMAScript版本中。
由于Firefox是基于一个名叫Spidermonkey的JavaScript引擎(Firefox的JavaScript引擎有很多种,"猴"系,Spidermonkey(用于在firefox 1.0-3.0)),firefox浏览器可以支持这些扩展特性。但由于这些语言特性是非标准的,本章的内容对于那些需要调试浏览器兼容性的开发者来说可能帮助不大。我们在本章对它们作必要的讲述是基于几点考虑:
在简单介绍JavaScript语言的子集之后,本章后面会开始介绍语言的扩展部分。由于这些扩展毕竟不是标准,因此这里只是一个指南形式的描述。
1.javascript的子集
大多数语句都支持他们的子集,用以安全的执行不可信的第三方代码。这里有一个很有趣的子集,定义这个子集的原因有些特殊。我们首先来看这个有趣的子集,然后再讨论安全的语言子集。
i.精华:
Douglas Crockford曾经写过一本很薄的书《JavaScript: The good parts》(O'reily 出版社)专门介绍JavaScript中值得发扬光大的精华部分。这个语言 子集的目标是简化这门语言,规避掉语言中的怪癖、缺陷部分,使编程更轻松、程序更健壮。 Douglas Crockford是这样介绍他的动机的:
大多数编程语言都有精华部分和鸡肋部分,我发现如果只使用精华部分而避免使用鸡肋的部分,我可以成为一名更好的程序员。
他提炼出的 子集部分不包含with和continue语句以及eval()函数。他提倡使用函数定义表达式而不是函数定义语句来定义函数 。 循环体和条件分支都使用花括号括起来,不允许在循环体和条件分支中只包含一条语句时省略花括号,任何语句只要不是以花括号结束都应当使用分号作结尾 。
由于JavaScript并不包含块级作用域,Crockford为我们提炼出的子集部分对var语句做了限制,var语句只能出现在函数体的顶部,并要求程序员将函数内所有的变量声明写在一个单独的var语句中。子集中禁止使用全局变量,但这个限制只是编程约定,并不是真正的语言上的限制。
它的网站http://www.jslint.com/是一个代码检测工具网站,提供了很多选项用来对代码的一致性进行增强。除了保证我们编写的代码保证子集推荐的特性之外,还对代码风格进行了约定,比如缩进等
在Crock ford出这本书 的时候,ECMAScript5的严格模式还没有出来,所以,在ECMAScript5严格模式中,很大一部分都和它有同样的限制。随着ECMAScript5广泛采用,.jslint.工具在选中The good parts时,程序必须包含use strict代码。
ii.子集的安全性
利用子集所追求的目标,我们可以设计出更美且提升程序员效率的javascript代码,为了让javascript安全的运行,我们必须移除一些javascript特性,使其安全子集的运行:
有一些限制,比如禁止使用eval()和with语句,并不会对开发者带来额外负担,毕竟这些特性本来就很少在JavaScript编程中用到。另外一些限制规则,比如使用方括号对属性进行存取的限制则对开发造成诸多不便,这时就有代码转换器的用武之地了。比如,转换器会自动将使用方括号的代码转换为函数调用的代码,以便能够对它执行运行时检查。有了这种代码转换,我们是可以安全的使用this关键字的。
有一些安全子集已经实现了,我们只做一些简要介绍
2.常量和局部变量
下面讨论语言的扩展,javascript1.5后续版本中可以使用const关键字定义常量,常量重新赋值会失败但不会报错 。对常量重复声明会报错。
const pi = 3.14150926; //定义一个常量并赋值 pi = 4; console.log(pi); //=> 3.14150926 赋值被忽略 const pi = 4; //=> Identifier 'pi' has already been declared 重新声明报错 var pi = 4; //=> Identifier 'pi' has already been declared
const和var相类似,因为JavaScript没有块级作用域,所用常量被提前至函数定义的顶部 。
一直以来,JavaScript的变量缺少块级作用域的支持被普遍认为是javascript的短板,javascript1.7针对这个缺陷增加了关键字let。关键字const一直都是JavaScript的保留字(没有使用),因此现有的代码不必作任何改动就可以增加常量,关键字let并不是保留字,javascript1.7及以后的版本才能识别。let有四种使用方式:
使用let最简单的方式就是替换程序中的var,通过var声明变量的函数内都是可用的,而 通过let声明的变量则只属于就近的花括号括起来的那块语句 (包括嵌套的语句) 。比如在循环体内使用let声明变量,那么这个循环之外是不可用的,如下代码:
function oddsum(n) { let total = 0; let result = []; for (let = 0, x < n; x++;) { //x只在循环内有定义 let odd = 2 * x - 1; total += odd; result.push(total); } return total; } oddsum(5)
我们注意到,这段代码中let还替代了for循环中的var。这时通过let创建的变量的作用域仅限于循环体、循环条件判断逻辑和自增操作表达式。同样,可以这样在for/in(以及for each,参照11.4.i)循环中使用let:
o = { 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,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
let语句中的变量初始化表达式并不是这个语句块的一部分,并且是在作用域外部被解析的,理解这一点至关重要 。
let关键字的最后一种用法是let语句写法的一个变体,其中有一个括号括起来的变量列表和初始化,紧跟着是一个表达式而不是语句块。我们把这种写法叫做let表达式,上面的代码可以写成这样:
let x=1, y=2; console.log(let (x=x+1,y=x+2) x+y); // 输出 5
3.解构赋值
Spidermonkey系javascript1.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
由于解构代码方式学习过去生疏难懂,此处略去内容,在javascript核心语法完成后我将把本书的扫描PDF文档分享给大家参考 。
4.迭代
mozilla的javascript扩展引入了一些新的迭代机制,包括for each循环和python风格的迭代器(iterator)和生成器(generator)。本文小节中将逐步介绍。
i.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);
当使用数组时,for/each循环遍历的元素(而不是索引),它通常按照数值顺序枚举它们,但实际上这并不是标准化或必须的:
注意,for/each循环并不仅仅针对数组本身的元素作遍历,它也会遍历数组中所有可枚举属性,包括继承来的可枚举的方法。因此,通常并不推荐for/each循环和数组一起使用 。在ECMAScript5 之前的 javascript 版本中是可以这样用的,因为自定义属性和方法不可能设置为可枚举的。
a = ['one', 'two', 'three']; for (let p in a) console.log(p); //=>0,1,2 数组索引: for each(let v in a) console.log(v); //=>'one', 'two', 'three' 数组元素
ii.迭代器
迭代器是一个对象,这个对象允许对它的值的集合作遍历,并保持任何必要的状态以便能够跟踪到当前遍历的“位置”。
迭代器必须包含next()方法,每一次对next()调用都返回集合中的下一个值。比如下面的counter()函数返回了一个迭代器,这个迭代器每次调用next()都会返回连续递增的整数。需要注意的是,这个函数利用闭包的特性实现了计数器状态的保存:
function counter(start) { let nextValue = Math.round(start); // 表示迭代器状态的一个私有成员 return { next: function() { return nextValue++; } }; // 返回迭代器对象 } let serialNumberGenerator = counter(1000); let sn1 = serialNumberGenerator.next(); // 1000 let sn2 = 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; } }
由于扩展迭代代码方式学习生疏难懂,此处略去内容,在javascript核心语法完成后我将把本书的扫描PDF文档分享给大家参考。
(本文未完,欢迎大家关注上一章: 第九章:Javascript类和模块 )
下面将更新:
5.迭代
6.函数简写
7.多catch从句
8.e4x:ECMAScript for XML