JavaScript项目已经发展到令人瞠目结舌的规模,社区已经开发了用于大规模工作的工具。你需要的最基本的东西之一是一个模块系统,这是一种将你的工作分散到多个文件和目录的方法——但仍然要确保你的所有代码片段可以根据需要相互访问——而且还要能够有效地加载所有代码。所以很自然,JavaScript有一个模块系统。实际上,有不少模块系统。还有一些包管理器,用于安装所有这些软件和处理高级依赖关系的工具。你可能会认为,拥有新的模块语法的ES6有点晚了。

今天我们将看到ES6是否会在这些现有系统中添加任何东西,以及未来的标准和工具是否能够在它的基础上构建。但首先,让我们深入了解一下ES6模块是什么样子的。

Module 基础知识

ES6模块是一个包含JS代码的文件。没有特殊的module关键字;模块读起来就像脚本。有两个区别。

  • ES6模块是自动的严格模式代码,即使你没有写use strict
  • 可以在模块中使用importexport

让我们先谈谈export。默认情况下,在模块中声明的所有内容都是该模块的局部内容。如果你希望在模块中声明的某些特性是公共的,以便其他模块可以使用它,则必须export该特性。有几种方法可以做到这一点。最简单的方法是添加export关键字。

// kittydar.js - Find the locations of all the cats in an image.

export function detectCats(canvas, options) {
var kittydar = new Kittydar(options);
return kittydar.detectCats(canvas);
} export class Kittydar {
... several methods doing image processing ...
} // This helper function isn't exported.
function resizeCanvas() {
...
}

可以export任何顶级functionclassvarletconst

这就是编写模块所需要知道的全部内容!你不需要把所有的东西都放到IIFE回调中。去声明你需要的东西吧。由于代码是一个模块,而不是一个脚本,所以所有的声明都将作用域限定在该模块,而不是在所有脚本和模块中全局可见

除了export之外,模块中的代码基本上都是普通代码。它可以使用全局变量,如ObjectArray。如果您的模块在web浏览器中运行,它可以使用documentXMLHttpRequest

在一个单独的文件中,我们可以导入并使用detectCats()函数:

// demo.js - Kittydar demo program

import {detectCats} from "kittydar.js";

function go() {
var canvas = document.getElementById("catpix");
var cats = detectCats(canvas);
drawRectangles(canvas, cats);
}

要从一个模块中导入多个名称,可以这样写:

import {detectCats, Kittydar} from "kittydar.js";

当你运行包含import声明的模块时,它首先加载所导入的模块,然后在依赖关系图的深度优先遍历中执行每个模块主体,通过跳过已经执行的任何内容来避免循环遍历。这些是模块的基础。这真的很简单。;-)

export 列表

你可以用花括号{}括起来,列出你想要导出的所有(方法,变量,类等)名称,而不是给每个导出的特性加上标签:

export {detectCats, Kittydar};

// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

export列表不一定是文件中的第一项内容;它可以出现在模块文件的顶级作用域中的任何地方。您可以有多个export列表,或者将export列表与其他export声明混合在一起,只要没有名称被多次重复导出。

重命名导入和导出

偶尔,导入的名称会与你需要使用的其他名称发生冲突。所以ES6允许你在导入时重命名:

// suburbia.js

// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...

类似地,您可以在导出时重命名它们。如果你想在两个不同的名称下导出相同的值,这是很方便的,这偶尔会发生:

// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be) function v1() { ... }
function v2() { ... } export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

默认导出

新标准旨在与现有的CommonJSAMD模块互操作。假设你有一个Node项目,你已经完成了npm install lodash。你的ES6代码可以从Lodash中导入单独的函数:

import {each, map} from "lodash";

each([3, 2, 1], x => console.log(x));

但也许你已经习惯看到_.each,而不是each,你仍然想要这样写。或者你把_作为函数使用,因为这在Lodash中很常用。为此,你可以使用稍微不同的语法:导入没有花括号的模块

import _ from "lodash";

这种简写等价于import {default as _} from "lodash"。所有的CommonJS和AMD模块在ES6中都有一个default export,这和你在require()函数中调用该模块时得到的是一样的,也就是exports对象

ES6模块被设计成允许你导出多个东西,但是对于现有的CommonJS模块,你只能得到默认的导出。例如,在撰写本文时,据我所知,著名的colors包没有任何特殊的ES6支持。它是CommonJS模块的集合,就像npm上的大多数包一样。但是你可以直接导入到你的ES6代码中。

// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";

如果你想要自己的ES6模块有一个默认的导出,这很容易做到。默认导出没有什么魔力;它就像任何其他导出一样,除了它被命名为default。你可以使用我们已经讨论过的重命名语法:

let myObject = {
field1: value1,
field2: value2
};
export {myObject as default};

或者更好的做法是,使用以下简写:

export default {
field1: value1,
field2: value2
};

关键字export default后面可以跟任何值:函数、类、对象字面量。

模块对象

import * as cows from "cows";

当你import *时,所导入的是一个模块名称空间对象(module namespace object)。它的属性是模块的exports。因此,如果cows模块导出了一个名为moo()的函数,那么在以这种方式导入cows之后,你可以写:cows.moo()

聚合模块

有时候,一个包的主模块比导入包的所有其他模块并以统一的方式导出它们大不了多少。为了简化这类代码,有一种一体化的import-and-export简写:

// world-foods.js - good stuff from all over

// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka"; // import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea"; // import "singapore" and export ALL of its exports
export * from "singapore";

每个export-from语句都类似于import-from语句后跟export语句。与真正的导入不同,它不会将重新导出的绑定添加到您的作用域。因此,如果你打算在world-food.js中编写一些利用Tea的代码,请不要使用这种简写。你会发现它并不存在。

如果singapore输出的任何名称与其他输出名称发生冲突,那将是一个错误,因此要小心使用export *

语法已经讲完了!现在说说有趣的部分。

import实际上是做什么的?

你会相信…什么都不没做吗?

哦,你没那么容易上当。你能相信标准并没有说import到底做了哪些事情吗?这是件好事吗?

ES6将模块加载的细节完全留给了实现。模块执行方式是详细指定的。粗略地说,当你告诉JS引擎运行一个模块时,它必须表现得像以下四个步骤正在发生:

  1. 解析(Parsing):

    实现读取模块的源代码并检查语法错误。

  2. 加载(Loading):

    实现加载所有导入的模块(递归地)。这部分还没有标准化。

  3. 链接(Linking):

    对于每个新加载的模块,实现创建一个模块作用域,并用该模块中声明的所有绑定填充它,包括从其他模块导入的内容。

    如果你试图import {cake} from "paleo",但是paleo模块实际上没有导出任何名为cake的东西,你会得到一个错误。这太糟糕了,因为你离真正运行一些JS代码已经很近了。

  4. 运行时(Runtime):

    最后,实现运行每个新加载模块的代码体中的语句。此时,导入处理已经完成,所以当执行到有import声明的代码行时……什么也没有发生!

看到了吗?我告诉过你答案是"import什么都没做",关于编程语言,我没有撒谎。

现在我们来看看这个系统中有趣的部分。有一个很酷的技巧。因为系统不指定加载是如何工作的,因为你可以提前通过查看源代码import声明算出所有的依赖关系,一种加载的实现方式是在编译时完成所有的工作,你所有的模块打包成一个文件,并把它放在网络上传输!像webpack这样的工具可以做到这一点。

这是一件大事,因为通过网络加载脚本需要花费时间,而且每次获取一个脚本时,您可能会发现它包含需要加载几十个以上的导入声明。一个简单的加载器将需要大量的网络往返通讯。但是有了webpack,你现在不仅可以使用带有模块的ES6,还可以在不影响运行时性能的情况下获得所有的软件工程好处。

ES6中模块加载的详细规范最初是计划并构建的。它没有出现在最终标准中的一个原因是,对于如何实现这个捆绑特性没有达成共识。我希望有人能解决这个问题,因为我们将看到,模块加载确实应该标准化。捆绑销售太好了,不能放弃。

静态 vs 动态,或者:规则以及如何打破规则

作为一种动态语言,JavaScript让自己拥有了一个令人惊讶的静态模块系统。

  • 在一个模块中,所有类型的导入和导出都只允许在顶层。没有条件导入或导出,并且不能在函数内使用导入。
  • 所有导出的标识符必须在源代码中按名称显式导出。你无法通过编程方式遍历数组并以数据驱动的方式导出一组名称。
  • 模块对象被冻结(无法修改)。没有办法将一个新特性hack到一个模块对象中,polyfill风格。
  • 在任何模块代码运行之前,模块的所有依赖项都必须被加载、解析和链接。没有语法可以实现按需惰性加载的导入
  • 导入发生错误没有错误恢复。一个应用程序可能包含数百个模块,如果有任何模块无法加载或链接,就无法运行。不能把import包裹在try/catch块中。(这里的好处是,因为系统是静态的,所以webpack可以在编译时检测到这些错误。)
  • 没有钩子允许模块在依赖项加载之前运行一些代码。这意味着模块无法控制它们的依赖项是如何加载的。

只要你的需求是静态的,系统就相当不错。但你难免有时候需要做一点定制,对吧?

这就是为什么无论你使用什么模块加载系统,都会有一个编程API来配合ES6的静态import/export语法。例如,webpack包含一个API,你可以用它来“分割代码”,按需惰性加载一些模块包。同样的API可以帮助您打破上面列出的大多数规则。

ES6模块语法是非常静态的,这很好——它以强大的编译时工具的形式得到了回报。但是这种静态语法被设计为与丰富的动态、程序化加载器API一起工作。

[ES6深度解析]15:模块 Module的更多相关文章

  1. [ES6深度解析]13:let const

    当Brendan Eich在1995年设计了JavaScript的第一个版本时,他犯了很多错误,包括从那时起就成为该语言一部分的一些错误,比如Date对象和当你不小心将它们相乘时对象会自动转换为NaN ...

  2. ES6深度解析3:Generators

    介绍ES6 Generators 什么是Generators(生成器函数)?让我们先来看看一个例子. function* quips(name) { yield "hello " ...

  3. [ES6深度解析]12:Classes

    我们将讨论一个老问题:在JavaScript中创建对象的构造函数. 存在的问题 假设我们想要创建最典型的面向对象设计的示例:Circle类.假设我们正在为一个简单的Canvas库编写一个Circle. ...

  4. [ES6深度解析]14:子类 Subclassing

    我们描述了ES6中添加的新类系统,用于处理创建对象构造函数的琐碎情况.我们展示了如何使用它来编写如下代码: class Circle { constructor(radius) { this.radi ...

  5. Unity加载模块深度解析(Shader)

    作者:张鑫链接:https://zhuanlan.zhihu.com/p/21949663来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 接上一篇 加载模块深度解析(二 ...

  6. Unity加载模块深度解析(网格篇)

    在上一篇 加载模块深度解析(一)中,我们重点讨论了纹理资源的加载性能.这次,我们再来为你揭开其他主流资源的加载效率. 这是侑虎科技第53篇原创文章,欢迎转发分享,未经作者授权请勿转载.同时如果您有任何 ...

  7. es6(六):module模块(export,import)

    es6之前,社区模块加载方案,主要是CommonJS(用于服务器)和AMD(用于浏览器) 而es6实现的模块解决方案完全可以替代CommonJS和AMD ES6模块设计思想:尽量静态化,在编译时就能确 ...

  8. Spring源码深度解析之Spring MVC

    Spring源码深度解析之Spring MVC Spring框架提供了构建Web应用程序的全功能MVC模块.通过策略接口,Spring框架是高度可配置的,而且支持多种视图技术,例如JavaServer ...

  9. [WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析

    [WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析 标签: webkit内核JavaScriptCore 2015-03-26 23:26 2285 ...

随机推荐

  1. shell脚本(2)-shell脚本语法

    一.如何抒写shell脚本 1.shell脚本的命名 名字要有意义,不要以a.b.c.1.2.3这种方式命令,建议以sh结尾,在30个字节内,例如:check_memory.sh  2.shell脚本 ...

  2. Linux常用命令 day day up系列3

    一.命令执行的优先级二.Linux目录结构三.cat--查看文件内容四.more--查看文件内容五.less--查看文件内容六.head.tail--查看文件内容七.wc--统计文件内容八.grep- ...

  3. 小刻也能看懂的Unraid系统使用手册:基础篇

    小刻也能看懂的Unraid系统使用手册 基础篇 Unraid系统简介 Unraid 的本体其实是 Linux,它主要安装在 NAS 和 All in One 服务器上,经常可以在 Linus 的视频里 ...

  4. PYTHON 当前.PY文件名不能与引入的模块同名

    当前文件名:sqlite3.py 文件引入import sqlite3 运行会出错,因为调用sqlite3的方法首先从当前文件找方法,当然找不到,所以会报错了

  5. node.js背后的引擎V8及优化技术

    本文将挖掘V8引擎在其它方面的代码优化,如何写出高性能的代码,及V8的性能诊断工具.V8是chrome背后的javascript引擎,因此本文的相关优化经验也适用于基于chrome浏览器的javasc ...

  6. 配置软ISCSI存储

    说明:这里是Linux服务综合搭建文章的一部分,本文可以作为单独使用RedHat Enterprise Linux 7搭建软ISCSI的参考. 注意:这里所有的标题都是根据主要的文章(Linux基础服 ...

  7. idea使用maven下载jar包,出现证书校验问题问题,unable to find valid certification path to requested target

    每次从github上下载下来的项目都报如下错误could not transfer artifact org.springframework.boot:spring-boot-starter-pare ...

  8. 13Java进阶——IO、线程

    1 字节缓冲流 BufferInputStream 将创建一个内部的缓冲区数组,内部缓冲区数组将根据需要从包含的输入流中重新填充,一次可以读取多个字节 BufferOutputStream 该类实现缓 ...

  9. 记一次系统崩溃事件【Mac版】

    事件:Mac系统崩溃,导致电脑数据丢失,以及数据安全备份措施的不到位的教训! 解决措施: 1.开机后按:Command+R 按开机键 ,进入Mac 实用工具, 选择磁盘工具.由于没有备份直接抹掉磁盘. ...

  10. Windows API 简介

    操作系统的作用之一就是屏蔽一些复杂的直接对硬件操作,并提供给用户一个简单明确的应用接口,类外对于一些基本的或常用的操作也以API的形式提供给用户,比如内存管理.文件管理等. 消息传递机制 消息循环是一 ...