ECMAScript 6 笔记(五)

Iterator和for...of循环

1. Iterator(遍历器)的概念

  Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环

  遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

  Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

  Iterator的遍历过程是这样的。

  (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

  (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

  (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

  每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

  在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for...of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了Symbol.iterator属性

2. 调用Iterator接口的场合

(1)解构赋值

  对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法。

let set = new Set().add('a').add('b').add('c');

let [x,y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

 

(2)扩展运算符

  扩展运算符(...)也会调用默认的iterator接口。

// 例一
var str = 'hello';
[...str] //  ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

 

(3)yield*

  yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

3. 字符串的Iterator接口

4. for...of循环 

  for...of循环,作为遍历所有数据结构的统一的方法。

  for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

  for...of循环可以代替数组实例的forEach方法。

  JavaScript原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6提供for...of循环,允许遍历获得键值。

  对于普通的对象,for...of结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。

var es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e);
}
// TypeError: es6 is not iterable

  for...in循环有几个缺点。

  • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for...in循环会以任意顺序遍历键名。

  总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

  • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  • 不同用于forEach方法,它可以与break、continue和return配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

Generator 函数的语法

  形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不同的内部状态(yield在英语里的意思就是“产出”)。

  Generator 函数是 ES6 提供的一种异步编程解决方案,从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

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

var hw = helloWorldGenerator();

 

  上面代码定义了一个Generator函数helloWorldGenerator,它内部有两个yield语句“hello”和“world”,即该函数有三个状态:hello,world和return语句(结束执行)。

  调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

  下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

 

1. yield语句

  由于Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。

  yield语句与return语句既有相似之处,也有区别。

  1. 相似之处在于,都能返回紧跟在语句后面的那个表达式的值。

  2. 区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

  3. 一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield语句。正常函数只能返回一个值,因为只能执行一次return;Generator函数可以返回一系列的值,因为可以有任意多个yield

  Generator函数可以不用yield语句,这时就变成了一个单纯的暂缓执行函数。

function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

 

  上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个Generator函数,就变成只有调用next方法时,函数f才会执行。

  另外需要注意,yield语句不能用在普通函数中,否则会报错。

  另外,yield语句如果用在一个表达式之中,必须放在圆括号里面。

console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK

 

  yield语句用作函数参数或赋值表达式的右边,可以不加括号。

2. next方法的参数

  yield句本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个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 }

 

  上面代码先定义了一个可以无限运行的 Generator 函数f,如果next方法没有参数,每次运行到yield语句,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

3. for...of循环

  for...of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用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

 

  上面代码使用for...of循环,依次显示5个yield语句的值。这里需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

Generator 函数的异步应用

1. 基本概念 

回调函数

  JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

 

Class

1. Class基本语法 

  基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

//es5语法
function Point(x, y) {
  this.x = x;
  this.y = y;
}

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

var p = new Point(1, 2);

//es6语法
//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

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

  Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

  ES6的类,完全可以看作构造函数的另一种写法。

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true
//上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

  使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

  构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

class Point {
  constructor(){
    // ...
  }

  toString(){
    // ...
  }

  toValue(){
    // ...
  }
}

// 等同于

Point.prototype = {
  toString(){},
  toValue(){}
};

  Object.assign方法可以很方便地一次向类添加多个方法。

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

  另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

  类的属性名,可以采用表达式。

let methodName = "getArea";
class Square{
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}
//上面代码中,Square类的方法名getArea,是从表达式得到的。

2. Class的继承 

基本用法

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

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

  子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

  Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

  (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

  (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

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

3. Class 的静态方法

  类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

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

  父类的静态方法,可以被子类继承。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod(); // 'hello'

  静态方法也是可以从super对象上调用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod();

4. Class的静态属性和实例属性 

  静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。

class Foo {
}

Foo.prop = 1;
Foo.prop // 1

  目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。

5. 类的私有属性

  目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。

修饰器

1. 类的修饰 

  修饰器(Decorator)是一个函数,用来修改类的行为。修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。

function testable(target) {
  target.isTestable = true;
}

@testable
class MyTestableClass {}

console.log(MyTestableClass.isTestable) // true

 

  上面代码中,@testable就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable

  基本上,修饰器的行为就是下面这样。

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

 

  也就是说,修饰器本质就是编译时执行的函数。

  修饰器函数的第一个参数,就是所要修饰的目标类。

2. 方法的修饰

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}
//上面代码中,修饰器readonly用来修饰“类”的name方法。

 

Module

  ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

 

1. 严格模式

  ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

2. export 命令

  模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

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

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

export {firstName, lastName, year};

 

  通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

 

  需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错
export 1;

// 报错
var m = 1;
export m;

 

  上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出1。1只是一个值,不是接口。正确的写法是下面这样。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

 

  上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

  export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

3. import 命令

  使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

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

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

 

  import语句会执行所加载的模块,因此可以有下面的写法。

import 'lodash';

 

import * as circle from './circle';

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

 

  为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

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

   上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

  其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

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

 

// 第一组
export default function crc32() { // 输出
  // ...
}

import crc32 from 'crc32'; // 输入

// 第二组
export function crc32() { // 输出
  // ...
};

import {crc32} from 'crc32'; // 输入

 

  上面代码的两组写法,第一组是使用export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。

  export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as xxx } from 'modules';
// 等同于
// import xxx from 'modules';

 

4. export 与 import 的复合写法

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

 

5. ES6模块加载的实质

  ES6 模块加载的机制,与 CommonJS 模块完全不同。CommonJS模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用。

6. 浏览器的模块加载 

  浏览器使用 ES6 模块的语法如下。

<script type="module" src="foo.js"></script>

 

  上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

7. 循环加载

  “循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

// a.js
var b = require('b');

// b.js
var a = require('a');

 

8. 跨模块常量

  本书介绍const命令的时候说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),可以采用下面的写法。

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

 

posted @ 2017-02-06 20:04  超然haha  阅读(925)  评论(2编辑  收藏  举报
back to top