JavaScript 標準參考教程-閱讀總結(二)

一、面向對象編程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命令時,它後面的函數依次執行下面的步驟。函數

  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方法,獲取原型對象

 

二、面向對象編程的模式

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。這樣作除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯。

 

三、異步操做

一、同步任務和異步任務

  程序裏面全部的任務,能夠分紅兩類:同步任務和異步任務。同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行後一個任務。異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認爲某個異步任務能夠執行了(好比 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()

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)的樣子,性能壓力就會減輕

 

五、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);

二、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構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolvereject。它們是兩個函數,由 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.logconsole.error,用法上有一點重要的區別。console.log只顯示step3的返回值,而console.error能夠顯示p1step1step2step3之中任意一個發生的錯誤。舉例來講,若是step1的狀態變爲rejected,那麼step2step3都不會執行了(由於它們是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)在下一輪事件循環開始時執行。

相關文章
相關標籤/搜索