从代码可维护性角度出发，命名导出比默认导出更好，因为它减少了因引用产生重命名情况的发生。

但命名导出与默认导出的区别不止如此，在逻辑上也有很大差异，为了减少开发时在这方面栽跟头，有必要提前了解它们的区别。

本周找来了这方面很好的的文章：[export-default-thing-vs-thing-as-default](https://jakearchibald.com/2021/export-default-thing-vs-thing-as-default/)，先描述梗概，再谈谈我的理解。

## 概述

一般我们认为，import 导入的是引用而不是值，也就是说，当导入对象在模块内值发生变化后，import 导入的对象值也应当同步变化。

```javascript
// module.js
export let thing = 'initial';

setTimeout(() => {
  thing = 'changed';
}, 500);
```

上面的例子，500ms 后修改导出对象的值。

```javascript
// main.js
import { thing as importedThing } from './module.js';
const module = await import('./module.js');
let { thing } = await import('./module.js');

setTimeout(() => {
  console.log(importedThing); // "changed"
  console.log(module.thing); // "changed"
  console.log(thing); // "initial"
}, 1000);
```

1s 后输出发现，前两种输出结果变了，第三种没有变。也就是对命名导出来说，前两种是引用，第三种是值。

但默认导出又不一样：

```javascript
// module.js
let thing = 'initial';

export { thing };
export default thing;

setTimeout(() => {
  thing = 'changed';
}, 500);
```

```javascript
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';

setTimeout(() => {
  console.log(thing); // "changed"
  console.log(defaultThing); // "initial"
  console.log(anotherDefaultThing); // "initial"
}, 1000);
```

为什么对默认导出的导入结果是值而不是引用？

原因是默认导出可以看作一种对 “default 赋值” 的特例，就像 `export default = thing` 这种旧语法表达的一样，本质上是一种赋值，所以拿到的是值而不是引用。

那么默认导出的另一种写法 `export { thing as default }` 也是如此吗？并不是：

```javascript
// module.js
let thing = 'initial';

export { thing, thing as default };

setTimeout(() => {
  thing = 'changed';
}, 500);
```

```javascript
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';

setTimeout(() => {
  console.log(thing); // "changed"
  console.log(defaultThing); // "changed"
  console.log(anotherDefaultThing); // "changed"
}, 1000);
```

可见，这种默认导出，导出的都是引用。所以导出是否是引用，不取决于是否是命名导出，**而是取决于写法**。不同的写法效果不同，哪怕相同含义的不同写法，效果也不同。

难道是写法的问题吗？是的，只要是 `export default` 导出的都是值而不是引用。但不幸的是，存在一个特例：

```javascript
// module.js
export default function thing() {}

setTimeout(() => {
  thing = 'changed';
}, 500);
```

```javascript
// main.js
import thing from './module.js';

setTimeout(() => {
  console.log(thing); // "changed"
}, 1000);
```

为什么 `export default function` 是引用呢？原因是 `export default function` 是一种特例，这种写法就会导致导出的是引用而不是值。如果我们用正常方式导出 Function，那依然遵循前面的规则：

```javascript
// module.js
function thing() {}

export default thing;

setTimeout(() => {
  thing = 'changed';
}, 500);
```

只要没有写成 `export default function` 语法，哪怕导出的对象是个 Function，引用也不会变化。所以取决效果的是写法，而与导出对象类型无关。

对于循环引用也有时而生效，时而不生效的问题，其实也取决于写法。下面的循环引用是可以正常工作的：

```javascript
// main.js
import { foo } from './module.js';

foo();

export function hello() {
  console.log('hello');
}
```

```javascript
// module.js
import { hello } from './main.js';

hello();

export function foo() {
  console.log('foo');
}
```

为什么呢？因为 `export function` 是一种特例，JS 引擎对其做了全局引用提升，所以两个模块都能各自访问到。下面方式就不行了，原因是不会做全局提升：

```javascript
// main.js
import { foo } from './module.js';

foo();

export const hello = () => console.log('hello');
```

```javascript
// module.js
import { hello } from './main.js';

hello();

export const foo = () => console.log('foo');
```

所以是否生效取决于是否提升，而是否提升取决于写法。当然下面的写法也会循环引用失败，因为这种写法会被解析为导出值：

```javascript
// main.js
import foo from './module.js';

foo();

function hello() {
  console.log('hello');
}

export default hello;
```

作者的探索到这里就结束了，我们来整理一下思路，尝试理解其中的规律。

## 精读

可以这么理解：

1. 导出与导入均为引用时，最终才是引用。
2. 导入时，除 `{} = await import()` 外均为引用。
3. 导出时，除 `export default thing` 与 `export default 123` 外均为引用。

对导入来说，`{} = await import()` 相当于重新赋值，所以具体对象的引用会丢失，也就是说异步的导入会重新赋值，而 `const module = await import()` 引用不变的原因是 `module` 本身是一个对象，`module.thing` 的引用还是不变的，即便 `module` 是被重新赋值的。

对导出来说，默认导出可以理解为 `export default = thing` 的语法糖，所以 `default` 本身就是一个新的变量被赋值，所以基础类型的引用无法被导出也很合理。甚至 `export default '123'` 是合法的，而 `export { '123' as thing }` 是非法的也证明了这一点，因为命名导出本质是赋值到 `default` 变量，你可以用已有变量赋值，也可以直接用一个值，但命名导出不存在赋值，所以你不能用一个字面量作命名导出。

而导出存在一个特例，`export default function`，这个我们尽量少写就行了，写了也无所谓，因为函数保持引用不变一般不会引发什么问题。

为了保证导入的总是引用，一方面尽量用命名导入，另一方面要注意命名导出。如果这两点都做不到，可以尽量把需要维持引用的变量使用 `Object` 封装，而不要使用简单变量。

最后对循环依赖而言，只有 `export default function` 存在申明提升的 Magic，可以保证循环依赖正常 Work，但其他情况都不支持。要避免这种问题，最好的办法是不要写出循环依赖，遇到循环依赖时使用第三个模块作中间人。

## 总结

一般我们都希望 import 到的是引用而不是瞬时值，但因为语义与特殊语法糖的原因，导致并不是所有写法效果都是一致的。

我也认为不需要背下来这些导入导出细枝末节的差异，只要写模块时都用规范的命名导入导出，少用默认导出，就可以在语义与实际表现上规避掉这些问题啦。


> 讨论地址是：[精读《export 默认/命名导出的区别》· Issue #342 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/342)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
