假設你寫了一個算總價的函式,放在 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,不叫 taxRateSyntaxError: 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。名稱固定,使用的人不會各自取不同名字,全專案搜尋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,第一眼就會知道對面那個檔案大概是怎麼設計的。