一、面向对象编程ajax
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。面向对象编程具备灵活、代码可复用、高度模块化等特色,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程,更适合多人合做的大型软件项目。编程
1)构造函数promise
典型的面向对象编程语言(好比 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。可是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。浏览器
JavaScript 语言使用构造函数(constructor)做为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,能够生成多个实例对象,这些实例对象都有相同的结构。服务器
var Vehicle = function () { this.price = 1000; };
上面代码中,Vehicle
就是构造函数。为了与普通函数区别,构造函数名字的第一个字母一般大写。异步
构造函数的特色有两个。编程语言
this
关键字,表明了所要生成的对象实例。new
命令。2)new命令模块化
使用new
命令时,它后面的函数依次执行下面的步骤。函数
prototype
属性。(经过同一个构造函数建立的全部对象继承自一个相同的对象)this
关键字。若是构造函数内部有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
上面代码中,cat1
和cat2
是同一个构造函数的两个实例,它们都具备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'
上面代码中,构造函数Animal
的prototype
属性,就是实例对象cat1
和cat2
的原型对象。原型对象上添加一个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
上面代码中,o1
和o2
都是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
方法,获取原型对象。
二、面向对象编程的模式
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
上面代码中,hasOwnProperty
是obj
对象继承的方法,若是这个方法一旦被覆盖,就不会获得正确结果。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
。这样作除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
三、异步操做
一、同步任务和异步任务
程序里面全部的任务,能够分红两类:同步任务和异步任务。同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务能够执行了(好比 Ajax 操做从服务器获得告终果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会立刻运行,也就是说,异步任务不具备”堵塞“效应。举例来讲,Ajax 操做能够看成同步任务处理,也能够看成异步任务处理,由开发者决定。若是是同步任务,主线程就等着 Ajax 操做返回结果,再往下执行;若是是异步任务,主线程在发出 Ajax 请求之后,就直接往下执行,等到 Ajax 操做有告终果,主线程再执行对应的回调函数。
四、定时器
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()
setTimeout
和setInterval
函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout
和clearInterval
函数,就能够取消对应的定时器。
4)debounce
有时,咱们不但愿回调函数被频繁调用。好比,用户填入网页输入框的内容,但愿经过 Ajax 方法传回服务器,jQuery 的写法以下。
$('textarea').on('keydown', ajaxAction);
这样写有一个很大的缺点,就是若是用户连续击键,就会连续触发keydown
事件,形成大量的 Ajax 通讯。这是没必要要的,并且极可能产生性能问题。正确的作法应该是,设置一个门槛值,表示两次 Ajax 通讯的最小间隔时间。若是在间隔时间内,发生新的keydown
事件,则不触发 Ajax 通讯,而且从新开始计时。若是过了指定时间,没有发生新的keydown
事件,再将数据发送出去。这种作法叫作 debounce(防抖动)。
5)运行机制
setTimeout
和setInterval
的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。若是到了,就执行对应的代码;若是不到,就继续等待。这意味着,setTimeout
和setInterval
指定的回调函数,必须等到本轮事件循环的全部同步任务都执行完,才会开始执行。因为前面的任务到底须要多少时间执行完,是不肯定的,因此没有办法保证,setTimeout
和setInterval
指定的任务,必定会按照预约时间执行。
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)
的样子,性能压力就会减轻。
五、Promise 对象
1)概述
Promise 对象是 JavaScript 的异步操做解决方案,为异步操做提供统一接口。Promise 可让异步操做写起来,就像在写同步操做的流程,而没必要一层层地嵌套回调函数。
Promise 的设计思想是,全部异步任务都返回一个 Promise 实例。Promise 实例有一个then
方法,用来指定下一步的回调函数。
var p1 = new Promise(f1); p1.then(f2);
上面代码中,f1
的异步操做执行完成,就会执行f2
。传统的写法可能须要把f2
做为回调函数传入f1
,好比写成f1(f2)
,异步操做完成后,在f1
内部调用f2
。Promise 使得f1
和f2
变成了链式写法。不只改善了可读性,并且对于多层嵌套的回调函数尤为方便。
// 传统写法 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);
二、Promise 对象的状态
Promise 对象经过自身的状态,来控制异步操做。Promise 实例具备三种状态:异步操做未完成(pending)、异步操做成功(fulfilled)、异步操做失败(rejected)。这三种的状态的变化途径只有两种:从“未完成”到“成功”、从“未完成”到“失败”。一旦状态发生变化,就凝固了,不会再有新的状态变化。
三、Promise 构造函数
JavaScript 提供原生的Promise
构造函数,用来生成 Promise 实例。
var promise = new Promise(function (resolve, reject) { // ... if (/* 异步操做成功 */){ resolve(value); } else { /* 异步操做失败 */ reject(new Error()); } });
上面代码中,Promise
构造函数接受一个函数做为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用本身实现。resolve
函数的做用是,将Promise
实例的状态从“未完成”变为“成功”,在异步操做成功时调用,并将异步操做的结果,做为参数传递出去。reject
函数的做用是,将Promise
实例的状态从“未完成”变为“失败”,在异步操做失败时调用,并将异步操做报出的错误,做为参数传递出去。
四、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.log
和console.error
,用法上有一点重要的区别。console.log
只显示step3
的返回值,而console.error
能够显示p1
、step1
、step2
、step3
之中任意一个发生的错误。举例来讲,若是step1
的状态变为rejected
,那么step2
和step3
都不会执行了(由于它们是resolved
的回调函数)。Promise 开始寻找,接下来第一个为rejected
的回调函数,在上面代码中是console.error
。这就是说,Promise 对象的报错具备传递性。
五、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)
在下一轮事件循环开始时执行。