ES6学习笔记(二)

遍历

Iterator

作用

Iterator的作用有三个:

  1. 为各种数据结构(ArraySetMap等),提供一个统一的、简便的访问接口;
  2. 使得数据结构的成员能够按某种次序排列;
  3. ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

原理

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

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

默认Iterator接口

ES6 规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。 原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

注:普通对象部署数组的Symbol.iterator方法,并无效果。

调用场景

  1. 解构赋值
  2. 扩展运算符
  3. for…of
  4. Array.from()
  5. Set()、Map()
  6. Promise.all()、Promise.race()

for…of循环

ES6借鉴C++JavaC#Python语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

对比.forEach

Array及部分对象都提供了forEach方法遍历元素,然而forEach依赖回调函数执行,所以无法使用breakcontinuereutrn中断循环执行。

对比for…in

for...offor...in都能通过简洁的语法遍历元素。然而,for...in遍历的是数组的键名;而且遍历键名的结果是字符串(即不能直接进行计算)。此外,for...in遍历的元素顺序未必是预期的顺序,甚至会遍历对象原型链上的可枚举属性。因此,for...in的设计更适合遍历对象属性。而for...of更适合顺序遍历数据结构内部各个元素。

Class

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

构造器constuctor

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。该方法相当于ES5中定义类的Function。类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

对象实例

通过new创建的class实例,同一个类的所有实例与ES5中的实例一样,均共享同一个原型。

class表达式

与函数一样,类也可以使用表达式的形式定义。

1
2
3
4
5
const MyClass = class Me {
getClassName() {
return Me.name;
}
};

采用 Class 表达式,可以写出立即执行的 Class。

1
2
3
4
5
6
7
8
9
10
11
let person = new class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}('张三');

person.sayName(); // "张三"

不存在变量提升

类不存在变量提升(hoist),这一点与 ES5 完全不同。

1
2
new Foo(); // ReferenceError
class Foo {}

Class 的取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

static静态方法

如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。父类的静态方法,可以被子类继承。

class继承

在ES6中为class提供了新的继承语法(本质上仍然是ES5中的原型链式继承),通过extends关键字注明继承对象,并在constructor中通过super()调用父类的构造方法通过super调用父类属性或者方法实现继承。

1
2
3
4
5
6
7
8
class A {
}

class B extends A {
}

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

注:extends除了继承class声明的类,还可以用来继承原生的构造函数(ES5是无法做到的,ES5中原生构造函数无法绑定this<被忽略>)。因此可以用来继承Array实现

Mixin 模式的实现(替代多继承)

1
2
3
4
5
6
7
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

Module

在 ES6 之前,社区制定了一些模块加载方案,最主要的有CommonJSAMD两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代CommonJSAMD规范,成为浏览器和服务器通用的模块解决方案。

静态加载(编译时加载)

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJSAMD 模块,都只能在运行时确定这些东西(运行时加载)。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

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

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

export

export用于向模块外部暴露接口(对象、方法、类),不可以是某个值在export之前声明的变量、方法、类,必须与变量名称建立一一对应的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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};

export function multiply(x, y) {
return x * y;
};

var firstName = 'Michael';
export firstName;
// 报错

export 1;
// 报错

function f() {}
export f;
// 报错

import…from

import用于引入export暴露出的接口。from指定引用模块的路径。

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

import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

注:由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

注:import语句是Singleton模式。同一个模块中import多次同样的模块实际上只会执行一次引入操作。

export default

export default用于输出匿名模块,引入它暴露的模块时import不需要加{},并且可以随意为匿名模块指定名字。

1
2
3
4
5
6
7
8
// export-default.js
export default function () {
console.log('foo');
}

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

此外,export defaultexport恰恰相反,不能在其后声明变量,但却可以直接暴露已经声明的变量(不需加{})。

1
2
3
4
5
6
7
8
9
// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;

as

as用于给暴露或引入的模块重新命名。

1
2
3
4
import * as x from 'xxx';

var a = 5;
export {a as abc};

export与import结合

有时候,需要二次暴露引入的模块,可以使用以下语法:

1
2
3
4
5
export {a as newA} from 'xxx';

// 等于
import {a} from 'xxx';
export {a as newA};

Generator

Generator函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

函数定义

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

1
2
3
4
5
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}

使用

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。

1
var hw = helloWorldGenerator(); // Iterator

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

1
2
3
4
5
6
7
8
9
10
11
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函数返回的是Iterator,所以可以通过for..of的方式遍历执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

Generator嵌套yield*

如果在一个Genrator中使用了另一个Generator函数,需要在被嵌套的函数前加上yield*方能生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* foo() {
yield 'a';
yield 'b';
}

function* bar() {
yield 'x';
yield* foo();
yield 'y';
}

// 等于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}

Decorator

ES6提供了类似JAVA的修饰器用于修饰类及类的属性、方法,用简洁的方式设定其特性。其本质是对修饰函数的语法糖。

语法

@加上修饰符放置在被修饰对象之前。

1
2
3
4
@testable
class MyTestableClass {
// ...
}

Decorator定义

@readOnly只读修饰器为例,声明一个函数,接受3个参数:第一个是对应类的原型,第二个是修饰参数的属性名,第三个参数是该属性的描述对象:

1
2
3
4
5
6
7
8
9
10
11
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}

core-decorators.js提供的常见修饰符

core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。

  1. @autobind:使得方法中的this对象,绑定原始对象;
  2. @readonly:使被修饰对象为只读;
  3. @override:检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

参考

《ECMAScript 6 入门》
《ECMAScript® 2018 Language Specification》