【译】JavaScript 模块:从当即执行函数 ( IIFEs ) 到 CommonJS 再到 ES6 模块

原文地址:JavaScript Modules: From IIFEs to CommonJS to ES6 Modules
原文做者:Tyler McGinnis
译者:FrankCheungjavascript

我教授 JavaScript 给不少不一样的人很长一段时间了。这门语言广泛最难懂的概念就是模块系统。固然,这是有缘由的,由于模块在 JavaScript 中有着一个奇怪的历史。在这篇文章中,咱们将重温这段历史,你将学习到过去的模块化方式,以更好地理解现在 JavaScript 模块的工做原理。html

在咱们学习怎么在 JavaScript 中建立模块以前,咱们首先必须明白什么是模块以及它们存在的意义。如今请你环顾四周,全部你看到的稍微复杂一点的物体,均可能是使用可以组合起来的、又相对独立的小零件拼装起来的。java

下面咱们以一块手表为例子。node

一块简单的腕表由成千上万个内部零件组成。对于如何和其余零件进行协同,每个小零件都有一个特定的用途和清晰做用范围。将全部的零件放到一块儿就能够组装出一块完整的手表。我不是一个手表工程师,可是上述方法的好处清晰可见。react

可复用性 ( Reusability )

再看一下上面的图表,留意一块表上使用了多少相同的小零件。经过将十分聪明的设计思想融入到模块化中,在手表设计的不一样层面均可以复用相同的零件。这种能够复用零件的能力简化了生产流程,与此同时,我猜测也增长了收益。jquery

可组合性 ( Composability )

上述图表是可组合性的一个很好的阐释。经过划分清楚每一个内部零件的做用范围,就能够将不一样的微小的、功能单一的零件组合起来,制造出一只功能完整的手表。webpack

杠杆做用 ( Leverage )

设想一下整个制造流程。这个公司并非在制造手表,而是在制造个别的手表零件。他们既能够选择由本身公司来生产,也能够选择将这项工做外包出去,利用其余工厂进行生产,这都没有问题。无论零件在哪里生产,最关键的一点是每个零件最后可以组合起来造成一块手表便可。git

独立性 ( Isolation )

要明白整个系统是困难的,由于一块手表是由不一样的功能单一的小零件组合而成的,每一个小零件均可以被独立地设计、制造或者修理。这种独立性容许在制造或者修理手表过程当中,多人同时独立工做,互不干扰。另外,若是手表的其中一个零件损坏了,你须要作的仅仅是换掉那个损坏的零件,而不是换掉整块手表。es6

可组织性 ( Organization )

可组织性是每一个零件具备清晰的做用范围的副产品。在此基础上,可组织性是天然而然产生的。github


咱们已经看到模块化应用在咱们平常生活中的事物,好比手表上的明显的好处,若是将模块化应用到软件上会怎么样呢?一样的方法将获得一样的好处,就像手表的设计同样,咱们应该将软件设计成由不一样的功能单一的有着特定用途和清晰的做用范围的小块组成。在软件中,这些小块被称为模块。在这一点上,一个模块听上去可能和一个函数或者一个 React 组件没有太大区别。那么,一个模块究竟包含了什么?

每一个模块分为三个部分 —— 依赖(也称为导入 ( imports ) ) ( dependencies ), 代码 ( code ) , 导出 ( exports )

imports code exports

依赖( 导入 )

当一个模块须要另外一个模块的时候,它能够 import 那个模块,将那个模块看成一个依赖。例如,当你想建立一个 React 组件时,你须要 import react 模块。若是你想使用一个库,如 lodash ,你须要 import lodash 模块。

代码

当引入了你的模块须要的依赖,接下来就是这个模块真正的代码。

导出

导出 ( exports ) 是一个模块的“接口”。无论你从这个模块中导出什么,对于该模块的导入者来讲都是能够访问到的。


已经谈论足够多的上层概念了,下面让咱们来深刻一些具体的例子。

首先,让咱们先看看 React Router 。十分方便,它们有一个模块文件夹,这个文件夹天然是充满了......模块。如此,在 React Router 中,什么是一个模块呢?大多数状况下,它们直接映射 React 组件到模块。这是可行的,而且模块化逻辑一般就是你在 React 项目中如何拆分组件的逻辑。这行得通,由于若是你重温上面关于手表的部分,而且用“组件 ( component ) ”替换全部“模块 ( module ) ”字眼,这个比喻仍然是成立的。

让咱们来看一下 MemoryModule 的代码。注意如今不要过度关注里面的代码,而是更应该着眼于这个模块的结构。

// 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;

复制代码

你会注意到模块的顶部定义了所须要的引入,或者是使 MemoryRouter 正确运行的其余模块。接下来是这个模块实际的代码。在这里,它们建立了一个新的名叫 MemoryRouter 的 React 组件。而后在底部,它们定义了这个模块导出的内容 MemoryRouter 。这意味着不管什么时候其余人引入 MemoryRouter 模块,都将获得该 MemoryRouter 组件。


如今咱们明白了什么是一个模块,让咱们回顾一下手表设计的好处,而且看一下如何在遵循类似的模块化方法的状况下,让软件设计获得一样的好处。

可复用性

模块最大化了可复用性,由于一个模块能够被其余任何须要的模块导入并使用。除此之外,若是一个模块对其余应用程序有用,你还能够建立一个包 ( package )。一个包 ( package ) 能够包含一个或多个模块而且能够被上传至 NPM 供其余人下载。 react, lodash, 以及 jquery 都是 NPM 包很好的例子,由于他们能够经过NPM地址进行安装。

可组合性

由于模块明肯定义了它们导入和导出的内容,因此它们能够很容易地被组合。不只如此,优秀软件的一个标志就是能够轻松地被移除。模块化也提升了代码的“可移除性”( delete-ability )。

杠杆做用

NPM有着世界上最大的免费的可复用模块集合。这个优点是若是你须要某个特定的包,NPM都会有。

独立性

咱们对手表独立性的描述在这里一样适用。“明白整个系统是困难的,由于(你的软件)是由不一样的功能单一的(模块)组合而成的,每一个(模块)均可以被独立地设计、建立或者修复。这种独立性容许在建立或者修复(程序)过程当中,多人同时独立工做,互不干扰。另外,若是其中一个(模块)出问题了,你须要作的仅仅是换掉那个出问题的(模块),而不是换掉整个(程序)。”

可组织性

可能模块化对于软件来讲最大的好处就是可组织性。模块提供了一个天然的分割点。由此,正如咱们即将看到的那样,模块将能防止你污染全局命名空间,而且帮助你避免命名冲突。


此刻你知道了模块的好处而且了解了模块的结构,是时候开始建立它们了。咱们的方法是十分详尽的,由于正如以前提到的,JavaScript 的模块有着奇怪的历史。尽管如今 JavaScript 有“更新”的方法建立模块,但一些旧的方法仍然存在而且你还将会不时看到它们。若是咱们一会儿跳到2018年的模块化方式,对于你理解模块化来讲是不利的。所以,咱们将回到2010年底,AngularJS 刚刚发布,jQuery 正盛行。公司最终仍是使用 JavaScript 来建立复杂的网页应用,正因如此,产生了经过模块来管理复杂网页应用的须要。

你建立模块的第一直觉多是经过建立不一样文件来拆分代码。

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

function getUsers() {
  return users
}

复制代码
// 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])
}

复制代码
<!-- 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>

复制代码

完整代码见此处

好了,咱们已经成功地将咱们的应用代码拆分红不一样的文件,但这是否意味着咱们已经成功地实现了模块化呢?不,这彻底不是模块化。从字面上来讲,咱们所作的只是将代码所在的位置进行了拆分。在 JavaScript 中建立一个新的做用域的惟一方法是使用一个函数。咱们声明的全部不在函数体内的变量都是存在于全局对象上的。你能够经过在控制台上打印出 window 对象来验证这一说法。你会意识到咱们能够访问,甚至更坏的状况是,改变 addUsers, users, getUsers, addUserToDOM 。这实质上就是整个应用程序。咱们彻底没有将代码拆分到模块里去,刚才所作的只是改变了代码物理上的存在位置。若是你是 JavaScript 初学者,这可能会让你大吃一惊,但这多是你对于如何在 JavaScript 中实现模块化的第一直觉。

若是拆分文件并无实现模块化,那该怎么作?还记得模块的优势 —— 可复用性、可组合性、杠杆做用、独立性、可组织性。JavaScript 是否有一个自然的特性可供咱们用于建立“模块”,而且带来上述的好处?经过一个普通的函数如何?想一下函数的好处你会发现,它们能够很好地与模块的好处对应上。那么该怎么实现呢?与其让整个应用存在于全局命名空间上,咱们不如暴露一个单独的对象,能够称之为 APP 。能够将全部应用程序运行须要的方法放入这个 APP 对象中,防止污染全局名命空间。而后能够将全部东西用一个函数包裹,让它们相对于应用程序的其余空间是封闭的。

// App.js
var APP = {}

复制代码
// users.js
function usersWrapper () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
}

usersWrapper()

复制代码
// 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()

复制代码
<!-- 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>

复制代码

完整代码见此处

如今若是你查看 window 对象,以前它拥有应用程序全部重要的部分,如今它只拥有 APP 以及包裹函数 ( wrapper functions ),usersWrapperdomWrapper 。更重要的是,咱们重要的代码(例如 users )不能被随意修改了,由于它们并不存在于全局命名空间上。

让咱们看看是否可以更进一步,是否有方法能够避免使用包裹函数?注意咱们定义了包裹函数而且立刻调用了它们,咱们赋予包裹函数一个名字的缘由只是为了能调用它们。是否有方法能够当即调用一个匿名函数,这样咱们就不须要赋予它们名字了?确实有这样的方法,而且这个方法还有一个很好的名字 —— 当即执行函数表达式 ( Immediately Invoked Function Expression ) 或者缩写为 IIFE

IIFE

下面是 IIFE 的大概样式

(function () {
  console.log('Pronounced IF-EE')
})()

复制代码

注意下面只是一个包裹在小括号中的匿名函数表达式。

(function () {
  console.log('Pronounced IF-EE')
})

复制代码

而后,就像其余函数同样,为了调用它,咱们在其后面添加了一对小括号。

(function () {
  console.log('Pronounced IF-EE')
})()

复制代码

如今为了不使用包裹函数,而且让全局名命空间变干净,让咱们使用 IIFEs 的相关知识改造咱们的代码。

// users.js

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

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
})()

复制代码
// 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])
  }
})()

复制代码

完整代码见此处

完美!如今若是你查看 window 对象,你将会看到只添加了 APP 到其上,做为该应用程序运行所需的全部方法的命名空间。

咱们能够称这个模式为 IIFE Module Pattern

IIFE Module Pattern 的好处是什么呢?首先而且最重要的是,避免了将全部东西都放置到全局命名空间上,这将有助于减小变量冲突以及让代码更私有化。这种模式是否有不足之处?固然,咱们仍在全局命名空间上建立了一个变量, APP 。若是碰巧另外一个库也使用相同的命名空间就会很麻烦。其二,index.html<script> 标签的顺序影响代码执行,若是不保持现有顺序,那么整个应用程序将会崩溃。

尽管上述解决方法并不是完美,但仍然是进步的。如今咱们明白了 IIFE module pattern 的优缺点,若是让咱们制定建立和管理模块的标准,会须要什么样的功能呢?

以前咱们将代码拆分为模块的第一直觉,是每一个文件都是一个新的模块。尽管在 JavaScript 中这并不起做用,但我认为这是一个明显的模块分割点。每一个文件就是一个独立的模块。基于此,还须要一个功能,就是让每一个文件定义明确的导入(或者说是依赖),以及对于导入模块可用的明确的导出

Our Module Standard

1) File based
2) Explicit imports
3) Explicit exports

复制代码

如今知道了咱们制定的模块标准须要的功能,下面能够来看一下 API 。惟一真实的咱们须要定义的 API 是导入和导出的实现。从导出开始,尽可能保持简单,任何关于模块的信息能够放置于在 module 对象中。而后咱们能够将想导出的内容添加到 module.exports 。跟下面的代码相似:

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

function getUsers() {
  return users
}

module.exports.getUsers = getUsers

复制代码

这意味着咱们也能够用下面的写法:

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

function getUsers() {
  return users
}

module.exports = {
  getUsers: getUsers
}

复制代码

不管有多少方法,均可以将他们添加到 exports 对象上。

// users.js

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

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

复制代码

如今咱们弄清楚了从一个模块导出内容是怎样的,下面须要弄清楚从模块导入内容的 API 是怎样的。简单而言,咱们假设有一个名叫 require 的函数。它将接收字符串路径做为第一参数,而后将会返回从该路径导出的内容。继续使用 users.js 做为例子,引入模块将会相似下面的方式:

var users = require('./users')

users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]

复制代码

很是顺手。使用咱们假想的 module.exportsrequire 语法,咱们保留了模块的全部好处而且避免了使用 IIFE Modules pattern 的两个缺点。

正如你目前为止所猜想的,这并非一个虚构的标准。这个标准是真实存在的,它叫作 CommonJS 。

CommonJS 定义了一个模块格式,经过保证每一个模块在其独自的命名空间内执行, 来解决 JavaScript 的做用域问题。这须要强制模块清晰地导出须要暴露给外界的变量,而且定义好代码正常工做须要引入的其余模块。 —— Webpack 文档

若是你以前使用过 Node ,CommonJS 看起来是类似的。这是由于为了实现模块化,Node (大多数状况下)使用了 CommonJS 的规范。所以,在 Node 中你使用以前看到过的,CommonJS 的 requiremodule.exports 语法来使用模块。然而,浏览器并不像 Node ,其并不支持 CommonJS 。事实上,不只仅是浏览器不支持 CommonJS 的问题,并且对于浏览器来讲, CommonJS 并非一个好的模块化解决方案,由于它对于模块的加载是同步的。在浏览器环境中,异步加载才是王道。

整体而言,CommonJS 有两个问题。第一个问题是浏览器并不支持,第二个问题是它的模块加载是同步的, 这样在浏览器端的用户体验是极差的。若是可以解决上述两个问题,状况将会大为不一样。那么若是CommonJS对于浏览器并不友好,咱们花时间讨论它的意义何在?下面将介绍一种解决方案,它被称为 模块打包器 ( module bundler ) 。

模块打包器 ( Module Bundlers )

JavaScript 模块打包器会检查你整个代码库,找到全部的导入和导出,而后智能地将全部模块打包成一个浏览器能识别的单独的文件。不须要像之前同样在 index.html 中按顺序引入全部的 scripts ,如今你须要作的是引入那个打包好的文件 bundle.js 便可。

app.js ---> |         |
users.js -> | Bundler | -> bundle.js
dom.js ---> |         |

复制代码

那么打包器其实是如何工做的呢?这真是一个大问题,而且这个问题我也没有彻底弄明白。但下面给出经过 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?`);}) }); 复制代码

你将留意到这里有大量魔术般的代码(若是你想弄明白究竟发生了什么,能够阅读注释),但有趣的是全部的代码被包裹在一个大的 IIFE 中了。这样一来,经过简单地利用原来的 IIFE Module Pattern,他们找到了一个能够获得一个优秀模块系统全部优势的同时,避免上文提到的缺点的方法。


将来真正证实了 JavaScript 是一门活的语言。 TC-39 ,JavaScript 标准委员会,一年讨论几回关于这门语言的潜在优化可能性。与此同时,能够清晰看到,模块化对于建立可扩展、可维护的 JavaScript 代码来讲是一个重要的功能。在2013年之前(也可能好久之前),JavaScript 很明显须要一个标准化的、内置的解决方案来处理模块。这开始了原生 JavaScript 实现模块化的进程。

基于你如今所知道的,若是你被赋予一项任务是为 JavaScript 创设一个模块系统,你设想会是怎样的? CommonJS 大部分实现是正确的。就像 CommonJS ,每一个文件是一个新的模块而且能清晰定义导入和导出的内容。—— 很明显,这是最重要的一点。CommonJS 的一个问题是它加载模块是同步的,这对服务器来讲是好的,可是对于浏览器来讲则偏偏相反。其中一个能够作出的改变是支持模块异步加载,另外一个能够作出的改变是定义新的关键字,而不是使用一个 require 函数调用,由于咱们须要的是让这门语言原生支持该功能。下面让咱们从 importexport 开始。

没有与上述咱们“假设的标准”相距太远,当TC-39 委员会创造出 "ES Modules"(目前在 JavaScript 中建立模块的标准方法)的时候,他们想到了这个彻底相同的设计思路。让咱们来看一下这个语法。

ES Modules

正如上文提到的,为了指定须要从模块导出的内容,须要使用 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]
}

复制代码

如今要导入 firstlast ,你有几个不一样的选择。其中一个是导入全部从 utils.js 中导出的东西。

import * as utils from './utils'

utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3

复制代码

但若是咱们并不想导入全部该模块导出的东西呢?具体到这个例子而言,若是咱们仅仅想导入 first 但不想导入 last 呢?这里可使用 名命导入 ( named imports ) (看起来像解构但其实并非)

import { first } from './utils'

first([1,2,3]) // 1

复制代码

ES Modules 很酷的地方不只仅是能够指定多个普通导出,并且也能够指定一个默认导出 ( default export )

// 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;
}

复制代码

当你使用默认导出时,这将改变你引入该模块的方式。不用像以前同样使用 * 语法或者名命导入,默认导出只须要使用 import name from './path 进行导入。

import leftpad from './leftpad'

复制代码

若是有一个模块既有默认导出,也有其余常规导出呢?没错,你能够用你期待的方式来进行导入。

// 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;
}

复制代码

这样的话,导入语法是什么样的呢?在这个例子中,一样,能够以你指望的方式导入。

import leftpad, { first, last } from './utils'

复制代码

很是顺手,对吧? leftpad 是默认导出, firstlast 是常规导出。

ES Modules 有趣的地方在于,由于是 JavaScript 的原生语法,现代浏览器不须要使用打包器就能够支持。看看教程一开始的简单的 Users 的例子使用 ES Modules后会是什么样子的。

完整代码见此处

// users.js

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

export default function getUsers() {
  return users
}

复制代码
// 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])
}

复制代码

这就是 ES Modules 神奇的地方。 使用 IIFE pattern, 仍然须要经过 script 标签引入每个 JS 文件(而且要按顺序)。使用 CommonJS 须要一个打包器,如 Webpack ,而后经过一个 script 标签引入 bundle.js 文件。使用 ES Modules, 在现代浏览器中,须要作的只是引入主文件(这个例子中的 dom.js ) 而且在 script 标签上添加 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>

复制代码

Tree Shaking

CommonJS 和 ES Modules 之间还有一个不一样点上文没有说起。

使用 CommonJS ,你能够在任何地方 require 一个模块,甚至是有条件地引入。

if (pastTheFold === true) {
  require('./parallax')
}

复制代码

由于 ES Modules 是静态的,导入声明必须位于模块的顶层。有条件地引入是不能够的。

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

复制代码

这样一个设计思路的缘由是,经过强制为静态模块,加载器能够静态分析模块树,找出实际被使用的代码,而且从代码束中丢弃没有被使用的代码。这是一个很大的话题,用另一种说法就是,由于 ES Modules 强制在模块顶层写导入声明,打包器能够快速了解代码的依赖树,据此检查哪些代码没有被使用并将他们从代码束中移除。这就叫作 Tree Shaking or Dead Code Elimination

这是一个 stage 3 proposal 关于 动态导入 ( dynamic imports ) 的介绍,这个语法容许有条件地使用 import() 导入模块。


我但愿经过深刻 JavaScript 模块的历史,不只能够帮助你更好地理解 ES Modules ,还能够帮你更好理解它们的设计思路。

相关文章
相关标签/搜索