最近整理 ES Module 的时候,又看了一遍默认导出和命名导出。这个点平时写项目天天会用,但面试问起来时,如果只回答“一个有大括号,一个没有大括号”,就有点太浅了。
我一般会从三个角度去理解:导出数量、导入时的命名规则、以及对 Tree Shaking 的影响。
一、先看最基本的代码示例
命名导出是给每个导出的变量一个明确名字:
// utils.js
export const formatDate = () => {};
export const parseQuery = () => {};
// 使用时
import { formatDate, parseQuery } from './utils';
默认导出是这个模块的“主出口”,一个模块只能有一个:
// Button.js
export default Button;
// 使用时
import Button from './Button';
这里最明显的区别就是:命名导出导入时要用 {},名字要对得上;默认导出导入时不用 {},名字可以自己取。
二、命名导出的名字是绑定的
命名导出更像是“我明确导出了这些东西,你要哪个就按名字拿哪个”。
export const count = 1;
import { count } from './state';
如果想换名字,需要用 as:
import { count as total } from './state';
这种写法的好处是关系很清楚,IDE 也更容易做自动补全、跳转和重命名。实际维护项目时,我更喜欢工具函数、hooks、常量这类东西用命名导出,因为别人一看 import 就知道这个模块里用了什么。
三、默认导出的名字由导入方决定
默认导出不要求导入时的名字一致:
// user.js
export default function getUser() {}
// 这些都可以
import getUser from './user';
import fetchUser from './user';
import aaa from './user';
这就是默认导出的灵活之处,也是它有时候不太好维护的地方。导入方可以随便命名,短期看方便,时间长了可能会出现同一个模块在不同文件里叫不同名字的情况,读代码时就要多绕一层。
所以我更倾向于把默认导出留给“这个文件确实只有一个主要内容”的场景,比如一个页面组件、一个 Vue 组件、一个 React 组件,或者一个类。
四、Tree Shaking 为什么更喜欢命名导出
Tree Shaking 简单说就是打包时把没用到的代码摇掉。命名导出对打包工具更友好,因为每个导出的名字都很明确。
// math.js
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b;
// 只用了 add
import { add } from './math';
这种情况下,打包工具比较容易判断 minus 没被用到。
但如果写成默认导出一个对象:
export default {
add(a, b) {
return a + b;
},
minus(a, b) {
return a - b;
},
};
import math from './math';
math.add(1, 2);
打包工具不一定能很精确地判断对象里的哪些属性一定没用,尤其是对象可能被动态访问时,摇掉未使用代码就会变难。
这也是为什么很多工具库会更推荐命名导出。不是说默认导出不能 Tree Shaking,而是命名导出的静态结构更清楚,更利于分析。
五、实际项目里怎么选
我的习惯比较简单:
- 工具函数、hooks、常量、多个独立能力:优先命名导出。
- 单文件组件、页面组件、一个模块只有一个主功能:可以默认导出。
- 公共库或 SDK:尽量多用命名导出,方便按需引入和 IDE 提示。
- 如果默认导出的是一个大对象,要注意它可能不利于 Tree Shaking。
比如下面这种 utils 文件,我会用命名导出:
export const formatPrice = () => {};
export const formatDate = () => {};
export const getQuery = () => {};
而组件文件可以用默认导出:
function UserCard() {
return <div>user</div>;
}
export default UserCard;