先记住这个答案
export ... from 是把另一个模块的导出直接转交出去的写法,最常见于组件库的 index.js 桶文件。有四种形式:export { a, b as c } from 挑着转交并可改名,export * from 转交全部具名导出,export * as ns from 把整个模块打包成一个具名导出,export { default } from 转交默认导出。具名和默认的区别在于:export * 不会带上 default,默认导出必须单独写出来。另外再导出不会在当前模块里创建变量,当前文件想用还得自己再 import。
export *不转发默认导出- 再导出不会在当前模块里创建变量,想用得另写
import - 两个
export *有同名导出时,这个名字会被排除 export * as ns把整个模块作为一个具名导出
一个组件库的桶文件
components/index.js 汇总各个组件,让使用方只从一个地方导入。
// components/index.js
export { default as Button } from './Button.js' // 默认导出改名成具名导出
export { default } from './Layout.js' // 继续作为本模块的默认导出
export * from './utils.js' // 全部具名导出,不含 default
export * as icons from './icons.js' // 使用方拿到 icons.Close、icons.Search
export { formatDate as format } from './date.js' // 挑一个并改名
// 下面这行会报 ReferenceError,再导出没有在这里创建变量
// console.log(Button)使用方可以写 import Layout, { Button, format, icons } from "./components/index.js"。
注意最后被注释掉的那行。export { default as Button } from 只是声明了「对外有一个叫 Button 的导出,来源是那个文件」,并没有在 index.js 里声明变量。桶文件自己要用 Button,得另写一行 import Button from "./Button.js"。
export * 的两条特殊规则
export * 看起来是「全部转交」,实际上有两个例外,面试里经常被追问。
- 不含
default,同名会被排除 - 第一,它不包含
default。一个模块只能有一个默认导出,如果export *会带上default,桶文件里写两个export *就会有两个默认导出,规范干脆把它排除了。 第二,两个export *来源里有同名的导出时,这个名字不会报错,而是被静默排除,两个都不导出。我在 Node.js 里实测,x1.js和x2.js都导出了dup,桶文件里各写一个export *,import * as b拿到的对象里没有dup;如果使用方明确写import { dup } from这个桶文件,则会报SyntaxError,提示有冲突的星号导出。 如果某个名字是当前模块自己显式导出的,或者用export { dup } from明确指定了来源,它的优先级高于export *,不算冲突。
容易答错的地方
- 以为
export *把默认导出也带出去了 - 这是最常见的错误。组件文件用
export default导出,桶文件只写export * from "./Button.js",使用方怎么都导入不到Button。要写成export { default as Button } from "./Button.js"。 - 以为再导出等于先
import再export - 效果接近,但再导出不会在当前模块作用域里留下变量。另外先
import再export时,名字在当前模块里可见,写错了也更容易被 ESLint 发现,两者适合的场景不同。
面试官还会怎么问?
桶文件会影响 tree shaking 吗?
对 ESM 包来说,打包器能顺着再导出追踪到真正用到的模块,没用到的通常能删掉。问题出在副作用上:桶文件把几十个模块都拉进了依赖图,只要其中有模块被判定为有副作用,就删不掉。开发环境里还会让 Vite 这类工具多处理很多文件,所以大型项目会限制桶文件的使用范围。
export * as ns from 和先 import * as ns 再 export { ns } 一样吗?
对使用方来说结果一样,都拿到一个叫 ns 的命名空间对象。区别仍然是前者不在当前模块里创建 ns 变量。这个语法是 ES2020 才加入的,更早的代码只能用两行写法。
CommonJS 里怎么做再导出?
常见写法是 module.exports = { ...require("./a"), ...require("./b") },或者 exports.Button = require("./Button")。展开运算符是在那一刻复制属性值,不是实时绑定,底层模块后来改了导出,这边看不到。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。