JavaScript子集
大多数语言都会定义它们的子集,用以更安全地执行不信任的第三方代码。
精华(The Good Parts)
Douglas Crockford曾写过一本很簿的书《JavaScript: The Good Parts》,专门介绍JavaScript中值得发扬光大的精华部分。这个语言子集的目标是规避语言中的怪癖、缺陷部分,最终编程更轻松、程序更健壮。
- 使用函数定义表达式而不是函数定义语句来定义函数。
- 循环体和条件分支都使用花括号括起来。
- 任何语句只要不是以花括号结束都应该使用分号做结尾。
- 推荐使用”===”和”!==”,不推荐使用”==”和”!=”(因为比较时会涉及到类型转换)。
- 由于JavaScript并不包含块级作用域,var语句最好出现在函数体的顶部。
- 禁止使用全局变量。
Crockford写过一个在线代码质量检测工具JSLint,可通过这个工具对代码进行检查。
子集的安全性
子集的设计目的是能在一个容器或”沙箱”中更安全地运行不可信的第三方JavaScript代码。所有能破坏这个沙箱并影响全局执行环境的语言特性和API在这个安全子集中都是禁止的。
为了让JavaScript代码静态地通过安全检查,必须移除一些JavaScript特性:
- 禁止使用this关键字,因为函数(在非严格模式中)可能通过this访问全局对象。
- 禁止使用with语句,因为with语句增加了静态代码检查的难度。
- 静态分析可以有效地防止带有点(.)运算符的属性存取表达式去读写特殊属性。但我们无法对方括号([])内的字符串表达式做静态分析。基于这个原因,安全子集禁止使用方括号,除非括号内是一个数字或字符串直接量。
- eval()和Function()构造函数在任何子集里都是禁止使用的,因为它们可以执行任意代码,而且JavaScript无法对这些代码做静态分析。
- 禁止使用全局变量,因此代码中不能有对Window对象的引用和对Document的引用。
- 禁止使用某些属性和方法,以免在水箱中的代码拥有过多的权限。如:arguments对象的两个属性caller和callee、函数的call()和apply()方法、以及constructor和prototype两个属性。
JavaScript扩展
常量和局部变量
在JavaScript1.5及后续版本中可以使用const关键字来定义常量,常量是不可重复赋值的变量。
const pi = 3.14;
pi = 4; // 对常量赋值会报"TypeError"
在JavaScript1.7中,添加了关键字let,可以定义带有作用域的变量:
下面列举各种场景使用let的作用域:
- 全局作用域(定义在函数之外)
let me = ‘go‘; // 全局作用域
var i = ‘able‘; // 全局作用域
- 函数作用域
function ingWithinEstablishedParameters() {
let terOfRecommendation = ‘awesome worker!‘; // 函数作用域
var sityCheerleading = ‘go!‘; // 函数作用域
};
- 块作用域
function allyIlliterate() {
// tuce在这里不可见
for( let tuce = 0; tuce < 5; tuce++ ) {
// tuce在这里可见
};
// tuce在这里不可见
};
function byE40() {
// nish在这里可见
for( var nish = 0; nish < 5; nish++ ) {
// nish在这里可见
};
// nish在这里可见
};
更多可参考:“let” keyword vs “var” keyword
例子如下:
o = {x:1, y:2};
for each(let v in o)
console.log(v); // 输出1和2
console.log(v); // 错误:v is not defined
在for循环中使用let关键字:
// 输出 5, 5, 5, 5, 5
for (var i = 0; i < 5; ++i) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 输出 1, 2, 3, 4, 5
for (let i = 0; i < 5; ++i) {
setTimeout(function () {
console.log(i);
}, 1000);
}
更多可参考:Explanation of let
and block scoping with for loops
注:var声明的变量在它们所声明的函数内始终是存在的,但直到代码执行行到var语句时才初始化变量,在var语句执行之前它的值是undefined。通过let声明的变量也与之类似。
解构赋值(destructuring assignment)
在解构赋值中,等号右侧是一个数组或对象(一个结构化的值),指定左侧一个或多个变量的语法和右侧的数组和对象直接量的语法保持格式一致。
数组解构赋值
- 简单的解构赋值
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]; // 交换2个变量的值
console.log([x, y]); // => [3, 2]
- 解构赋值左右的变量不一定要一一对应,左侧多余的变量赋值为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 = 3
注:JavaScript并未提供将右侧多余的值以数组的形式赋值给左侧变量的语法。比如,上面代码的第2行,并不能将[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
对象解构赋值
- 在这种情况下,解构赋值的左侧看起来是一个对象直接量,对象中是一个名值对的列表,冒号右侧的值是变量名。
let transparent = {r:0.0, g:0.0, b:0.0, a:1.0};
let {r:red, g:green, b:blue} = transparent; // red=0.0, green=0.0, blue=0.0
- 解构赋值可使用对象嵌套的语法。
let data = {
name: "destructuring assignment",
type: "extension",
impl: [{engine: "spidermonkey", version: 1.7},
{engine: "rhino", version: 1.7}]
};
let ({name: feature, impl: [{engine:impl1, version: v1}, {engine:impl2}]} = data) {
console.log(feature); // => "destructuring assignment"
console.log(impl1); // => "spidermonkey"
console.log(v1); // => 1.7
console.log(impl2); // => "rhino"
}
迭代
for VS for/each
for循环是遍历对象的属性,而for/each遍历对象属性的值。
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"
迭代器
迭代器是一个对象,这个对象允许对它的值集合进行遍历,并保持任何必要的状态以便能够跟踪到当前遍历的”位置”。迭代器必须包含next()方法,每一次调用next()都返回集合中的下一个值。
下面的counter()返回一个迭代器对象:
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()会抛出StopIterator,StopIterator是全局对象的属性,一个普通的对象(它自身没有属性),只是为了终结迭代而保留的一个对象。
可迭代对象
- 可迭代对象表示一组可迭代处理的值,可迭代对象必须包含一个
_iterator_()
的方法,用以返回这个集合的迭代器对象。 - for/in循环会自动调用它的
_iterator_()
方法来获得一个迭代器对象(类型自动转换),然后调用迭代器的next()方法。for/in循环会自动处理StopIteration异常,而且处理过程对开发者是不可见的。
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++;
}
};
}
};
}
for(let i in range(1, 10)) console.log(i); // => 输出1~10之间的数字
Iterator()函数
- 如果这个函数的参数是一个可迭代对象,那么它将返回这个对象的
_iterator_()
方法的调用结果,返回一个迭代器对象。 - 如果传入的对象或者数组没有定义
_iterator_()
方法,它会返回这个对象的一个可迭代的自定义迭代器。每次调用这个迭代器的next()方法都会返回包含2个值的数组,第1个数组元素是属性名,第2个数组元素是属性的值。
for(let [k, v] in Iterator({a:1, b:2}))
console.log(k + "=" + v); // => "a=1", "b=2"
- Iterator()函数返回的迭代器还有2个重要的特性。第一,它只对自有属性进行遍历而忽略继承的属性。第二,如果给Iterator()传入第2个参数true,返回的迭代器只对属性名进行遍历,而忽略属性值。
o = {x:1, y:2};
Object.prototype.z = 3;
for(p in o) console.log(p); // => "x", "y", "z"
for(p in Iterator(o)) console.log(p); // => ["x", 1], ["y", 2],注:不会遍历继承属性
for(p in Iterator(o, true)) console.log(p); // => "x", "y", 注:只遍历属性名
生成器函数 Generator function
任何使用关键字yield的函数都称为”生成器函数”。
- yield和return的区别在于,使用yield的函数可保持函数内部状态的值。
- 生成器函数通过yield返回值,不建议使用return返回值。
- 和普通函数一样,生成器函数也通过关键字function声明,typeof运算符返回”function”,并从Function.prototype继承属性和方法。
- 对生成器函数的调用不会执行函数体,而是返回一个生成器对象。
- 如果不再使用生成器对象,可通过close()方法来释放它。调用close()相当于在函数运行挂起的位置执行了一条return语句,如果如果当前挂起的位置在try语句块中,那么将首先执行finally语句,再执行close()返回,如果finally语句块产生了异常,这个异常会传播给close()。
下面是一个普通的生成器函数:
function range(min, max) {
for(let i=Math.ceil(min); i <= max; i++) {
yield i;
}
}
var f = range(3, 8); // 返回一个生成器对象
console.log(f.next()); // 3,返回单独值
console.log(f.next()); // 4,返回单独值
如果在生成器函数声明时加个”*”,则yield会返回对象值:
// function后添加"*"
function *range(min, max) {
for(let i=Math.ceil(min); i <= max; i++) {
yield i;
}
}
var f = range(3, 8); // 返回一个生成器对象
console.log(f.next()); // { value=3, done=false, z=3},返回对象
console.log(f.next()); // { value=4, done=false, z=3},返回对象
yield不仅可以返回值,还可以用于接收值,可通过next()或send()传递yield的接收值:
function *test() {
var i = yield 2;
yield i;
};
var f = test();
console.log(f.next()); // { value=2, done=false, z=3}, 第1次调用时,没有执行到yield语句,不需要接收值
console.log(f.next(5)); // { value=5, done=false, z=3}, yield接收变量5,并赋值给i
console.log(f.next(6)); // { done=true, z=3, value=undefined},最后一个值为undefined
更多可参考:
数组推导 array comprehension
数组推导是一种利用另外一个数组或可迭代对象来初始化数组元素的技术。
数组推导的语法如下:
[expression for ( variable in object ) if ( condition )]
定义一个数组:
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);
}
数组推导表达式特点:
- 在变量之前没有关键字var和let,其实这里使用了隐式的let。
- if语句是可选的,如果省略的话,相当于给数组推导补充一条if(true)从句。
生成器表达式 generator expression
将数组推导中的方括号替换成圆括号,它就成了一个生成器表达式。比如:
let h = (f(x) for(x in g));
这段代码与下面的代码等价:
function map(i, g) {
for(let x in g) yield f(x);
}
函数简写
如果函数只计算一个表达式并返回它的值,关键字return和花括号都可以省略。
比如,对数组按降序排列:
data.sort(function(a,b) b-a); // 在"b-a"的前面省略了"{}"和return
多catch从句
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从句没有一个是true,那么程序会向上抛出这个未捕获的异常。
E4X: ECMAScript for XML
E4X为处理XML文档定义了一系列强大的特性。XML对象和原始的JavaScript对象不同,对它们进行typeof运算的结果是”xml”。XML对象和DOM对象没有任何关系。
XML对象创建
- 直接量创建
// 创建一个XML对象
var pt = <periodictable>
<element id="1"><name>Hydrogen</name></element>
<element id="2"><name>Helium</name></element>
</periodictable>;
// 添加一个新元素
pt.element += <element id="3"><name>Lithium</name></element>;
- 构造函数构造
// 创建单个节点 XML
pt.element += new XML(‘<element id="4"><name>Boron</name></element>‘);
// 创建多个节点 XMLList
pt.element += new XMLList(‘<element id="4"><name>Boron</name></element>‘ +
‘<element id="5"><name>Lithium</name></element>‘);
XML对象访问
- 普通访问
// 得到所有<element>标签的 列表
var elements = pt.element;
// 得到所有的<name>标签的 列表
var names = pt.element.name;
// "Hydrogen",name的第0个标签内容
var n = names[0];
- 通配符(*)访问
// 得到所有<element>标签的所有子节点(即<name>标签列表)
var names = pt.element.*;
- 使用字符@区分属性和标签名
var pt = new XML(‘<periodictable><element id="1"><name>Hydrogen</name></element><element id="2"><name>Helium</name></element></periodictable>‘);
// 访问id属性,输出"2"
var a = pt.element[1][email protected];
// 获取所有的<element>标签的所有属性
var b = [email protected]*;
- 过滤列表
// 对所有的<element>元素组成的列表进行过滤
// 过滤出那些id属性小于3的元素
var c = pt.element.(@id < 3);
- 删除标签和属性
delete pt.element; // 删除所有的<element>标签
delete pt.element[0][email protected]; // 删除一个属性
命名空间
E4X是支持命名空间的,它为使用XML命名空间提供了语法支持和API支持。
// 声明默认的命名空间
default xml namespace = "http://www.w3.org/1999/xhtml";