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
命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
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
方法,获取原型对象。
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
上面代码中,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
。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
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()
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)
的样子,性能压力就会减轻。
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
内部调用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);
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
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 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.log
和console.error
,用法上有一点重要的区别。console.log
只显示step3
的返回值,而console.error
可以显示p1
、step1
、step2
、step3
之中任意一个发生的错误。举例来说,如果step1
的状态变为rejected
,那么step2
和step3
都不会执行了(因为它们是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)
在下一轮事件循环开始时执行。