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

posted @ 2018-04-23 14:12  Colorful_coco  阅读(175)  评论(0编辑  收藏  举报