2022-09-23前沿技术00
请注意,本文编写于 579 天前,最后修改于 579 天前,其中某些信息可能已经过时。

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

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

本周找来了这方面很好的的文章:export-default-thing-vs-thing-as-default,先描述梗概,再谈谈我的理解。

概述

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

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

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

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

// 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 后输出发现,前两种输出结果变了,第三种没有变。也就是对命名导出来说,前两种是引用,第三种是值。

但默认导出又不一样:

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

export { thing };
export default thing;

setTimeout(() => {
  thing = 'changed';
}, 500);
// 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 } 也是如此吗?并不是:

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

export { thing, thing as default };

setTimeout(() => {
  thing = 'changed';
}, 500);
// 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 导出的都是值而不是引用。但不幸的是,存在一个特例:

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

setTimeout(() => {
  thing = 'changed';
}, 500);
// main.js
import thing from './module.js';

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

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

// module.js
function thing() {}

export default thing;

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

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

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

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

foo();

export function hello() {
  console.log('hello');
}
// module.js
import { hello } from './main.js';

hello();

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

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

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

foo();

export const hello = () => console.log('hello');
// module.js
import { hello } from './main.js';

hello();

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

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

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

foo();

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

export default hello;

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

精读

可以这么理解:

  1. 导出与导入均为引用时,最终才是引用。
  2. 导入时,除 {} = await import() 外均为引用。
  3. 导出时,除 export default thingexport 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 到的是引用而不是瞬时值,但因为语义与特殊语法糖的原因,导致并不是所有写法效果都是一致的。

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

本文作者:前端小毛

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!