一、ES6模块化规范

    1. 在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
    1. ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

1.概述

  • 1.1 ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
  • 1.2 对比CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
  • 1.3 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

2.严格模式

  • 2.1 ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。
  • 2.2 限制:
  1. 变量必须声明后再使用
  2. 函数的参数不能有同名属性,否则报错
  3. 不能使用with语句
  4. 不能对只读属性赋值,否则报错
  5. 不能使用前缀 0 表示八进制数,否则报错
  6. 不能删除不可删除的属性,否则报错
  7. 不能删除变量delete prop,会报错,只能删除属性delete global.prop
  8. eval不会在它的外层作用域引入变量
  9. eval和arguments不能被重新赋值
  10. arguments不会自动反映函数参数的变化
  11. 不能使用arguments.callee
  12. 不能使用arguments.caller
  13. 禁止this指向全局对象
  14. 不能使用fn**.caller和fn.arguments获取函数调用的堆栈
  15. 增加了保留字**(比如protected、static和interface)
  • 2.3 注意:函数参数的初始化或自动采用严格模式。

3.export命令和import命令

  • 3.1 模块功能主要由两个命令构成:export和import。export 命令用于规定模块的对外接口, import 命令用于导入其他模块提供的功能。
  • 3.2 导出方式
// 1.单独导出
export var id = 1001;

// 2.集体导出
var id=1002;
var userName = 'XX';
export{ id,userName };

// 3.导出函数
export function fn(x, y) {
  return x * y;
};

// 4.可以使用 as 关键字重命名。
var array_1=[0],array_2=[2,3]
export {
  arr1 as array_1,
  arr2 as arrar_2,
};

// 5.export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
export 1; // 报错
var m = 1;
export m; // 报错
var m = 1;
export {m};// √

// 6.export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// 输出变量 foo ,值为 bar ,500 毫秒之后变成 baz 

// 7.export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错
function foo() {
    export default 'bar' // SyntaxError
}
foo()

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

// 1.已知导出的变量名
import { id } from './test.js';

// 2.使用as关键字重命名
import { userId as id } from './test.js';

// 3.导入的变量只读不可更改
import {a} from './test.js'
a = {}; // Syntax Error : 'a' is read-only;

// 4.如果变量是对象,可改写属性,且其他模块也可读到改写后的值
import {a} from './test.js'
a.foo = 'hello'; // 合法操作

// 5.import 后面的 from 指定模块文件的位置,可以是相对路径,也可以是绝对路径, .js 后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import {a} from './test'

// 6.*注意, import 命令具有提升效果,会提升到整个模块的头部,首先执行。
//  import 命令是编译阶段执行的
foo();
import { foo } from './test';

// 7.由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
import { 'f' + 'oo' } from './test'; // ×

let module = './test';
import { foo } from module; // ×

if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
} // ×

// 8.仅执行 lodash 模块,但是不输入任何值。
// 多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次。
import './test';
import './test';

4.模块整体加载

// 4.1 除了指定加载某个输出值,还可以使用整体加载,即用星号( * )指定一个对象,所有输出值都加载在这个对象上面。
// 导出area和circumference:
export function area(radius) {
  return Math.PI * radius * radius;
}
export function circumference(radius) {
  return 2 * Math.PI * radius;
}
// 导入:
import * as test from './test';
console.log('圆面积:' + test.area(4));
console.log('圆周长:' + test.circumference(14));

// 4.2 注意,模块整体加载所在的那个对象(上例是 test ),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
circle.foo = 'hello';
circle.area = function () {};

5.export default命令

使用 import 命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。export default命令实未知变量名导入。

export default function () {
  console.log('foo');
}
// 1.其他模块加载该模块时,import 命令可以为该匿名函数指定任意名字。
import customName from './test';
customName(); // 'foo'

// 2.区别:
// 第一组
export default function crc32() { // 输出
  console.log('put01')
}
import crc32 from './test'; // 输入
// 第二组
export function crc32() { // 输出
  console.log('put02');
};
import {crc32} from './test'; // 输入

// 3.本质上, export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。
function add(x, y) {
  return x * y;
}
export {add as default}; // 等同于export default add;

import { default as foo } from './test';// 等同于import foo from 'modules';

// 4.它后面不能跟变量声明语句。
export default var a = 1;// 报错

// 5.export default 命令的本质是将后面的值,赋给 default 变量
export default 42; // 正确
export 42; // 报错

// 6.想在一条 import 语句中,同时输入默认方法和其他接口
// 导出:
export default function () {
    console.log('default');
}
export function fn01() {
    console.log('fn01');
}
export { fn01 as fn02 };
// 导入:
import _, { fn01, fn02 } from './test';

// 7.类:
// 导出:
export default class { ... }
// 导入:
import MyClass from './tets';
let o = new MyClass();

6.export 与 import 的复合写法

// 6.1 在一个模块之中,先导入后导出同一个模块
// 写成一行以后, foo 和 bar 实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用 foo 和 bar 。
export { foo, bar } from './test';

// 6.2 接口改名
export { foo as myFoo } from './test';

// 6.3 整体输出
export * from './test';

// 6.4 默认接口
export { default } from './test';
export { default as defau } from './test';

// 6.5 ES2020新增
export * as ns from "./test";

7.模块的继承

// 7.1 testSon 继承了 test 模块
export * from './test';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}
// 7.2 改名后输出  
// 上面代码中的 import exp 表示,将 testSon 模块的默认方法加载为 exp 方法。
export { area as circleArea } from './test';

// 7.3 main.js 加载上面模块
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

8.跨模块常量

// 8.1  const 声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

// 8.2 文件目录
// 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'];
// 合并
export {db} from './db';
export {users} from './users';
// 使用直接加载 index
import {db, users} from './constants/index';

9.import()

// 9.1 import命令会被JavaScript 引擎静态分析,先于模块内的其他语句执行
// 报错
if (x === 2) {
  import test from './test';
}

// 9.2 ES2020提案 引入 import() 函数,支持动态加载模块。
// import() 返回一个 Promise 对象。
const main = document.querySelector('main');
import( ./modules/${someVariable}.js )
.then(module => {
  module.loadPageInto(main);
})
.catch(err => {
  main.textContent = err.message;
});
  • 适用场合:
// 1. 按需加载
button.addEventListener('click', event => {
  import('./test.js')
  .then(test => {
    test.open();
  })
  .catch(error => {
    /* 错误处理 */
  })
});

// 2. 条件加载
if (condition) {
  import('./test_1').then(...);
} else {
  import('./test_2').then(...);
}

// 3.动态的模块路径--允许模块动态生成
import(f())
.then(...);
// 注意点:
// 4.import() 加载模块成功以后,这个模块会作为一个对象,当作 then 方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./test.js')
.then(({export1, export2}) => { 
  // export1 和 export2 都是 test.js 的输出接口,可以解构获得。
});

// 5.如果模块有 default 输出接口,可以用参数直接获得。
import('./test.js')
.then(myModule => {
  console.log(myModule.default);
});

// 6.加载多个模块
Promise.all([
  import('./test1.js'),
  import('./test2.js'),
  import('./test3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

// 7.import() 也可以用在 async 函数之中。
async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();

二、ES6 Module 的加载实现

1.浏览器加载之传统方法

<!-- 1.1 由于浏览器脚本的默认语言是 JavaScript,因此 type="application/javascript"可以省略。 -->
<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

<!-- 1.2 如果脚本体积很大,下载和执行的时间就会很长,建议使用异步加载提升用户体验 -->

2.浏览器加载之加载规则

  • 2.1 浏览器加载 ES6 模块,加入type=“module” 属性。

  • 2.2 浏览器对于带有 type="module"的 ,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本

  • 2.3 如果网页有多个模块,它们会按照在页面出现的顺序依次执行。

  • 2.4 一旦使用了async 属性,模块就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

  • 2.5 ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

  • 2.6 对于外部的模块脚本(上例是 foo.js),有几点需要注意:

1.代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
2.模块脚本自动采用严格模式,不管有没有声明 use strict。
3.模块之中,可以使用 import命令加载其他模块( .js 后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口。
4.模块之中,顶层的 this 关键字返回 undefined,而不是指向 window 。也就是说,在模块顶层使用this关键字,是无意义的。
5.同一个模块如果加载多次,将只执行一次。

3.node.js加载

    1. 加载原理

1.1 Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
1.2 Node.js 要求 ES6 模块采用 .mjs 后缀文件名。也就是说,只要脚本文件里面使用 import或者 export 命令,那么就必须采用 .mjs后缀名。Node.js 遇到 .mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定 “use strict”。
1.3 如果不希望将后缀名改成 .mjs ,可以在项目的 package.json 文件中,指定 type 字段为 module 。一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。
1.4 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成 .cjs 。如果没有 type 字段,或者 type 字段为 commonjs ,则 .js 脚本会被解释成 CommonJS 模块。
1.5 总结:.mjs 文件总是以 ES6 模块加载, .cjs 文件总是以 CommonJS 模块加载, .js 文件的加载取决于 package.json 里面 type 字段的设置。
1.6 注意,ES6 模块与 CommonJS 模块尽量不要混用。 require 命令不能加载 .mjs 文件,会报错,只有 import 命令才可以加载 .mjs 文件。反过来, .mjs 文件里面也不能使用 require 命令,必须使用 import 。

    1. main 字段

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

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

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

// package.json 文件的 exports 字段可以指定脚本或子目录的别名。
{
  "exports": {
    "./submodule": "./src/submodule.js"
  }
}

(2)main 的别名:exports 字段的别名如果是 . ,就代表模块的主入口,优先级高于 main 字段,并且可以直接简写成 exports 字段的值。

// 2.1 exports 字段的别名如果是 . ,就代表模块的主入口,优先级高于 main 字段,并且可以直接简写成 exports 字段的值。
{
  "exports": {
    ".": "./main.js"
  }
}
// 等同于
{
  "exports": "./main.js"
}

// 2.2 由于 exports 字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。
{
  "main": "./main-legacy.cjs",
  "exports": {
    ".": "./main-modern.cjs"
  }
}// 上面代码中,老版本的 Node.js (不支持 ES6 模块)的入口文件是 main-legacy.cjs ,新版本的 Node.js 的入口文件是 main-modern.cjs 。

(3)条件加载:利用 . 这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开[–experimental-conditional-exports]标志。

// 1. 利用 '.' 这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开 --experimental-conditional-exports 标志。
{
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs",
      "default": "./main.js"
    }
  }
}// 上面代码中,别名 . 的 require 条件指定 require() 命令的入口文件(即 CommonJS 的入口), default 条件指定其他情况的入口(即 ES6 的入口)。

// 2.上面的写法可以简写如下。
{
  "exports": {
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

// 3.注意,如果同时还有其他别名,就不能采用简写,否则或报错。
{
  // 报错
  "exports": {
    "./feature": "./lib/feature.js",
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

三、ES6 模块与 CommonJS 模块的差异

    1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

1.1 CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
1.2 ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import ,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

这是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐