AI智能
改变未来

js promise

  • 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:成功之后的回调
    • reject: 失败之后的回调

    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一起使用
    • 错误处理: try – catch
      async function f() {
      try {
      await new Promise((resolve, reject) => {
      throw new Error(‘出错了’);
      })
      } catch(e) {
      }
      return await(‘hello world’)
      }
    • 注意点:await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中
    • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发
      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
    • await命令只能用在async函数之中,如果用在普通函数,就会报错
  • 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__属性,表示构造函数的继承,总是指向父类
    • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性
      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

  • 赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » js promise