先记住这个答案
export { a } 和 export const a = 1 都会在模块的导出列表里创建一个叫 a 的条目。一个模块不能有两个同名的导出,所以同时写会在解析阶段就报 SyntaxError——代码连执行的机会都没有。解决办法很简单:要么只用其中一种形式,要么用 as 给其中一个导出改个名字。
- 同名导出是
SyntaxError,不管用哪种语法 export { a }引用已声明的局部变量aexport const a同时声明变量和创建导出- 用
as重命名解决冲突
两种导出语法的区别
先搞清楚两种写法各自做了什么。声明导出同时做了两件事:声明一个变量,并把它导出。列表导出只做一件事:把一个已经声明好的局部变量导出。
// 声明导出:声明 + 导出一步到位
export const count = 0
export function increment() { /* ... */ }
// 列表导出:先声明,再统一导出
const count = 0
function increment() { /* ... */ }
export { count, increment }
// 冲突:两种形式都创建了叫 count 的导出
// export const count = 0
// export { count } // SyntaxError: Duplicate export 'count'两种形式最终的效果完全一样——都是创建一个具名导出。唯一的区别是代码组织方式不同。声明导出让你在声明的地方就能看到它是被导出的;列表导出把所有导出集中到一起,模块底部一眼就能看到所有的导出项。
列表导出的真正用途
列表导出的优势在于可以用 as 给导出改名字,以及可以把声明和导出分开管理。
在写 index.js 汇总文件时,列表导出特别有用。从子模块导入具名变量,再用列表导出统一暴露出去,中间可以根据需要重命名。
// index.js —— 统一出口
import { formatDate } from './date'
import { formatNumber } from './number'
// 如果两个子模块恰好都有一个叫 format 的导出
// 可以在这里用 as 区分
export { formatDate, formatNumber }
// 或者简写成重导出
// export { formatDate } from './date'
// export { formatNumber } from './number'重导出语法 export { x } from './module' 可以直接把子模块的导出透传出去,不需要先 import 再 export。在写库的入口文件时非常常用。
容易答错的地方
- 以为
export { a }是导出一个对象 export { a }里的花括号不是对象字面量。这是导出列表的专用语法,只能出现在export语句里。export { a: 1 }会报语法错误——这里不支持键值对的写法。- 以为重命名之后原名还能用
export { a as b }之后,导入方只能用import { b },写import { a }会报模块里没有a这个导出。不过在源模块内部,局部变量a还是叫a,只是对外暴露的名字变成了b。
面试官还会怎么问?
一个模块里最多能有多少个具名导出?
语言规范没有数量限制。你可以在一个模块里导出几百个具名变量,但从代码组织的角度看,如果一个模块的导出太多,通常意味着它承担了太多职责,应该考虑拆分。
export { } 空列表合法吗?
合法。export {} 不导出任何东西,但它让这个文件被识别为一个 ESM 模块。在 TypeScript 里,有时候一个文件没有 import 也没有 export,TypeScript 会把它当成全局脚本而不是模块。加一个 export {} 就能告诉 TypeScript 这是一个模块。
重导出时可以改名吗?
可以。export { formatDate as format } from './date' 把 date 模块的 formatDate 以 format 的名字重新导出。导入方用 import { format } from './index' 就行。重导出加重命名在写库的公共 API 时很常见,可以在不改内部实现的前提下调整对外暴露的名字。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。