Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

JavaScript 模組匯出教學:export 與 export default 的用法與差異

最後更新:2026年8月31日JavaScript

假設你寫了一個算總價的函式,放在 priceUtils.js 裡:

// priceUtils.js
function calculateTotal(items) {
  let sum = 0;
  for (const item of items) {
    sum += item.price;
  }
  return sum;
}

現在你想在另一個檔案 checkout.js 裡用它:

// checkout.js
console.log(calculateTotal([{ price: 300 }, { price: 900 }]));

這樣不會動:

ReferenceError: calculateTotal is not defined

「calculateTotal 沒有被定義」——但你明明定義了,就在隔壁那個檔案裡。

原因是這兩個檔案各自被當成一個模組(module),而模組會把自己的東西關在裡面。

checkout.js 看不到 priceUtils.js 裡的任何東西,除非 priceUtils.js 主動把它交出來。

這個「主動交出來」的動作就叫 export(匯出),另一邊接手的動作叫 import(匯入)。

所以「把程式碼拆成好幾個模組」講的就是:拆成好幾個檔案,各自管好自己的東西,只把需要給別人的部分交出去。

JavaScript 有兩種交出東西的方式,而 import 必須跟著配對:

// 寫法一
export default calculateTotal;                     // priceUtils.js
import calculateTotal from './priceUtils.js';      // checkout.js

// 寫法二
export { calculateTotal };                         // priceUtils.js
import { calculateTotal } from './priceUtils.js';  // checkout.js

大括號是一組的:匯出時有,匯入時就要有;匯出時沒有,匯入時也不能加。

跨行配錯了,JavaScript 會說這個模組沒有提供這個名字,即使函式明明就在那裡。

這篇文章要回答的就是:這兩種方式差在哪、什麼時候該用哪一種。

開始之前:先讓 import 和 export 跑得起來

這套 import / export 的機制有個正式名稱,叫 ES Module(ECMAScript Module,常簡稱 ESM),後面會一直用到這個詞。

而一個 .js 檔案不會自動被當成模組。如果你直接寫下 import 就執行,很可能會先撞到這個:

SyntaxError: Cannot use import statement outside a module

最短的解法:

  • 瀏覽器:script 標籤加上 type="module"。另外網頁要從 http:// 開啟(用 VS Code 的 Live Server,或 npx serve),在檔案總管按兩下會被瀏覽器擋掉。
  • Node.js:在 package.json 裡加一行 "type": "module"。
  • 用 Vite、Next.js 這類工具建立的專案:已經設定好了,不用管。

為什麼要有這些設定、又是誰在決定一個檔案算不算模組,我在〈JavaScript 模組作用域是什麼?為什麼沒有 import 就讀不到別的檔案的變數〉裡講得比較完整。

接下來我們專心看 export 本身。

export:具名匯出,每樣東西都帶著自己的名字

export 是用來把模組內部的變數、函式或類別對外公開的語法。

它又叫具名匯出(Named Export),因為每一個被匯出的東西都帶著自己的名字,匯入的一方要靠這個名字來拿。

我們用一個計算商品價格的模組當作例子,這個模組會從頭跟到尾:

// priceUtils.js
export const TAX_RATE = 0.05;

export function formatPrice(amount) {
  return `NT$ ${amount.toLocaleString()}`;
}

這個檔案對外公開了兩樣東西:一個叫 TAX_RATE 的稅率常數,一個叫 formatPrice 的格式化函式。

export 要寫在哪裡?宣告時直接加,或最後集中列出

上面那種在 const、function 前面直接加 export 的寫法,叫宣告式匯出。

好處是你一眼就能從程式碼看出哪些東西是對外的、哪些是內部用的。

另一種是集中式匯出,先把所有東西定義完,最後用一組大括號一次列出來:

// priceUtils.js
const TAX_RATE = 0.05;

function formatPrice(amount) {
  return `NT$ ${amount.toLocaleString()}`;
}

function roundToCents(amount) {
  return Math.round(amount * 100) / 100;
}

export { TAX_RATE, formatPrice };

這裡刻意多加了一個 roundToCents,而且沒有把它放進匯出清單裡。

它就是這個模組的內部函式,只有 priceUtils.js 自己能用,外面拿不到。

集中式匯出的價值就在這裡:檔案最後那一行等於是整個模組的對外目錄,你不用把整個檔案掃過一遍,就知道它提供了什麼。

兩種寫法產生的結果完全一樣,選哪一種是團隊習慣問題。

export { } 的大括號,是在匯出一個物件嗎?

export { TAX_RATE, formatPrice } 這行的大括號,非常容易被誤讀。

它看起來很像在匯出一個物件,但它不是。

// ❌ 錯誤的理解:以為在匯出一個物件
export { TAX_RATE, formatPrice };
// 「所以我匯出了 { TAX_RATE: 0.05, formatPrice: fn } 這個物件?」

// ✅ 正確的理解:這是一份匯出清單
export { TAX_RATE, formatPrice };
// 「我要公開兩個東西,名字分別是 TAX_RATE 和 formatPrice。」

差別在哪?如果它真的是物件,你應該可以這樣寫:

export { name: TAX_RATE };  // ❌ SyntaxError

但這會直接報錯,因為物件字面量的 key: value 語法在這裡根本不成立。

大括號在 export 和 import 裡是一種獨立的語法結構,它唯一的功能是「列出名字」,跟物件沒有關係。

這件事現在弄清楚很重要,因為下一節的 import { ... } 會用到同一組大括號,而它同樣不是解構賦值。

import 進來的名字,一定要跟 export 出去的一樣嗎?

具名匯出的東西,匯入時也要用大括號,而且名字要對得上:

// checkout.js
import { TAX_RATE, formatPrice } from './priceUtils.js';

console.log(formatPrice(1200));        // NT$ 1,200
console.log(1200 * (1 + TAX_RATE));    // 1260

如果名字拼錯或大小寫不對,程式會直接掛掉:

import { taxRate } from './priceUtils.js';  // ❌ 模組裡叫 TAX_RATE,不叫 taxRate
SyntaxError: The requested module './priceUtils.js'
does not provide an export named 'taxRate'

值得注意的是錯誤發生的時機:這是 SyntaxError,而不是等你真的用到 taxRate 才報的執行期錯誤。

ES Module 在程式開始跑之前,會先把所有 import 和 export 對照一遍,確認每個要的名字都真的有人提供。

所以名字打錯這種問題,第一時間就會被抓出來,不會潛伏到某個分支被執行時才爆掉。

不過「名字要對上」講的是匯入的那一刻,不代表你之後只能用這個名字。

用 as 就能在匯入時改名:

import { TAX_RATE as VAT, formatPrice as toCurrency } from './priceUtils.js';

console.log(toCurrency(1200));  // NT$ 1,200

這在兩個模組匯出了同名的東西、或是原本的名字跟你這個檔案的用途對不上時很好用。

要注意的是 as 前面那個名字仍然必須跟模組裡的一模一樣,改的是你這邊要怎麼稱呼它。

export default:每個模組可以指定一個主角

除了具名匯出,一個模組還可以指定一樣東西當作預設匯出(Default Export)。

它的意思是「這個模組的主要功能就是這個」,所以每個模組最多只能有一個 export default,寫兩個會直接報錯。

回到我們的價格模組,把主要的結帳計算加進去:

// priceUtils.js
export const TAX_RATE = 0.05;

export function formatPrice(amount) {
  return `NT$ ${amount.toLocaleString()}`;
}

export default function calculateTotal(items) {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  return subtotal * (1 + TAX_RATE);
}

現在這個模組的定位很清楚了:主角是 calculateTotal,TAX_RATE 和 formatPrice 是旁邊的配角。

export default 匯入時,名字可以自己取嗎?

預設匯出跟具名匯出最明顯的差別,在匯入的那一行:

// checkout.js
import calculateTotal from './priceUtils.js';

沒有大括號,而且 calculateTotal 這個名字是你自己取的,跟模組裡的名稱無關。

換成別的名字一樣能跑:

import getOrderTotal from './priceUtils.js';   // ✅ 完全合法
import whatever from './priceUtils.js';        // ✅ 也合法,雖然不建議

const items = [{ price: 300 }, { price: 900 }];
console.log(getOrderTotal(items));  // 1260

為什麼可以這樣?

因為具名匯出是靠名字配對的,但預設匯出一個模組只有一個,根本不需要配對。

JavaScript 只要知道「你要的是那個預設的」就夠了,至於你打算把它叫什麼,它不在意。

如果你想看證據:預設匯出在模組裡登記的名字其實就叫 default,你可以直接用具名匯入的語法把它拿出來。

import calculateTotal from './priceUtils.js';                   // 平常的寫法
import { default as calculateTotal } from './priceUtils.js';    // 完全等價

這兩行拿到的是同一個函式,只是沒有人會寫第二種,因為第一種短多了。

export default 後面可以直接放一個值嗎?

export 只能匯出「有名字的東西」,但 export default 後面接的是一個值,所以連沒有名字的東西也能交出去。

這個值可以是:

  • 數字或字串:export default 0.05;
  • 陣列:export default [1, 2, 3];
  • 物件:export default { currency: 'TWD' };
  • 匿名函式:export default function () {};

匿名函式那一種在框架裡特別常見,因為函式本身根本不需要名字——反正匯入的人會自己取。

匯出物件也很常用,通常出現在設定檔或工具集合:

// config.js
export default {
  currency: 'TWD',
  taxRate: 0.05,
  locale: 'zh-TW'
};
// app.js
import config from './config.js';

console.log(config.currency);  // TWD

這裡有一點要注意:這個物件裡的 currency、taxRate、locale 不會變成三個具名匯出。

整包物件是一個預設匯出,你只能整包拿進來再從裡面挖,不能寫 import { currency } from './config.js'。

兩個最常見的語法錯誤

這兩個錯誤幾乎每個人都會寫過一次,而且錯誤訊息不太好懂。

為什麼 export name; 不能寫?

// ❌ 有問題的寫法
const TAX_RATE = 0.05;
export TAX_RATE;
SyntaxError: Unexpected token 'TAX_RATE'

你會這樣寫,多半是從 export const TAX_RATE = 0.05 推過來的:變數前面已經宣告好了,把 const 那段拿掉,應該就是「匯出這個變數」的意思吧。

但 export 後面能接的東西是固定的,只有三種:

  • 一個宣告:export const TAX_RATE = 0.05;
  • 一組大括號清單:export { TAX_RATE, formatPrice };
  • default:export default calculateTotal;

單獨一個變數名不在這三種裡面。

這件事其實也不難理解:大括號那種寫法本來就能一次列出好幾個名字,只有一個名字當然也列得下,語言沒有必要再為它多開一種寫法。

所以解析器讀到 TAX_RATE 的時候,三種形式都對不上,就停在那裡了——錯誤訊息 Unexpected token 'TAX_RATE' 講的正是這件事:沒料到這個位置會冒出 TAX_RATE。

改法就是選前兩種其中一種:

// ✅ 改法一:宣告時就匯出
export const TAX_RATE = 0.05;

// ✅ 改法二:用大括號清單
const TAX_RATE = 0.05;
export { TAX_RATE };

改法二看起來跟原本的錯誤寫法只差一組大括號,但 export TAX_RATE; 和 export { TAX_RATE }; 在語法上是完全不同的兩件事。

為什麼 export default const TAX_RATE = 0.05; 不能寫?

// ❌ 有問題的寫法
export default const TAX_RATE = 0.05;

奇怪的是,export const TAX_RATE = 0.05 明明可以,export default function calculateTotal() {} 也可以,怎麼偏偏 export default const 不行?

關鍵在於一個 var、let、const 可以一次宣告好幾個變數:

const TAX_RATE = 0.05, SHIPPING_FEE = 60;  // 一行宣告了兩個

如果 export default 允許接變數宣告,遇到上面這行,JavaScript 就沒辦法決定預設匯出到底是 TAX_RATE 還是 SHIPPING_FEE。

而預設匯出只能有一個,這裡沒有合理的答案,所以語言乾脆禁止這種寫法。

那 function 和 class 為什麼可以?因為它們一次只能宣告一個名字,不會有這種模稜兩可的情況。

改法有兩種:

// ✅ 改法一:先宣告,再匯出這個值
const TAX_RATE = 0.05;
export default TAX_RATE;

// ✅ 改法二:直接把值寫上去,不用變數
export default 0.05;

記住一句話就夠了:export default 後面要接的是一個值,不是一句宣告。

同一個模組可以兩種匯出都用嗎?

可以,而且很常見。我們一路用到現在的價格模組就是這樣寫的:

// priceUtils.js
export const TAX_RATE = 0.05;                      // 具名匯出

export function formatPrice(amount) {              // 具名匯出
  return `NT$ ${amount.toLocaleString()}`;
}

export default function calculateTotal(items) {    // 預設匯出
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  return subtotal * (1 + TAX_RATE);
}

三樣東西,兩個具名匯出、一個預設匯出,彼此不衝突。

匯入的時候,預設匯出寫在前面不加大括號,具名匯出跟在後面加大括號:

// checkout.js
import calculateTotal, { TAX_RATE, formatPrice } from './priceUtils.js';

const items = [{ price: 300 }, { price: 900 }];

console.log(formatPrice(calculateTotal(items)));  // NT$ 1,260
console.log(`稅率:${TAX_RATE * 100}%`);           // 稅率:5%

順序是固定的,預設匯出一定在逗號前面。

你也可以只拿其中一邊,不需要全部匯入:

import calculateTotal from './priceUtils.js';        // 只要主功能
import { formatPrice } from './priceUtils.js';       // 只要工具函式

什麼時候用哪一種?

比較項目export(具名匯出)export default(預設匯出)
一個模組可以有幾個任意多個最多一個
匯入語法import { name } from '...'import anyName from '...'
要不要大括號要不要
匯入時的名稱必須對上,可用 as 改名完全自訂
可以匯出沒有名字的值嗎不行可以
export(具名匯出)任意多個
export default(預設匯出)最多一個
export(具名匯出)import { name } from '...'
export default(預設匯出)import anyName from '...'
export(具名匯出)要
export default(預設匯出)不要
export(具名匯出)必須對上,可用 as 改名
export default(預設匯出)完全自訂
export(具名匯出)不行
export default(預設匯出)可以

實務上的選擇大致是這樣:

  • 模組提供一組平行的工具(像是一堆格式化函式、一堆常數)→ 用 export。名稱固定,使用的人不會各自取不同名字,全專案搜尋 formatPrice 也找得到所有用到的地方。
  • 模組就是一樣東西(React 元件、一個 class、一個設定物件)→ 用 export default。這個檔案的內容就是這樣東西,預設匯出直接把這件事講明白,匯入時也短。
  • 有一個主功能,外加幾個配套 → 兩種一起用,就像這篇的 priceUtils.js。

有些團隊會刻意完全不用 export default,理由是名稱可以亂取反而讓程式碼難以搜尋和重構。

這是取捨,沒有標準答案,跟著你專案現有的慣例走就好。

回到最開始那個問題

文章一開始那兩行 import,差別現在應該很清楚了。

大括號不是可加可不加的裝飾,它是在說「我要的東西有名字」。

export 是具名匯出,一個模組要幾個都行,每樣東西都帶著自己的名字出去。

所以匯入的一方得用大括號把名字列出來,而且要跟模組裡寫的一致。

真的不喜歡那個名字,用 as 改成你這邊順手的稱呼就好。

export default 是預設匯出,一個模組最多一個,代表「這個檔案就是這樣東西」。

既然只有一個,匯入時就不需要指名道姓,名字你自己取,大括號也不必了。

它接的是一個值,所以連字串、數字、匿名函式這種沒有名字的東西都交得出去。

至於那兩個報錯,理由其實是同一句話的兩面。

export 後面只認得宣告、大括號清單、default 這三種形式,所以 export TAX_RATE; 對不上。

而 export default 後面要的是一個值,不是一句宣告,所以 export default const x = 1; 也走不通。

這些規則說穿了都圍著同一個分岔打轉:一個模組要嘛把東西一樣一樣具名交出去,要嘛指定其中一個當主角。

大括號要不要加、名字能不能自己取、能不能交出一個沒有名字的值,全都是從這個分岔長出來的。

想通這件事之後,你再去看別人專案裡的 import,第一眼就會知道對面那個檔案大概是怎麼設計的。

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

JavaScript

目錄

  • 開始之前:先讓 import 和 export 跑得起來
  • export:具名匯出,每樣東西都帶著自己的名字
  • export 要寫在哪裡?宣告時直接加,或最後集中列出
  • export { } 的大括號,是在匯出一個物件嗎?
  • import 進來的名字,一定要跟 export 出去的一樣嗎?
  • export default:每個模組可以指定一個主角
  • export default 匯入時,名字可以自己取嗎?
  • export default 後面可以直接放一個值嗎?
  • 兩個最常見的語法錯誤
  • 為什麼 export name; 不能寫?
  • 為什麼 export default const TAX_RATE = 0.05; 不能寫?
  • 同一個模組可以兩種匯出都用嗎?
  • 什麼時候用哪一種?
  • 回到最開始那個問題