io.js入门(二)—— 所支持的ES6(上)
io.js的官网上有专门介绍其所支持的ES6特性的页面(点我查看),上面介绍到,相比nodeJS,io.js已从根本上支持了新版V8引擎上所支持的ES6特性,无需再添加任何运行时标志(如 --harmony )。
有的朋友可能对Node不熟悉,不太知道harmony标志的情况,这里简单介绍下。
在NodeJS中,如果所要执行的脚本(假设为app.js)是基于ES6来编写的,那么得在命令的脚本路径前加上 “--harmony” 运行时标志,也就是这样执行脚本:
node --harmony app.js
“--harmony” 前缀表示让Node支持除了 “typeof” 外的所有标准ES6特性,除此之外还有 “--harmony_typeof”(开启typeof支持)之类的前缀,具体可以参详stackOverflow上的一个问答。
在io.js中,所有的ES6特性被划分为三大类—— 已标准化的(completed)特性、已确定将标准化的(staged)特性、仍处草案待定状态(in progress)的特性。
针对这三大类,io.js做了分别的处理,而不像Node那样都得加harmony标签:
⑴ 已标准化的ES6特性,如我们上文提到的,直接用指令执行即可,无需再加任何运行时标志;
⑵ 已确定将标准化的ES6特性,执行时需要加上运行时标志 “ --es_staging ” ,当然你也可以使用它的同义词 “ --harmony ” ;
⑶ 在草案上但仍未确定将标准化的ES6特性,执行时需要加上它们自己对应的harmony标志,比如你在脚本中使用了 arrow_functions 特性,那么需要加上标志 “--harmony_arrow_functions”。
io.js建议不要使用 ⑵ 和 ⑶ 的不稳定的特性。
下面将较详细地来介绍io.js原生支持的标准ES6特性。
标准ES6特性
该系列特性可直接使用,无需添加运行时标志,但小部分特性要求只能在严格模式("use strict";)下使用。
1. 块级作用域/Block scoping(需要在严格模式下使用)
○ let
○ const
2. 集合/Collections
○ Map
○ WeakMap
○ Set
○ WeakSet
3. Generators
4. 二进制和八进制语法/Binary and Octal literals
5. Promises
6. 新的String方法/New String methods
7. 符号/Symbols
8. 字符串模板/Template strings
let (只能在严格模式下使用)
类似于var,声明一个变量,但只在声明处所处的块级作用域内有效:
"use strict"; //使用严格模式 { var a=1; let b=2; console.log(a); console.log(b); } console.log(a); console.log(b); //undefined
上述代码执行如下:
使用 let 可以用于解决变量提升问题:
"use strict"; //使用严格模式 for (var i = 0, a=[]; i < 10; i++) { var c = i; a[i] = function () { console.log(c); }; } a[0](); // 9 for (var i = 0, b=[]; i < 10; i++) { let c = i; //使用let解决变量提升问题 b[i] = function () { console.log(c); }; } b[0](); // 0
const (只能在严格模式下使用)
类似于var,声明一个“常量” —— 初始赋值后将无法修改其值的变量。
也类似于let,只能在其所声明的块级作用域内来访问到:
"use strict"; //使用严格模式 { const i = 1; } console.log(typeof i); //undefined { const a = 123; a = 567; //报错,a是常量不能修改 }
块级作用域中的函数 (只能在严格模式下使用)
严格模式下,函数本身的作用域,在其所处的块级作用域内,可以以此解决函数声明提升问题:
"use strict"; //使用严格模式 function f() { console.log('outside!'); } (function () { if(false) { // 重复声明一次函数f function f() { console.log('inside!'); } } f(); //严格模式(ES6)下输出outside;非严格模式(ES5)输出inside }());
Map
新的js集合类型,类似与对象,属于键值对的集合,但其“键”可以为任何类型,而不仅仅局限于字符串:
var map = new Map(), //新建一个map对象 o = {"a": 1, "b": 2}, s = "sth"; //map.set(key,val) 表示为该map对象添加一个新的键值对 map.set(o,"ooo"); map.set("a",123); map.set(null,"it`s null"); map.set(s,o); //map.delete(key) 表示删除该map对象的某个键值对 map.delete(s); //map.has(key) 表示检查该map对象是否存在某键名,返回对应的boolean值 console.log(map.has(o)); //true console.log(map.has(s)); //false //map.has(key) 表示获取该map对象中某键名所对应的值 console.log(map.get(o)); //"ooo" console.log(map.get("a")); //123 console.log(map.get(null)); //"it`s null" //map.clear() 表示清空map对象中的全部键值对 map.clear(); console.log(map.get(o)); //undefined
Map支持链式写法,故上方的某段代码我们可以这么写:
map.set(o,"ooo") .set("a",123) .set(null,"it`s null") .set(s,o) .delete(s);
我们可以用 map.size 属性(而不是length)获取Map中键值对的个数:
var map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'] ]); console.log(map.size); //3
Map提供了三种遍历器:
map.keys() //返回键名的遍历器
map.values() //返回键值的遍历器
map.entries() //返回所有成员的遍历器
我们可以使用 for...of... 方法来遍历map的遍历器:
"use strict"; let map = new Map([ ['F', 'no'], ['T', 'yes'] ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者 let arr = [[],[]]; for (arr of map.entries()) { console.log(arr[0], arr[1]); } // 等同于使用map.entries() for (arr of map) { console.log(arr[0], arr[1]); }
其中map的entries方法等同为Map结构的默认遍历器接口(Symbol.iterator):
console.log(map[Symbol.iterator] === map.entries); // true
另外,我们也可以使用 forEach 方法来遍历Map对象:
var map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'] ]); map.forEach(function(value, key){ console.log("Key: %s, Value: %s", key, value); }); // Key: 1, Value: one // Key: 2, Value: two // Key: 3, Value: three
WeakMap
WeakMap结构与Map结构基本类似,区别是它只接受对象作为键名(null除外),不接受基础类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。
另外,WeakMap只有 get()、set()、has()、delete() 这么四个方法,而没有 clear() 、size属性和遍历器。
WeakMap示例如下:
var wm = new WeakMap(), o = {}; // o = document.getElementById("idname"); wm.set(o,"DOM"); console.log(wm.get(o)); //"DOM"
WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
Set
类似于数组,但是成员的值都是唯一的,没有重复的值。另外Set也支持链式写法:
var items = new Set([1,2,null,3,5,5,5,5]); //重复的值将无效处理 console.log(items.size); //5 //set.add(value) 表示添加一个set成员 items.add(2).add("aaa"); //“2”已存在,将无效处理 console.log(items.size); //6 //set.delete(value) 表示删除一个set成员 items.delete(2); //set.has(value) 表示检查是否存在某成员,返回相应boolean值 console.log(items.has(2)); //false items.clear(); //表示删除全部成员 console.log(items.size); //0
跟Map一样,Set也有keys()、values()、entries() 三种遍历器,但遍历到的key名等同与value值。
不过Set的默认遍历器接口是 values() :
console.log(Set[Symbol.iterator] === Set.values); //ture
同样的,我们也可以通过 for...of... 和 forEach 来遍历Map:
var items = new Set([1,2,null,3,5,5,5,5]); var arr = [[],[]]; for (arr of items.entries()) { console.log(arr[0], arr[1]); } items.forEach(function(value, key){ console.log("Key: %s, Value: %s", key, value); });
Set没有 .get(val) 方法,也不像数组那样可以直接用索引下标取值,常规只是把Set作为数据库索引使用(如为Redis提供索引和查询)。
WeakSet
了解了Map和WeakMap的关系之后,相信你也很容易理解WeakSet跟Set的关系。WeakSet类同于Set,但其成员只能是对象,而且WeakSet无法遍历也没有size属性。
相比Set,WeakSet能使用的方法只有 add(val) 、delete(val)、has(val) :
var ws = new WeakSet(), obj = {}, foo = {}; ws.add(foo); ws.add(obj); ws.delete(obj); console.log(ws.has(foo)); // true console.log(ws.has(obj)); // false
同WeakMap一样,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,对象删除时,WeakSet对应引用该对象的成员也跟着删除。
Generators
Generator在ES6中是一个备受瞩目的一个函数类型,它通过 function * funName 的形式来声明一个Generator函数,并以 yield 语句来生成函数内部的遍历器成员。
调用Generator函数时,函数并不执行,而是返回一个遍历器(可以理解成暂停执行)。以后,每次调用这个遍历器的next方法,就从函数体的头部或者上一次停下来的地方开始执行(可以理解成恢复执行),直到遇到下一个yield语句为止:
function * gFun() { yield "第一个遍历器成员,第一次调用next()会停在这里"; //生成遍历器成员 var i = 0; while (true){ console.log("第"+i+"次循环"); yield i++; //生成遍历器成员 console.log("继续第"+i+"次循环"); } } var g = gFun(); //没有.next()语句的调用,Generator函数不会执行,故不会造成while无限循环 console.log(g.next().value); //"第一个遍历器成员,第一次调用next()会停在这里" console.log(g.next().value); //第0次循环 //0 console.log(g.next().value); //继续第1次循环 //第1次循环 //1
.next().value返回了遍历器成员的值,常规为 “yield” 或 "return" 语句所生成/返回的值。
.next() 方法除了 value 属性外,还有判断遍历是否结束的 done 属性,它返回一个表示当前遍历是否结束的boolean值:
function * gFun() { yield "第一个遍历器成员"; var i = 0; yield ++i; } var g = gFun(); //没有.next()语句的调用,Generator函数不会执行,故不会造成while无限循环 console.log(g.next()); //{ value: '第一个遍历器成员', done: false } console.log(g.next()); //{ value: 1, done: false } console.log(g.next()); //{ value: undefined, done: true }
Generator函数里比较有意思的地方是,可以给 .next() 加一个参数,该参数数值就会覆盖掉上一个yield语句的返回值,我们可以利用该特性来进行一些有趣的运算:
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var g = foo(5); console.log(g.next()); //{ value:6, done:false } console.log(g.next(12)); // (y /3)=(2*12 /3)=8 //{ value:8, done:false } console.log(g.next(13)); // (x + y + z)=(5 + 2*12 + 13)=42 //{ value:42, done:true }
二进制和八进制语法
原先的JS对二进制/八进制的转换处理不太友好,我们得动用parseInt()或toString()方法,并使用对应的进制位参数:
var num = 11; console.log(parseInt(num,2)); //把num作为二进制转为十进制 //3 console.log(parseInt(num,8)); //把num作为八进制转为十进制 //9 console.log(num.toString(2)); //把num作为十进制转为二进制 //1011 console.log(num.toString(8)); //把num作为十进制转为八进制 //13
在ES6中有些许改善,比如以“0b”开头表示二进制,以“0”开头表示八进制,以“0x”开头表示十六进制:
console.log(0b1101); //13 console.log(0B11); //3 console.log(071); //57 console.log(081); //81 (因为“8”超过了八进制可用数值,这里被当作十进制来转换) console.log(0x11); //17 console.log(0X2A); //42
Promise
如果你熟悉jQuery的deferred对象,那么你会很轻松地理解ES6的Promise特性 —— 用于延迟、异步状态处理。
一般Promise会有三种状态:
待定(pending):初始状态,执行、等待中,没有被履行或拒绝。
完成(fulfilled/resolved):操作成功
拒绝(rejected):操作失败。
Promise是一个构造函数,用来生成Promise实例。它接受一个函数作为参数,该函数又有两个参数——resolve方法和reject方法。如果异步操作成功,则用resolve方法将Promise实例的状态变为“成功”;如果异步操作失败,则用reject方法将状态变为“失败”。
promise实例生成以后,可以用then方法分别指定resolve方法和reject方法的回调函数:
var promise = new Promise(function (resolve, reject) { if (/* 异步操作成功 */) { resolve(value); //返回resolved状态并触发resolve回调 } else { reject(error); //返回rejected状态并触发reject回调 } }); promise.then(function (value) { // resolve,即成功的回调 }, function (error) { // reject,即失败的回调 });
这种机制对Node/io.js来说是非常有用的,因为我们知道,Node/io.js走的无阻塞异步I/O,js部分在V8执行,I/O部分在线程池做异步处理,如果我们希望在I/O操作成功或失败后执行相应的回调函数,以常规的做法不得不在事件的回调中继续嵌套I/O状态的回调。但Promise的出现,很好地解决了该问题。
拿Ajax举个例子:
var getJSON = function(url) { var promise = new Promise(function(resolve, reject){ var client = new XMLHttpRequest(); client.open("GET", url); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader("Accept", "application/json"); client.send(); function handler() { if (this.status === 200) { resolve(this.response); //成功则触发resolve回调 } else { reject(new Error(this.statusText)); //失败则触发reject回调 } } }); return promise; }; getJSON("/posts.json").then(function(json) { //resolve的回调 console.log('Contents: ' + json); }, function(error) { //reject的回调 console.error('出错了', error); });
.then() 方法返回的是一个新的Promise对象,它支持链式写法,可以让代码逻辑更清晰。如上述 getJSON() 执行的代码段可写为:
getJSON("/posts.json").then(function (json) { console.log('Contents: ' + json); }, function (error) { console.error('出错了', error); }).then(function () { //在上个then执行完之后才会执行 console.log("已经执行完并输出信息了") });
Promise对象还有 .all() 和 .trace() 方法,Promise.all 方法接受一个数组作为参数,数组对象为不同的Promise对象:
//接之前的代码段 var promises = [getJSON("/post/a.json"), getJSON("/post/b.json"),getJSON("/post/c.json")]; Promise.all(promises).then(function (json) { console.log('Contents: ' + json); }, function (error) { console.error('出错了', error); });
当数组对象的状态都变为resolved时,Promise.all(promises)的状态也跟着变为resolved。如果其中一个数组对象的状态为rejected,那Promise.all(promises)则变为rejected状态。
Promise.trace 方法跟 Promise.all 方法类似,不过只要Promise.trace的参数中的某一个对象率先改变了状态,那么 Promise.trace(promises) 的状态也会变为该状态。
如上文提到的,作为trace 方法跟 all 方法的参数,要求其必须为Promise实例组成的数组,如果有非Promise实例的对象想加入到数组参数中,我们可以先通过 Promise.resolve 方法将其转为Promise实例:
var ES6Promise = Promise.resolve($.ajax('/whatever.json')); //将一个jQuery deferred对象转化为ES6的Promise实例
另外,我们可以使用 .catch() 来捕捉Promise对象的错误信息,要知道的是,Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获:
getJSON("/post/1.json").then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { // some code }).catch(function(error) { // 处理前两个回调函数的错误 });
限于篇幅,ES6特性将分为两篇文章来介绍,不便之处请谅解。
另本文大部分内容参考自阮一峰老师的ES6入门,但本文实例已事先对所有代码进行了校正(包括在io.js上的兼容性、代码错误的堪正等)。
共勉~