JavaScript 默认导出和命名导出有什么区别

2022/08/25

最近整理 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,而是命名导出的静态结构更清楚,更利于分析。

五、实际项目里怎么选

我的习惯比较简单:

  1. 工具函数、hooks、常量、多个独立能力:优先命名导出。
  2. 单文件组件、页面组件、一个模块只有一个主功能:可以默认导出。
  3. 公共库或 SDK:尽量多用命名导出,方便按需引入和 IDE 提示。
  4. 如果默认导出的是一个大对象,要注意它可能不利于 Tree Shaking。

比如下面这种 utils 文件,我会用命名导出:

export const formatPrice = () => {};
export const formatDate = () => {};
export const getQuery = () => {};

而组件文件可以用默认导出:

function UserCard() {
  return <div>user</div>;
}

export default UserCard;