咱們先看看這幾個來自大廠的面試題javascript
面試題1:java
const promise = new Promise(function(resolve,reject){
console.log(1)
resolve()
console.log(2)
})
console.log(3)
複製代碼
面試題2:git
setTimeout(function () {
console.log(1);
}, 0)
new Promise(function (resolve) {
console.log(2);
for (var i = 0; i < 100; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
})
console.log(5);
複製代碼
面試題3:github
Promise.resolve(1)
.then((res)=>{
console.log(res)
return 2
})
.catch( (err) => 3)
.then(res=>console.log(res))。
複製代碼
面試題4:面試
Promise.resolve(1)
.then( (x) => x + 1 )
.then( (x) => {throw new Error('My Error')})
.catch( () => 1)
.then( (x) => x + 1)
.then((x) => console.log(x))
.catch( (x) => console.log(error))
複製代碼
若是你看完這些題一臉懵逼,恭喜你,你能夠繼續往下看了,else
,出門左拐大佬。編程
首先簡單介紹一些Promise
。promise
Promises
對象是CommonJS
工做組提出的一種規範,目的是爲異步操做提供統一接口。bash
那麼,什麼是Promises
?首先,它是一個對象,也就是說與其餘JavaScript
對象的用法,沒有什麼兩樣;其次,它起到代理做用(proxy)
,使得異步操做具有同步操做(synchronous code)
的接口,即充當異步操做與回調函數之間的中介,使得程序具有正常的同步運行的流程,回調函數沒必要再一層層包裹起來。app
簡單說,它的思想是,每個異步任務馬上返回一個Promise
對象,因爲是馬上返回,因此能夠採用同步操做的流程。異步
Promise
表示一個異步操做的最終結果,與之進行交互的方式主要是then
方法,該方法註冊了兩個回調函數,用於接收promise
的終值或本promise
不能執行的緣由。
下圖是一張Promise
的API結構圖。(引自https://github.com/leer0911/myPromise)
const promise = new Promise(function(resolve,reject){
console.log(1)
resolve()
console.log(2)
})
console.log(3)
// 1
// 2
// 3
複製代碼
解析:
Promise
相似於XMLHttpRequest
,從構造函數Promise
來建立一個新Promise
對象做爲接口。
要想建立一個Promise
對象嗎可使用new
來調用Promise
的構造器來進行實例化。
var promise = new Promise(function(resolve, reject) {
// 異步處理
// 處理結束後、調用resolve 或 reject
});
複製代碼
new Promise
的時候, 須要傳遞一個executor
執行器 ,執行器函數會默認被內部所執行。借用VSCode
編輯器咱們能夠看到一些內部的參數和返回值。
new Promise
內部的執行器會當即執行它裏面的代碼,這裏的resolve()
並不會阻塞下面的代碼執行,咱們能夠理解new Promise(function(){...})
這個就是一段同步代碼而已。因此第一道題的答案顯而易見。
鞏固一下,下面的代碼你必定能夠準確的得出結果了。
setTimeout(function () {
console.log('setTimeout')
}, 0);
let p = new Promise(function (resolve,reject) {
resolve();
console.log('a')
});
複製代碼
結果是:
// a
// setTimeout
複製代碼
這裏會有EventLoop
的一些知識,補充知識連接:說一說javascript的異步編程,到目前來講能夠獲得兩點:
1. 咱們徹底能夠把`new Promise(function(){...})`當作是同步代碼。
2. `Promise`會優先於`setTimeout`執行。
複製代碼
不一樣於老式的傳入回調,在應用 Promise
時,咱們將會有如下約定:
Promise
最直接的好處就是鏈式調用setTimeout(function () {
console.log(1);
}, 0)
new Promise(function (resolve) {
console.log(2);
for (var i = 0; i < 100; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
})
console.log(5);
// 2
// 3
// 5
// 4
// 1
複製代碼
解析:
能夠把 Promise
當作一個狀態機。初始是 pending
狀態,能夠經過函數 resolve
和 reject
,將狀態轉變爲 resolved
或者 rejected
狀態,狀態一旦改變就不能再次變化。
then
函數會返回一個 Promise
實例,而且該返回值是一個新的實例而不是以前的實例。由於 Promise 規範規定除了 pending
狀態,其餘狀態是不能夠改變的,若是返回的是一個相同實例的話,多個 then
調用就失去意義了。
MDN
中對於promise
的介紹一個小案例很是的簡單易懂,以下:
一個常見的需求就是連續執行兩個或者多個異步操做,這種狀況下,每個後來的操做都在前面的操做執行成功以後,帶着上一步操做所返回的結果開始執行。咱們能夠經過創造一個 Promise chain
來完成這種需求。
then
函數會返回一個新的 Promise
,跟原來的不一樣:
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
複製代碼
或者
const promise2 = doSomething().then(successCallback, failureCallback);
複製代碼
第二個對象(promise2)
不只表明doSomething()
函數的完成,也表明了你傳入的 successCallback
或者failureCallback
的完成,這也多是其餘異步函數返回的 Promise
。這樣的話,任何被添加給 promise2
的回調函數都會被排在 successCallback
或 failureCallback
返回的 Promise
後面。
特別重申一下,then
函數必定返回一個新的 Promise
,跟原來的不一樣,下面的是錯誤的應用:
Promise
對象的運行結果,最終只有兩種。
fulfilled
rejected
promise.then(onFulfilled, onRejected);
複製代碼
promise
對象的then
方法用來添加回調函數。它能夠接受兩個回調函數,第一個是操做成功(fulfilled
)時的回調函數,第二個是操做失敗(rejected
)時的回調函數(能夠不提供)。一旦狀態改變,就調用相應的回調函數。
onFulfilled
和 onRejected
都是可選參數。
若是 onFulfilled
是函數,當 promise
執行結束後其必須被調用,其第一個參數爲 promise
的終值,在 promise
執行結束前其不可被調用,其調用次數不可超過一次
若是 onRejected
是函數,當 promise
被拒絕執行後其必須被調用,其第一個參數爲 promise
的據因,在 promise
被拒絕執行前其不可被調用,其調用次數不可超過一次
onFulfilled
和 onRejected
只有在執行環境堆棧僅包含平臺代碼 ( 指的是引擎、環境以及 promise
的實施代碼 )時纔可被調用
實踐中要確保 onFulfilled
和 onRejected
方法異步執行,且應該在 then
方法被調用的那一輪事件循環以後的新執行棧中執行。
onFulfilled
和 onRejected
必須被做爲函數調用即沒有 this
值 ( 也就是說在 嚴格模式(strict
) 中,函數 this
的值爲 undefined
;在非嚴格模式中其爲全局對象。)
then
方法能夠被同一個 promise
調用屢次
then
方法必須返回一個 promise
對象
爲了不意外,即便是一個已經變成 resolve
狀態的 Promise
,傳遞給 then
的函數也老是會被異步調用:
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
複製代碼
至此,咱們再回過頭來看面試題:
setTimeout(function () {
console.log(1);
}, 0)
new Promise(function (resolve) {
console.log(2);
for (var i = 0; i < 100; i++) {
i == 99 && resolve();
}
console.log(3);
}).then(function () {
console.log(4);
})
console.log(5);
// 2
// 3
// 5
// 4
// 1
複製代碼
new Promise
構造器以後,會返回一個promise
對象,對於這個promise
對象,咱們調用他的then
方法來設置resolve
後的回調函數。
該promise
對象會在for
循環知足i==99
時被resolve()
,這時then
的回調函數會被調用。
基於第一道題的基礎,咱們知道最後被打印的必定是1
,而後是2
,接着是3
,因爲.then
是一個異步函數,用來接受promise
的返回結果,很顯然應該是5
,setTimeout
因爲EventLoop
的緣由是最後執行,因此後面是4
,最後是1
.
Promise.resolve(1)
.then((res)=>{
console.log(res)
return 2
})
.catch( (err) => 3)
.then(res=>console.log(res))
// 1
// 2
複製代碼
解析:
靜態方法Promise.resolve(value)
能夠認爲是new Promise()
方法的快捷方式。 好比Promise.resolve(1);
,能夠認爲是一下代碼的語法糖:
new Promise(function(resolve){
resolve(1);
});
複製代碼
在這段代碼中的 resolve(1);
會讓這個 promise
對象當即進入肯定(即resolved
)狀態, 並將 1
傳遞給後面then
裏所指定的 onFulfilled
函數。
方法Promise.resolve(value)
的返回值也是一個promise
對象,因此咱們能夠像下面那樣 接着對其返回值進行.then
調用。
Promise.resolve(42).then(function(value){
console.log(value)
})
複製代碼
簡單總結一下 Promise.resolve
方法的話,能夠認爲它的做用就是將傳遞給它的參數填 充(Fulfilled
)到promise
對象後並返回這個promise
對象。
Promise.resolve(value)
方法返回一個以給定值解析後的Promise
對象。但若是這個值是個thenable
(即帶有then
方法),返回的promise
會「跟隨」這個thenable
的對象,採用它的最終狀態(指resolved/rejected/pending/settled
);若是傳入的value
自己就是promise
對象,則該對象做爲Promise.resolve
方法的返回值返回;不然以該值爲成功狀態返回promise
對象。
語法爲:
Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable);
複製代碼
咱們知道.then()
一樣是返回一個promise
對象才能實現鏈式調用,因此連續的.then()
是一樣的道理,多個 then
方法調用串連在了一塊兒,各函數也會嚴 格按照 resolve → then → then → then
的順序執行,而且傳給每一個 then
方法的 value
的值都是前一個promise
對象經過 return
返回的值。而且上面已經提到then
方法每次都會建立並返回一個新的promise
對象。
then
方法執行完會判斷返回的結果,若是是promise
會把這個promise
執行,會取到它的結果。成功態(onFulfilled
)和失敗態(onRejected
)。若是成功了就會把成功的結果傳遞給後面then
裏所指定的 onFulfilled
函數,失敗了傳遞給onRejected
函數。
Promise.resolve(1)
.then((value)=>{
console.log('value',value)
},
(reason)=>{
console.log('reason',reason)
})
// value 1
複製代碼
Promise.reject(2)
.then((value)=>{
console.log('value',value)
},
(reason)=>{
console.log('reason',reason)
})
// reason 2
複製代碼
上面的兩個案例能夠很清楚的看到,promise
分別在成功態和失敗態的時候傳遞給後面的then
函數的調用輸出結果。下一層的then
是調成功仍是失敗是根據上面的promise
返回的是成功仍是失敗決定的。
說到這裏第三題的答案已經不用說了。
Promise.resolve(1)
.then( (x) => x + 1 )
.then( (x) => {throw new Error('My Error')})
.catch( () => 1)
.then( (x) => x + 1)
.then((x) => console.log(x))
.catch( (x) => console.log(error))
// 2
複製代碼
在一個失敗操做(即一個 catch
)以後能夠繼續使用鏈式操做,即便鏈式中的一個動做失敗以後還能有助於新的動做繼續完成。請閱讀下面的例子:
new Promise((resolve, reject) => {
console.log('Initial');
resolve();
})
.then(() => {
throw new Error('Something failed');
console.log('Do this');
})
.catch(() => {
console.log('Do that');
})
.then(() => {
console.log('Do this whatever happened before');
});
複製代碼
輸出結果以下:
Initial
Do that
Do this whatever happened before
複製代碼
注意,因爲「Something failed」
錯誤致使了拒絕操做,因此「Do this」
文本沒有被輸出。
解析:
這道題惟一的疑惑多是在第二個then
這裏,這個錯誤狀態爲何沒有被打印出來,咱們知道catch
是用來捕獲錯誤的,可是這裏catch
是能夠捕獲到錯誤的,可是這段代碼沒有對捕獲的錯誤進行處理而是繼續返回了1
做爲下一個Promise
的參數,因此在第三個then
中咱們獲取到了一個1
做爲成功態,而後又對其進行+1
處理返回給了下一個then
的promise
的成功態,這時候最後一個then
的第一個函數onFulfilled
就能獲取到一個value
打印出來就是2
,沒有錯誤信息返回因此最後的catch
沒有輸出。
咱們也能夠對上面的題改一種寫法,就是另外一種答案了:
Promise.resolve(1)
.then( (x) => x + 1 )
.then( (x) => {throw new Error('My Error')})
.catch( (err) => console.log(err))
.then( (x) => {
console.log(x)
return x + 1
})
.then((x) => console.log(x))
.catch( (x) => console.log(error))
// Error: My Error
at Promise.resolve.then.then
// undefined
// NaN
複製代碼
咱們在第一個catch
中對錯誤信息進行了處理,可是咱們沒有給下面的then
返回一個成功態的結果,因此默認是undefined
,這樣就會致使後面的結果徹底不同。
你們能夠把這些面試題進行屢次變形,改寫來去理解promise
的執行順序,以及參數傳遞,這樣就能繞過更多的坑。
以上內容若是錯誤,歡迎指正,共同進步~