[译] 为什么我不再使用 export default 来导出模块

栏目: JavaScript · 发布时间: 5年前

内容简介:在与默认导出(export default)死缠烂打了这么多年后,我改变了主意。上个星期,我发了条推特,收到了不少出人意料的回复:2019年,我要做的其中一件事就是不再从我的 CommonJS/ES6 模块中导出默认值。

在与默认导出(export default)死缠烂打了这么多年后,我改变了主意。

上个星期,我发了条推特,收到了不少出人意料的回复:

2019年,我要做的其中一件事就是不再从我的 CommonJS/ES6 模块中导出默认值。

导入一个默认值感觉上就像抛硬币一样,有一半的概率会猜错。比如我有时就会搞不清楚导入的到底是 class 还是 function。

— Nicholas C. Zakas (@slicknet)January 12, 2019

我意识到我所遇到的大多数与 JavaScript 模块有关的问题都可以归咎于默认导出,于是就发了这条推特。不管我用的是 JavaScript 模块(或者 ECMAScript 模块,很多人喜欢这么叫它)还是 CommonJS,都会深陷于默认导出的泥潭。那条推特收到了各种各样的评论,很多人都在问我我是如何得出这个结论的。在这篇文章中,我将尽可能地解释我的思考历程。

一些澄清

正如所有的推文一样,我的推文不过是我的看法的一个缩影,而不是我完整看法的规范性参考。首先我要澄清推文里让人困惑的几点:

  • 关于不知道导出的是 function 还是 class 这一点,它只是我在使用中所遇到的诸多问题中的一个例子。这不是命名导出为我解决的 唯一的一个 问题。
  • 我所遇到的问题不只出现在我自己的项目中,当引入某些第三方库和 工具 模块时,也会出现这些问题。这意味着文件名的命名约定并不能解决所有问题。
  • 我并不是要所有人都放弃默认导出。我只是说在我写的模块中,我会选择不去用默认导出。当然你可以有你自己看法。

希望以上澄清可以避免后文可能产生的一些误会。

默认导出:最初的选择

据我所知,默认导出是最先从 CommonJS 流行开来的。模块可以通过如下方式导出某个默认值:

class LinkedList {}
module.exports = LinkedList;
复制代码

这段代码导出了 LinkedList 类,但是并没有规定它被引用时应该使用的名称。假设该文件名为 linked-list.js ,你可以通过如下方式在其它模块中导入它:

const LinkedList = require("./linked-list");
复制代码

我只是碰巧把 require() 还是返回的值命名为 LinkedList ,以匹配文件名 linked-list.js ,但是我也完全可以叫它 fooMountain 或者其它随便什么名称。

默认模块导出在 CommonJS 中的流行,说明 JavaScript 模块生来就支持这种模式:

ES6 偏好单一/默认导出的风格,而且为默认导入提供了甜蜜的语法糖。

— David HermanJune 19, 2014

因此,在 JavaScript 模块中,你可以通过如下方式导出默认值:

export default class LinkedList {}
复制代码

然后,你可以这样来导入它:

import LinkedList from "./linked-list.js";
复制代码

再次说明,这里的 LinkedList 这是个随意的选择(如果不是特别合理的话),并没有特殊含义,也可以是 Dog 或者 symphony 诸如此类。

另一个选择:命名导出

除了默认导出以外,CommonJS 和 JavaScript 模块都支持命名导出。在导入时,命名导出允许保留被导出的函数、类或者变量的名称。

在 CommonJS 中,你可以通过在 exports 对象上添加某对键值来创建命名导出:

exports.LinkedList = class LinkedList {};
复制代码

然后,你可以在另一个文件中使用如下方法来导入它们:

const LinkedList = require("./linked-list").LinkedList;
复制代码

再次说明, const 之后的名字是任取的,但是为了导出时的名称一致,这里我选择使用 LinkedList

在 JavaScript 模块中,命名导出看上去像这样:

export class LinkedList {}
复制代码

然后你可以这样来导入它:

import { LinkedList } from "./linked-list.js";
复制代码

这里, LinkedList 不可以取任意的标识符,必须与命名导出使用的名称一致。对于这篇文章要讲的东西而言,这是与 CommonJS 唯一的重要区别。

所以说,这两种模块化方案都支持默认导出和命名导出。

个人偏好

在进一步深入之前,我需要说明一下我自己在写代码时的一些个人偏好。这是我写代码的总体原则,与语言本身无关。

  1. 明了胜于晦涩。我不喜欢有秘密的代码。某个东西是干嘛的,应该如何调用,诸如此类,在任何可能的情况下,都应该明确且清晰。

  2. 名称应该在所有文件中保持一致。如果某样东西在这个文件里叫 Apple ,那么在另一个文件里就不该叫 OrangeApple 永远都是 Apple

  3. 尽早并经常抛出错误。如果某样东西有可能缺失,那么最好就尽早检查它,接着,在最理想的情况下,抛出一个错误,让我知道问题在哪儿。我不想等着代码全部执行完后才发现出了问题,然后再去搜查问题出在哪儿。

  4. 更少地抉择意味着更快地开发速度。我的很多编程偏好都是为了减少编码过程中的抉择。每做一个决定,你都会慢上一点。这就是为什么代码规范可以提高开发速度的原因。我喜欢预先决定好所有事情,然后直接放手去做。

  5. 中途打断会拖慢开发速度。当你在编码过程中不得不停下来查找一些东西时,这就是我所说的『中途打断』。打断有时候是必要的,但是过多不必要的打断则会拖你的后腿。我想写出尽可能不需要『中途打断』的代码。

  6. 认知负荷会拖慢开发速度。简单来说,编码时,你需要记忆的用来保证效率的细节越多,你的开发速度越慢。

对开发速度的关注对我而言是个很现实的问题。多年来,我一直为自己的健康所困扰,我能用于写代码的精力越来越少。任何能帮我在保证完成度的前提下,减少编码时间的操作都很关键。

我遇到的那些问题

在上述前提下,这里是我在使用默认导出时遇到的主要问题,以及为什么我相信在大多数情况下命名导出都是更好的选择。

那究竟是啥?

正如我在之前那条推文上说的,如果模块只有一个默认导出,我很难弄清楚我导入的是什么。如果你正在用一个不熟悉的模块或文件,你很难弄清楚返回的是什么。举个例子:

const list = require("./list");
复制代码

这里,你预想中 list 应该是什么?虽然不太可能是基本类型数据,但从逻辑上讲可以是函数、类或者其它类型的对象。我怎么才能确定呢?我需要中途打断一下。当前情况下,这可能意味着:

list.js
list.js

不管是那种情况,你不得不把这段额外的信息记在脑海里,以避免当你需要再次从 list.js 导入时发生打断。如果你从各种模块中引入了很多默认值,要么你的认知负荷会增加,要么你不得不中途打断多次。两者都不理想,而且很叫人沮丧。

有人可能会说,IDE 可以解决这些问题。那么 IDE 应该足够聪明,聪明到可以弄明白正在导入的是什么,然后告诉你。当然我是支持使用聪明的 IDE 来帮助开发者的,不过我觉得要求 IDE 来有效地使用语言特性是会有问题的。

名称匹配问题

命名导出要求模块的消费者至少得指定导入东西的名称。这有个好处,我可以方便地在代码库中查找所有用到 LinkedList 的地方,知道它们都指代的同一个 LinkedList 。因为默认导出并不能限定导入时使用的名称,给导入命名会为每个开发者带来更多的认知负荷。你需要决定正确的命名规范,另外,你还得确保团队中的每个开发者对同一个事物使用相同的名称。(当然你也可以允许每一位开发者使用不同的命名,但是这会为整个团队带来更多的认知负荷。)

使用命名导出意味着至少在它被用到的地方引用的都是定好的名称。就算你选择重命名某个导入,你也得显示说明出来,不可能在不引用规定名称的情况下实现。在 CommonJS 中:

const MyList = require("./list").LinkedList;
复制代码

在 JavaScript 模块中:

import { LinkedList as MyList } from "./list.js";
复制代码

在这两种情况下,你都得显示地声明 LinkedList 被改为 MyList

如果名称在代码库中保持一致,你就可以做到以下事情:

  1. 查找代码库,了解使用情况。
  2. 在整个代码库的范围内,重命名某个东西。

如果使用默认导出和特定命名的话,这些操作可以实现吗?我猜是可以的,但是会复杂得多,也容易出现错误。

导入错误的东西

相对于默认导出,命名导出有个明显的好处。那就是,当试图导入模块中不存在的东西时,命名导入会抛出错误。考虑以下代码:

import { LinkedList } from "./list.js";
复制代码

如果 list.js 中不存在 LinkedList ,则会报错。另外,也方便像 IDE 和 ESLint这样的工具在代码执行之前检测不存在的引用。

糟糕的工具支持

提到 IDE,WebStorm 可以帮你书写 import 语句。当你在打完一个当前文件内未定义的标识符后,WebStorm 会在项目内查找模块,检查该标识符是否是某一个文件的命名导出。这时,它会做如下事情:

import
import

Visual Studio Code有一个插件可以实现类似的功能。这种功能无法通过默认导出实现,因为你想导入的东西没有确定的名称。

结论

当我在项目中使用默认导出时,我遇到严重的工作效率问题。然而这些问题并不是无解的,使用命名导出和导入可以更好地配合我的编程习惯。清晰明确的代码和对工具的重度依赖使我成为高效的程序员。只要命名导出可以帮我做到这些,在可预见的未来内,我都会支持它们。当然,我无法决定我用的第三方模块如何导出,但我可以控制我自己写的模块如何导出,我会选择命名导出。

正如前文说的,得提醒一下,这只是我个人的看法,你也许觉得我的论证没有足够的说服力。这篇文章并不是想劝阻任何使用默认导出,而是作为对那些询问我为什么停止使用默认导出的一个更好的回答。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Game Programming Patterns

Game Programming Patterns

Robert Nystrom / Genever Benning / 2014-11-2 / USD 39.95

The biggest challenge facing many game programmers is completing their game. Most game projects fizzle out, overwhelmed by the complexity of their own code. Game Programming Patterns tackles that exac......一起来看看 《Game Programming Patterns》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

html转js在线工具
html转js在线工具

html转js在线工具