zoukankan      html  css  js  c++  java
  • JavaScript 标准参考教程-阅读总结(二)

    1、面向对象编程

      面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程,更适合多人合作的大型软件项目。

    1)构造函数

      典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)

      JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

    var Vehicle = function () {
      this.price = 1000;
    };

    上面代码中,Vehicle就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写

    构造函数的特点有两个。

    • 函数体内部使用了this关键字,代表了所要生成的对象实例。
    • 生成对象的时候,必须使用new命令。

    2)new命令

    使用new命令时,它后面的函数依次执行下面的步骤。

    1. 创建一个空对象,作为将要返回的对象实例。
    2. 将这个空对象的原型,指向构造函数的prototype属性。(通过同一个构造函数创建的所有对象继承自一个相同的对象)
    3. 将这个空对象赋值给函数内部的this关键字。
    4. 开始执行构造函数内部的代码。

    如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。使用new命令时,根据需要,构造函数可以接受参数。如果忘了使用new命令,直接调用构造函数,这种情况下,构造函数就变成了普通函数,并不会生成实例对象。this这时代表全局对象。

    function Vehicle(p) {
        this.price = p;
    }
    var v1 = new Vehicle(500); // Vehicle{price:500}
    var v2 = Vehicle(1000); // undefined
    price; // 1000(生成了一个全局变量price

    因此,应该非常小心,避免不使用new命令、直接调用构造函数。为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict(严格模式中,函数内部的this不能指向全局对象,默认等于undefined。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。

    function Vehicle(p) {
        'use strict';
        this.price = p;
    }
    var v = Vehicle(1000); // Uncaught TypeError: Cannot set property 'price' of undefined

     3)原型对象prototype

    通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。

    function Cat(name, color) {
      this.name = name;
      this.color = color;
      this.meow = function () {
        console.log('喵喵');
      };
    }
    
    var cat1 = new Cat('大毛', '白色');
    var cat2 = new Cat('二毛', '黑色');
    cat1.meow === cat2.meow // false

    上面代码中,cat1cat2是同一个构造函数的两个实例,它们都具有meow方法。由于meow方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。这个问题的解决方法,就是 JavaScript 的原型对象。

    JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系

    JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型

    function Animal(name) {
      this.name = name;
    }
    Animal.prototype.color = 'white';
    
    var cat1 = new Animal('大毛');
    var cat2 = new Animal('二毛');
    cat1.color // 'white'
    cat2.color // 'white'

    上面代码中,构造函数Animalprototype属性,就是实例对象cat1cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。

    原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。

    Animal.prototype.color = 'yellow';
    cat1.color // "yellow"
    cat2.color // "yellow"

    上面代码中,原型对象的color属性的值变为'yellow',两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法;如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法

    cat1.color = 'black';
    
    cat1.color // 'black'
    cat2.color // 'yellow'
    Animal.prototype.color // 'yellow';

    原型对象的作用,就是定义所有实例对象共享的属性和方法。

    4)原型链

    JavaScript 规定,所有对象都有自己的原型对象。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”。如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,Object.prototype没有原型

    读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”。

    5)constructor属性

    prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。

    function P() {}
    var p = new P();
    
    p.constructor === P // true
    p.constructor === P.prototype.constructor // true
    p.hasOwnProperty('constructor') // false

    上面代码中,p是构造函数P的实例对象,但是p自身没有constructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性。constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的

    constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错

    function Person(name) {
      this.name = name;
    }
    
    Person.prototype = {
      method: function () {}
    };
    Person.prototype.constructor === Object // true

    上面代码中,构造函数Person的原型对象改掉了,但是没有修改constructor属性,导致这个属性不再指向Person。由于Person的新原型是一个普通对象,而普通对象的contructor属性指向Object构造函数,导致Person.prototype.constructor变成了Object。所以,修改原型对象时,一般要同时修改constructor属性的指向。

    // 坏的写法
    C.prototype = {
      method1: function (...) { ... },
      // ...
    };
    
    // 好的写法
    C.prototype = {
      constructor: C,
      method1: function (...) { ... },
      // ...
    };
    
    // 更好的写法
    C.prototype.method1 = function (...) { ... };

    6)instanceof运算符

    instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象,是否在左边对象的原型链上。由于instanceof检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回true

    var d = new Date;
    d instanceof Date; // true
    d instanceof Object; // true
    d instanceof Number; // false
    var arr = [1,2,3];
    arr instanceof Array; // true
    arr instanceof Object; // true

    instanceof运算符的一个用处,是判断值的类型

    var x = [1, 2, 3];
    var y = {};
    x instanceof Array // true
    y instanceof Object // true

    利用instanceof运算符,还可以巧妙地解决,调用构造函数时,忘了加new命令的问题

    function Fubar (foo, bar) {
      if (this instanceof Fubar) {
        this._foo = foo;
        this._bar = bar;
      } else {
        return new Fubar(foo, bar);
      }
    }

    上面代码使用instanceof运算符,在函数体内部判断this关键字是否为构造函数Fubar的实例。如果不是,就表明忘了加new命令。

    7)Object对象的相关方法

    7.1)__proto__属性

    实例对象的__proto__属性(前后各两个下划线),返回该对象的原型。该属性可读写。根据语言标准,__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是Object.getPrototypeof()Object.setPrototypeOf(),进行原型对象的读写操作

    var obj = {};
    var p = {};
    obj.__proto__ = p;
    Object.getPrototypeOf(obj) === p // true

    7.2)Object.getPrototypeOf()、Object.setPrototypeOf()

    Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。Object.setPrototypeOf方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。

    var a = {};
    var b = {x: 1};
    Object.setPrototypeOf(a, b);
    
    Object.getPrototypeOf(a) === b // true
    a.x // 1

    上面代码中,Object.setPrototypeOf方法将对象a的原型,设置为对象b,因此a可以共享b的属性。

    7.3)Object.create()、Object.prototype.isPrototypeOf()

    JavaScript 提供了Object.create方法,该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

    // 原型对象
    var A = {
      print: function () {
        console.log('hello');
      }
    };
    // 实例对象
    var B = Object.create(A);
    
    Object.getPrototypeOf(B) === A // true
    B.print() // hello

    上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

    实例对象的isPrototypeOf方法,用来判断该对象是否为参数对象的原型。

    var o1 = {};
    var o2 = Object.create(o1);
    var o3 = Object.create(o2);
    
    o2.isPrototypeOf(o3) // true
    o1.isPrototypeOf(o3) // true

    上面代码中,o1o2都是o3的原型。这表明只要实例对象处在参数对象的原型链上,isPrototypeOf方法都返回true

    7.4)获取原型对象方法的比较

    获取实例对象obj的原型对象,有三种方法。

    • obj.__proto__
    • obj.constructor.prototype
    • Object.getPrototypeOf(obj)

    上面三种方法之中,前两种都不是很可靠。__proto__属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype在手动改变原型对象时,可能会失效

    var P = function () {};
    var p = new P();
    
    var C = function () {};
    C.prototype = p;
    var c = new C();
    c.constructor.prototype === p // false

    上面代码中,构造函数C的原型对象被改成了p,但是实例对象的c.constructor.prototype却没有指向p。所以,在改变原型对象时,一般要同时设置constructor属性

    C.prototype = p;
    C.prototype.constructor = C;
    var c = new C();
    c.constructor.prototype === p // true

     因此,推荐使用第三种Object.getPrototypeOf方法,获取原型对象

    2、面向对象编程的模式

    1)Function.prototype.call()

    var obj = {};
    var f = function () {
      return this;
    };
    f() === window // true
    f.call(obj) === obj // true

    上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f

    var obj = { n: 456 };
    function a() {
      console.log(this.n);
    }
    a.call(obj) // 456

    call方法还可以接受多个参数。call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数

    call方法的一个应用是调用对象的原生方法

    var obj = {};
    obj.hasOwnProperty('toString') // false
    
    // 覆盖掉继承的 hasOwnProperty 方法
    obj.hasOwnProperty = function () {
      return true;
    };
    obj.hasOwnProperty('toString') // true
    Object.prototype.hasOwnProperty.call(obj, 'toString') // false

    上面代码中,hasOwnPropertyobj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。

    2)构造函数的继承

    让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。

    第一步是在子类的构造函数中,调用父类的构造函数

    function Animal(name) {
        this.name = name;
    }
    function Dog(name, color) {
        Animal.call(this, name);
        this.color = color;
    }
    var d = new Dog('jack', 'black'); // Dog {name: "jack", color: "black"}

    上面代码中,Dog是子类的构造函数,this是子类的实例。在实例上调用父类的构造函数Animal,就会让子类实例具有父类实例的属性。

    第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型

    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog;
    Dog.prototype.speak = function() {
        console.log('汪汪');
    };

    上面代码中,Dog.prototype是子类的原型,要将它赋值为Object.create(Animal.prototype),而不是直接等于Animal.prototype。否则后面两行对Dog.prototype的操作,会连父类的原型Animal.prototype一起修改掉

    另外一种写法是Dog.prototype等于一个父类实例。

    Dog.prototype = new Animal('rose');

    上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法

    3)模块

    JavaScript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,JavaScript不是一种模块化编程语言,ES5不支持”类”,更遑论”模块”了。ES6正式支持”类”和”模块”,但还没有成为主流

    模块是实现特定功能的一组属性和方法的封装。

    3.1)使用“立即执行函数”,将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的

    var module1 = (function () {
     var _count = 0;
     var m1 = function () {
       //...
     };
     var m2 = function () {
      //...
     };
     return {
      m1 : m1,
      m2 : m2
     };
    })();
    console.info(module1._count); //undefined

    使用上面的写法,外部代码无法读取内部的_count变量。

    3.2)如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”

    var module1 = (function (mod){
     mod.m3 = function () {
      //...
     };
     return mod;
    })(module1);

    上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

    在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用”宽放大模式”

    var module1 = ( function (mod){
     //...
     return mod;
    })(window.module1 || {});

    与”放大模式”相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。

    3.3)输入全局变量

    独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

    var module1 = (function ($, YAHOO) {
     //...
    })(jQuery, YAHOO);

    上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

    3、异步操作

    1、同步任务和异步任务

      程序里面所有的任务,可以分成两类:同步任务和异步任务。同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数

    4、定时器

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

    1)setTimeout()

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

    setTimeout(func|code, delay);-》setTimeout函数接受两个参数,第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒数。

    如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象

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

    2)setInterval()

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

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

    var timer = setTimeout(function f() {
      // ...
      timer = setTimeout(f, 2000);
    }, 2000);
    // 下一次执行总是在本次执行结束之后的2000毫秒开始

    3)clearTimeout(),clearInterval()

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

    4)debounce

    有时,我们不希望回调函数被频繁调用。比如,用户填入网页输入框的内容,希望通过 Ajax 方法传回服务器,jQuery 的写法如下。

    $('textarea').on('keydown', ajaxAction);

    这样写有一个很大的缺点,就是如果用户连续击键,就会连续触发keydown事件,造成大量的 Ajax 通信。这是不必要的,而且很可能产生性能问题。正确的做法应该是,设置一个门槛值,表示两次 Ajax 通信的最小间隔时间。如果在间隔时间内,发生新的keydown事件,则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,再将数据发送出去。这种做法叫做 debounce(防抖动)。

    5)运行机制

      setTimeoutsetInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval指定的任务,一定会按照预定时间执行

    setTimeout(someTask, 100);
    veryLongTask();
    /*上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面的veryLongTask函数(同步任务)运行时间非常长,过了100毫秒还无法结束,
    那么被推迟运行的someTask就只有等着,等到veryLongTask运行结束,才轮到它执行。
    */

    6)setTimeout(f, 0)

    6.1)含义

      setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),那么会立刻执行吗?答案是不会。因为上一节说过,必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f。也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行

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

    6.2)应用

    setTimeout(f, 0)有几个非常重要的用途。它的一大应用是,可以调整事件的发生顺序。比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,想让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)

    // HTML 代码如下
    // <input type="button" id="myButton" value="click">
    
    var input = document.getElementById('myButton');
    input.onclick = function() {
      setTimeout(function() {
        // ...
      }, 0)
    };
    document.body.onclick = function() {
      // ...
    };

    另一个应用是,用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,keypress事件会在浏览器接收文本之前触发

    // HTML 代码如下
    // <input type="text" id="input-box">
    document.getElementById('input-box').onkeypress = function (event) {
      this.value = this.value.toUpperCase();
    }
    /*上面代码想在用户每次输入文本后,立即将字符转为大写。但是实际上,它只能将本次输入前的字符转为大写,因为浏览器此时还没接收到新的文本,
    所以this.value(event.target.value)取不到最新输入的那个字符。
    */ /*将代码放入setTimeout之中,就能使得它在浏览器接收到文本之后触发*/ document.getElementById('input-box').onkeypress = function() { var self = this; setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); }

    由于setTimeout(f, 0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f, 0)里面执行

    var div = document.getElementsByTagName('div')[0];
    
    // 写法一
    for (var i = 0xA00000; i < 0xFFFFFF; i++) {
      div.style.backgroundColor = '#' + i.toString(16);
    }
    
    // 写法二
    var timer;
    var i=0x100000;
    function func() {
      timer = setTimeout(func, 0);
      div.style.backgroundColor = '#' + i.toString(16);
      if (i++ == 0xFFFFFF) clearTimeout(timer);
    }
    timer = setTimeout(func, 0);

    上面代码有两种写法,都是改变一个网页元素的背景色。写法一会造成浏览器“堵塞”,因为 JavaScript 执行速度远高于 DOM,会造成大量 DOM 操作“堆积”,而写法二就不会,这就是setTimeout(f, 0)的好处。另一个使用这种技巧的例子是代码高亮的处理。如果代码块很大,一次性处理,可能会对性能造成很大的压力,那么将其分成一个个小块,一次处理一块,比如写成setTimeout(highlightNext, 50)的样子,性能压力就会减轻

    5、Promise 对象

    1)概述

    Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数

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

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

    上面代码中,f1的异步操作执行完成,就会执行f2。传统的写法可能需要把f2作为回调函数传入f1,比如写成f1(f2),异步操作完成后,在f1内部调用f2Promise 使得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);

    2、Promise 对象的状态

      Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态:异步操作未完成(pending)、异步操作成功(fulfilled)、异步操作失败(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实例的状态从“未完成”变为“成功”,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去

    4、Promise.prototype.then()

    Promise 实例的then方法,用来添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时时的回调函数,第二个是异步操作失败时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。

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

     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、Promise 的实例

    5.1)加载图片

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

    5.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);

    6)微任务

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

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

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

    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)在下一轮事件循环开始时执行。

  • 相关阅读:
    ZooKeeper概述(转)
    ZooKeeper典型应用场景(转)
    部署与管理ZooKeeper(转)
    Hbase分布式安装部署过程
    HBase安装
    使用info命令查看Redis信息和状态
    java定时调度器解决方案分类及特性介绍
    谈谈应用层切面设计
    七层协议和四层协议
    HTTP协议详解
  • 原文地址:https://www.cnblogs.com/colorful-coco/p/8884727.html
Copyright © 2011-2022 走看看