-
Promise(承诺)
基本概念
简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
从语法上说,Promise 是一个对象,
从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理 - Promise对象有以下两个特点对象的状态不受外界影响
Promise对象代表一个异步操作,
有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果
Promise对象的状态改变,只有两种可能:
从pending变为fulfilled和从pending变为rejected。
只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,
这时就称为 resolved(已定型)
如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果
这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的
resolved统一只指fulfilled状态(成功)
基本用法
const promise = new Promise((resolve, reject) => {
// … some code
if (/* 异步操作成功 */){
resolve(value)
} else {
reject(error)
}
})
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
它们是两个函数,由 JavaScript 引擎提供,不用自己部署
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved)
在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected)
在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
总结:
- resolve:成功之后的回调
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数
promise.then((response) => {
// success
}, (error) => {
// failure
})
then方法可以接受两个回调函数作为参数
第一个回调函数是Promise对象的状态变为resolved时调用
第二个回调函数是Promise对象的状态变为rejected时调用
其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数
let promise = new Promise((resolve, reject) => {
console.log(‘Promise’)
resolve()
})
promise.then(() => {
console.log(‘resolved.’)
})
console.log(‘Hi!’)
promise 新建后立即执行,所以首先输出的是Promise
然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出
Promise对象实现的 Ajax 操作
const getJSON = function(url) {
const promise = new Promise((resolve, reject) => {
const handler = function() {
if (this.readyState !== 4) {
return
}
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
const client = new XMLHttpRequest()
client.open(“GET”, url)
client.onreadystatechange = handler
client.responseType = “json”
client.setRequestHeader(“Accept”, “application/json”)
client.send()
})
return promise
}
getJSON(’./data.json’).then(function(json) {
console.log(\’Contents: ’ + json)
}, function(error) {
console.error(‘出错了’, error)
})
getJSON是对 XMLHttpRequest 对象的封装,
用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象
需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数
resolve用于成功处理,reject用于错误处理
如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数
reject函数的参数通常是Error对象的实例,表示抛出的错误
resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例
const p1 = new Promise((resolve, reject) => {
// …
})
const p2 = new Promise((resolve, reject) => {
// …
resolve(p1)
})
p1和p2都是 Promise 的实例,但是p2的resolve方法将p1作为参数,
即一个异步操作的结果是返回另一个异步操作
这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态
如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变
如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行
const p1 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error(‘fail’)), 3000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
注意,调用resolve或reject并不会终结 Promise 的参数函数的执行
new Promise((resolve, reject) => {
resolve(1)
console.log(2)
}).then(r => {
console.log®
})
一般来说,调用resolve或reject以后,Promise 的使命就完成了,
后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面
所以,最好在它们前面加上return语句,这样就不会有意外
new Promise((resolve, reject) => {
return resolve(1)
// 后面的语句不会执行
console.log(2)
}).then(r => {
console.log®
})
-
then方法
then方法是定义在原型对象Promise.prototype上的
它的作用是为 Promise 实例添加状态改变时的回调函数
then方法的第一个参数是resolved状态的回调函数,
第二个参数(可选)是rejected状态的回调函数
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)
因此可以链式调用
getJSON(’./data.json’).then((json) => {
return json.post;
}).then((post) => {
// …
}) -
catch方法
用于指定发生错误时的回调函数
getJSON(’./data.json’).then(json => {
return json.post
}).catch(error => {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log(‘发生错误!’, error)
})getJSON方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then方法指定的回调函数
如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。
另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获如果没有使用catch方法指定错误处理的回调函数
Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应
因此,建议总是使用catch方法,而不使用then方法的第二个参数 -
all方法
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.all([p1, p2, p3])
Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例p的状态由p1、p2、p3决定,分成两种情况
1.只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled
此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数2.只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,
此时第一个被reject的实例的返回值,会传递给p的回调函数 -
如果作为参数的 Promise 实例,自己定义了catch方法,
那么它一旦被rejected,并不会触发Promise.all()的catch方法const p1 = new Promise((resolve, reject) => {
resolve(‘hello’)
})
.then(result => result)
.catch(e => e)const p2 = new Promise((resolve, reject) => {
throw new Error(‘报错了’)
})
.then(result => result)
.catch(e => e)Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e))p1会resolved,p2首先会rejected,但是p2有自己的catch方法,
该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例
该实例执行完catch方法后,也会变成resolved,
导致Promise.all()方法参数里面的两个实例都会resolved,
因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数如果p2没有自己的catch方法,就会调用Promise.all()的catch方法
const p1 = new Promise((resolve, reject) => {
resolve(‘hello’)
})
.then(result => result)const p2 = new Promise((resolve, reject) => {
throw new Error(‘报错了’)
})
.then(result => result)Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e))
async + await
- 用法:
async自定义一个函数,然后函数体当中放异步请求,与await一起使用,然后用一个变量接收请求的结果
async function getData() {
let res = await getJSON(’./data.json’)
console.log(res)
}
getData()
async可以单独使用,但是await一定要与async一起使用
async function f() {
try {
await new Promise((resolve, reject) => {
throw new Error(‘出错了’);
})
} catch(e) {
}
return await(‘hello world’)
}
let foo = await getFoo()
let bar = await getBar()写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
class(类)
新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已
构造函数写法
function Person(name = ‘jack’, age = 18) {
this.name = name
this.age = age
}
Person.prototype.study = function () {
console.log(this.name + this.age)
}
let p = new Person()
p.study()
console.log(Person.prototype.constructor)
class类写法
class Person {
constructor(name = ‘jack’, age = 18) {
this.name = name
this.age = age
}
study() {
console.log(this.name + this.age)
}
}
let p = new Person()
p.study()
上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,this关键字则代表实例对象。
也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法
定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了
另外,方法之间不需要逗号分隔,加了会报错
ES6 的类,完全可以看作构造函数的另一种写法
typeof Person
Person.prototype.constructor
类的数据类型就是函数,类本身就指向构造函数
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致
构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面
class Person {
constructor() {
// …
}
toString() {
// …
}
toValue() {
// …
}
}
等同于
Person.prototype = {
constructor() {},
toString() {},
toValue() {},
}
在类的实例上面调用方法,其实就是调用原型上的方法
class B {}
let b = new B()
b.constructor === B.prototype.constructor
prototype对象的constructor属性,直接指向“类”的本身
constructor 方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法
一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加
class Person {
}
等同于
class Person {
constructor() {}
}
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象
class Foo {
constructor() {
return Object.create(null)
}
}
console.log(new Foo() instanceof Foo)
类必须使用new调用,否则会报错
类的实例
生成类的实例的写法,与 ES5 完全一样,也是使用new命令
实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
类的所有实例共享一个原型对象
let p1 = new Person(2,3)
let p2 = new Person(3,2)
p1.proto === p2.proto // true
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。
如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,
而是直接通过类来调用,这就称为“静态方法”
class Foo {
static classMethod() {
return ‘hello’
}
}
Foo.classMethod()
let foo = new Foo()
foo.classMethod()
如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法
如果静态方法包含this关键字,这个this指的是类,而不是实例
class Foo {
static bar() {
this.baz()
}
static baz() {
console.log(‘hello’)
}
baz() {
console.log(‘world’)
}
}
Foo.bar()
静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,
等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名
父类的静态方法,可以被子类继承
静态方法也是可以从super对象上调用的
class继承
class 可以通过extends关键字实现继承
class Person {
constructor(name = ‘jack’, age = 18) {
this.name = name
this.age = age
}
}
class Student extends Person {
constructor(name, age, score = 90) {
super(name, age) // 调用父类的constructor(x, y)
this.score = score
}
study () {}
}
通过extends关键字,继承了Person类的所有属性和方法
super关键字,它在这里表示父类的构造函数,用来新建父类的this对象
类必须在constructor方法中调用super方法,否则新建实例时会报错
这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,
然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象
class Person { /* … */ }
class Student extends Person {
constructor() {
}
}
let s = new Student()
ES6 的继承机制实质是先将父类实例对象的属性和方法,
加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
如果子类没有定义constructor方法,这个方法会被默认添加,
也就是说,不管有没有显式定义,任何一个子类都有constructor方法
class Student extends Person {
}
// 等同于
class Student extends Person {
constructor(…args) {
super(…args)
}
}
在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错
这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
class Student extends Person {
constructor(name, age, color) {
super(name, age);
this.color = color
}
}
父类的静态方法,也会被子类继承
super关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同
class A {}
class B extends A {
constructor() {
super()
}
}
子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错
super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,
因此super()在这里相当于A.prototype.constructor.call(this)
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错
super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类
class A {
p() {
return 2
}
}
class B extends A {
constructor() {
super()
console.log(super.p())
}
}
let b = new B()
子类B当中的super.p(),就是将super当作一个对象使用
这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()
由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
class A {
constructor() {
this.p = 2
}
}
class B extends A {
get m() {
return super.p
}
}
let b = new B()
console.log(b.m)
p是父类A实例的属性,super.p就引用不到它
如果属性定义在父类的原型对象上,super就可以取到
class A {}
A.prototype.x = 2
class B extends A {
constructor() {
super();
console.log(super.x)
}
}
let b = new B()
在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例
class A {
constructor() {
this.x = 1
}
print() {
console.log(this.x)
}
}
class B extends A {
constructor() {
super()
this.x = 2
}
m() {
super.print()
}
}
let b = new B()
console.log(b.m())
类的 prototype 属性和__proto__属性
- 子类的__proto__属性,表示构造函数的继承,总是指向父类
class A {
}
class B extends A {
}
B.proto === A
B.prototype.proto === A.prototype
class A {
}
A.proto === Function.prototype
A.prototype.proto === Object.prototype
模块化
ES6之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器
// CommonJS模块
module.exports 导出 require导入
let { stat, exists, readFile } = require(‘fs’)
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入
// ES6模块
import { stat, exists, readFile } from ‘fs’
import axios from “axios”
export命令
模块功能主要由两个命令构成:export和import
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取
如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
// profile.js
export let firstName = ‘Michael’
export let lastName = ‘Jackson’
export let year = 1958
ES6将其视为一个模块,里面用export命令对外部输出了三个变量
export的写法,除了像上面这样,还有另外一种
// profile.js
let firstName = ‘Michael’
let lastName = ‘Jackson’
let year = 1958
export {firstName, lastName, year}
export命令除了输出变量,还可以输出函数或类(class)
export function add(x, y) {
return x + y
}
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名
function v1() { … }
function v2() { … }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
}
上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
// 报错
export 1
// 报错
let m = 1
export m
// 写法一
export let m = 1
// 写法二
let m = 1
export {m}
// 写法三
let n = 1
export {n as m}
同样的,function和class的输出,也必须遵守这样的写法
// 报错
function f() {}
export f
// 正确
export function f() {}
// 正确
function f() {}
export {f}
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错
function foo() {
export default ‘bar’
}
foo()
import 命令
用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块
import {firstName, lastName, year} from ‘./profile.js’
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名
import { lastName as surname } from ‘./profile.js’
import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口
import {a} from ‘./xxx.js’
a = {} // Syntax Error : ‘a’ is read-only
脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的
import {a} from ‘./xxx.js’
a.foo = ‘hello’ // 合法操作
a的属性可以成功改写,并且其他模块也可以读到改写后的值
不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,轻易不要改变它的属性
mport后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。
如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置
import命令具有提升效果,会提升到整个模块的头部,首先执行
foo()
import { foo } from ‘my_module’
上面的代码不会报错,因为import的执行早于foo的调用
这种行为的本质是,import命令是编译阶段执行的,在代码运行之前
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构
// 报错
import { ‘f’ + ‘oo’ } from ‘my_module’;
// 报错
let module = ‘my_module’;
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from ‘module1’
} else {
import { foo } from ‘module2’
}
import语句会执行所加载的模块,因此可以有下面的写法
import ‘lodash’
import ‘./main.css’
如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面
import * as circle from ‘./circle’
export default 命令
使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载
export default命令,为模块指定默认输出
// export-default.js
export default function () {
console.log(‘foo’)
}
上面代码是一个模块文件export-default.js,它的默认输出是一个函数
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字
// import-default.js
import customName from ‘./export-default’
customName()
面代码的import命令,可以用任意名称指向export-default.js输出的方法,
这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号
export default命令用在非匿名函数前,也是可以的
// export-default.js
export default function foo() {
console.log(‘foo’)
}
// 或者写成
function foo() {
console.log(‘foo’)
}
export default 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命令后面才不用加大括号,因为只可能唯一对应export default命令
export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后
// 正确
export default 42
// 报错
export 42