类型别名
允许使用 type 关键字声明类型别名:
type PrimitiveArray = Array<string | number | boolean>
type MyNumber = number
type NgScope = ng.IScope
type Callback = () => void
环境声明
允许在 TypeScript 中创建一个不会被编译到 Javascript 中的变量。这个特性是用来促进与现有 Javascript 代码、DOM(文档对象模型)、BOM(浏览器对象模型)结合而设计的。
有时,我们希望调用一个未被定义的对象上的方法,比如 window 对象上的 console 方法。
当访问 DOM 或 BOM 对象时,我们没有遇到错误,是因为这些对象已经在一个特殊的 Typescript 文件(被称为 声明文件)中被声明了。可以使用 declare 操作符创建一个环境声明。
下面代码中,声明一个被 customConsole 对象实现的接口。然后使用 declare 操作符在作用域中增加一个 customConsole 对象:
interface ICustomConsole {
log(arg: string): void
}
declare var cuntomConsole: ICustomConsole
然后就可以在没有编译错误的情况下使用 cuntomConsole
cuntomConsole.log("测试")
Typescript 默认包含一个名为 lib.d.ts 的文件,它提供了像 DOM 这种 Javascript 内置库的接口声明。
使用 .d.ts 结尾的声明文件,是用来提高 Typescript 对第三方库和像 Node.js 或浏览器这种运行时环境的兼容性的。
算数运算符
+、-、*、/、%、++、——
接口
在 TS 中,可以使用接口来确保类拥有指定的结构
interface loggerInterface {
log(arg: any): void
}
class Logger implements loggerInterface {
log(arg) {
if (typeof console.log === 'function') {
console.log(arg)
}else {
alert(arg)
}
}
}
上面定义了一个名为 loggerInterface 的接口,和一个实现了它的 Logger 类。TS 允许使用接口来约束对象。这让我们可以避免很多潜在的小错误:
interface UserInterface {
name: string,
password: string
}
let user: UserInterface = {
name: '',
pasword: '' //遗漏错误属性 '{ name: string; pasword: string; }' is not assignable to type 'UserInterface'
}
命名空间
又称内部模块,被用于组织一些具有某些内在联系的特性和对象。命名空间能够使代码结构更清晰,可以使用 namespace 和 export 关键字,在 TS 中声明命名空间。
namespace Geometry {
interface VectorInterface {
}
export interface Vector2dInterface {
}
export interface Vector3dInterface {
}
export class Vector2d implements VectorInterface, Vector2dInterface {
}
export class Vector3d implements VectorInterface, Vector3dInterface {
}
}
const vector2dInstance: Geometry.Vector2dInterface = new Geometry.Vector2d()
const vector3dInstance: Geometry.Vector3dInterface = new Geometry.Vector3d()
上面声明了一个包含 Vector2d、Vector3d类和VectorInterface、
Vector2dInterface、Vector3dInterface 接口的命名空间。注意,命名空间内的第一个接口声明前并没有 export 关键字。所以在命名空间外部,访问不到它。
综合运用
下面的例子用到了模块、类、函数和类型注解:
module Geometry {
export interface Vector2dInterface {
toArray(callback: (x: number[]) => void): void
length(): number
normalize()
}
export class Vector2d implements Vector2dInterface {
private _x: number
private _y: number
constructor(x: number, y: number) {
this._x = x
this._y = y
}
toArray(callback: (x: number[]) => void): void {
callback([this._x, this._y])
}
length(): number {
return Math.sqrt(this._x * this._x + this._y * this._y)
}
normalize() {
let len = 1 / this.length()
this._x *= len
this._y *= len
}
}
}
const vector: Geometry.Vector2dInterface = new Geometry.Vector2d(2, 3)
vector.normalize()
vector.toArray((vectorAsArray: number[]) => {
console.log(vectorAsArray)
})
类型检查和智能提示会帮助创建 Vector2d 类的实例,单位化它的值,将其转化为数组。
自动化任务工具
自动化任务工具用来自动化地执行开发过程中需要重复进行的任务。这些任务包括编译 Ts 文件、压缩 Js 文件等等。
Gulp 使用的是流,Gulp 插件更倾向于使用代码来描述任务,这使得 Gulp 任务的可读性更高。
安装 Gulp
pnpm add gulp -D
创建一个 gulpfile.js
,写完任务后执行 gulp 命令即可执行任务。
自动化测试工具
函数类型
通过使用可选的类型声明注解来显式声明一个元素的类型:
function greetNamed(name: string):string {
if (name) {
return `Hi! ${name}`
}
}
在上述函数中,定义了参数 name 的类型(string)和返回值类型(string)。有时候不只需要定义函数中元素的类型,还需要定义函数本身的类型。如下:
let greetUnnamed:(name: string) => string
greetUnnamed = function (name: string): string {
if (name) {
return `Hi! ${name}`
}
}
在上述例子中,声明了变量 greetUnnamed 及其类型。greetUnnamed 的类型是一个只包含一个名为 name 的 string
类型参数、在调用后会返回类型为 string 的函数。在声明了这个变量之后,一个完全符合变量类型的函数被赋值给了它。
也可以声明 greetUnnamed 的类型,并在同一行中将一个函数赋值给它,而不是分割成两行,例如:
let greedUnnamed:(name: string) => string = function (name: string):string {
if (name) {
return `Hi! ${name}`
}
}
如上,之前的代码片段中同样声明了变量 greedUnnamed 和它的类型。我们也可以在同一行中把一个函数赋值给这个变量,被赋值的函数类型必须与变量类型相同。
有可选参数的函数
与 JS 不同,调用函数时传参的数量或类型不符合函数中定义的参数要求时,TS 编译器会报错。
function add(foo: number, bar: number, foobar: number): number {
return foo + bar + foobar
}
上述函数名为 add 并包含三个 number 类型的参数:foo、bar 和 foobar。如果调用这个函数时没有完整地传入三个参数,会得到一个编译错误,提示提供地参数与函数声明中地参数无法匹配。
在一些场景下,我们也许想调用这个函数且不提供所有的参数。TS 提供了一个函数可选参数的特性,可以帮助增加这个函数的灵活性。可以在 TS 中通过在函数参数后追加一个字符 " ? ",指定函数参数是可选的。更新一下前面的函数,将 foobar 参数从必选参数修改为可选参数。
function add(foo: number, bar: number, foobar?: number): number {
let result = foo + bar
if (foobar !== undefined) result += foobar
return result
}
修改后,提供两个或三个参数调用这个函数时,TS 编译器不再抛出错误。
值得注意的是,可选参数必须位于必选参数列表的后面。
有默认参数的函数
当函数有可选参数时,我们必须检测参数是否被传递了。
在一些场景中,应为一个可选参数设置默认值。
function add(foo: number, bar: number, foobar?: number): number {
return foo + bar + (foobar !== undefined ? foobar : 0)
}
这个函数并没有错误,但是可以通过提供 foobar 参数的默认值,来代替标记其为可选参数,以改善其可读性。
function add(foo: number, bar: number, foobar: number = 0): number {
return foo + bar + foobar
}
只需要在声明函数时使用 " = " 操作符提供一个默认值,即可指定函数参数是可选的。TS 编译器会在 JS 输出结果中生成一个 if 结构,在 foobar 参数没有传递给函数时设置一个默认值。
function add(foo, bar, foobar) {
if (foobar === void 0) {
foobar = 0
}
return foo + bar + foobar
}
void 0 是 TS 编译器检测一个变量是否为 undefined 的用法。几乎所有开发者都使用 undefined 变量,几乎所有编译器都使用 void 0。
和可选参数一样,默认参数必须位于所有必选参数列表的后面。
有剩余参数的函数
已经学习了如何调用 add 函数并传递两个或三个参数,但是如果希望允许其他开发者传递四个或者五个参数呢?不得不再添加两个额外的默认参数或者可选参数。那如果希望允许开发者传递任意数量的参数呢?解决方案是使用剩余参数。剩余参数语法允许把不限量的参数表示为一个数组:
function add(...foo:number[]):number {
let result = 0
for (let i = 0, j = foo.length; i < j; i++) {
result += foo[i]
}
return result
}
看一下上述代码片段,用一个参数 foo 替换了 参数 foo、bar 和 foobar。参数 foo 前面有一个三个点的省略号。一个剩余参数必须包含一个数组类型,否则就会出现编译错误。现在可以以任意数量的参数调用 add 函数:
add() // 0
add(2) // 2
add(2, 2) // 4
add(2, 2, 2) // 6
add(2, 2, 2, 2) // 8
add(2, 2, 2, 2, 2) // 10
虽然没有具体的参数数量限制,理论上可以取数字类型的最大值。但实际上,这依赖于如何调用这个函数。
Javascript 函数有一个被称为 arguments 的内建对象,这个对象可以通过 arguments 局部变量取到。arguments 变量是一个非常像数组的对象,包含了调用函数时的所有参数。
- arguments 对象暴露出一些标准数组中的属性和方法,但不是全部
检查 Javascript 的输出会发现,Typescript 遍历 arguments 参数,以便将所有参数添加到 foo 变量中:
function add() {
let foo = []
for (let i = 0, j = arguments.length; i < j; i++) {
foo[i - 0] = arguments[i]
}
let result = 0
for (let i = 0, j = foo.length; i < j; i++) {
result += foo[i]
}
return result
}
虽然难以想象这个额外的遍历是否会成为一个性能瓶颈,但是如果认为这可能会对应用程序带来性能问题,应考虑不使用剩余参数而是只使用一个数组作为函数参数。
function add(foo: number[]):number {
let result = 0
for (let i =0, j = foo.length; i < j; i++) {
result += foo[i]
}
return result
}
上述代码只有一个包含了 number 类型的数组。调用 API 会和剩余参数有一些不同,但是可以去除对函数参数列表进行遍历的操作:
add() // 提供的参数不匹配函数的签名
add(2) // 提供的参数不匹配函数的签名
add(2, 2) // 提供的参数不匹配函数的签名
add(2, 2, 2) // 提供的参数不匹配函数的签名
add([]) // 返回0
add([2]) // 返回2
add([2, 2]) // 返回4
add([2, 2, 2]) // 返回6
函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
在 TypeScript 中,可以通过声明一个函数的所有函数签名,然后再将一个签名作为实现。
function test(name: string):string // 重载签名
function test(age: number): string // 重载签名
function test(single: boolean):string // 重载签名
function test(value: string | number | boolean): string { // 实现签名
switch (typeof value) {
case 'string':
return `My name is ${value}`
case 'number':
return `Im ${value} years old`
case 'boolean':
return value ? `I'm single` : `I'm not single`
default:
console.log('Invalid Operation!')
}
}
特定重载签名
interface Document {
createElement(tagName: 'div'):HTMLDivElement
createElement(tagName: 'span'):HTMLSpanElement
createElement(tagName: 'canvas'):HTMLCanvasElement
createElement(tagName: string): HTMLElement
}
为函数 createElement 声明了三个特定重载签名和一个非特定重载签名。
当在一个对象中声明特定签名时,这个对象中必须被赋予至少一个非特定重载签名。从上面例子中可以发现,createElement 属性属于一个包含了三种特定重载签名的类型,并被赋予到非特定重载签名中。
当编写重载声明时,必须在最后列出非重载签名。
- 也可以通过使用联合类型来创建有同样名称和参数数量但是参数类型不一样的函数
函数作用域
一些低级语言(C)包含了底层内存管理特性。在拥有更高层抽象的编程语言(Typescript)中,当变量被创建时,内存就已经被分配,并且在它们不被使用时会被清理掉。这个清理内存的过程被称为垃圾回收,由 Javascript 运行时的垃圾回收器实现。
垃圾回收器通常很高效,但希望它能帮我们处理所有内存泄漏时就会有问题。垃圾回收器会在变量脱离作用域时清理掉它们,所以理解 Typescript 的作用域的工作机制对我们来说是非常重要的,这样就能理解变量的生命周期。
一些编程语言使用程序的源代码结构来指定哪些变量被引用(词法作用域),另一些编程语言使用程序的运行时堆栈状态来指定哪些变量被引用(动态作用域)。主要的现代编程语言使用词法作用域(包括 Typescript),词法作用域往往比动态作用域更容易被人和分析工具理解。
在绝大多数词法作用域编程语言中,变量的作用域为代码块(一段花括号包起来的代码)。在 TS 和 JS 中,变量的作用域在一个函数中:
function foo():void {
if (true) {
var bar: number = 0
}
alert(bar)
}
foo()
上面这个 foo 函数包含了一个 if 结构。在 if 结构中声明了一个名为 bar 的数字类型的变量,之后试图通过 alert 函数显示这个变量的值。
可能会认为这段代码会在第 5 行报错,因为 bar 变量应该在该函数调用时超出作用域。但是,当调用这个 foo 函数的时候,alert 函数会显示变量的值,并且不报任何错误,因为所有函数中的变量都在整个函数体的作用域内,即使是在另一个代码块中(除了函数代码块之外)。
在运行时中,所有的变量声明都会在函数执行前移动到函数的顶端,这种行为被称为变量提升。
- TS 先被编译为 JS 代码然后再执行,这意味着 TS 应用在运行时是一个 JS 应用。因为这个原因,当提及 TS 运行时的时候,就是在说 JS 运行时。
在上述代码被执行之前,运行时会将变量声明提升到函数的顶部。
function foo():void {
var bar: number
if (true) {
bar = 0
}
alert(bar)
}
这意味着可以在变量声明前直接使用它:
function foo2():void {
bar = 0
var bar: number
alert(bar)
}
foo2()
在上述代码片段中,定义了一个函数 foo2,并且在它的函数体中,先给一个名为 bar 的变量赋值为 0 。此时,变量还没有被声明。在第二行,才实际声明了变量 bar 和它的类型。在最后一行,使用 alert 函数显示变量 bar 的值。
立即调用函数
立即调用函数(IIFE)是一种设计模式,使用函数作用域作为一个词法作用域。IIFE 可以被用于防止全局作用域中的变量提升导致的污染。举例如下:
var bar = 0; // 全局的
(function () {
var foo: number = 0 // 在函数作用域中
bar = 1 // 在全局作用域中
console.log(bar) // 1
console.log(foo) // 0
})()
console.log(bar) // 1
console.log(foo) // 错误
上述例子中,使用 IIFE 包装了两个变量的声明(foo 和 bar)。foo 变量作用于 IIFE 函数中并且在全局作用域中不可用,这解释了为什么当我们尝试在最后一行访问它的时候会产生一个错误。
也可以给 IIFE 传递一个变量,以便更好地控制在作用域之外创建的变量。
var bar = 0; // 全局的
(function (global) {
var foo: number = 0 // 在函数作用域中
bar = 1 // 在全局作用域中
console.log(global.bar) // 1
console.log(foo) // 0
})(this)
console.log(bar) // 1
console.log(foo) // 错误
这一次,因为没有在 IIFE 中调用 this 操作符,所以把指向全局作用域的 this 操作符作为唯一的参数传递给了 IIFE。在 IIFE 中,this 操作符传递来的参数称为 global,我们可以更好地控制全局作用域中声明的 bar 变量,而不是 foo 变量。
此外,IIFE 允许我们访问公开方法,隐藏函数内的私有变量:
class Counter {
private _i:number
constructor() {
this._i = 0
}
get():number {
return this._i
}
set(val: number): void {
this._i = val
}
increment(): void {
this._i ++
}
}
var counter = new Counter()
console.log(counter.get()) // 0
counter.set(2)
console.log(counter.get()) // 2
counter.increment()
console.log(counter.get()) // 3
console.log(counter._i) // 错误 _i 为私有属性
- 通常约定,TS 和 JS 开发者使用下划线开始的变量名作为私有变量名。
定义了一个名为 Counter 的类,它有一个数字类型的私有属性 _i。这个类也有 get 和 set 方法,用来设置和获取这个私有属性 _i 的值。还创建了 Counter 类的实例并调用了 set 、get 和 increment 方法来观察代码是否如设想般正常工作。如果尝试访问 Counter 类实例的 _i 属性,将得到一个错误,因为这个属性是私有的。
编译代码得到:
var Counter = /** @class */ (function () {
function Counter() {
this._i = 0;
}
Counter.prototype.get = function () {
return this._i;
};
Counter.prototype.set = function (val) {
this._i = val;
};
Counter.prototype.increment = function () {
this._i++;
};
return Counter;
}());
生成的 JS 代码在大部分场景下都能完美运行,但如果在浏览器内执行这段代码,创建一个 Counter 的实例并且访问它的 _i 属性,也不会遇到任何错误,因为 TS 不会生成运行时的私有属性。有时候需要编写在运行时拥有私有变量的函数,比如,假设要发布一个 JS 开发者使用的库,可以使用 IIFE 来模拟允许公共地访问一个方法,但在函数内部有一个私有的值:
var Counter = (function () {
var _i:number = 0
function Counter() {
}
Counter.prototype.get = function (){
return _i
}
Counter.prototype.set = function (val: number){
_i = val
}
Counter.prototype.increment = function () {
_i++
}
return Counter
})()
var a = new Counter()
console.log(a.get()) // 0
a.set(2)
console.log(a.get()) // 2
a.increment()
console.log(a.get()) // 3
console.log(a._i) // undefined
在上面例子中,所有的代码都跟 TS 生成的 JS 代码类似,除了 _i 并不是 Counter 类的属性,而是一个在 Counter 闭包内的变量。
- 闭包的职责是引用自由变量。换句话说,函数定义的位置所在的闭包会记住它所在的环境(作用域内的变量)
- 在一些场景下,对作用域和闭包进行精确的控制,代码会和编译后的JS很像。但记住,只要编写的组件是被其他TS组件使用的,就不需要为实现运行时私有属性而烦恼。
范型
DRY原则(don’t repeat yourself)旨在减少各种类型的信息重复。现在来看下什么是范型函数,并理解它是如何帮助遵守 DRY 原则的。
可以使用范型来避免代码重复。范型编程是一种程序语言的风格,它允许程序员使用以后才会定义的类型。我们将编写一个名为 getEntities 的范型函数,它接受两个参数:
function getEntities<T>(url: string, cb: (list : T[]) => void):void {
}
在函数名后面增加一对角括号来表明这是一个范型函数。如果角括号内是一个字符 T,它表示一个类型。函数的第一个参数是字符串类型的url,第二个参数是函数类型的cb,它接受一个 T 类型的参数作为唯一的参数。
tag 函数和标签模板
可以使用 tag 函数扩展和修改模板字符串的行为。在模板字符串上应用一个 tag 函数时,这个模板字符串就变成了标签模板。
将实现一个名为 htmlEscape 的 tag 函数。要使用 tag 函数,必须在 tag 函数后紧跟一个模板字符串:
一个标签模板必须返回一个字符串,并接受下面的参数:
- 第一个参数是一个数组,它包含了模板字符串中所有的静态字面量
- 剩余的参数是模板字符串中所有的变量
tag(literals: string[], ...values: any[]): string
var html = htmlEscape `<h1>${name}${username}</h1>`
function htmlEscape(literals, ...placeholders) {
let result = ''
for (let i = 0; i < placeholders.length; i++) {
result += literals[i]
result += placeholders[i]
.replace(/&/g, '$amp;')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>')
}
result += literals[literals.length - 1]
return result
}
上面的函数逐字迭代字符串和值来确保所有的 HTML 代码被正确转义,防止代码注入攻击。
使用 tag 函数最大的好处是它允许我们创建一个自定义的模板字符串处理器。
回调和高阶函数
在 TS 中,函数可以作为参数传给其他函数。被传递给其他函数的函数叫做回调。函数也可以被另一个函数返回。那些接受函数为参数或返回另一个函数的函数被称为高阶函数。回调通常被用在异步函数中:
var foo = function () {
// 回调
console.log('foo');
};
function bar(cb) {
// 高阶函数
console.log('bar');
cb();
}
bar(foo); // 输出 bar 然后 输出 foo
箭头函数
在 TS 中,可以使用 function 表达式或者箭头函数定义一个函数。箭头函数是 function 表达式的缩写,并且这种词法会在其作用域内绑定 this 操作符。
在 TS 中,this 操作符的行为和其他语言有一点不一样。当在 TS 中定义一个类的时候,可以使用 this 指向这个类自身的属性。看一个例子:
class Person {
name: string
constructor(name: string) {
this.name = name
}
greet() {
console.log(`My name is ${this.name}`)
}
}
let test = new Person('Test')
test.greet()
定义了一个 Person 类,并包含了一个名为 name 的字符串类型的属性。
这个类有一个构造函数和一个叫做 greet 的方法。可以新建一个名为 test 的实例,并调用 greet 方法,它在内部使用 this 操作符访问 test 的 name 属性。在 greet 方法内部,this 操作符指向装载了 greet 方法的对象。
必须谨慎使用 this 操作符,因为在一些场景下它将会指向错误的值:
class Person {
name: string
constructor(name: string) {
this.name = name
}
greet() {
console.log(`My name is ${this.name}`)
}
greetDelay(time: number) {
setTimeout(function () {
console.log(`My name is ${this.name}`)
}, time)
}
}
let test = new Person('Test')
test.greet()
test.greetDelay(1000)
在 greetDelay 中实现了一个和 greet 方法几乎相同的功能。但这个方法接受一个名为 time 的参数,它用来延迟问候信息的发出。
为了延迟问候消息,使用了 setTimeout 函数和一个回调函数。当定义了一个异步函数时(包含回调),this 关键字就会改变它指向的值,指向匿名函数。
箭头函数表达式的词法会绑定 this 操作符。这意味着可以增加函数而不用担心 this 的指向。现在将上面例子中的 function 表达式替换成箭头函数:
class Person {
name: string
constructor(name: string) {
this.name = name
}
greet() {
console.log(`My name is ${this.name}`)
}
greetDelay(time: number) {
setTimeout( () => {
console.log(`My name is ${this.name}`)
}, time)
}
}
let test = new Person('Test')
test.greet()
test.greetDelay(1000)
通过使用箭头函数,可以保证 this 操作符指向的是 Person 的实例而不是 setTimeout 的回调函数。
下面代码是 TS 编译生成的。当编译箭头函数时,TS 编译器会生成一个 this 的别名,名为 _this。这个别名用来确保 this 指向的是正确的对象。
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log("My name is " + this.name);
};
Person.prototype.greetDelay = function (time) {
var _this = this;
setTimeout(function () {
console.log("My name is " + _this.name);
}, time);
};
return Person;
}());
var test = new Person('Test');
test.greet();
test.greetDelay(1000);
回调地狱
回调的使用可能会导致称为回调地狱的维护性问题。
- 让代码难以理解
- 让代码难以维护(重构、重用等)
- 让异常处理更加困难
promise
promise 背后最主要的思想是对异步操作结果的一个承诺。一个 promise 一定是以下几种状态之一。
- pending:promise 的初始化状态
- fulfilled:代表异步操作成功的 promise 状态
- rejected:代表异步操作失败的 promise 状态
当一个 promise 处于 fulfilled 或 rejected 状态后,它的状态就永远不可更改了。看一下 promise 的基本语法:
function foo(value) {
return new Promise((fulfill, reject) => {
try {
fulfill(value)
}catch (e) {
reject(e)
}
})
}
foo().then(function (value) {
console.log(value)
}).catch(function (e) {
console.log(e)
})
- 这里的 try…catch 语句只是为了展示如何 fulfill 和 reject 一个 promise。在 Promise 函数内部,并不需要 try…catch 语句,因为 promise 会在有异常抛出的时候自动进入 rejected 状态。
- 上面代码声明了一个名为 foo 的函数,它返回一个 promise。一个 promise 包含了一个名为 then 的方法,它接受一个函数,在 promise 被 fulfilled 后调用。promise 还提供了一个名为 catch 的方法,它在 promise 被 rejected 之后调用。
使用 promise 可以在 render 方法中更好的控制执行流程。
- 并行:异步任务将会并行执行
- 串行:一组任务串行,前一个任务完成后不会传参给后一个任务
- 瀑布流:一组任务串行,每一个任务会将结果传到下一个任务中。这个实现在任务互相依赖时非常有用
- 混合:这是一种并行、串行和瀑布流的任意组合实现
生成器
如果在 TS 中调用一个函数,可以肯定一旦这个函数开始运行,在它运行完成之前其他代码都不能运行。然而,一种新的函数可能会在函数执行的过程中将这个函数暂停一次或多次,并在随后恢复它的运行,而且可以让其他代码在暂停的过程中运行,在 TS 何 ES6 中新型函数被称为生成器。
一个生成器代表了一个值的序列。生成器对象的接口只是一个迭代器,可以调用 next() 函数使它产出结果。
可以使用 function 关键字后面跟着一个星号定义一个生成器的构造函数。yield 关键字被用来暂停函数的执行并返回一个值:
function *foo() {
yield 1
yield 2
yield 3
yield 4
return 5
}
let a = new foo()
a.next() // { value: 1, done: false }
a.next() // { value: 2, done: false }
a.next() // { value: 3, done: false }
a.next() // { value: 4, done: false }
a.next() // { value: 5, done: true }
a.next() // { value: undefined, done: true }
这个迭代器有 5 个步骤。第一次调用 next() 的时候,函数会执行到第一个 yield 的位置,并且它会返回值1并且停止运行,直到 next() 再次被调用。现在可以终止函数的执行了。可以像下面这样编写一个无限循环而不会导致栈溢出:
function *foo() {
let i = 1
while (true) {
yield i++
}
}
let a = new foo()
a.next() // { value: 1, done: false }
a.next() // { value: 2, done: false }
a.next() // { value: 3, done: false }
a.next() // { value: 4, done: false }
a.next() // { value: 5, done: false }
a.next() // { value: 6, done: false }
a.next() // { value: 7, done: false }
生成器给了我们以同步的方式编写异步代码的可能性,只要我们在异步事件发生的时候调用生成器的 next() 方法就能做到这一点。