ts建议看这个网站: ts.xcatliu.com/
TypeScript基础语法
◦ TypeScript的基础特性
从 TypeScript 的名字就可以看出来,「类型」是其最核心的特性。 JavaScript 是一门非常灵活的编程语言:没有类型约束、隐式类型转换、原型的面向对象编程,TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。
TypeScript 是静态类型
类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。
动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。
静态类型是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。
TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以TypeScript 是静态类型。
TypeScript 是弱类型
类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。
ts是完全兼容js的,不会修改js运行时的特性,他们都是弱类型。python是强类型。
适用于任何模型
TS适合大项目和小项目
与标准同步发展
TypeScript坚持与 ECMAScript 标准[10]同步发展。
总结
◦ TypeScript如何编译运⾏
编译ts文件
tsc hello.ts
使用ts写React时,后缀变成.tsc
TypeScript 编译的时候即使报错了,还是会生成编译结果,如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json
中配置 noEmitOnError
即可。
运行js
node xxxx.js
- ts转换类型上的错误不会影响到把ts转换为js,并且js可以运行
- js是根据值推断类型
◦ TypeScript变量的定义、作⽤域及运⾏时类型
原始数据类型
JavaScript 的类型分为两种:原始数据类型和对象类型。
原始数据类型包括:布尔值、数值、字符串、null
、undefined
以及 ES6 中的新类型 Symbol
和 ES10 中的新类型 BigInt
。
布尔值
let isDone: boolean = false;
构造函数创建的对象不是boolean值,而是boolean对象
let createdByNewBoolean: boolean = new Boolean(1);
直接调用 Boolean
也可以返回一个 boolean
类型:
let createdByBoolean: boolean = Boolean(1);
如果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;
任意值上允许访问任何属性和调用任何方法。可以认为声明一个变量后,对他的任何操作返回的内容类型都是任意值。
未声明类型的变量
变量如果在声明的时候,未指定类型,那么他会被识别为任意类型
类型推论
如果没有明确的指定类型,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.y
在obj
的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值。
使用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()
setTimeout
和setInterval
函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout
和clearInterval
函数,就可以取消对应的定时器。
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: 失败
上面代码中,p1
和p2
都是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.log
和console.error
,用法上有一点重要的区别。console.log
只显示step3
的返回值,而console.error
可以显示p1
、step1
、step2
、step3
之中任意一个发生的错误。
如果step1
的状态变为rejected
,那么step2
和step3
都不会执行了(因为它们是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