JavaScript 入門:實現非同步函式:callback、promise、async/await

更新 發佈閱讀 10 分鐘

現代 JavaScript 開發中,「非同步操作」幾乎無所不在,例如網路請求、定時器、讀取檔案等。如果沒有良好的非同步處理方式,程式碼很容易變得混亂、難以維護。

JavaScript 的非同步處理方式經歷了三個重要階段:

  • Callback(回呼函式)
  • Promise
  • Async / Await

本文將從最原始的 Callback 開始,逐步介紹 Promise 與 Async/Await 的演進過程,幫助你建立完整的非同步觀念,理解它們之間的差異與使用時機。


Callback

接著我們來看一下最原始的非同步函式長啥樣:

function fetchData(callback){
setTimeout(()=>{
callback("取回資料");
},2000)
}
fetchData(function(data){
console.log(data);
});

在這段範例中:

  1. fetchData 是一個高階函式
  2. 傳入的匿名函式是回呼函式
  3. 回呼函式會在 2000 毫秒後執行

Callback hell

用 callback 實現非同步操作乍看之下沒有什麼問題,但是當流程變多時,你的程式碼可能會變成所謂的 callback hell。假設我們要模擬一個「早上起床到出門」

以「起床到出門」的流程為例:

setTimeout(()=>{
console.log("1. 起床了");

setTimeout(()=>{
console.log("2. 刷完牙了");

setTimeout(()=>{
console.log("3. 吃完早餐了");

setTimeout(()=>{
console.log("4. 換好外出服了");

setTimeout(()=>{
console.log("5. 終於出門了")

},1000)
},1000)
},1000)
},1000)
},1000)

為何這被稱為「地獄」?

  1. 難閱讀:程式碼在多個非同步函式中不斷向右縮排,邏輯嵌套太深,一眼看不出流程。
  2. 難維護:如果現在想要在「吃完早餐」與「換好衣服」中間加入一個「整理背包」,必須要拆解中間的嵌套,非常容易出錯。
  3. 錯誤處理困難:如果要在每一層都加錯誤判斷(例如:牙膏沒了、早餐燒焦等),程式碼就會變得超腫超亂。

雖然這麼做還是可以達成我們想要的非同步效果,但是這種寫法長期下來會讓你非常痛苦。為了解決這樣的問題,JS 推出了 Promise


Promise

Promise 是 ES6 引入的一個建構函式,用來表示一個「還沒有結果,但是未來一定會有結果的值」,用於處理非同步操作。

諸如網路請求、檔案讀寫、定時器等,都是 JavaScript 中的非同步操作,為了提升使用者體驗,需要一種機制來處理非同步操作的結果,Promise 就是為了因應這個問題而生,Promise 將使用 callback 的「橫向嵌套」程式碼變成了「縱向」結構。

Promise 的三種狀態

因為 Promise 是用來表示一個「尚未完成,但最後一定會得到結果」的值,所以 Promise 會有三種狀態:

  • Pending: 初始的狀態,代表尚未完成或失敗
  • Fulfilled: 操作成功,並返回結果
  • Rejected: 操作失敗,並返回錯誤原因

一旦 promise 的狀態從 pending 變成 fulfilled 或 rejected,就不會再改變了。

建立 Promise

這邊以一個 delay function 為例,傳入毫秒做為參數,並返回一個 Promise。

function delay(ms){
    return new Promise((resolve, reject)=>{
        if(typeof ms !== 'number'){
            reject("錯誤:請輸入數字");
            return;
        }
        setTimeout(()=>{
            resolve(`等待了 ${ms} 毫秒,執行完畢`);
        }, ms);
    });
}
  1. new 關鍵字建立 Promise 物件
  2. Promise 物件會接收一個叫做 executor 的函式做為參數。
  3. executor 又接收 resolvereject 兩個函式做為參數,分別對應著請求成功請求失敗時要調用的函式(executor 會在 promise 建立時立刻執行,並在適當時機呼叫 resolvereject 傳)。
  • 請求成功 ⭢ 調用 resolve 函式
  • 請求失敗 ⭢ 調用 reject 函式

使用 Promise

有了 Promise 物件,要用 .then().catch().finally() 處理結果。

  1. .then():當Promise 變成 Fullfilled 時執行,它可以接收 resolve 傳出的資料
  2. .catch():當 Promise 變成 Rejected 時執行,它可以接收 reject 傳出的錯誤訊息
  3. .finally():無論成功或失敗,最終都會執行。常用於關閉載入動畫
delay(1000).then(msg =>{
    console.log(msg); // 等待了 1000 毫秒,執行完畢
}).catch(err=>{
    console.error(err); // 錯誤:請輸入數字
}).finally(()=>{
    console.log("結束");
})

resolve/reject 與 .then()/.catch() 差異

  • resolve/ reject 在 Promise 內部,用來決定 Promise 最終狀態 ⮕ 用來製造 Promise
  • .then()/.catch() 在 Promise 外部,用來處理 Promise 最終狀態 ⮕ 用來使用 Promise

Async/Await

async/await 是 Promise 的語法糖,用簡潔值觀的方式處理非同步操作,讓非同步「看起來」像是同步操作。

async

  • async 關鍵字將函式標記為非同步,並且返回一個 Promise。

await

  • await 是一個運算子,通常搭配 async function 一起用。(在 ES2022 之前 await 只能在 async function 中, ES2022+ 可以在 ES module 的頂層使用)
  • 使用 await 時會暫停 async function 執行,直到 Promise 解析完成(得到 resolved/ rejected)
  • await 只會暫停 async 函式內部,因此不會阻塞整個 JS 執行緒

範例 - 將使用 promise 的範例改為 async/await

async function runTimer(ms) {
    try{
        const result = await delay(ms);
        console.log(result);
    }catch(err){
        console.error(err);
    }finally{
        console.log("結束");
    }
}
runTimer(1000);

結論

JavaScript 的非同步處理方式從 Callback 發展到 Promise,再到現代常用的 Async/Await,本質上都是為了解決「等待任務完成後再繼續執行」的問題。

三者的關係可簡單理解為:

  • Callback:最原始的非同步寫法,但容易產生 callback hell
  • Promise:改善巢狀結構,提供相對清晰的流程與錯誤處理
  • Async/Await:基於 Promise 的語法糖,讓非同步程式碼更接近同步思維

實務開發中,Async/Await 已經成為主流寫法,但理解 Callback 與 Promise 的運作原理,還是很重要,因為 Async/Await 的底層機制是建立在 Promise 之上。


​參考

  1. Promise 是什麼?有什麼用途?
  2. Promise
  3. JavaScript 中的 async/await 是什麼?和 promise 有什麼差別?
留言
avatar-img
Elaine 粼粼的林林總總
7會員
39內容數
不定期地分享程式/旅遊/學習/閱讀或各式各樣的文章,如果對我的分享有興趣,歡迎來找我玩~
2026/02/14
本文介紹 JavaScript 中的同步與非同步概念,透過簡單的程式範例說明兩者的執行順序差異,並解釋為什麼 setTimeout 會在主程式執行完後才觸發回呼函式。說明非同步的重要性及它如何避免耗時任務阻塞主執行緒。
2026/02/14
本文介紹 JavaScript 中的同步與非同步概念,透過簡單的程式範例說明兩者的執行順序差異,並解釋為什麼 setTimeout 會在主程式執行完後才觸發回呼函式。說明非同步的重要性及它如何避免耗時任務阻塞主執行緒。
2026/02/10
為什麼說 JavaScript 函式是一等公民 ? 本篇文章透過超簡單白話文範例,帶你理解一級函式、高階函式 (Higher-order function) 與回呼函式 (Callback function) 的定義與關係。學會如何將函式當作參數傳遞與回傳,並為接下來的非同步程式設計打下堅實基礎!
2026/02/10
為什麼說 JavaScript 函式是一等公民 ? 本篇文章透過超簡單白話文範例,帶你理解一級函式、高階函式 (Higher-order function) 與回呼函式 (Callback function) 的定義與關係。學會如何將函式當作參數傳遞與回傳,並為接下來的非同步程式設計打下堅實基礎!
2026/02/08
本文深入解析 JavaScript 中的函式 (Function),涵蓋函式宣告式、函式表達式(包括匿名函式)及 ES6 箭頭函式。解釋函式的提升、參數、回傳、作用域,並透過範例說明不同定義方式的語法與特性。
2026/02/08
本文深入解析 JavaScript 中的函式 (Function),涵蓋函式宣告式、函式表達式(包括匿名函式)及 ES6 箭頭函式。解釋函式的提升、參數、回傳、作用域,並透過範例說明不同定義方式的語法與特性。
看更多
你可能也想看
Thumbnail
創作不只是個人戰,在 vocus ,也可以是一場集體冒險、組隊升級。最具代表性的創作者社群「vocus 野格團」,現在有了更強大的新夥伴加入!除了大家熟悉的「官方主題沙龍」,這次我們徵召了 8 位領域各異的「個人主題專家」,將再度嘗試創作的各種可能,和格友們激發出更多未知的火花。
Thumbnail
創作不只是個人戰,在 vocus ,也可以是一場集體冒險、組隊升級。最具代表性的創作者社群「vocus 野格團」,現在有了更強大的新夥伴加入!除了大家熟悉的「官方主題沙龍」,這次我們徵召了 8 位領域各異的「個人主題專家」,將再度嘗試創作的各種可能,和格友們激發出更多未知的火花。
Thumbnail
看完上篇 4 位新成員的靈魂拷問,是不是意猶未盡?別急,野格團新血的驚喜正接著登場!今天下篇接力的另外 4 位「個人主題專家」,戰力同樣驚人──領域從旅行美食、運動、商業投資到自我成長;這些人如何維持長跑般的創作動力?在爆紅的文章背後,又藏著哪些不為人知的洞察?5 大靈魂拷問繼續出擊
Thumbnail
看完上篇 4 位新成員的靈魂拷問,是不是意猶未盡?別急,野格團新血的驚喜正接著登場!今天下篇接力的另外 4 位「個人主題專家」,戰力同樣驚人──領域從旅行美食、運動、商業投資到自我成長;這些人如何維持長跑般的創作動力?在爆紅的文章背後,又藏著哪些不為人知的洞察?5 大靈魂拷問繼續出擊
Thumbnail
本文章提供前端開發的完整知識地圖,涵蓋 JavaScript 基礎概念、進階概念、前端開發基礎、前端框架與工具、系統設計與架構,以及開發工具與實作等面向,並以 SEO 友善的方式撰寫,適合想學習前端開發或準備面試的讀者。
Thumbnail
本文章提供前端開發的完整知識地圖,涵蓋 JavaScript 基礎概念、進階概念、前端開發基礎、前端框架與工具、系統設計與架構,以及開發工具與實作等面向,並以 SEO 友善的方式撰寫,適合想學習前端開發或準備面試的讀者。
Thumbnail
每年一次的 State of JS 問卷調查,不只是觀察前端技術趨勢的工具,更是檢視自身技能與學習方向的絕佳機會。透過這份調查,你可以了解前端生態的變化,確保自己沒有錯過重要資訊,並規劃未來的學習路線。
Thumbnail
每年一次的 State of JS 問卷調查,不只是觀察前端技術趨勢的工具,更是檢視自身技能與學習方向的絕佳機會。透過這份調查,你可以了解前端生態的變化,確保自己沒有錯過重要資訊,並規劃未來的學習路線。
Thumbnail
上一篇文章分享了 TypeScript 的定義、前端角色定位,如果你不是很確定「TypeScript 是什麼?」、「TypeScript 作為 JavaScript 的超集,在網頁開發扮演怎麼樣的角色?」這兩個問題的答案,建議可以回到上一篇先了解一下。
Thumbnail
上一篇文章分享了 TypeScript 的定義、前端角色定位,如果你不是很確定「TypeScript 是什麼?」、「TypeScript 作為 JavaScript 的超集,在網頁開發扮演怎麼樣的角色?」這兩個問題的答案,建議可以回到上一篇先了解一下。
Thumbnail
在先前的型別文章中,我們曾經聊過 JavaScript 常用的一些型別,但針對布林這個型別,我們沒有做太多的解釋,原因在於布林值在 JavaScript 會有一個特殊的規則:自動轉型 。 自動轉型可說是讓 JavaScript 為弱型別、且難以管理的最重要的要素,接著就來讓我們來聊聊什麼是自動轉型
Thumbnail
在先前的型別文章中,我們曾經聊過 JavaScript 常用的一些型別,但針對布林這個型別,我們沒有做太多的解釋,原因在於布林值在 JavaScript 會有一個特殊的規則:自動轉型 。 自動轉型可說是讓 JavaScript 為弱型別、且難以管理的最重要的要素,接著就來讓我們來聊聊什麼是自動轉型
Thumbnail
在剛開始寫 JavaScript 可能大多數的人不會特別意識到 JavaScript 的型別系統有什麼特別之處,我是在看完 Youtube 上 CS50 的課程,才理解到在不同的程式語言中,會因為語言的特性而有不同的系統,JavaScript 就是偏向比較特別的那一種。
Thumbnail
在剛開始寫 JavaScript 可能大多數的人不會特別意識到 JavaScript 的型別系統有什麼特別之處,我是在看完 Youtube 上 CS50 的課程,才理解到在不同的程式語言中,會因為語言的特性而有不同的系統,JavaScript 就是偏向比較特別的那一種。
Thumbnail
在前端開發中,很常會有需要轉址的需求,且處理的手法滿因人而異的,所以今天就想要來整理一些常見的 JavaScript 頁面轉址方式,以及各自的差異。
Thumbnail
在前端開發中,很常會有需要轉址的需求,且處理的手法滿因人而異的,所以今天就想要來整理一些常見的 JavaScript 頁面轉址方式,以及各自的差異。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News