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
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。其中,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
句本身没有返回值,或者说总是返回undefined
。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 }
上面代码先定义了一个可以无限运行的 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
属性为true
,for...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 命令
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,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