美文网首页
promise 对象

promise 对象

作者: wit92 | 来源:发表于2020-06-19 21:38 被阅读0次

    1.概述

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

    注意,本章只是 Promise 对象的简单介绍。为了避免与后续教程的重复,更完整的介绍请看《ES6 标准入门》《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);
    

    上面代码中,f1的异步操作执行完成,就会执行f2

    传统的写法可能需要把f2作为回调函数传入f1,比如写成f1(f2),异步操作完成后,在f1内部调用f2。Promise 使得f1f2变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。

    // 传统写法
    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // ...
          });
        });
      });
    });
    
    // Promise 的写法
    (new Promise(step1))
      .then(step2)
      .then(step3)
      .then(step4);
    

    从上面代码可以看到,采用 Promises 以后,程序流程变得非常清楚,十分易读。注意,为了便于理解,上面代码的Promise实例的生成格式,做了简化,真正的语法请参照下文。

    总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promise 就是解决这个问题,使得异步流程可以写成同步流程。

    Promise 原本只是社区提出的一个构想,一些函数库率先实现了这个功能。ECMAScript 6 将其写入语言标准,目前 JavaScript 原生支持 Promise 对象。

    2.Promise 对象的状态

    Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。

    异步操作未完成(pending)
    异步操作成功(fulfilled)
    异步操作失败(rejected)
    

    上面三种状态里面,fulfilledrejected合在一起称为resolved(已定型)。

    这三种的状态的变化途径只有两种。

    从“未完成”到“成功”
    从“未完成”到“失败”
    

    一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。

    因此,Promise 的最终结果只有两种。

    异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
    异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。
    

    3.Promise 构造函数

    JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。

    var promise = new Promise(function (resolve, reject) {
      // ...
    
      if (/* 异步操作成功 */){
        resolve(value);
      } else { /* 异步操作失败 */
        reject(new Error());
      }
    });
    

    上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。

    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

    4.Promise.prototype.then()

    Promise 实例的then方法,用来添加回调函数。

    then方法可以接受两个回调函数:

    第一个是异步操作成功时(变为fulfilled状态)时的回调函数;

    第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。

    一旦状态改变,就调用相应的回调函数。

    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的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。

    then方法可以链式使用。

    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 对象的报错具有传递性。

    5.then() 用法辨析

    Promise 的用法,简单说就是一句话:使用then方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里?

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

    为了便于讲解,下面这四种写法都再用then方法接一个回调函数f3。写法一的f3回调函数的参数,是f2函数的运行结果。

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

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

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

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

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

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

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

    6.Promise 的实例

    6.1 加载图片

    我们可以把图片的加载写成一个Promise对象。

    var preloadImage = function (path) {
      return new Promise(function (resolve, reject) {
        var image = new Image();
        image.onload  = resolve;
        image.onerror = reject;
        image.src = path;
      });
    };
    

    6.2 Ajax 操作

    Ajax 操作是典型的异步操作,传统上往往写成下面这样。

    function search(term, onload, onerror) {
      var xhr, results, url;
      url = 'http://example.com/search?q=' + term;
    
      xhr = new XMLHttpRequest();
      xhr.open('GET', url, true);
    
      xhr.onload = function (e) {
        if (this.status === 200) {
          results = JSON.parse(this.responseText);
          onload(results);
        }
      };
      xhr.onerror = function (e) {
        onerror(e);
      };
    
      xhr.send();
    }
    
    search('Hello World', console.log, console.error);
    

    如果使用 Promise 对象,就可以写成下面这样。

    function search(term) {
      var url = 'http://example.com/search?q=' + term;
      var xhr = new XMLHttpRequest();
      var result;
    
      var p = new Promise(function (resolve, reject) {
        xhr.open('GET', url, true);
        xhr.onload = function (e) {
          if (this.status === 200) {
            result = JSON.parse(this.responseText);
            resolve(result);
          }
        };
        xhr.onerror = function (e) {
          reject(e);
        };
        xhr.send();
      });
    
      return p;
    }
    
    search('Hello World').then(console.log, console.error);
    

    加载图片的例子,也可以用 Ajax 操作完成。

    function imgLoad(url) {
      return new Promise(function (resolve, reject) {
        var request = new XMLHttpRequest();
        request.open('GET', url);
        request.responseType = 'blob';
        request.onload = function () {
          if (request.status === 200) {
            resolve(request.response);
          } else {
            reject(new Error('图片加载失败:' + request.statusText));
          }
        };
        request.onerror = function () {
          reject(new Error('发生网络错误'));
        };
        request.send();
      });
    }
    

    7.小结

    Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。

    而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。

    Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

    8. 微任务

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

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

    上面代码会先输出2,再输出1。因为console.log(2)是同步任务,而then的回调函数属于异步任务,一定晚于同步任务执行。

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

    setTimeout(function() {
      console.log(1);
    }, 0);
    
    new Promise(function (resolve, reject) {
      resolve(2);
    }).then(console.log);
    
    console.log(3);
    // 3
    // 2
    // 1
    

    上面代码的输出结果是321。这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行。

    严格模式

    除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。

    同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。

    1.设计目的

    早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。

    严格模式是从 ES5 进入标准的,主要目的有以下几个。

    • 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
    • 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
    • 提高编译器效率,增加运行速度。
    • 为未来新版本的 JavaScript 语法做好铺垫。

    总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。

    2.启用方法

    进入严格模式的标志,是一行字符串use strict

    'use strict';
    

    老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。

    严格模式可以用于整个脚本,也可以只用于单个函数。

    (1) 整个脚本文件

    use strict放在脚本文件的第一行,整个脚本都将以严格模式运行。如果这行语句不在第一行就无效,整个脚本会以正常模式运行。(严格地说,只要前面不是产生实际运行结果的语句,use strict可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面。)

    <script>
      'use strict';
      console.log('这是严格模式');
    </script>
    
    <script>
      console.log('这是正常模式');
    </script>
    

    上面代码中,一个网页文件依次有两段 JavaScript 代码。前一个<script>标签是严格模式,后一个不是。

    如果use strict写成下面这样,则不起作用,严格模式必须从代码一开始就生效。

    <script>
      console.log('这是正常模式');
      'use strict';
    </script>
    

    (2)单个函数

    use strict放在函数体的第一行,则整个函数以严格模式运行。

    function strict() {
      'use strict';
      return '这是严格模式';
    }
    
    function strict2() {
      'use strict';
      function f() {
        return '这也是严格模式';
      }
      return f();
    }
    
    function notStrict() {
      return '这是正常模式';
    }
    

    有时,需要把不同的脚本合并在一个文件里面。如果一个脚本是严格模式,另一个脚本不是,它们的合并就可能出错。严格模式的脚本在前,则合并后的脚本都是严格模式;如果正常模式的脚本在前,则合并后的脚本都是正常模式。这两种情况下,合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中

    (function () {
      'use strict';
      // some code here
    })();
    

    3.显式报错

    严格模式使得 JavaScript 的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错。

    3.1 只读属性不可写

    严格模式下,设置字符串的length属性,会报错。

    'use strict';
    'abc'.length = 5;
    // TypeError: Cannot assign to read only property 'length' of string 'abc'
    

    上面代码报错,因为length是只读属性,严格模式下不可写。正常模式下,改变length属性是无效的,但不会报错。

    严格模式下,对只读属性赋值,或者删除不可配置(non-configurable)属性都会报错。

    use strict';
    Object.defineProperty({}, 'a', {
      value: 37,
      writable: false
    });
    obj.a = 123;
    // TypeError: Cannot assign to read only property 'a' of object #<Object>
    
    // 删除不可配置的属性会报错
    'use strict';
    var obj = Object.defineProperty({}, 'p', {
      value: 1,
      configurable: false
    });
    delete obj.p
    // TypeError: Cannot delete property 'p' of #<Object>
    

    3.2 只设置了取值器的属性不可写

    严格模式下,对一个只有取值器(getter)、没有存值器(setter)的属性赋值,会报错。

    'use strict';
    var obj = {
      get v() { return 1; }
    };
    obj.v = 2;
    // Uncaught TypeError: Cannot set property v of #<Object> which has only a getter
    

    上面代码中,obj.v只有取值器,没有存值器,对它进行赋值就会报错。

    3.3 禁止扩展的对象不可扩展

    严格模式下,对禁止扩展的对象添加新属性,会报错。

    'use strict';
    var obj = {};
    Object.preventExtensions(obj);
    obj.v = 1;
    // Uncaught TypeError: Cannot add property v, object is not extensible
    

    上面代码中,obj对象禁止扩展,添加属性就会报错。

    3.4 eval、arguments 不可用作标识名

    严格模式下,使用eval或者arguments作为标识名,将会报错。下面的语句都会报错。

    'use strict';
    var eval = 17;
    var arguments = 17;
    var obj = { set p(arguments) { } };
    try { } catch (arguments) { }
    function x(eval) { }
    function arguments() { }
    var y = function eval() { };
    var f = new Function('arguments', "'use strict'; return 17;");
    // SyntaxError: Unexpected eval or arguments in strict mode
    

    3.5 函数不能有重名的参数

    正常模式下,如果函数有多个重名的参数,可以用arguments[i]读取。严格模式下,这属于语法错误。

    function f(a, a, b) {
      'use strict';
      return a + b;
    }
    // Uncaught SyntaxError: Duplicate parameter name not allowed in this context
    

    3.6 禁止八进制的前缀0表示法

    正常模式下,整数的第一位如果是0,表示这是八进制数,比如0100等于十进制的64。严格模式禁止这种表示法,整数第一位为0,将报错。

    'use strict';
    var n = 0100;
    // Uncaught SyntaxError: Octal literals are not allowed in strict mode.
    

    4.5 增强的安全措施

    严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误。

    4.1 全局变量显式声明

    正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。

    'use strict';
    
    v = 1; // 报错,v未声明
    
    for (i = 0; i < 2; i++) { // 报错,i 未声明
      // ...
    }
    
    function f() {
      x = 123;
    }
    f() // 报错,未声明就创建一个全局变量
    

    因此,严格模式下,变量都必须先声明,然后再使用。

    4.2 禁止 this 关键字指向全局对象

    正常模式下,函数内部的this可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。

    // 正常模式
    function f() {
      console.log(this === window);
    }
    f() // true
    
    // 严格模式
    function f() {
      'use strict';
      console.log(this === undefined);
    }
    f() // true
    

    上面代码中,严格模式的函数体内部thisundefined

    这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加new,这时this不再指向全局对象,而是报错。

    严格模式下,函数直接调用时(不使用new调用),函数内部的this表示undefined(未定义),因此可以用callapplybind方法,将任意值绑定在this上面。正常模式下,this指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而nullundefined这两个无法转成对象的值,将被忽略。

    // 正常模式
    function fun() {
      return this;
    }
    
    fun() // window
    fun.call(2) // Number {2}
    fun.call(true) // Boolean {true}
    fun.call(null) // window
    fun.call(undefined) // window
    
    // 严格模式
    'use strict';
    function fun() {
      return this;
    }
    
    fun() //undefined
    fun.call(2) // 2
    fun.call(true) // true
    fun.call(null) // null
    fun.call(undefined) // undefined
    

    上面代码中,可以把任意类型的值,绑定在this上面。

    4.3禁止使用 fn.callee、fn.caller

    函数内部不得使用fn.callerfn.arguments,否则会报错。这意味着不能在函数内部得到调用栈了。

    function f1() {
      'use strict';
      f1.caller;    // 报错
      f1.arguments; // 报错
    }
    
    f1();
    

    4.4禁止使用 arguments.callee、arguments.caller

    arguments.calleearguments.caller是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用arguments.calleearguments.caller将会报错。

    'use strict';
    var f = function () {
      return arguments.callee;
    };
    
    f(); // 报错
    

    4.5禁止删除变量

    严格模式下无法删除变量,如果使用delete命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除。

    'use strict';
    var x;
    delete x; // 语法错误
    
    var obj = Object.create(null, {
      x: {
        value: 1,
        configurable: true
      }
    });
    delete obj.x; // 删除成功
    

    5.静态绑定

    JavaScript 语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。

    严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。

    具体来说,涉及以下几个方面。

    5.1禁止使用 with 语句

    严格模式下,使用with语句将报错。因为with语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果。

    'use strict';
    var v  = 1;
    var obj = {};
    
    with (obj) {
      v = 2;
    }
    // Uncaught SyntaxError: Strict mode code may not include a with statement
    

    5.2 创设 eval 作用域

    正常模式下,JavaScript 语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:eval作用域。

    正常模式下,eval语句的作用域,取决于它处于全局作用域,还是函数作用域。严格模式下,eval语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,eval所生成的变量只能用于eval内部。

    (function () {
      'use strict';
      var x = 2;
      console.log(eval('var x = 5; x')) // 5
      console.log(x) // 2
    })()
    

    上面代码中,由于eval语句内部是一个独立作用域,所以内部的变量x不会泄露到外部。

    注意,如果希望eval语句也使用严格模式,有两种方式。

    // 方式一
    function f1(str){
      'use strict';
      return eval(str);
    }
    f1('undeclared_variable = 1'); // 报错
    
    // 方式二
    function f2(str){
      return eval(str);
    }
    f2('"use strict";undeclared_variable = 1')  // 报错
    

    上面两种写法,eval内部使用的都是严格模式。

    5.3 arguments 不再追踪参数的变化

    变量arguments代表函数的参数。严格模式下,函数内部改变参数与arguments的联系被切断了,两者不再存在联动关系。

    function f(a) {
      a = 2;
      return [a, arguments[0]];
    }
    f(1); // 正常模式为[2, 2]
    
    function f(a) {
      'use strict';
      a = 2;
      return [a, arguments[0]];
    }
    f(1); // 严格模式为[2, 1]
    

    上面代码中,改变函数的参数,不会反应到arguments对象上来。

    6.向下一个版本的 JavaScript 过渡

    JavaScript语言的下一个版本是 ECMAScript 6,为了平稳过渡,严格模式引入了一些 ES6 语法。

    6.1 非函数代码块不得声明函数

    ES6 会引入块级作用域。为了与新版本接轨,ES5 的严格模式只允许在全局作用域或函数作用域声明函数。也就是说,不允许在非函数的代码块内声明函数。

    'use strict';
    if (true) {
      function f1() { } // 语法错误
    }
    
    for (var i = 0; i < 5; i++) {
      function f2() { } // 语法错误
    }
    

    上面代码在if代码块和for代码块中声明了函数,ES5 环境会报错。

    注意,如果是 ES6 环境,上面的代码不会报错,因为 ES6 允许在代码块之中声明函数。

    6.2 保留字

    为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用这些词作为变量名将会报错。

    function package(protected) { // 语法错误
      'use strict';
      var implements; // 语法错误
    }
    

    相关文章

      网友评论

          本文标题:promise 对象

          本文链接:https://www.haomeiwen.com/subject/pyagxktx.html