coolriver

2年前
  • 5513

    浏览
  • 3

    评论
  • 1

    收藏

ES6学习笔记

本文作者:imweb coolriver 原文出处:imweb社区 未经同意,禁止转载

本文为初步阅读ECMAScript6入门后的一些记录与感想。

简介

ES6的设计目标,是使得JavaScript语言可以用来编写大型复杂的应用程序,成为企业级开发语言。ES6将于2015年6月正式发布。

现在最新版本的Nodejs上可以通过`--harmony`参数开启ES6的部分支持。如果旧的Nodejs版本不支持或者想体验更多的ES6 特性
可以 使用Google的[traceur](https://github.com/google/traceur-compiler)工具。traceur可以在前端编译ES6代码,也可以
在node上 编译或执行ES6代码。
  • traceur在node上安装: npm install -g traceur
  • 直接执行ES6文件: traceur test.es6.js
  • 编译ES6文件到ES5文件:traceur --script test.es6.js --out test.es5.js ES6在很方面都对ES5有增强,下面将从不同方面对ES6在ES5上的加强进行介绍。

新增块级作用域(let与const关键字)

let声明变量

  在ES5中,除了全局作用域外,限定所声明的变量的作用域是函数作用域。这使得ES5在很多情况下为了模拟块级作用域(避免变量名污染)需要使用立即执行的匿名函数。在ES6中新增了声明块级使用域变量的关键字let。与var相比,使用let声明的变量有如下特点:

  1. let声明的变量所在的作用域为块级。
  2. let声明的变量不存在变量提升。
  3. let声明的变量不允许重复声明,否则会报错。 使用let可以替代ES5中为了模拟块级作用域而使用的立即执行的匿名函数:
//ES5实现方法:
(function(){
    for (var i = 0;i < 10;i++){
        //doSomething
    }
})();

//ES6实现方法:
for (let i = 0;i < 10;i++){
    //doSomething
}
  • 需要注意的是,ES6中函数本身的作用域在其块级作用域之类(相当于使用let声明了),这样,在if条件内声明的函数就不会像ES5因函数提升而总会被声明。

const声明常量

  ES6中可以使用const关键字来声明常量,被声明的常量不能被修改。与使用let声明的变量类似,const声明的常量为块级作用域,不存在变量提升,且不可重复声明。
  const只限定就是所以的地址不能改变,意味着如果声明的目标为对象,那么对象的属性是可以修改的。书中建议如果要使对象为常量的话可以配合Object.freeze()函数来实现:

const foo = Object.freeze({a:1});

//foo = {b:2} 无法修改foo
//foo.a = 2   无法修改foo.a

  以上方法中的Object.freeze()函数本身有局限性,它只能冻结对象的属性不被修改,并不能冻结它的属性的属性被修改。如果要实现将对象内部所有属性冻结,需要使用自定义的强化的冻结函数。关于深度冻结对象的方法在codewars上的一个题目有描述,具体方案如下:


Object.deepFreeze = function (object) {
    Object.freeze(object);
    Object.keys(object).forEach(function(key) { 
        if(typeof(object[key]) == 'object') 
        return Object.deepFreeze(object[key]); 
    });
}

   通过以上deepFreeze即可实现完全将对象常量化。效果如下:


const foo = Object.freeze({a:[1]}); //使用原始的冻结函数
foo.a.push(2); //本操作可以使foo.a变为[1,2]

const foo2 = Object.deepFreeze({a:[1]}); //使用深度冻结函数
foo2.a.push(2); //本操作无法改变foo2.a

全局对象属性

  全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。ES5规定,所有全局变量都是全局对象的属性。
  ES6规定,var命令和function命令声明的全局变量,属于全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。

变量的解构赋值

  ES6中新增了一种赋值方法,可以批量地从对象(或数组)中提取出相应的值赋值到一组对应的变量上。下面为数组形式的解构赋值:

//数组的解构赋值
var [a,b,c] = [1,2,3];

//相当于
var a = 1,b = 2,c = 3;

  下面为对象形式的解构赋值:

//对象的解构赋值方式一
var {bar, foo} = {foo: "aaa", bar: "bbb"};
foo //"aaa"
bar //"bbb"

//对象的解构赋值方式二
var {first: f, last: l} = {first: "hello", last: "world"};
f //"hello"
l //"world"

解构赋值可以带来很多便利:

  • 变量交换 :[x, y] = [y, x]
  • 函数返回多个值赋值给多个变量
  • 可以方便地将一组参数与变量名对应起来。
  • 方便提取JSON数据
  • 方便设置函数参数默认值。(解构赋值可以指定默认值)
  • 遍历Map:for (let [key, value] of map){}
  • 加载模块时指定需要加载的方法:const {SourceMapConstumer, SourceNode} = require("source-map")

字符串扩展

  • ES6中提供了一级新的方法来解决ES5里某些中文字符(UTF-16编码大于0XFFFF)编码的问题。另外ES5中使用码点"\uXXXX"来表示字符的方法不能表示大于0XFFFF的字符,在ES6中可以使用大括号将码点括起来就可以表示大于0XFFFF的字符:"\u{XXXX}"
  • ES6对正则表达式添加了u修饰符,用来正确处理大于\uFFFF的Unicode字符。点(.)字符在正则表达式中,解释为除了换行以外的任意单个字符。对于码点大于0xFFFF的Unicode字符,点字符不能识别,必须加上u修饰符:
var s = "𠮷";

/^.$/.test(s) // false
/^.$/u.test(s) // true

*传统上,JavaScript只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。

includes():返回布尔值,表示是否找到了参数字符串.
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部.
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部.

  • repeat()返回一个新字符串,表示将原字符串重复n次。可以方便地构建由重复字符或字符串构成的字符串:
"x".repeat(3) // "xxx"
"hello".repeat(2) // "hellohello"
  • 字符串模板。模板字符串用``进行定义,中间使用到的变量值使用${}进行嵌入。使用字符串模板可以很方便地将变量与字符串结合,而不用使用大量的加号进行拼接:
$("#result").append(`
  There are ${basket.count} items
   in your basket,${basket.onSale}
  are on sale!
`);

数值扩展

  • ES6提供了新的表示二进制和八进制数据的写法,二进制用前缀0b表示,八进制用前缀0o表示。新的八进制表示方法弥补了ES5中前缀0写法存在的问题。
0b111110111 === 503 // true
0o767 === 503 // true
  • ES6将ES5中的全局方法:isFinite(),isNaN(),parseInt(), parseFloat()移至Number上,分别变为Number.isFinite(),Number.isNaN(),Number.parseInt(), Number.parseFloat()。这样做是为了逐步减少全局方法,使语言逐步模块化。
  • Math对象新增的方法:

Math.trunc(n) 去除一个数的小数部分
Math.sign(b) 判断一个数是正数、负数还是0.
Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
Math.cbrt(x) 返回x的立方根
Math.clz32(x) 返回x的32位二进制整数表示形式的前导0的个数
Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
Math.expm1(x) 返回eˆx - 1
Math.fround(x) 返回x的单精度浮点数形式
Math.hypot(...values) 返回所有参数的平方和的平方根
Math.imul(x, y) 返回两个参数以32位整数形式相乘的结果
Math.log1p(x) 返回1 + x的自然对数
Math.log10(x) 返回以10为底的x的对数
Math.log2(x) 返回以2为底的x的对数
Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)

数组的扩展

  • 新增方法Array.from(),可以将类数组对象(例如函数中的arguments)和遍历对象(如ES6中的Map和Set对象)。该函数可以方便地替代ES5中使用Array.prototype.slice来进行数组转换。
  • 新增方法Array.of(),用来将一组值转换为数组。该函数主要用于解决ES5中使用Array()构造数组的不足(因为参数个数的不同导致构造行为的不同):
//ES5:
Array() // []
Array(3) // [undefined, undefined, undefined]
Array(3,11,8) // [3, 11, 8]

//ES6:
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
  • 新增数组实例方法:find()findIndex()。两者的参数都是一个回调函数,返回第一个回调函数返回值为true的元素的值(或下标)。这两个函数解决了ES5中indexOf()函数不能找到NaN元素的问题。
  • 新增数组实例方法:fill(),使用指定值对数组进行填充。参数为一个时将数组所有元素替换为参数的值,参数为三个时,将指定起始位置(第二个参数)和终止位置(第三个参数)替换为目标值(第一个参数)
  • 新增数组实例方法:ectries(),keys(),values(),三个都返回遍历器:
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
  • 新增数组推导的写法,用于快捷地从当前数组生成指定数组(ES5中可以使用map()filter()实现):
//ES5:
var a1 = [1, 2, 3, 4];
var a2 = a1.map(function(i){return i * 2});
var a3 = a1.filter(function(i){return i > 2});
var a4 = a1.filter(function(i){return i > 2})
           .map(function(i){return i * 2});
a2 // [2, 4, 6, 8]
a3 // [3, 4]
a4 // [6,8]

//ES6:
var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];
var a3 = [for (i of a1) if (i > 2) i];
var a4 = [for (i of a1) if (i > 2) i * 2];//同时实现ES5中的map和filter 

a2 // [2, 4, 6, 8]
a3 // [3, 4]
a4 // [6,8]
  • 新增方法Array.observe()Array.unobserve()方法用于监听(取消监听)数组的变化(包括add,update,delete,splice四种操作引起的数组变化)

对象的扩展

  • 新增简洁的对象属性写法:
function f( x, y ) {
  return { x, y, method(){return 1;}};
}

// 等同于

function f( x, y ) {
  return { x: x, y: y, method: function(){return 1}};
}
  • 允许在定义字面量对象时,使用表达式作为对象的属性名,通过把表达式放在方括号内:
let propKey = 'foo';

let obj = {
   [propKey]: true,
   ['a'+'bc']: 123,
   ['h'+'ello'](){return 'hi';} 
};
  • 新增Object.is()方法。用于比较两个值是否严格相等,相对于运算符===有两个不同:一是+0不等于-0,二是NaN等于自身。
  • 新增Object.assign()方法。将源对象(第一个参数后的所有参数)的所有可枚举属性复制到目标对象(第一个参数),后面的属性值会覆盖前面的同名属性。可用于:

为对象添加属性和方法
克隆对象(不能复制原型链上的属性,可以和Object.create()配合来弥补)
合并多个对象
为属性指定默认值

  • 通过__proto__属性直接获取和操作对象的方式,虽然不在ES6标准中,但是所有最新版本的浏览器都部署了这个属性。ES6推荐使用Object.setPrototypeOf()来设置对象的原型,使用Object.getPrototypeOf()来获取对象的原型。
  • 新增Symbol类型来解决属性名冲突的问题:
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();

s1 === s2 // false

// 有参数的情况
var s1 = Symbol("foo");
var s2 = Symbol("foo");

s1 === s2 // false
  • 新增Proxy类型,且物修改某些操作的默认行为。相当于在语言层面做出修改,属于“元编程”,即对编程语言进行编程:
var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.times // 35
proxy.name // 35
proxy.title // 35
  • 新增Object.observe()Object.unobserve()方法用于监听(和取消监听)对象(以及数组的变化)。可以方便地实现数据绑定。以上两方法不属于ES6,而属于ES7,但是在Chrome 33起就已经支持。

函数的扩展

  • 设置函数参数的默认值:
//ES5:
function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

//ES6:
function log(x, y = 'World') {
  console.log(x, y);
}

//参数默认值和解构赋值配合使用:
function foo({x, y = 5}) {
  console.log(x, y);
}
foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
  • 新增rest参数来获取函数的多余参数,这样就不需要用arguments对象。rest参数搭配一个数组就是,多余的参数会放入数组中:
function add(...values) {
   let sum = 0;
   for (var val of values) {
      sum += val;
   }
   return sum;
}

add(2, 5, 3) // 10

rest参数注意点:

rest参数后不能再有其他参数,否则会报错
函数的length属性不包括rest参数

  • 扩展运算符...,相当于rest参数的逆运算,将一个数组转换为用逗号分隔的参数序列,主要用于函数调用(ES5中需要使用apply函数来实现):
//ES:
function f(x, y, z) { }
var args = [0, 1, 2];
f.apply(null, args);

//ES6:
function f(x, y, z) { }
var args = [0, 1, 2];
f(...args);

扩展运算符可以将一个数值扩展成数组,还可以将类似数组的对象转换为真正的数组:

[...5]
// [0, 1, 2, 3, 4, 5]

[..."hello"]
// [ "h", "e", "l", "l", "o" ]

var nodeList = document.querySelectorAll('div');
var array = [...nodeList];

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);


let arr = [...map.keys()]; // [1, 2, 3]

var go = function*(){
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]
  • 使用箭头来定义函数:=>。箭头函数能够简化函数定义的写法,且能解决常规定义函数时this变量在挂靠 时的变更。
var f = v => v;
//等同于下面写法
var f = function(v) {
    return v;
};

//箭头函数支持的写法如下:
var f = () => 5;
var sum = (num1, num2) => num1 + num2;
var sum = (num1, num2) => { return num1 + num2; }

使用箭头函数时需要注意以下几点:

函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象
不可当作构造函数,也就是说不能使用new命令,否则会抛出错误
不可以使用arguments对象,因为该对象在函数体内不存在

  • ES6中对函数有尾调用优化。尾调用在函数式编程中是一个重要概念,指某个函数的最后一步是调用另一个函数:
//下面属于尾调用:
function f(x){
  return g(x);
}

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

//下面不属于尾调用:
function f(x){
  let y = g(x);
  return y;
}

function f(x){
  return g(x) + 1;
}

尾调用优化只保留内层函数的调用帧,而不需要将上层函数的调用帧存放于调用栈中。尾调用优化可以节省内存。在递归函数中,如果调用自身的函数为尾调用,那么就可以进行尾递归优化,很大地节省了递归函数执行过程中耗费的内存。如将一些递归函数改写为尾调用的模式即可极大地优化程序执行效率和耗费内存:

//原始写法:
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

//改写为尾调用(尾递归):
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

Set和Map数据结构

  • 新增Set数据结构,类似于数组,但是成员的值是唯一的,不存在重复的值。添加重复的值不会改变原结构中的内容。在Set结构中加入值时不进行类型转换,且判断新增值与原有值是否相同的比较方法类似于===运算符,唯一的例外是NaN等于自身。Set的基本使用方法如下:
var s = new Set();

[2,3,5,4,5,2,2].map(x => s.add(x))

for (i of s) {console.log(i)}
// 2 3 4 5

var items = new Set([1,2,3,4,5,5,5,5]);

items.size 
// 5

Set结构有以下属性:

Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set的成员总数。

Set结构有以下方法:

add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值
values():返回一个值的遍历器
keys():返回一个键的遍历器
entries():返回一个键值对的遍历器
forEach(fn):对每个成员执行某种操作,返回修改后的Set结构

  • 新增WeakSet结构。与Set类似,也是不重复的值的集合。与Set有两个不同点:
  1. WeakSet中的成员只能是对象而不能是其它类型的值。
  2. WeakSet中的对象都是弱引用,垃圾回收机制不考虑WeakSet对该对象的引用(因此Wea kSet中的成员是可遍历的)

WeakSet结构没有size属性,有以下三个方法:

add(value):向WeakSet实例添加一个新成员
delete(value):清除WeakSet实例的指定成员
has(value):返回一个布尔值,表示某个值是否在WeakSet实例中

WeakSet的一个用处是存储DOM节点,这样就不用担心节点从文档被移除时引起内存泄漏  
  • 新增Map数据结构。类似于对象,是一个键值对的集合,但是键的范围不像对象一样仅限于字符串,各类型的值(包括对象)都可以当作Map实例的键值。基本使用方法如下:
var m = new Map();
var o = {p: "Hello World"};

m.set(o, "content")
m.get(o) // "content"

var map = new Map([ ["name", "张三"], ["title", "Author"]]);

map.size // 2
map.has("name") // true
map.get("name") // "张三"
  如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map将其视为一个键,包括0和-0。另外,虽然NaN不严格相等于自身,但Map将其视为同一个键。  
  如果Map的键是对象(或数组),则只有两个对象的地址相同时,才将两者视为同一个键。

Map结构有如下属性和方法:

size:返回成员总数
set(key, value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则 键值会被更新,否则就新生成该键。
get(key):读取key对应的键值,如果找不到key,返回undefined。
has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
delete(key):删除某个键,返回true。如果删除失败,返回false。 clear(): 清除所有成员,没有返回值。
keys():返回键名的遍历器。
values():返回键值的遍历器。
entries():返回所有成员的遍历器。

  • 新增WeakMap结构。与Map类似,区别是它只接受对象作为键名(null除外),且键名指向的对象不讲稿垃圾回收机制。用于存储DOM元素时,可防止内存泄漏:
var wm = new WeakMap();
var element = document.querySelector(".element");

wm.set(element, "Original");
wm.get(element) // "Original"

element.removeChild(element);
element = null;
wm.get(element) // undefined

WeakMap实例没有size属性,且只有四个方法可用:get(),set(),has(),delete()

新增Iterator(遍历器)和for...of循环

  Iterator是一种接口规格,任何数据结构只要部署了这个接口就可以使用for...of进行遍历操作。Iterator为各种数据结构白日做梦了统一简便的接口,且使得数据结构中的成员能按照某种次序排列。
  Iterator提供一个指针,默认指向数据结构的起始位置,第一次调用Iterator的next方法可以将指针指向第一个成员,第二次调用就指向第二个成员,直到指向数据结构的结束位置。ES6中的Iterator接口要求在每次调用next方法时返回一个{value: v, done: bool}格式的对象,value表示当前成员的值,done表示遍历是否结束。下面的例子在现有数组上模拟了一个Iterator:

function makeIterator(array){
  var nextIndex = 0;
  return {
    next: function(){
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  }
}

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
  • Iterator接口的目的是为所有数据结构提供一种统一访问的机制,如for...of循环。当执行for...of循环时遍历某种结构时,会自动寻找Iterator接口。ES6中的默认Iterator接口部署在结构的Symbol.iterator上。有三类数据结构有原生的Iterator接口:数组,Set和Map,某些类似数组对象。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

默认的iterator接口会在以下场合中被调用:

  1. 解构赋值
  2. 扩展运算符...
  3. 其他场合

Generator函数

Generator函数的写法类似于变通函数,有两点与普通函数不同,一是function命令与函数名之间有一个星号,二是函数内使用yield定义遍历器的每个成员。执行它时会返回一个函数状态的遍历器。遍历器每次执行next方法时会遍历函数内部通过yield定义的状态:

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }
  • next方法可以带参数。yield语句本身没有返回值,当next方法带了一个参数,这个参数就会被当作上一个yield语句的返回值:
function* f() {
  for(var i=0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
  • yield *语句表示后面接的是跟遍历器:
let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

Generator的应用场景

  • 异步操作同步化表达:
function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();
  • 控制流管理:
//回调函数写法:
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

//Promise写法:
Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
    // Do something with value4
}, function (error) {
    // Handle any error from step1 through step4
})
.done();

//Generator写法:
function* longRunningTask() {
  try {    
    var value1 = yield step1();
    var value2 = yield step2(value1);
    var value3 = yield step3(value2);
    var value4 = yield step4(value3);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

scheduler(longRunningTask());

function scheduler(task) {
  setTimeout(function() {
    var taskObj = task.next(task.value);
    // 如果Generator函数未结束,就继续调用
    if (!taskObj.done) {
      task.value = taskObj.value
      scheduler(task);
    }
  }, 0);
}
  • 部属Iterator接口:
function* makeSimpleGenerator(array){
  var nextIndex = 0;

  while(nextIndex < array.length){
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(['yo', 'ya']);

gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done  // true
  • 实现状态机:
//ES5:
var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

//ES6:
var clock = function*(_) {
  while (true) {
    yield _;
    console.log('Tick!');
    yield _;
    console.log('Tock!');
  }
};

新增Promise对象

ES6提供了原生的Promise对象用来方便地表达异步操作。Promise的基本用法如下:

var promise = new Promise(function(resolve, reject) {
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(value) {
  // failure
});
  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject方法,分别用来处理异步操作成功和失败。也可以在Promise实例生成后,通过then方法指定resolvereject方法:
function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

timeout(100).then(() => {
  console.log('done');
});
  • Promise.prototype.then()返回的是一个新的Promise对象,所以可使用链式写法。如果链式写法中前一个then函数返回的是Promise对象,后一个回调函数会等待该Promise对象有运行结果才会执行:
getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // proceed
});

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // 对comments进行处理
});
  • Promise.all()Promise.race()用来将多个Promise实例包装成一个新的Promise实例。
var p = Promise.all([p1,p2,p3]);

Promise.all()的规则如下:

1. 只有p1p2p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。  
2. 只要p1p2p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。  

Promise.race()的规则如下:
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

  • Promise.resolve()用来将现有对象转换为Promise对象
  • Promise.reject(reason)返回一个新的Promise对象,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

Class和Module

ES5中没有类的概念,而是通过构造函数来进行对象的构建以及相关的面向对象操作。ES6中新增了class关键字来定义类,使ES更具面向对象语言的语法:

//ES5:
function Point(x,y){
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '('+this.x+', '+this.y+')';
}

//ES6:
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '('+this.x+', '+this.y+')';
  }

}
  • constructor方法是类的默认方法,通过new命令生成实例时会调用此方法。如果没有显示定义此方法,会默认被添加。

  • 如果生成class定义的类的实例时没有使用new命令,不会像ES5一样在全局对象上添加属性,而是会报错。

  • class定义的类中除非在constructor方法中显示定义在其本身(this对象)上,否则都是定义在原型对象上:
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '('+this.x+', '+this.y+')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
  • class声明的类不存在变量提升:
new Foo(); // ReferenceError

class Foo {}
  • 类和模块的内部默认是严格模式。

  • 类的继承使用extends关键字,相比于ES5的修改原型链和调用构造函数要清晰和方便:

class ColorPoint extends Point {

  constructor(x, y, color) {
    super(x, y); // 等同于parent.constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 等同于parent.toString()
  }

}
子类必须在constructor方法中调用super方法,否则新建实例时会报错。  
  • Class的静态方法。在方法前加上static关键字,表示该方法不会被实例继承。静态方法可以被子类继承。静态方法直接通过类来调用:
class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: undefined is not a function

class Bar extends Foo {
}

Bar.classMethod(); // 'hello'
  • ES6中的模块。在ES6之前在服务器端和浏览器端分别有对应的CommonJS和AMD规范。ES6的模块设计思想是使模块静态化,即在编译时就能确定模块的依赖关系,输入和输出变量。CommonJS和AMD都只能在运行时确定这些东西。ES6可将独立的JS文件作为模块,使用export关键字来输出变量:
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

//或者如下写法:
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

使用import关键字从其它模块加载变量:

// main.js

import {firstName, lastName, year} from './profile';

function sfirsetHeader(element) {
  element.textContent = firstName + ' ' + lastName;
}

总结

纵观本书,ES6相对于E5在多个方面进行了增强和优化。主要分为四类:

  1. 弥补ES5中存在的问题。这里说弥补,而不是修改,是因为ES6基本没有改动ES5现有的特性和方法,而是添加新的语法或方法来解决ES5中需要用额外的技巧和手段来解决的问题。例如块级作用域、例如函数在定义时和在执行时this对象的变更。
  2. 增强现有语法,增加新语法元素,使程序表达更加简洁。例如模板字串,例如解构赋值。这些新增的语法能够在很多情况下能以极少的代码实现出需要的功能。
  3. 优化语言执行效率。主要是尾调用优化和尾递归优化。
  4. 增加体现编程思想的原生语言特性。例如面向对象,例如模块化。这些新特性使得语言本身更贴近进行企业级开发的需求。

最后发表一下本人的看法:

ES6新增很多很赞的语法特性:模板字符串,解构赋值,箭头函数等等。这些特性可以使写出的代码更加简洁。块级作用域的出现  
使变量的命名更加安全和规范。强大的Generator函数可以实现意想不到的功能。  

新增的模块特性使语言本身能够实现静态化模块依赖,相比于使用requireJS等动态模块来说有更高的效率。只是从现有的规范的
模块移植到ES6原生的模块还需要借助模块转换 工具(或者手动更改模块的写法)。

新增的类定义方法使语言终于能像主流的面向对象语言一样使用类了。但是语言在对象上的内部机制还是没变,依然是原型式继
承,依然有构造函数。使用类的面具将背后构造函数的本质遮盖起来,虽然使语言更“面向对象”,但不了解后面本质的人在使
用类时可能出现意想不到的问题。

最后,由于ES标准基本保持向下兼容,ES6在推出到普及这期间内,JS代码风格会变得更加多样化。甚至会让人怀疑这些代码
真的是同一种语言吗。。。
3 条评论