ES6 Module模块

一、Module简介

1.历史

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS、CMD( seaJS) 和 AMD ( RequireJS )等。ES6 在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案。

扩展:node.js 遵循的是 CommonJS 的规范

2.概述

ES6 引入了模块化,其设计思想是在编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES6 的模块化分为导出(export) @与导入(import)两个模块。

3.特点

ES6 的模块自动开启严格模式,不管你有没有在模块头部加上 use strict;。

模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。

每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。

每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。

二、Module 用法

1.export 命令

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

export的两种语法:

//语法1
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

//语法2(推荐)
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };

as关键字:
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

export使用注意:
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

//错误写法一
export 1;
//错误写法二
var m = 1;
export m;

//正确写法一
export var m = 1;
//正确写法二
var m = 1;
export {m};
//正确写法三
var n = 1;
export {n as m};

动态值:
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo,值为bar,500 毫秒之后变成baz。

2.import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

import { firstName, lastName, year } from './profile.js';
function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { lastName as surname } from './profile.js';

import命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();
import { foo } from 'my_module';
//上面的代码不会报错,因为import的执行早于foo的调用

3.模块整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

// 分开加载
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

//整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

4.export default 命令

  • 在一个文件或模块中,export、import 可以有多个,export default 仅有一个。
  • export default 中的 default 是对应的导出接口变量。
  • 通过 export 方式导出,在导入时要加{ },export default 则不需要。
  • export default 向外暴露的成员,可以使用任意变量来接收。
//导出
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

//导入
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';

5.export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

需要注意的是,写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。

6.跨模块常量

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的时候,直接加载index.js就可以了。

// script.js
import {db, users} from './constants/index';

7.import()

ES2020提案 引入import()函数,支持动态加载模块。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

三、Module的加载实现

1.浏览器加载

(1).传统加载

HTML 网页中,浏览器通过

<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

defer是“渲染完再执行”,async是“下载完就执行”.

(2).Module加载

浏览器加载 ES6 模块,也使用

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

2.ES6 模块与 CommonJS 模块的差异

ES6 模块与 CommonJS 模块,有三个重大差异:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

第一个差异解释如下:
CommonJS 模块输出的是值的拷贝

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

ES6 模块的JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。原始值变了,import加载的值也会跟着变。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

四、Node.js 的模块加载方法

1.概述

JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。

CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。

{
   "type": "module"
}

.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置

注意,ES6 模块与 CommonJS 模块尽量不要混用。

2.package.json 的 main 字段

package.json文件有两个字段可以指定模块的入口文件:main和exports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。

// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

3.package.json 的 exports 字段

exports字段的优先级高于main字段。它有多种用法。

(1).子目录别名

package.json文件的exports字段可以指定脚本或子目录的别名。
指定脚本别名

/ ./node_modules/es-module-package/package.json
{
  "exports": {
    "./submodule": "./src/submodule.js"
  }
}

import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js

指定子目录别名

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/": "./src/features/"
  }
}

import feature from 'es-module-package/features/x.js';
// 加载 ./node_modules/es-module-package/src/features/x.js

扩展-严格模式

严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)

猜你喜欢

转载自blog.csdn.net/qq_41466440/article/details/111245239