JavaScript 模块:从 IIFE 到 CommonJS 再到 ES6 模块

2025-06-07

JavaScript 模块:从 IIFE 到 CommonJS 再到 ES6 模块

我教过很多人 JavaScript 很久了。这门语言最常被低估的部分就是模块系统。这是有原因的。JavaScript 中的模块有着一段奇特而又不稳定的历史。在这篇文章中,我们将回顾这段历史,并学习过去的模块,以便更好地理解 JavaScript 模块如今的工作原理。


如果你更喜欢观看而不是阅读,这里有一个涵盖相同内容的视频。如果你喜欢,欢迎订阅。


在学习如何在 JavaScript 中创建模块之前,我们首先需要了解它们是什么以及它们存在的原因。现在看看你周围。你能看到的任何稍微复杂的东西,很可能都是由一些独立的部件组成的,这些部件组装在一起就形成了完整的整体。

我们以手表为例。

手表内部结构

一块简单的腕表由数百个内部零件组成。每个零件都有特定的用途,并明确了与其他零件的相互作用。所有这些零件组合在一起,构成了腕表的整体。我不是腕表工程师,但我认为这种设计方法的好处显而易见。

可重用性

再看一下上面的图表。注意整个手表使用了多少相同的部件。通过以模块化为中心的高度智能的设计决策,他们能够在手表设计的不同方面重复使用相同的组件。这种部件重复使用的能力简化了制造流程,我猜想,这也会提高利润。

可组合性

这张图完美地展现了可组合性。通过为每个组件建立清晰的边界,他们能够将各个部件组合在一起,从而由微小而专注的部件构建出一块功能齐全的手表。

杠杆作用

想想制造过程。这家公司不是在制造手表,而是在制造组装成手表的各个部件。他们可以自己生产这些部件,也可以外包,并利用其他制造工厂,这都无关紧要。最重要的是,每个部件最终组装成一块手表——这些部件在哪里制造并不重要。

隔离

理解整个系统并非易事。由于手表由细小、专注的部件组成,因此每个部件都可以单独思考、制造或维修。这种独立性使得多个人可以独立地操作手表,而不会互相干扰。此外,如果其中一个部件坏了,你只需更换损坏的部件,而不必更换整块手表。

组织

组织性是每个独立部分之间交互方式清晰边界的副产品。有了这种模块化,组织性自然而然地发生,无需过多思考。


我们已经见证了模块化在手表等日常用品上带来的显著优势,那么在软件方面呢?事实证明,两者有着相同的理念和优势。就像手表的设计一样,我们应该将软件设计成不同的部分,每个部分都有特定的用途,并有明确的界限来规定它们如何与其他部分交互。在软件中,这些部分被称为模块。模块听起来可能与函数或 React 组件没什么区别。那么,模块究竟包含哪些内容呢?

每个模块都有三个部分 - 依赖项(也称为导入)、代码和导出。

imports

code

exports
Enter fullscreen mode Exit fullscreen mode
依赖项(导入)

当一个模块需要另一个模块时,它可以import将该模块作为依赖项。例如,每当你想要创建一个 React 组件时,你都需要importreact模块。如果你想使用像 这样的库lodash,你也需要importlodash模块。

代码

确定模块所需的依赖项后,下一部分是模块的实际代码。

出口

导出是模块的“接口”。从模块导出的任何内容,导入该模块的用户都可以使用。


说了足够多的高层次内容,让我们深入研究一些真实的例子。

首先,我们来看看 React Router。它有一个modules文件夹,方便快捷。这个文件夹里自然充满了……模块。那么在 React Router 中,“模块”的定义是什么呢?事实证明,在大多数情况下,React Router 会将 React 组件直接映射到模块。这很合理,而且通常也是在 React 项目中分离组件的方式。之所以有效,是因为即使你重新阅读上面的代码,将“模块”替换成“组件”,这些比喻仍然适用。

让我们看一下模块的代码MemoryRouter。现在先不要担心实际的代码,而是集中精力于模块的结构。

// imports
import React from "react";
import { createMemoryHistory } from "history";
import Router from "./Router";

// code
class MemoryRouter extends React.Component {
  history = createMemoryHistory(this.props);
  render() {
    return (
      <Router
        history={this.history}
        children={this.props.children}
      />;
    )
  }
}

// exports
export default MemoryRouter;
Enter fullscreen mode Exit fullscreen mode

你会注意到,在模块的顶部,他们定义了导入的内容,或者说,为了使MemoryRouter模块正常工作,他们需要哪些其他模块。接下来,他们有自己的代码。在本例中,他们创建了一个名为 的新 React 组件MemoryRouter。然后在最底部,他们定义了导出的内容,MemoryRouter。这意味着,每当有人导入该MemoryRouter模块时,他们都会获得该MemoryRouter组件。


现在我们了解了什么是模块,让我们回顾一下手表设计的好处,看看如何通过遵循类似的模块化架构,将这些好处应用到软件设计中。

可重用性

模块可以最大限度地提高可重用性,因为模块可以被导入到任何需要它的模块中。此外,如果某个模块对其他程序有用,您可以将其创建一个包。一个包可以包含一个或多个模块,并且可以上传到NPM供任何人下载。reactlodash、 和jquery都是 NPM 包的示例,因为它们可以从 NPM 目录安装。

可组合性

由于模块明确定义了它们的导入和导出,因此它们易于组合。更重要的是,易于删除是优秀软件的标志。模块提高了代码的“可删除性”。

杠杆作用

NPM 仓库拥有世界上最大的免费可复用模块集合(确切地说超过 70 万个)。如果你需要某个特定的软件包,NPM 很可能能帮你搞定。

隔离

我们用来描述手表隔离性的文字在这里也非常贴切。“理解整个系统很困难。因为(你的软件)是由一些小型、专注的(模块)组成的,所以每个(模块)都可以单独思考、构建和修复。这种隔离允许多个人单独开发(应用程序),而不会互相干扰。此外,如果其中一个(模块)出现故障,你只需更换损坏的(模块),而不是更换整个(应用程序)。”

组织

模块化软件最大的好处或许在于其组织性。模块提供了一个自然的分离点。此外,正如我们即将看到的,模块还能防止污染全局命名空间,并避免命名冲突。


至此,您已经了解了模块的优势和结构。现在是时候真正开始构建它们了。我们将采用非常系统的方法。原因在于,正如前文所述,JavaScript 中的模块有着一段奇特的历史。尽管现在有一些“更新”的 JavaScript 模块创建方法,但一些较老的版本仍然存在,您会不时看到它们。如果我们在 2018 年就直接讨论模块,那对您来说就太不公平了。话虽如此,我们将回到 2010 年末。当时 AngularJS 刚刚发布,jQuery 风靡一时。企业终于开始使用 JavaScript 构建复杂的 Web 应用程序,而这种复杂性带来了对它的管理需求——通过模块。

创建模块的第一个直觉可能是按文件分离代码。

// users.js
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}
Enter fullscreen mode Exit fullscreen mode
// dom.js

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = window.getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

完整代码可以在这里找到。

好的。我们已经成功地将我们的应用程序分离成它自己的文件。这是否意味着我们已经成功地实现了模块?不,绝对不是。从字面上看,我们所做的只是分离代码所在的位置。在 JavaScript 中创建新作用域的唯一方法是使用函数。我们声明的所有不在函数中的变量都存在于全局对象中。您可以通过window在控制台中记录对象来看到这一点。您会注意到我们可以访问,更糟糕的是,更改addUsersusersgetUsersaddUserToDOM。这基本上就是我们的整个应用程序。我们没有将代码分离成模块,我们所做的只是按物理位置将其分开。如果您是 JavaScript 新手,这可能会让您感到惊讶,但这可能是您对如何在 JavaScript 中实现模块的第一直觉。

那么,如果文件分离不能给我们带来模块,那什么能呢?记住模块的优势——可重用性、可组合性、可利用性、隔离性和可组织性。JavaScript 是否有原生特性可以用来创建我们自己的“模块”,并带来同样的好处?普通的函数怎么样?当你想到函数的好处时,它们与模块的好处完美契合。那么这将如何实现呢?如果我们不将整个应用程序放在全局命名空间中,而是公开一个对象,我们将其称为APP。然后,我们可以将应用程序运行所需的所有方法放在 下APP,这将防止我们污染全局命名空间。然后,我们可以将其他所有内容包装在一个函数中,以将其与应用程序的其余部分隔离开来。

// App.js
var APP = {}
Enter fullscreen mode Exit fullscreen mode
// users.js
function usersWrapper () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
}

usersWrapper()
Enter fullscreen mode Exit fullscreen mode
// dom.js

function domWrapper() {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
}

domWrapper()
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="app.js"></script>
    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

完整代码可以在这里找到。

现在,如果你查看window对象,它不再包含我们应用程序的所有重要部分,而只包含APP我们的包装函数usersWrapperdomWrapper。更重要的是,我们所有重要的代码(例如users)都无法修改,因为它们不再位于全局命名空间中。

让我们看看能否更进一步。有没有办法摆脱包装函数?注意,我们定义它们之后会立即调用它们。我们给它们命名的唯一原因就是为了能够立即调用它们。有没有办法立即调用匿名函数,这样我们就不用给它们命名了?事实证明是有的,而且它甚至还有一个更酷的名字——Immediately Invoked Function Expression或者IIFE简称。

当前执行函数

它看起来是这样的。

(function () {
  console.log('Pronounced IF-EE')
})()
Enter fullscreen mode Exit fullscreen mode

请注意,它只是一个用括号 () 包裹的匿名函数表达式。

(function () {
  console.log('Pronounced IF-EE')
})
Enter fullscreen mode Exit fullscreen mode

然后,就像任何其他函数一样,为了调用它,我们在它的末尾添加另一对括号。

(function () {
  console.log('Pronounced IF-EE')
})()
Enter fullscreen mode Exit fullscreen mode

现在让我们利用对 IIFE 的了解来摆脱丑陋的包装函数并进一步清理全局命名空间。

// users.js

(function () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
})()
Enter fullscreen mode Exit fullscreen mode
// dom.js

(function () {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
})()
Enter fullscreen mode Exit fullscreen mode

完整代码可以在这里找到。

厨师之吻。现在,如果您查看该window对象,您会注意到我们向其中添加的唯一内容是APP,我们将其用作应用程序正常运行所需的所有方法的命名空间。

我们将此模式称为IIFE 模块模式

IIFE 模块模式有什么好处?首先,我们避免了将所有内容都转储到全局命名空间。这有助于避免变量冲突,并使我们的代码更加私密。它有什么缺点吗?当然有。我们在全局命名空间中仍然有一项, 。如果其他库碰巧使用了相同的命名空间,我们就有麻烦了。其次,你会注意到文件中标签APP的顺序很重要。如果你没有按照现在的顺序排列脚本,应用程序就会崩溃。<script>index.html

尽管我们的解决方案并不完美,但我们正在不断进步。既然我们了解了 IIFE 模块模式的优缺点,那么如果我们要制定自己的模块创建和管理标准,它会具备哪些特性呢?

之前,我们分离模块的初衷是为每个文件创建一个新的模块。虽然 JavaScript 并非开箱即用,但我认为这是模块分离的明显起点。每个文件都是一个独立的模块。这样一来,我们唯一需要的功能就是让每个文件定义显式的导入(或依赖)和显式的导出,以便其他任何导入该模块的文件都能访问它们。

Our Module Standard

1) File based
2) Explicit imports
3) Explicit exports
Enter fullscreen mode Exit fullscreen mode

现在我们已经了解了模块标准所需的功能,让我们深入研究一下 API。我们唯一需要定义的 API 是导入和导出。我们先从导出开始。为了简单起见,任何关于模块的信息都可以放在对象上module。然后,任何我们想从模块导出的内容都可以放在 上module.exports。像这样

var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports.getUsers = getUsers
Enter fullscreen mode Exit fullscreen mode

这意味着我们可以用另一种方式来写它

var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports = {
  getUsers: getUsers
}
Enter fullscreen mode Exit fullscreen mode

无论我们有多少种方法,我们都可以将它们添加到exports对象中。

// users.js

var users = ["Tyler", "Sarah", "Dan"]

module.exports = {
  getUsers: function () {
    return users
  },
  sortUsers: function () {
    return users.sort()
  },
  firstUser: function () {
    return users[0]
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们已经搞清楚了模块导出是什么样子的,接下来我们需要搞清楚导入模块的 API 是什么样子的。为了保持这一点的简单性,我们假设有一个名为 的函数require。它将一个字符串路径作为第一个参数,并返回从该路径导出的任何内容。按照users.js上面的文件,导入该模块的代码如下:

var users = require('./users')

users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]
Enter fullscreen mode Exit fullscreen mode

相当巧妙。通过我们的假设module.exportsrequire语法,我们保留了模块的所有优点,同时摆脱了 IIFE 模块模式的两个缺点。

你可能已经猜到了,这不是一个虚构的标准。它是真实存在的,叫做 CommonJS。

CommonJS 小组定义了一种模块格式,通过确保每个模块在其自己的命名空间中执行来解决 JavaScript 作用域问题。这是通过强制模块显式导出它想要暴露给“universe”的变量,以及定义正常工作所需的其他模块来实现的。

-Webpack 文档

如果您以前使用过 Node,那么 CommonJS 应该很熟悉。这是因为 Node(在大多数情况下)使用 CommonJS 规范来实现模块。因此,使用 Node,您可以使用前面提到的 CommonJSrequiremodule.exports语法来获取开箱即用的模块。然而,与 Node 不同,浏览器不支持 CommonJS。事实上,浏览器不仅不支持 CommonJS,而且 CommonJS 本身就不是一个很好的浏览器解决方案,因为它是同步加载模块的。在浏览器领域,异步加载器才是王道。

总结一下,CommonJS 有两个问题。首先,浏览器无法理解它。其次,它会同步加载模块,这在浏览器中会带来糟糕的用户体验。如果我们能解决这两个问题,那就万事大吉了。既然 CommonJS 连浏览器都不支持,那我们花这么多时间讨论它还有什么意义呢?其实有一个解决方案,那就是模块打包器。

模块捆绑器

JavaScript 模块打包器的作用是检查你的代码库,查看所有导入和导出语句,然后智能地将所有模块打包成一个浏览器可以理解的单一文件。这样,你就不用把所有脚本都打包到 index.html 文件中,也不用担心它们的加载顺序了,只需bundle.js打包器自动生成的单一文件即可。

app.js ---> |         |
users.js -> | Bundler | -> bundle.js
dom.js ---> |         |
Enter fullscreen mode Exit fullscreen mode

那么打包器究竟是如何工作的呢?这是一个很大的问题,我自己也不太明白,不过这是我们用Webpack(一个流行的模块打包器)运行简单代码后的输出。

完整的 CommonJS 和 Webpack 代码可以在这里找到。你需要下载代码,运行“npm install”,然后运行“webpack”。

(function(modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // Execute the module function
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
  }
  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;
  // expose the module cache
  __webpack_require__.c = installedModules;
  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(
        exports,
        name,
        { enumerable: true, get: getter }
      );
    }
  };
  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string')
      for(var key in value)
        __webpack_require__.d(ns, key, function(key) {
          return value[key];
        }.bind(null, key));
    return ns;
  };
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
  };
  // __webpack_public_path__
  __webpack_require__.p = "";
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({

/***/ "./dom.js":
/*!****************!*\
  !*** ./dom.js ***!
  \****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval(`
  var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
  function addUserToDOM(name) {\n
    const node = document.createElement(\"li\")\n
    const text = document.createTextNode(name)\n
    node.appendChild(text)\n\n
    document.getElementById(\"users\")\n
      .appendChild(node)\n}\n\n
    document.getElementById(\"submit\")\n
      .addEventListener(\"click\", function() {\n
        var input = document.getElementById(\"input\")\n
        addUserToDOM(input.value)\n\n
        input.value = \"\"\n})\n\n
        var users = getUsers()\n
        for (var i = 0; i < users.length; i++) {\n
          addUserToDOM(users[i])\n
        }\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),

/***/ "./users.js":
/*!******************!*\
  !*** ./users.js ***!
  \******************/
/*! no static exports found */
/***/ (function(module, exports) {

eval(`
  var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
  function getUsers() {\n
    return users\n}\n\nmodule.exports = {\n
      getUsers: getUsers\n
    }\n\n//# sourceURL=webpack:///./users.js?`);})
});
Enter fullscreen mode Exit fullscreen mode

你会注意到这里面有很多神奇之处(如果你想知道具体是怎么回事,可以看看注释),但有一点很有趣,他们把所有代码都包裹在一个大的 IIFE 函数式中。所以他们找到了一种方法,只需利用我们古老的 IIFE 模块模式,就能获得模块系统的所有优点,而不用承担模块系统的缺点。


真正能证明 JavaScript 面向未来发展的是,它是一门充满活力的语言。JavaScript 标准委员会 TC-39 每年召开几次会议,讨论该语言的潜在改进。目前,模块对于编写可扩展、可维护的 JavaScript 至关重要,这一点应该已经非常清楚。早在 2013 年左右(或许更早),JavaScript 就需要一个标准化的内置模块处理解决方案。这开启了 JavaScript 原生模块实现的进程。

了解了现在的知识,如果你被要求为 JavaScript 创建一个模块系统,它会是什么样子?CommonJS 基本上做到了。和 CommonJS 一样,每个文件都可以是一个新模块,并且有清晰的方式定义导入和导出——显然,这就是关键所在。我们在使用 CommonJS 时遇到的一个问题是它同步加载模块。这对服务器来说很棒,但对浏览器来说却并非如此。我们可以做的一个改进是支持异步加载。另一个改进是require,既然我们讨论的是语言本身的新增功能,我们可以定义新的关键字,而不是函数调用。我们就用importand 吧export

TC-39 委员会并没有再沿着“假设的、虚构的标准”这条路走太远,他们在创建“ES 模块”时也做出了完全相同的设计决策。ES 模块现在是 JavaScript 中创建模块的标准化方式。让我们来看看它的语法。

ES 模块

如上所述,为了指定应该从模块导出什么,您可以使用export关键字。

// utils.js

// Not exported
function once(fn, context) {
  var result
  return function() {
    if(fn) {
      result = fn.apply(context || this, arguments)
      fn = null
    }
    return result
  }
}

// Exported
export function first (arr) {
  return arr[0]
}

// Exported
export function last (arr) {
  return arr[arr.length - 1]
}
Enter fullscreen mode Exit fullscreen mode

现在,为了导入firstlast,您有几种不同的选择。一种是导入从中导出的所有内容utils.js

import * as utils from './utils'

utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3
Enter fullscreen mode Exit fullscreen mode

但是如果我们不想导入模块导出的所有内容怎么办?在这个例子中,如果我们想导入first但又不想导入怎么办last?这时你就可以使用所谓的named imports(它看起来像解构,但实际上不是)。

import { first } from './utils'

first([1,2,3]) // 1
Enter fullscreen mode Exit fullscreen mode

ES 模块的妙处在于,您不仅可以指定多个导出,还可以指定一个default导出。

// leftpad.js

export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len & 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}
Enter fullscreen mode Exit fullscreen mode

使用default导出时,导入模块的方式会发生变化。现在,您无需使用*语法或命名导入,只需使用 即可import name from './path'

import leftpad from './leftpad'
Enter fullscreen mode Exit fullscreen mode

现在,如果你有一个模块,它不仅导出了某个default导出项,还导出了其他常规导出项,该怎么办?好吧,你会按照你期望的方式去做。

// utils.js

function once(fn, context) {
  var result
  return function() {
    if(fn) {
      result = fn.apply(context || this, arguments)
      fn = null
    }
    return result
  }
}

// regular export
export function first (arr) {
  return arr[0]
}

// regular export
export function last (arr) {
  return arr[arr.length - 1]
}

// default export
export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len & 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}
Enter fullscreen mode Exit fullscreen mode

现在导入语法应该是什么样子的?在这种情况下,它应该符合你的预期。

import leftpad, { first, last } from './utils'
Enter fullscreen mode Exit fullscreen mode

很巧妙,是吧?leftpaddefault出口,而firstlast只是常规出口。

ES 模块的有趣之处在于,由于它本身就是 JavaScript 的原生特性,现代浏览器无需使用打包器即可支持它们。让我们回顾一下本教程开头的简单用户示例,看看使用 ES 模块后会是什么样子。

完整代码可以在这里找到。

// users.js

var users = ["Tyler", "Sarah", "Dan"]

export default function getUsers() {
  return users
}
Enter fullscreen mode Exit fullscreen mode
// dom.js

import getUsers from './users.js'

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
Enter fullscreen mode Exit fullscreen mode

现在到了最酷的部分。使用我们的 IIFE 模式,我们仍然需要在每个 JS 文件中引入一个脚本(并且按顺序引入)。使用 CommonJS 时,我们需要使用像 Webpack 这样的打包工具,然后将脚本引入到bundle.js文件中。使用 ES 模块,在现代浏览器中,我们只需要引入主文件(在本例中为dom.js),并type='module'在脚本标签页中添加一个属性即可。

<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users">
    </ul>
    <input id="input" type="text" placeholder="New User"></input>
    <button id="submit">Submit</button>

    <script type=module src='dom.js'></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

摇树

CommonJS 模块和 ES 模块之间还有一个区别我们上面没有提到。

使用 CommonJS,您可以require在任何地方使用模块,甚至有条件地使用。

if (pastTheFold === true) {
  require('./parallax')
}
Enter fullscreen mode Exit fullscreen mode

由于 ES 模块是静态的,因此 import 语句必须始终位于模块的顶层。您无法有条件地导入它们。

if (pastTheFold === true) {
  import './parallax' // "import' and 'export' may only appear at the top level"
}
Enter fullscreen mode Exit fullscreen mode

做出此设计决策的原因是,通过强制模块静态化,加载器可以静态分析模块树,找出实际使用的代码,并从打包文件中移除未使用的代码。这可不是说说而已。换句话说,由于 ES 模块强制你在模块顶部声明 import 语句,打包器可以快速理解你的依赖关系树。理解了依赖关系树后,它就能找出哪些代码未被使用,并将其从打包文件中移除。这被称为Tree Shaking 或 Dead Code Elimination

有一个关于动态导入的第三阶段提案,它将允许您通过 import() 有条件地加载模块。


我希望深入了解 JavaScript 模块的历史不仅能帮助您更好地欣赏 ES 模块,还能更好地理解它们的设计决策。


这最初发布在ui.dev上,是其高级 JavaScript课程的一部分

文章来源:https://dev.to/tylermcginnis/javascript-modules-from-iifes-to-commonjs-to-es6-modules-33p1
PREV
异步 JavaScript 的演变:从回调到 Promises,再到 Async/Await
NEXT
回顾使用 Node.js 的一年以及为什么我应该坚持使用 Laravel