一、面向對象編程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)
在下一輪事件循環開始時執行。