【译】《Understanding ECMAScript6》- 第八章-Module

目录

JavaScript令人困惑并且易引发错误的特性之一是以“一切皆共享”的方式加载代码。所有文件内定义的一切代码都共享一个全局作用域,这一点是JavaScript落后于其他编程语言之处(比如Java中的package)。随着web应用变得越来越庞大复杂,“一切皆共享”的方式暴露出一系列弊端,比如命名冲突、安全性等等。ES6的目标之一便是解决这种问题,增强JavaScript代码组织的有序性。这就是Module(模块)的作用。

module是什么

Module可以简单理解为加载JavaScript文件的一种特殊方式。目前,不论是浏览器还是NodeJS,都没有实现原生ES6 Module的支持,但是我们可以期待Module作为一种默认的机制被广泛使用。模块化的代码与非模块的代码有以下区别:

  1. 模块化代码强制在严格模式下执行;
  2. 一个模块最顶层作用域中定义的变量不会暴露在共享的全局域内;
  3. 一个模块的最顶层作用域中的this值为undefined;
  4. 不支持html格式的注释语法();
  5. 一个模块必须导出可供模块以外代码使用的接口。

模块化JavaScript文件和常规的文件相同,都是通过文本编辑器撰写,使用.js扩展名。唯一的区别是,模块化代码使用全新的代码语法。

使用基础

export关键字用来导出一个模块暴露给外部的代码。最简单的一种使用方式是在任何变量、函数、class声明语句的前面使用export。如下:

// export data
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;

// export function
export function sum(num1, num2) {
    return num1 + num1;
}

// export class
export class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
}

// this function is private to the module
function subtract(num1, num2) {
    return num1 - num2;
}

// define a function
function multiply(num1, num2) {
    return num1 * num2;
}

// export later
export multiply;

需要注意以下几点:

  1. 不论使用export与否,声明语句的语法与常规一致;
  2. 被export导出的函数和class必须有明确的类名/函数名。匿名函数/类不能使用上述语法导出;
  3. export不仅可以在声明语句前使用,也可以用在引用前面,如上述代码中的multiply;
  4. 没有被明确导出的变量、函数、class被称为当前模块的私有成员,不能被外部代码访问,如上述代码中的substract()函数。

使用export的一个重要限制是,必须在当前模块的最顶层作用域使用,否则会抛出语法错误。如下:

if (flag) {
    export flag;    // syntax error
}

上述代码中,export在if块级域内使用会抛出语法错误。export不能以任何动态的方式导出,这样做的好处是可以令JavaScript引擎对导出的模块进行清晰地管理。因此,export只能在一个模块的最顶层作用域内使用。

某些转译器(如Babel.js)可以打破这种限制,开发者可以在任何位置使用export。但是这种模式只在代码被转译为ES5规范时能够正常工作,并不支持原生的ES6模块系统。

一旦使用export导出某个模块的功能,便可以在其他模块中通过import关键字使用它。import语句包括两部分:被导入的标识符和此标识符的源模块。如下:

import { identifier1, identifier2 } from "module";

花括号内的标识符代表的是从指定模块中导出的变量。关键字from后的模块名代表的是被导出变量的指定模块。模块名是一个字符串。截止到本书撰写日期,模块名的书写规范仍然未最终定稿。

尽管import后的花括号形式与解构Object类似,但它只是导出标识符的列表,并不是解构Object。

使用import从模块中导出的变量类似于使用const定义的常量。也就是说,在同一作用域内,不能定义与之同名的变量,不能在import之前使用它,也不能重新赋值。

本章第一个例子中的模块我们命名为“example”,你可以使用多种方式导出example模块的标识符,最简单的方式如下:

// import just one
import { sum } from "example";

console.log(sum(1, 2));     // 3

sum = 1;        // error

上述代码导出了example模块的sum()函数。不论example模块export多少个接口,开发者可以根据不同的使用场景import任意个数的接口。上述代码中尝试对sum重新赋值,抛出语法错误,验证了被导入的接口变量不能被重新赋值这条规则。

如果想import多个接口变量,可以使用以下方式:

// import multiple
import { sum, multiply, magicNumber } from "example";
console.log(sum(1, magicNumber));   // 8
console.log(multiply(1, 2));        // 2

上述代码中导入了example模块的三个接口变量:sum、multiply和magicNumber。

你还可以将整个模块导出为一个独立的对象,其被export的接口变量作为这个对象的属性使用。如下:

// import everything
import * as example from "example";
console.log(example.sum(1,
        example.magicNumber));          // 8
console.log(example.multiply(1, 2));    // 2

上述代码中,example模块作为一个整体被导入,以一个名为example的对象使用,example模块暴露出来的sum()、multiply()和magicNumber作为example对象的属性使用。

需要注意的是,不论使用import多次导入一个模块,被导入模块内部的代码只会被执行一次。如下:

import { sum } from "example";
import { multiply } from "example";
import { magicNumber } from "example";

上述代码中,使用import导入了3次example模块,但是example模块背部的代码钟会被执行一次。在第一次被导入后,example模块被实例化,随后此实例引用将储存在内存中。在此之后,不论import多少次,甚至被多个不同的模块import,都将使用内存中的example模块实例,而不必重复执行模块内部的代码。

接口标识符重命名

通常情况下,为了增强代码的易读性,我们往往不直接使用某个变量、函数或者class的原始名称。ES6的模块规范允许在导出或导入时修改接口标识符的名称。

比如,在导出某个函数时希望更改函数名,可以使用as关键字进行如下修改:

function sum(num1, num2) {
    return num1 + num2;
}

export { sum as add };

上述代码中sum()函数在被导出时将接口函数名更改为add(),其他模块在导入此接口函数时必须使用add标识符,如下:

import { add } from "example";

同理,在导入某个模块接口函数时,也可以使用as关键字修改标识符名称:

import { add as sum } from "example";
console.log(typeof add);            // "undefined"
console.log(sum(1, 2));             // 3

上述代码在导入接口函数add()时,将标识符名称修改为sum。

导入绑定

需要注意import表达式非常重要的一个细节:import的变量、函数或class并不是简单的引用关系,而是创建了一种绑定关系。换句话说,虽然不能手动修改导入的接口成员,但是可以通过源模块的逻辑进行修改。比如:

export var name = "Nicholas";
export function setName(newName) {
    name = newName;
}

当在其他模块中导入name和setName()后,可以通过调用setName()修改name的值:

import { name, setName } from "example";

console.log(name);       // "Nicholas"
setName("Greg");
console.log(name);       // "Greg"

name = "Nicholas";       // error

调用setName("Greg")时,实际上回到了setName的源模块内执行,从而将name的值修改为“Greg”,并且修改后的结果自动映射到了导入name的模块。

缺省接口

模块export的缺省接口是由default关键字修饰的一个单独的变量、函数或者class。如下:

export default function(num1, num2) {
    return num1 + num2;
}

上述代码是一个典型的export缺省接口。default关键字表明这是一个缺省接口,并且缺省接口的函数不需要指定具体的函数名,因为模块本身就代表着此接口函数。

也可以将缺省接口重命名,如下:

// equivalent to previous example
function sum(num1, num2) {
    return num1 + num2;
}

export { sum as default };

上述代码等价于前例,as default表明sum函数作为缺省接口被导出。

每个模块只能被定义一个缺省接口。尝试定义多个缺省接口会引起语法错误。

导入缺省接口的语法与前文提到的导入整个模块的语法类似:

// import the default
import sum from "example";

console.log(sum(1, 2));     // 3

上述代码导入example模块的缺省接口。请注意导入的缺省接口标识符并没有包裹在花括号内。这种简洁的语法形式将成为web应用导入已存对象的常用格式:

import $ from "jquery";

如果需要导入某个模块的缺省接口和非缺省接口,可以在一个表达式中实现。比如某个模块暴露出以下接口:

export let color = "red";

export default function(num1, num2) {
    return num1 + num2;
}

可以通过以下形式导入:

import sum, { color } from "example";

console.log(sum(1, 2));     // 3
console.log(color);         // "red"

上述代码有两点需要注意:

  1. 使用逗号分隔缺省接口与非缺省接口;
  2. 非缺省接口包裹在花括号内。

导入缺省接口时可以重命名标识符:

// equivalent to previous example
import { default as sum, color } from "example";

console.log(sum(1, 2));     // 3
console.log(color);         // "red"

上述代码中,缺省接口标识符default被重命名为sum,连同非缺省接口color一起被包裹在花括号内。

Re-exporting

某些场景下,开发者需要将导入的模块再次导出,可以使用以下模式:

import { sum } from "example";
export { sum }

除此之外,还有一种更简洁的形式:

export { sum } from "example";

上述代码将example模块的sum接口再次导出。当然,可以使用as在导出时进行重命名:

export { sum as add } from "example";

上述代码导入example模块的sum接口,随后重命名为add再次导出。

使用通配符*可以将模块作为整体导出:

export * from "example";

使用上述代码导出整个example模块时,example模块的缺省接口和非缺省接口全部包括在内,会影响当前模块的导出行为。比如,如果example模块有缺省接口,那么就不能在当前模块中另行定义缺省接口。

非绑定import

某些模块可能只是对某个全局变量进行了修改,并未导出任何接口。虽然模块内部的变量、函数和类并不暴露在全局作用域内,但并不意味着模块内部不能访问全局域的成员。在某个模块内对内置对象(比如Array或Object)进行了扩展修改,其他模块中也会受到影响。

比如,假设现在对Array对象增加一个扩展方法pushAll(),可以在某个模块内进行以下操作:

// module code without exports or imports
Array.prototype.pushAll = function(items) {

    // items must be an array
    if (!Array.isArray(items)) {
        throw new TypeError("Argument must be an array.");
    }

    // use built-in push() and spread operator
    return this.push(...items);
};

虽然上述模块没有导出/导入任何接口,但它本身是一个符合规范的模块。上述代码可以当作一个模块使用,也可以作为一段普通的脚本。由于模块未导出任何接口,你可以使用简化的import表达式执行模块代码,而不必创建绑定关系。如下:

import from "example";

let colors = ["red", "green", "blue"];
let items = [];

items.pushAll(colors);

上述代码将example模块导入并执行,Array的扩展方法pushAll()有效,可以在当前模块的使用。

非绑定的import通常被用来创建polyfill和shim。

译者注:shim和polyfill是JavaScript应用开发中解决兼容性的方案用语。简单来说就是使用旧环境的API实现新API。感兴趣的读者可自行查阅相关资料。

总结

ES6引入模块机制的目标是提供一种代码功能化的封装模式。模块与普通脚本的最大的不同在于其顶层作用域内的变量、函数和class并不会暴露在全局域内,而且this的值为undefined。工作原理的不同,也需要一套全然不同的载入方式支持。

如果想在模块外部使用本模块的某些功能,必须使用export关键字将其导出。任何变量、函数和class都可以被导出。此外,每个模块只能导出一个缺省接口。被导出后,其他模块便可以导入部分或者真个模块。被导入的接口标识符类似const定义的常量,拥有块级域绑定特性。

另外,没有导出任何接口的模块在被其他模块导入时不会创建绑定关系。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏haifeiWu与他朋友们的专栏

Redis协议规范(译文)

Redis客户端使用名为RESP(Redis序列化协议)的协议与Redis服务器进行通信。 虽然该协议是专为Redis设计的,但它可以用于其他CS软件项目的通讯...

1383
来自专栏IT笔记

Linux下安装Redis3

下载 下载地址 http://redis.io/download Linux 下执行 wget http://download.redis.io/relea...

3725
来自专栏陈纪庚

angularjs directive学习心得

transclude有三个选项,true, false, 和object.如果不显示指明的话,默认为false. 当为false的时候,则那个directiv...

1331
来自专栏北京马哥教育

ansible之playbook功能简述

playbooks剧本简介 playbooks是ansible更为强大的配置管理组件,实现基于文本文件编排执行的多个任务,且多次重复执行。其是使用YAML(Ye...

3675
来自专栏大闲人柴毛毛

“备忘录模式”就这么简单

备忘录模式的官方定义: 在不破坏封装性的前提下,获取一个对象的内部状态,并在该对象之外保存这些状态。这样以后就可以通过该对象恢复到原先保存的状态。 大白话说: ...

35511
来自专栏大内老A

.NET Core采用的全新配置系统[3]: “Options模式”下的配置是如何绑定为Options对象

配置的原子结构就是单纯的键值对,并且键和值都是字符串,但是在真正的项目开发中我们一般不会单纯地以键值对的形式来使用配置。值得推荐的做法就是采用《.NET Cor...

19610
来自专栏海天一树

小朋友学Java(12):包

包(package)是Java语言提供的一种区别类名字命名空间的机制,它是类的一种文件组织和管理方式、是一组功能相似或相关的类或接口的集合。Java packa...

2976
来自专栏python学习之旅

Python笔记(十一):多线程

(二)和(三)不感兴趣的可以跳过,这里参考了《深入理解计算机系统》第一章和《Python核心编程》第四章 (一)      多线程编程 一个程序包含多个子任务,...

4207
来自专栏Youngxj

Goto循环-c语言学习笔记

1513
来自专栏java 成神之路

JVM 类加载机制深入浅出

27211

扫码关注云+社区

领取腾讯云代金券