从代码可维护性角度出发,命名导出比默认导出更好,因为它减少了因引用产生重命名情况的发生。 但命名导出与默认导出的区别不止如此,在逻辑上也有很大差异,为了减少开发时在这方面栽跟头,有必要提前了解它们的区别。 本周找来了这方面很好的的文章:[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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** > 关注 **前端精读微信公众号** > 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))