张颖

2015-08-31 00:35

ECMAScript 6 新特性总结

本文作者:IMWeb 张颖 原文出处:IMWeb社区 未经同意,禁止转载

前言

个人感觉ECMAScript 6总体上来说:添加了块级作用域,增加了一些语法糖,增强了字符串的处理,引入Generator函数控制函数的内部状态的变化,原生提供了Promise对象,引入了Class(类)的概念,并且在语言规格的层面上实现了模块功能。 注:

1、ES6的支持性可以查看:http://kangax.github.io/compat-table/es6/

2、Google V8引擎已经部署了ES6的部分特性,使用Node.js 0.12版,可以试验这些特性。

3、使用Traceur转码器、Babel转码器等可以将ES6方式编写的程序转为ES5代码。

一、let和const命令

1.1 块级作用域

一个花括号{}代表一个块级作用域,作用域嵌套时外层代码块不受内层代码块的影响,立即执行匿名函数(IIFE)原本的作用是为了形成局部作用域,防止变量污染,块级作用域的的出现使得获得广泛应用的立即执行匿名函数不再必要了。

需要注意: 函数本身的作用域,在其所在的块级作用域之内。ES5存在函数提升,不管函数在何处声明,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,其内部声明的函数皆不会影响到作用域的外部。

function f() { console.log('I am outside!'); }
(function () {
  if(false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
//ES5 result: I am inside!
//ES6 result: I am outside!

1.2 let命令

let命令,用来声明变量,它的用法类似于var,但是所声明的变量,只在let命令所在代码块内有效。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); 
//result: 10
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6]();
//result: 6

使用时有几点需要需要注意:

  • let不会像var一样声明提前,只能在定义之后使用,之前使用会抛出ReferenceError;
  • 并且只要作用域内有let声明的变量,这个变量就会被绑定,不受原来变量声明规则的影响。即ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些命令,就会报错。这在语法上,称为“暂时性死区”(temporal dead zone,TDZ)。
var tmp = 123;
if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}
  • 函数的作用域是其声明时所在的作用域。
  • 不允许在相同作用域内,重复声明同一个变量。因此,不能在函数内部重新声明参数。

1.3 const命令

const用来声明常量,一旦声明,常量的值就不能改变。

使用时需注意:

  • 对常量重新赋值不会报错,只会默默地失败。
  • 与let命令相同,只在声明所在的块级作用域内有效。
  • const命令也不存在提升,只能在声明的位置后面使用,提前使用同样会抛出ReferenceError。
  • 同样不可重复声明。
  • const命令只是指向变量所在的地址,如果将const变量赋值为一个对象,则此常量储存的是一个地址,不可变的只是这个地址,但对象本身是可变的,依然可以为其添加新属性。如果真的想将对象冻结,应该使用Object.freeze方法。
const foo = {};
foo.prop = 123;

foo.prop
// 123

foo = {} // 不起作用
const foo = Object.freeze({});
foo.prop = 123; // 不起作用

1.4 全局对象的属性

全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。ES5规定,所有全局变量都是全局对象的属性。

ES6规定,var命令和function命令声明的全局变量,属于全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。


二、变量的解构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。解构只能用于数组或对象,所以应该注意,其他原始类型的值都可以转为相应的对象,除了undefined和null。

本质上,解构写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

  • 对数组的结构赋值,允许指定默认值。(只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值,所以对于Set结构是可以使用的。)
let [foo, [[bar], baz]] = [1, [[2], 3]];
let [x, y='b'] = ['a', undefined]; // x='a', y='b'
let [a, b, c] = new Set(["a", "b", "c"]);
  • 对对象的解构,属性没有次序,变量必须与属性同名,才能取到正确的值。对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。对象的解构同样可以指定默认值,并且可以与函数参数的默认值一起使用。
var { foo, bar } = { foo: "aaa", bar: "bbb" };

var {x, y = 5} = {x: 1};
console.log(x, y) // 1, 5

let { log, sin, cos } = Math;
function move({x=0, y=0} = {}) {
  return [x, y];
}

move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

三、字符串的扩展

3.1 字符串处理的增强

JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),JavaScript会认为它们是两个字符。在ECMAScript6中,增强了对码点大于0xFFFF的字符的整体处理和正则匹配。

具体增加的一些处理方法如下:

  • codePointAt():会正确返回四字节的UTF-16字符的码点,对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。

  • String.fromCodePoint():正确返回编号大于0xFFFF的码点对应的字符,弥补了String.fromCharCode方法的不足。

  • at():返回字符串给定位置的字符,如果该字符的Unicode编号大于0xFFFF,可以返回正确的字符。而charAt()方法只能返回UTF-16编码的第一个字节,不能正确返回。

  • 字符的Unicode表示法:"\u{20BB7}"的形式可以正确表示超出\uFFFF的双字节字符。

  • 正则表达式的u修饰符:对于正则表达式中的.字符、\u{20BB7}大括号字符、量词、\S、i修饰符等,如果需要正确识别码点编号大于0xFFFF的字符,必须添加了u修饰符。比如:

var s = " ";

/^.$/.test(s) // false
/^.$/u.test(s) // true
  • normalize():ES6提供String.prototype.normalize()方法,用来将Unicode中字符的不同表示方法统一为同样的形式。(目前不能识别三个或三个以上字符的合成。)

  • includes():返回布尔值,表示是否找到了参数字符串。支持第二个参数,表示开始搜索的位置。

  • startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。支持第二个参数,表示开始搜索的位置。

  • endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。支持第二个参数,表示对前n个字符进行搜索。

  • repeat():返回一个新字符串,表示将原字符串重复n次。

  • “粘连”(sticky)修饰符y:全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始,y修饰符确保匹配必须从剩余的第一个位置开始。换而言之,y修饰符号隐含了头部匹配的标志ˆ。

    3.2 模板字符串

    模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

使用规则:

  • 在模板字符串中嵌入变量,需要将变量名写在${}之中。
  • 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
  • 大括号内部可以进行运算,以及引用对象属性,其中还能调用函数。 // 普通字符串
`In JavaScript'\n' is a line-feed.`

// 多行字符串
console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

//反斜杠转义
var greeting = `\`Yo\` World!`;

//对象属性
var obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`)

//函数调用
function fn() {
  return "Hello World";
}
console.log(`foo ${fn()} bar`);

模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。函数的参数第一个为模板字符串中没有变量替换的部分组成的数组,第一个参数之后的参数,都是模板字符串各个变量依次被替换后的值。

var a = 5;
var b = 10;

tag`Hello ${ a + b } world ${ a * b}`;
//等价于
tag(['Hello ', ' world '], 15, 50);

注意处理函数的第一个参数,拥有一个raw属性。它也是一个数组,成员与处理函数的第一个参数完全一致,唯一的区别是字符串是被转义前的原始格式。


四、数值的扩展

4.1 二进制和八进制表示法

二进制和八进制数值的新的写法,分别用前缀0b和0o表示

0b111110111 === 503 // true
0o767 === 503 // true

4.2 扩展函数

  • Number.isFinite()用来检查一个数值是否非无穷(infinity);Number.isNaN()用来检查一个值是否为NaN。它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回false。

  • Number.parseInt(), Number.parseFloat():ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。

  • Number.isInteger():用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。

  • Number.isSafeInteger()则是用来判断一个整数是否落在Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量表示的上下限范围内。

  • ES6在Math对象上还提供了许多新的数学方法:

    • Math.trunc(x)方法用于去除一个数的小数部分,返回整数部分;
    • Math.sign(x)方法用来判断一个数到底是正数、负数、还是零;
    • 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)。

五、数组的扩展

5.1 数组推导

数组推导就是直接通过现有数组生成新数组的一种简化写法,通过for...of结构,允许多重循环。注:新数组会立即在内存中生成,这时如果原数组是一个很大的数组,将会非常耗费内存。

var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];
a2 // [2, 4, 6, 8]

var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
[for (year of years) if (year > 2000 && year < 2010) year];
// [ 2006]

5.2 数组处理的扩展方法

  • Array.from():用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象,其中包括ES6新增的Set和Map结构。Array.from()还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理。
Array.from({ 0: "a", 1: "b", 2: "c", length: 3 });
// [ "a", "b" , "c" ]

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
  • Array.of()方法用于将一组值,转换为数组。弥补数组构造函数Array()的不足。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]

Array(3) // [undefined, undefined, undefined]
  • 数组实例的find()用于找出第一个符合条件的数组元素;数组实例的findIndex()用于返回第一个符合条件的数组元素的位置。这两个方法都可以发现NaN,弥补了IndexOf()的不足。

  • 数组实例的fill()使用给定值,填充一个数组。

  • 数组实例的entries(),keys()和values()用于遍历数组,它们都返回一个遍历器,可以用for...of循环进行遍历。keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。


六、对象的扩展

6.1 增强的对象写法

ES6允许直接写入变量和函数,作为对象的属性和方法。

var Person = {
  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }
};

ES6允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内,允许变量渗入 key 中。

var lastWord = "last word";

var a = {
    "first word": "hello",
    [lastWord]: "world"
};

a["first word"] // "hello"
a[lastWord] // "world"
a["last word"] // "world"

6.2 Object.is()和Object.assign()

Object.is():用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

Object.assign():用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

var target = { a: 1, b: 1 };

var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

6.3 proto属性

  • 用来读取或设置当前对象的prototype对象。
  • Object.setPrototypeOf()方法的作用与proto相同,用来设置一个对象的prototype对象,它是ES6正式推荐的设置原型对象的方法。
  • Object.getPrototypeOf()方法用于读取一个对象的prototype对象。(浏览器(包括IE11)早就部署了这个属性,只是在 ES6 才被纳入标准中,之前我们常用这个属性来判断是否为 IE 。)

    6.4 Symbol

    Symbol是一种新的原始数据类型,表示独一无二的ID,它通过Symbol函数生成。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。Symbol函数可以接受一个字符串作为参数,表示Symbol实例的名称。
var mySymbol = Symbol('Test');
mySymbol.name
// Test

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

s1 === s2 // false

//不能与其他类型的值进行运算
var sym = Symbol('My symbol');
'' + sym
// TypeError: Cannot convert a Symbol value to a string

//可以转换为字符串
String(sym)
// 'Symbol(My symbol)'

注意:

  • Symbol.for():在全局环境中搜索指定key的Symbol值,如果存在就返回这个Symbol值,否则就新建一个指定key的Symbol值并返回。与Symbol()的区别是,Symbol.for()会被登记在全局环境中供搜索,不会建立相同Key的Symbol值,而Symbol()则完全相反。

  • Symbol.keyFor()方法返回一个已登记的Symbol类型值的key。

  • Symbol作为属性名,该属性不会出现在for...in循环中,也不会被Object.keys()、Object.getOwnPropertyNames()返回,要使用对应的Object.getOwnPropertySymbols方法,以及Object.getOwnPropertyKeys方法获取相应的Symbol属性名。

    6.5 Proxy

    Proxy用于修改某些操作的默认行为,等于在目标对象之前,架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。而Proxy.revocable()方法则返回一个可取消的Proxy实例。

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
    get: function(target, property) {
        return property in target ? target[property] : "米有";
    }
});

proxy.name // "张三"
proxy.title // "米有"
let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy支持的拦截操作:

属性 返回值 拦截操作
defineProperty(target, propKey, propDesc) 布尔值(Boolean) Object.defineProperty(proxy, propKey, propDesc)
deleteProperty(target, propKey) 布尔值 delete proxy[propKey]
enumerate(target) 遍历器 for (x in proxy)
get(target, propKey, receiver) 类型不限 对象属性的读取
set(target, propKey, value, receiver) 布尔值 对象属性的设置
getOwnPropertyDescriptor(target, propKey) 属性的描述对象 Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target) 对象 Object.getPrototypeOf(proxy)
has(target, propKey) 布尔值 propKey in proxy
isExtensible(target) 布尔值 Object.isExtensible(proxy)
ownKeys(target) 数组 Object.getOwnPropertyPropertyNames(proxy)、Object.getOwnPropertyPropertySymbols(proxy)、Object.keys(proxy)
preventExtensions(target) 布尔值 Object.preventExtensions(proxy)
setPrototypeOf(target, proto) 布尔值 Object.setPrototypeOf(proxy, proto)
apply(receiver, ...args) 不限 函数的调用、call和apply操作
construct 对象 Proxy实例作为构造函数调用的操作,比如new proxy(···)
### 6.6 Object.observe(),Object.unobserve() Object.observe()方法用来监听对象(以及数组)的变化。一旦监听对象发生变化,就会触发回调函数。Object.unobserve()方法用来取消监听。 Object.observe方法目前共支持监听以下六种变化:
操作 作用
add 添加属性
update 属性值的变化
delete 删除属性
setPrototype 设置原型
reconfigure 属性的attributes对象发生变化
preventExtensions 对象被禁止扩展

七、函数的扩展

7.1 函数参数的默认值

ES6允许为函数的参数设置默认值,使用=形式直接写在参数定义的后面。

使用注意事项:

  • 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。
  • 参数默认值所处的作用域,不是全局作用域,而是函数作用域。
function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

var p = new Point();
// p = { x:0, y:0 }
  • 还可以设置双重默认值,比如下面代码调用函数fetch时,如果不含第二个参数,则默认值为一个空对象;如果包含第二个参数,则它的method属性默认值为GET。
fetch(url, { method='GET' } = {}){
  console.log(method);
}

7.2 rest运算符(...)

...+变量名形式与...+数组形式相当于互逆操作:

  • ...变量名:将多余的参数放入一个数组中,rest参数必须在最后一个;函数的length属性,不包括rest参数。
function add(...values) {
   let sum = 0;

   for (var val of values) {
      sum += val;
   }

   return sum;
}

add(2, 5, 3) // 10
(function(a, ...b) {}).length  // 1
  • ...数组:将一个数组转为用逗号分隔的参数序列。
const date = new Date(...[2015, 1, 1]);

7.3 箭头函数(=>)

函数定义的简化表示法,

var f = () => 5;
// 等同于
var f = function (){ return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
    return num1 + num2;
};

使用箭头函数需要注意:

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

    7.4 尾调用优化

    尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数。

为什么尾调用会优化:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。所以如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。

尾调用的应用——尾递归:递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。比如阶乘函数的为递归改写:

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

八、Set和Map数据结构

8.1 数据结构Set

Set结构类似于数组,但是成员的值都是唯一的,没有重复的值。

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

for (i of items) {console.log(i)}
// 1 2 3 4 5
items.size 
// 5

注意:向Set加入值的时候,不会发生类型转换,所以5和“5”是两个不同的值。Set内部判断两个值是否不同,使用的算法类似于精确相等运算符(===),唯一的例外是NaN等于自身。这意味着,两个对象总是不相等的。

let set = new Set();

set.add({})
set.size // 1

set.add({})
set.size // 2

Set结构有以下属性:

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

Set数据结构有以下方法:

  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

Set结构有一个values方法,返回一个遍历器,同时Set结构的默认遍历器就是它的values方法,所以可以直接用for...of循环进行遍历。

WeakSet是一个与Set类似的结构,也是不重复的值的集合。但是,它与Set有两个区别:

  1. WeakSet的成员只能是对象,而不能是其他类型的值。任何具有iterable接口的对象,都可以作为WeakSet的对象,比如数组或者类数组的对象。
  2. WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。(WeakSet的一个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。

    8.2 数据结构Map

    Map结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

注意Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。只有对同一个对象的引用,Map结构才将其视为同一个键。

var map = new Map();

map.set(['a'], 555); 
map.get(['a']) // undefined

Map结构的属性和方法:

  • size:返回成员总数。
  • set(key, value):设置一个键值对。
  • get(key):读取一个键。
  • has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
  • delete(key):删除某个键。
  • clear():清除所有成员。
  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。

同样与WeakSet类似,WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受原始类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制,有助于防止内存泄漏。


九、遍历器(Iterator)

9.1 Iterator(遍历器)

遍历器属于一种接口规格,任何数据结构只要部署这个接口,就可以完成遍历操作,即依次处理该结构的所有成员。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得数据结构的成员能够按某种次序排列。

简化的讲,所谓Iterator接口,就是指调用这个接口,会返回一个遍历器对象。该对象具备next方法,每次调用该方法,会返回一个具有value和done两个属性的新对象,指向部署了Iterator接口的数据结构的一个成员。下面代码就展示了一个典型的Iterator接口:

function idMaker(){
  var index = 0;

  return {
    next: function(){
      return {value: index++, done: false};
    }
  }
}

var it = idMaker();

it.next().value // '0'
it.next().value // '1'
it.next().value // '2'

9.2 for...of

当使用for...of循环,遍历某种数据结构时,该循环会自动去寻找Iterator接口。在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。

默认的Iterator接口部署在数据结构的Symbol.iterator属性,也就是说调用Symbol.iterator方法,就会得到当前数据结构的默认遍历器。

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 }
const arr = ['red', 'green', 'blue'];
let iterator  = arr[Symbol.iterator]();

for(let v of arr) {
    console.log(v); // red green blue
}

for(let v of iterator) {
    console.log(v); // red green blue
}

除了上述三类之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

function Obj(value){
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function(){

  var iterator = {
    next: next
  };

  var current = this;

  function next(){
    if (current){
      var value = current.value;
      var done = current == null;
      current = current.next;
      return {
        done: done,
        value: value
      }
    } else {
      return {
        done: true
      }
    }
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;

for (var i of one){
  console.log(i)
}
// 1
// 2
// 3

9.3 调用默认iterator接口的场合

进行以下操作时,会自动去调用默认iterator接口:

  • 首先是上面介绍的for...of结构;
  • 对数组和Set结构进行解构赋值时,会默认调用iterator接口;
  • 扩展运算符(...)也会调用默认的iterator接口;
  • yield*
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()
  • Promise.all(), Promise.race()

十、Generator 函数

10.1 Generator 函数的用法

Generator是一个普通函数,有两个特征:一是function命令与函数名之间有一个星号;二是函数体内部使用yield语句,定义遍历器的每个成员。

Generator可以理解成一个函数的内部状态的遍历器,每调用一次,函数的内部状态发生一次改变。换句话说,它就是一个改装了的 Iterator 遍历器,通过 yield 来增加一个 next() 节点。

先看一下Generator函数的完整作用: 用Generator函数定义helloWorldGenerator,它的遍历器有两个成员“hello”和“world”。当调用Generator函数的时候,该函数并不执行,而是返回一个遍历器(可以理解成暂停执行)。以后,每次调用这个遍历器的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 }

for...of循环可以自动遍历Generator函数,且此时不再需要调用next方法。

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

10.2 next方法的参数

yield语句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数会被当作上一个yield语句的返回值。这个功能有很重要的语法意义,Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var it = foo(5);

it.next()
// { value:6, done:false }
it.next(12)
// { value:8, done:false }
it.next(13)
// { value:42, done:true }

10.3 yield*语句

如果yield命令后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器,则遍历有递归的效果。如果yield*后面跟着一个数组,就表示该数组会返回一个遍历器,因此就会遍历数组成员。

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

使用yield*语句遍历完全二叉树的应用如下:

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉树
function make(array) {
  // 判断是否为叶节点
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

10.4 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();
  • 控制流管理:yield语句是同步运行,所以多层回调函数可以改写为直线执行的形式。
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});
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
  }
}

function scheduler(task) {
  setTimeout(function() {
    var taskObj = task.next(task.value);
    // 如果Generator函数未结束,就继续调用
    if (!taskObj.done) {
      task.value = taskObj.value
      scheduler(task);
    }
  }, 0);
}

scheduler(longRunningTask());
  • 在任意对象上部署iterator接口。
function* iterEntries(obj) {
    let keys = Object.keys(obj);
    for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
    }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
    console.log(key, value);
}

// foo 3
// bar 7

十一、Promise对象

11.1 Promise对象基本介绍

先解释下在Promises/A规范中,每个任务都有三种状态:默认(pending)、完成(fulfilled)、失败(rejected)。

  • 默认状态可以单向转移到完成状态,这个过程叫resolve,对应的方法是deferred.resolve(promiseOrValue);
  • 默认状态还可以单向转移到失败状态,这个过程叫reject,对应的方法是deferred.reject(reason);
  • 默认状态时,还可以通过deferred.notify(update)来宣告任务执行信息,如执行进度; 状态的转移是一次性的,一旦任务由初始的pending转为其他状态,就会进入到下一个任务的执行过程中。

ES6的Promise对象是一个构造函数,用来生成Promise实例。Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve方法和reject方法。

  • 如果异步操作成功,则用resolve方法将Promise对象的状态变为“成功”(即从pending变为resolved);
  • 如果异步操作失败,则用reject方法将状态变为“失败”(即从pending变为rejected)。

promise实例生成以后,可以用then方法分别指定resolve方法和reject方法的回调函数。

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

promise.then(function(value) {
  // success
}, function(value) {
  // failure
});

Promise.prototype.then方法返回的是一个新的Promise对象,因此可以采用链式写法。多个then执行时前一个回调函数完成以后,会将返回结果作为参数,传入后一个回调函数。如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的别名,用于指定发生错误时的回调函数。

getJSON("/posts.json").then(function(posts) {
  // some code
}).catch(function(error) {
  // 处理前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

11.2 Promise.all(),Promise.race()

Promise.all():用于将多个Promise实例,包装成一个新的Promise实例。

 var p = Promise.all([p1,p2,p3]);

p的状态由p1、p2、p3决定,分成两种情况:

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

Promise.race():与Promise.all()形式类似,不同的是只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。


十二、Class

12.1 Class基本语法

Class定义了一个“类”,constructor方法表示构造方法,而this关键字则代表实例对象。

//定义类
class Point {

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

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

}

Point.prototype在ES6继续存在,也就是说,除了constructor方法以外,类的方法都定义在类的prototype属性上面。

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

12.2 继承

Class之间可以通过extends关键字,实现继承。

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方法,否则新建实例时会报错。
  • 如果子类没有定义constructor方法,这个方法会被默认添加
  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。

12.3 继承类的prototype属性和proto属性

Class作为构造函数的语法糖,同时有prototype属性和proto属性:

  • 子类的proto属性,表示构造函数的继承,总是指向父类。
  • 子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。
class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

有三种特殊情况需要注意:

  • 子类继承Object时,子类其实就是构造函数Object的复制,子类的实例就是Object的实例。
  • 形如class A {}的类,就是一个普通函数,所以直接继承Funciton.prototype。但是A调用后返回一个空对象(即Object实例),所以A.prototype.proto指向构造函数(Object)的prototype属性。
  • 子类继承null时,相当与一个普通函数直接继承Funciton.prototype,但是子类调用后返回的对象不继承任何方法,所以结果如下:
class A extends null {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === null // true

十三、Module

13.1 export命令,import命令,module命令

ES6模块通过export命令显式指定输出的代码,输入时也采用静态命令的形式。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

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

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

module命令可以取代import语句,达到整体输入模块的作用。

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

// main.js
module circle from 'circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

export default命令定义模块的默认方法。

// export-default.js
export default function () {
    console.log('foo');
}

// import-default.js
import customName from './export-default';
customName(); // 'foo'

13.2 模块的继承

模块之间的继承使用这条命令:

// circleplus.js
export * from 'circle';

circleplus模块,继承了circle模块,“export *”表示输出circle模块的所有属性和方法。

13.3 ES6模块的转码

浏览器目前还不支持ES6模块,为了现在就能使用,可以将其转为ES5的写法,目前比较好用的工具有:

  • ES6 module transpiler是square公司开源的一个转码器,可以将ES6模块转为CommonJS模块或AMD模块的写法,从而在浏览器中使用。
  • SystemJS是一个垫片库(polyfill),可以在浏览器内加载ES6模块、AMD模块和CommonJS模块,将其转为ES5格式,它在后台调用的是Google的Traceur转码器。

十四、总结

整个ES6的新特性和用法学习下来,觉得比较有用的点如下:

  • 习惯用let取代var,因为两者语义相同,而且let没有副作用。
  • 优先使用const,方便阅读,也防止不小心的串改。
  • 以后的编程,会偏向严格模式。
  • 熟悉新加入的对4字节字符串的处理函数,感觉简化了很多之前要特意针对四字节字符串单独进行处理的过程。
  • 动态字符串使用反引号,个人感觉这个简化的动态字符串模板还是蛮好用的。
  • 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
  • 使用匿名函数的场合,一律改为使用箭头函数。箭头函数取代Function.prototype.bind,不应再用 self / _this / that 绑定 this。
  • 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
  • 如果只是需要key:value的数据结构,记得使用Map,因为Map有内建的遍历机制。
  • 理解Iterator(遍历器)的实现。
  • 习惯使用Generator 函数和Promise对象,将异步操作的同步化,避免了层层嵌套的回调函数。
  • 使用import取代require,使用export取代module.exports。这样可以在编译时就完成模块编译,效率要比CommonJS模块高。
7条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。