8

精读《默认、命名导出的区别》

 3 years ago
source link: https://segmentfault.com/a/1190000040439815
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

精读《默认、命名导出的区别》

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

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

本周找来了这方面很好的的文章: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 到的是引用而不是瞬时值,但因为语义与特殊语法糖的原因,导致并不是所有写法效果都是一致的。

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

讨论地址是:精读《export 默认/命名导出的区别》· Issue #342 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

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

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK