先记住这个答案
具名导出(named export)通过 export 把变量、函数或类导出,一个模块可以有任意多个。导入方必须用大括号并且写对名字,比如 import { formatDate } from './utils'。默认导出(default export)用 export default,每个模块最多一个。导入方可以起任意名字,比如 import Whatever from './Button'。两者可以在同一个模块里共存。
- 具名导出可以有多个,导入时名字必须对上
- 默认导出每个模块最多一个,导入时可以随意命名
- 具名导出对 tree-shaking 和 IDE 自动补全更友好
- 默认导出适合模块只有一个主要导出的情况
语法对比:两种导出长什么样
先看代码,把两种写法放在一起就能看清区别。
// utils.js —— 具名导出
export const PI = 3.14159
export function formatDate(date) {
return date.toISOString().slice(0, 10)
}
// Button.js —— 默认导出
export default function Button(props) {
return `<button>${props.label}</button>`
}
// 导入方式
// import { PI, formatDate } from './utils' 具名:大括号 + 对应名字
// import MyButton from './Button' 默认:任意名字,不用大括号
// import Button, { PI } from './combined' 混合:默认在前,具名在后具名导出的名字是固定的。写 import { PI } from './utils' 的时候,PI 必须和导出端的名字一模一样。想换名字要显式用 as,比如 import { PI as pi } from './utils'。默认导出没有这个限制,导入端想叫什么就叫什么。
实际项目里怎么选
工具库、常量文件这类一个模块导出很多东西的场景,适合具名导出。React 组件、页面入口这类一个文件就是一个主要"东西"的场景,适合默认导出。
具名导出对 IDE 更友好。你输入 import { 之后,IDE 能根据目标模块的导出列表给你补全可用的名字。默认导出做不到这一点——IDE 不知道你想叫什么名字,只能猜一个文件名。
另一个区别在 tree-shaking 上。具名导出让打包工具清楚地知道哪些导出被用到了,没用到的可以安全删掉。默认导出本身不影响 tree-shaking,但如果默认导出的是一个大对象(比如 export default { a, b, c }),打包工具没办法分析对象里哪些属性被用了,只能全部保留。
大型团队有时会规定"只用具名导出",避免不同人给同一个默认导出起不同的名字导致 import Button 和 import Btn 指向同一个东西但在代码里看不出来的问题。
export default 背后的特殊之处
export default 在语法层面其实是把值绑定到一个叫 default 的具名导出上。所以 import Button from './Button' 等价于 import { default as Button } from './Button'。理解这一点能帮你看懂一些重导出的写法。
export default 后面如果跟的是表达式(比如 export default 42 或 export default someVar),它会立刻求值,把当前的值绑定到 default 导出。后续即使 someVar 变了,默认导出不会跟着变。但如果用 export { someVar as default } 这种写法,就是实时绑定的——源模块改了 someVar,导入方也能读到新值。
容易答错的地方
- 以为默认导出和具名导出不能共存
- 一个模块完全可以同时有默认导出和多个具名导出。React 库就是这样做的——默认导出
React对象,同时具名导出useState、useEffect这些 Hook。导入时写import React, { useState } from 'react'就行。 - 以为具名导出导入时可以随便改名
- 具名导出导入时名字必须对上,想改名只能用
as。写import { formatDate as fmt } from './utils'是对的,直接写import { fmt } from './utils'会报模块里不存在fmt这个导出。
面试官还会怎么问?
把多个具名导出收进一个对象再默认导出,好不好?
不好。export default { formatDate, parseDate } 这样写会让 tree-shaking 失效,因为打包工具只能看到一个对象被导出了,没办法判断对象里的 parseDate 到底有没有被外部用到。直接用具名导出,让每个函数都是独立的导出项,tree-shaking 才能工作。
重导出(re-export)怎么处理默认导出?
export { default } from './Button' 把 Button 的默认导出当作具名的 default 再导出。export { default as Button } from './Button' 把它改名成 Button 再导出。用这些写法可以在 index.js 里把多个模块的默认导出统一收拢成具名导出。
TypeScript 里的 export = 和 import = require() 是怎么回事?
这是 TypeScript 兼容 CommonJS 模块的写法。export = 等价于 module.exports =,import x = require('./a') 等价于 CJS 的 require。在纯 ESM 项目里不需要这些写法,用标准的 export default 和 import 就够了。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。