先记住这个答案
export default 后面跟的是表达式还是声明,决定了两件事:有没有提升,以及有没有实时绑定。跟表达式时(比如 export default someVar 或 export default (function() {})()),引擎立刻对表达式求值,把当前值作为默认导出。跟函数或类声明时(比如 export default function foo() {}),声明本身会被提升,foo 在模块内可以在声明之前使用。
export default加表达式:立刻求值,不是实时绑定export default function foo(){}有提升,foo在模块内可用- 想要默认导出保持实时绑定,用
export { x as default } - 匿名函数或类声明的默认导出在调试堆栈里没有名字
表达式形式:求值后就定了
当 export default 后面跟一个变量名、字面量或者任意表达式时,引擎会在执行到这一行时对表达式求值,把得到的值作为默认导出。之后即使源模块修改了这个变量,默认导出的值不会变。
// config.mjs
let mode = 'development'
export default mode
// 默认导出是 'development' 这个字符串值
export function setProduction() {
mode = 'production'
}
// main.mjs
// import config from './config.mjs'
// import { setProduction } from './config.mjs'
// console.log(config) // 'development'
// setProduction()
// console.log(config) // 还是 'development' —— 没有实时绑定export default mode 在执行时把 mode 的当前值 'development' 取出来,绑定到 default 导出上。后面 mode 变成了 'production',但 default 导出已经和 mode 脱钩了。
这是很多人踩过的坑:以为 export default someVar 和 export { someVar as default } 效果一样,其实不一样。
声明形式:有提升也有本地绑定
export default function foo() {} 和 export default class Bar {} 是声明形式。函数声明会被提升到模块顶部,foo 在模块内部可以在声明之前使用。同时 foo 和默认导出指向同一个函数,不存在脱钩的问题。
但要注意,如果是匿名声明,比如 export default function() {} 或 export default class {},模块内部没有可以引用的名字。函数的 .name 属性会是 'default',在调试堆栈里不太好辨认,建议尽量给个名字。
想要默认导出保持实时绑定的写法
如果确实需要默认导出跟着源变量一起变化,用导出列表的写法。
export { x as default } 保留实时绑定JavaScript// counter.mjs
let count = 0
export function increment() { count++ }
// 这种写法:默认导出是 count 变量的实时绑定
export { count as default }
// main.mjs
// import count from './counter.mjs'
// console.log(count) // 0
// increment()
// console.log(count) // 1 —— 实时绑定生效了export { count as default } 建立的是一个实时绑定,和具名导出的 export { count } 机制完全一样,只是导出名变成了 default。这样导入方用 import count from './counter.mjs' 就能跟踪到源模块的变化。
这个写法在日常项目里很少用到。大多数情况下,默认导出的是组件、类或工厂函数,不需要实时绑定。
容易答错的地方
- 以为
export default function和export default加函数表达式一样 export default function foo() {}是声明,foo被提升,模块内可用。export default (function foo() {})是表达式(外面套了括号),foo只在函数内部可用,不会被提升。写法差一个括号,行为完全不同。- 以为
export default someVar之后改someVar还能影响导出 - 不能。
export default someVar在执行那一刻就取了someVar的值,之后someVar怎么变都和默认导出无关了。想要实时绑定,必须用export { someVar as default }。
面试官还会怎么问?
export default 42 合法吗?
合法。任何表达式都可以放在 export default 后面,42、'hello'、[1, 2, 3]、null 都行。但 export 42 不行——没有 default 的 export 后面只能跟声明或导出列表。
export default 能出现多次吗?
不能。一个模块只能有一个 default 导出,写两个 export default 会报 SyntaxError。如果需要导出多个值,要么用具名导出,要么把多个值包在一个对象里做默认导出(但这会影响 tree-shaking)。
为什么 React 社区习惯用 export default 导出组件?
因为一个组件文件通常就只导出一个组件,用 export default 导入时不用写大括号,代码更简洁。import Button from './Button' 比 import { Button } from './Button' 少一层大括号。不过也有不少团队转向了具名导出,因为它对 IDE 重构、搜索和自动导入更友好。
用 TypeScript 的 isolatedModules 时有什么影响?
开启 isolatedModules 后,不能对一个类型做 export default。比如 export default interface Foo {} 会报错,因为 TypeScript 在单文件编译时无法判断这个默认导出是值还是类型。解决办法是先用 interface Foo {} 声明,再用 export { type Foo as default } 导出。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。