《DApp设计与开发》TypeScript模块整理

161 阅读17分钟

ts建议看这个网站: ts.xcatliu.com/

TypeScript基础语法

◦ TypeScript的基础特性

从 TypeScript 的名字就可以看出来,「类型」是其最核心的特性。 JavaScript 是一门非常灵活的编程语言:没有类型约束、隐式类型转换、原型的面向对象编程,TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。

TypeScript 是静态类型
类型系统按照「类型检查的时机」来分类,可以分为动态类型静态类型
动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。 静态类型是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。
TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以TypeScript 是静态类型。

TypeScript 是弱类型
类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型弱类型。 ts是完全兼容js的,不会修改js运行时的特性,他们都是弱类型。python是强类型。

适用于任何模型
TS适合大项目和小项目

与标准同步发展
TypeScript坚持与 ECMAScript 标准[10]同步发展。

总结

image.png

◦ TypeScript如何编译运⾏

编译ts文件

tsc hello.ts

使用ts写React时,后缀变成.tsc
TypeScript 编译的时候即使报错了,还是会生成编译结果,如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 即可。

运行js

node  xxxx.js
  • ts转换类型上的错误不会影响到把ts转换为js,并且js可以运行
  • js是根据值推断类型 image.png

image.png

image.png

◦ TypeScript变量的定义、作⽤域及运⾏时类型

原始数据类型

JavaScript 的类型分为两种:原始数据类型和对象类型。

原始数据类型包括:布尔值、数值、字符串、nullundefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt

布尔值

let isDone: boolean = false;

构造函数创建的对象不是boolean值,而是boolean对象

let createdByNewBoolean: boolean = new Boolean(1);

直接调用 Boolean 也可以返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);

image.png

如果boolean不赋值就是undefined类型,两次取反后(!!)结果是false

数值

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

编译结果:

var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;

无论用什么类型去赋值,结果都是十进制

字符串

let myName: string = 'Tom';
let sentence: string = `Hello, my name is ${myName}.`

编译结果:

var myName = 'Tom';
var sentence = "Hello, my name is " + myName + "."

空值
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

function alertName(): void {
    alert('My name is Tom');
}

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null

let unusable: void = undefined;

Null 和 Undefined

与void不同,null和undefined是所有类型的子类型

// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;

而 void 类型的变量不能赋值给 number 类型的变量.

定义为undefined的打印出来是undefined类型,定义为null打印出来是object类型

  • 可以把null、undefined赋值给number,也就是说null、undefined可以赋值给其他类型
  • 如果输出一个没有定义的东西结果是undefined,如果输出一个定义但没有赋值的东西结果是null.
  • 声明变量时不赋值就是一个any类型的变量,如果是普通类型,赋值过程中改变类型是不被允许的,但是如果是any类型,则被允许赋值为任意类型。

在任意值上访问任何属性都是允许的,也允许调用任何方法,可以认为,声明一个变量为任意值之后,对他任何操作返回的内容类型都是任意值

  • 如果没有指定类型,那么typescript会依照推论的规则推断出一个类型。
  • 变量如果在声明的时候,未指定其类型,那么它会被识别为任意类型。
  • 联合类型表示取值可以为多种类型中的一种。联合类型使用|分隔每个类型。
  • 当ts不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

任意值
任意值可以用来表示允许赋值为任意类型。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

任意值上允许访问任何属性和调用任何方法。可以认为声明一个变量后,对他的任何操作返回的内容类型都是任意值。

未声明类型的变量
变量如果在声明的时候,未指定类型,那么他会被识别为任意类型

image.png

类型推论
如果没有明确的指定类型,ts会依照类型推论规则推断出一个类型。

let myFavoriteNumber = 'seven';

等价于

let myFavoriteNumber: string = 'seven';

联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

对象的类型-接口

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

接口一般首字母大写,定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的。
可选属性
有时我们希望不要完全匹配一个形状,那么可以用可选属性:

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};

不过仍然不允许添加未定义的属性

任意属性
我们希望一个接口允许有任意的属性,可以使用如下方式:

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

使用 [propName: string] 定义了任意属性取 string 类型的值。 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

上述代码就会因为age的值是number而报错。

只读属性
我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

数组的类型
「类型 + 方括号」表示法

let fibonacci: number[] = [1, 1, 2, 3, 5];

数组泛型(Array Generic) Array<elemType> 来表示数组

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

接口也可以用来描述数组

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

any 在数组中的应用:
一个比较常见的做法是,用 any 表示数组中允许出现任意类型:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

函数的类型
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

// 函数声明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

在 ES6 中,=> 叫做箭头函数,应用十分广泛

可选参数

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

可选参数必须接在必需参数后面。
参数默认值

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

不受「可选参数必须接在必需参数后面」的限制

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。

as 类型

类型断言的常见用途有以下几种:

  • 将一个联合类型断言为其中一个类型
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

这样就可以解决访问 animal.swim 时报错的问题了。需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误

  • 将一个父类断言为更加具体的子类
    当类之间有继承关系时,类型断言也是很常见的。不过此时用nstanceof更合适。如果是接口之间的话就只能用类型断言。

  • 将任何一个类型断言为 any

window.foo = 1;

上面的例子中,我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。

此时我们可以使用 as any 临时将 window 断言为 any 类型:

(window as any).foo = 1;

在 any 类型的变量上,访问任何属性都是允许的

  • 将 any 断言为一个具体的类型
function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

上面的例子中,我们调用完 getCacheData 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。

类型断言 vs 类型转换 类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:

function toBoolean(something: any): boolean {
    return something as boolean;
}

toBoolean(1);
// 返回值为 1

在编译后会变成:

function toBoolean(something) {
    return something;
}

toBoolean(1);
// 返回值为 1

若要进行类型转换,需要直接调用类型转换的方法:

function toBoolean(something: any): boolean {
    return Boolean(something);
}

toBoolean(1);
// 返回值为 true

声明语句

  • declare var/function/class/enum 声明全局变量/方法/类/枚举类型

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $ 或 jQuery 了。但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么东西。这时,我们需要使用 declare var 来定义它的类型

declare var jQuery: (selector: string) => any;

jQuery('#foo');

上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

jQuery('#foo');

声明文件
声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件,声明文件必需以 .d.ts 为后缀。

元组

直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

let tom: [string, number] = ['Tom', 25];

当赋值或访问一个已知索引的元素时,会得到正确的类型

let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

枚举
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}

我们也可以给枚举项手动赋值,未手动赋值的枚举项会接着上一个枚举项递增。

enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的,最好不要出现这种覆盖的情况。

常数枚举(不能包含计算成员)

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

外部枚举

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

declare 定义的类型只会用于编译时的检查,编译结果中会被删除

public private 和 protected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

当构造函数修饰为 protected 时,该类只允许被继承
构造函数修饰为 private 时,该类不允许被继承或者实例化:

抽象类 抽象类是不允许被实例化的,抽象类中的抽象方法必须被子类实现,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类.

类实现接口 用 implements 关键字来实现

interface Alarm {
    alert(): void;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}

class Car implements Alarm,Light  {
 alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

}

接口继承接口
接口与接口之间可以是继承关系

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

接口继承类

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

作用域

var global_num = 12          // 全局变量
class Numbers { 
   num_val = 13;             // 实例变量
   static sval = 10;         // 静态变量
   
   storeNum():void { 
      var local_num = 14;    // 局部变量
   } 
} 
console.log("全局变量为: "+global_num)  
console.log(Numbers.sval)   // 静态变量
var obj = new Numbers(); 
console.log("实例变量: "+obj.num_val)

JS 编译后

var global_num = 12; // 全局变量
var Numbers = /** @class */ (function () {
    function Numbers() {
        this.num_val = 13; // 实例变量
    }
    Numbers.prototype.storeNum = function () {
        var local_num = 14; // 局部变量
    };
    Numbers.sval = 10; // 静态变量
    return Numbers;
}());
console.log("全局变量为: " + global_num);
console.log(Numbers.sval); // 静态变量
var obj = new Numbers();
console.log("实例变量: " + obj.num_val);

◦ TypeScript异步语法

异步操作概述 - JavaScript 教程 - 网道 (wangdoc.com)

程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。

同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

异步操作的模式

回调函数

function f1() {
  // ...
}

function f2() {
  // ...
}

f1();
f2();

上面代码的问题在于,如果f1是异步操作,f2会立即执行,不会等到f1结束再执行。

这时,可以考虑改写f1,把f2写成f1的回调函数。

function f1(callback) {
  // ...
  callback();
}

function f2() {
  // ...
}

f1(f2);

事件监听

采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
首先,为f1绑定一个事件

f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2
f1进行改写:

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2

发布/订阅

事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。

首先,f2向信号中心jQuery订阅done信号。

jQuery.subscribe('done', f2);

然后,f1进行如下改写。

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。
f2完成执行后,可以取消订阅

jQuery.unsubscribe('done', f2);

异步操作的流程控制

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

上面代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。
如果有六个这样的异步任务,需要全部完成后,才能执行最后的final函数

function final(value) {
  console.log('完成: ', value);
}

async(1, function (value) {
  async(2, function (value) {
    async(3, function (value) {
      async(4, function (value) {
        async(5, function (value) {
          async(6, final);
        });
      });
    });
  });
});

运行结果:

// 参数为 1 , 1秒后返回结果
// 参数为 2 , 1秒后返回结果
// 参数为 3 , 1秒后返回结果
// 参数为 4 , 1秒后返回结果
// 参数为 5 , 1秒后返回结果
// 参数为 6 , 1秒后返回结果
// 完成:  12

串行执行

编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

function series(item) {
  if(item) {
    async( item, function(result) {
      results.push(result);
      return series(items.shift());
    });
  } else {
    return final(results[results.length - 1]);
  }
}

series(items.shift());

上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。

注意,上面的写法需要六秒,才能完成整个脚本。
结果:


参数为 1 , 1秒后返回结果
参数为 2 , 1秒后返回结果
参数为 3 , 1秒后返回结果
参数为 4 , 1秒后返回结果
参数为 5 , 1秒后返回结果
参数为 6 , 1秒后返回结果
完成:  12
Result的结果:[ 2, 4, 6, 8, 10, 12 ]

并行执行

流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

items.forEach(function(item) {
  async(item, function(result){
    results.push(result);
    if(results.length === items.length) {
      final(results[results.length - 1]);
    }
  })
});

上面代码中,forEach方法会同时发起六个异步任务,等到它们全部完成以后,才会执行final函数。

相比而言,上面的写法只要一秒,就能完成整个脚本。

并行与串行的结合

所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

function launcher() {
  while(running < limit && items.length > 0) {
    var item = items.shift();
    async(item, function(result) {
      results.push(result);
      running--;
      if(items.length > 0) {
        launcher();
      } else if(running == 0) {
        final(results);
      }
    });
    running++;
  }
}

launcher();

上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。

这段代码需要三秒完成整个脚本.

定时器

提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()setInterval()这两个函数来完成。它们向任务队列添加定时任务。

setTimeout()

setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

var timerId = setTimeout(func|code, delay);

第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒数。第二个参数如果省略,则默认为0。

setTimeout(function (a,b) {
  console.log(a + b);
}, 1000, 1, 1);

上面代码中,setTimeout共有4个参数。最后那两个参数,将在1000毫秒之后回调函数执行时,作为回调函数的参数。
一个需要注意的地方,如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象。

var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(obj.y, 1000) // 1

上面代码输出的是1,而不是2。因为当obj.y在1000毫秒后运行时,this所指向的已经不是obj了,而是全局环境。

防止出现这个问题,一种解决方法是将obj.y放入一个函数

var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(function () {
  obj.y();
}, 1000);
// 2

上面代码中,obj.y放在一个匿名函数之中,这使得obj.yobj的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值。

使用bind方法,将obj.y这个方法绑定在obj上面。

var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(obj.y.bind(obj), 1000)
// 2

setInterval()

setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行

setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。

clearTimeout(),clearInterval()

setTimeoutsetInterval函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeoutclearInterval函数,就可以取消对应的定时器。

var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);

clearTimeout(id1);
clearInterval(id2);

setTimeout(f, 0)

setTimeout(function () {
  console.log(1);
}, 0);
console.log(2);

上面代码先输出2,再输出1。因为2是同步任务,在本轮事件循环执行,而1是下一轮事件循环执行。

Promise 对象

Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

Promise 是一个对象,也是一个构造函数。

function f1(resolve, reject) {
  // 异步代码...
}

var p1 = new Promise(f1);

Promise构造函数接受一个回调函数f1作为参数,f1里面是异步操作的代码。然后,返回的p1就是一个 Promise 实例。

Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。

var p1 = new Promise(f1);
p1.then(f2);

Promise 构造函数

var promise = new Promise(function (resolve, reject) {
  // ...

  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});

resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100)

上面代码中,timeout(100)返回一个 Promise 实例。100毫秒以后,该实例的状态会变为fulfilled

Promise.prototype.then()

var p1 = new Promise(function (resolve, reject) {
  resolve('成功');
});
p1.then(console.log, console.error);
// "成功"

var p2 = new Promise(function (resolve, reject) {
  reject(new Error('失败'));
});
p2.then(console.log, console.error);
// Error: 失败

上面代码中,p1p2都是Promise 实例,它们的then方法绑定两个回调函数:成功时的回调函数console.log,失败时的回调函数console.error(可以省略)。p1的状态变为成功,p2的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。

p1
  .then(step1)
  .then(step2)
  .then(step3)
  .then(
    console.log,
    console.error
  );

上面代码中,p1后面有四个then,意味依次有四个回调函数。只要前一步的状态变为fulfilled,就会依次执行紧跟在后面的回调函数。

最后一个then方法,回调函数是console.logconsole.error,用法上有一点重要的区别。console.log只显示step3的返回值,而console.error可以显示p1step1step2step3之中任意一个发生的错误。

如果step1的状态变为rejected,那么step2step3都不会执行了(因为它们是resolved的回调函数)。Promise 开始寻找,接下来第一个为rejected的回调函数,在上面代码中是console.error。这就是说,Promise 对象的报错具有传递性。

then() 用法辨析

// 写法一
f1().then(function () {
  return f2();
});

// 写法二
f1().then(function () {
  f2();
});

// 写法三
f1().then(f2());

// 写法四
f1().then(f2);

接一个回调函数f3

f1().then(function () {
  return f2();
}).then(f3);

写法一的f3回调函数的参数,是f2函数的运行结果。

f1().then(function () {
  f2();
  return;
}).then(f3);

写法二的f3回调函数的参数是undefined

f1().then(f2())
  .then(f3);

写法三的f3回调函数的参数,是f2函数返回的函数的运行结果。

f1().then(f2)
  .then(f3);

写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果。

微任务

Promise 的回调函数属于异步任务,会在同步任务之后执行。

Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。

setTimeout(function() {
  console.log(1);
}, 0);

new Promise(function (resolve, reject) {
  resolve(2);
}).then(console.log);

console.log(3);

上面代码的输出结果是321