ES-module

224 阅读5分钟

模块化

一个模块就是一个独立的文件,该文件内部的变量、函数、类等内容外部无法访问

      历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

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

  • 后端模块化规范:CommonJS 同步加载
  • 前端模块化规范:异步加载
  • 如: AMD require.js , CMD sea.js , ES6 module。

ES6 module

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

  1. script 标签设置 type="module", 浏览器才会把相关的代码当作ES6的module来对待
  2. 不能写“裸”路径,即使是同一层级下面的文件,也要加上 './name.js'
  3. ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
  4. ES模块需要在服务器环境运行
  • ES6 模块功能主要由两个命令构成:export 和 import
  • export命令用于输出模块的对外的功能
  • import命令用于输入其他模块提供的功能

export 关键字

如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。 假设以下代码放在 module1.js 文件中

// 不推荐 export var userName = 'xiaoming';
          export var userSex = '男';
          export var userAge = 27;

// 推荐 var userName = 'xiaoming';
        var userSex = '男'; var userAge = 27; 
        export { userName, userSex, userAge };

        export命令除了输出变量,还可以输出对象、函数或类(classexport function multiply(x, y) { return x * y; };

// 上面代码对外输出一个函数multiply
//  通常情况下,export输出的变量就是本来的名字,可以使用as关键字重命名

function f1() { ... } function f2() { ... } export { f1 as test1, f2 as test2, f2 as test3 };

   上面代码使用as关键字,重命名了函数f1和f2的对外接口
   重命名后,f2可以用不同的名字输出两次

错误的写法:  报错 
             export 1;
             var m = 1; export m;
             function f() {} export f;

正确的写法:  正确 
                export var m = 1;
                var m = 1; export {m};
                var n = 1; export {n as m};
                export function f() {};
                function f() {} export {f};

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

export var abc = '123'; 
setTimeout(() => abc = '456', 500); 
上面代码输出变量abc,值为'123'500 毫秒之后变成'456'

import 关键字

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。 以下代码假设放在 test1.js 文件中

import { userName, userSex, userAge } from './module1.js';
console.log(userName); console.log(userSex); 
console.log(userAge);

如果想为输入的变量重新取一个名字,import命令要使用as关键字将变量重命名。
import { userName as xm } from './module1.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。
也就是说,不允许在加载模块的脚本里面,改写接口。 
import {a} from './xxx.js' a = 123; // Syntax Error : 'a' is read-only;

如果a是一个对象,改写a的属性是允许的。
import {a} from './xxx.js' a.foo = 'hello'; // 合法操作

注意,import命令具有提升效果,会提升到整个模块的顶部先执行 show(); import { show } from './module1.js';

除了指定加载某些输出值,还可以使用整体加载,即用星号(*)指定一个对象,
所有输出值都加载在这个对象上面。(不包含默认导出) 
import * as obj from './module1.js'; console.log(obj.userName); 
console.log(obj.userSex); console.log(obj.userAge);

引入module1.js(拿到这来执行) 
import './module1.js'; import './module1.js'; 同一个模块多次导入,不会重复执行,只执行第一次

export default

使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。 为了给用户提供方便,用export default命令,为模块指定默认输出。

下面比较一下默认输出和正常输出:

// 第一组 export default function () { // 输出 // ... } import test from './test.js'; // 输入

// 第二组 export function test() { // 输出 // ... }; import { test } from './test.js'; // 输入

第一组是使用export default时,对应的import语句不需要使用大括号;
第二组是不使用export default时,对应的import语句需要使用大括号。

一个模块只能有一个默认输出,因此export default命令只能使用一次。

因为export default命令其实只是输出一个叫做default的变量,
所以它后面不能跟变量声明语句。

// 正确 export var a = 1;
// 正确 var a = 1; export default a;
// 错误 export default var a = 1;
// 正确 export default 42;
// 报错 export 42;

注意点

浏览器对于带有type="module"的script标签,都是异步加载,不会造成堵塞浏览器 即等到整个页面DOM加载完成,再执行模块脚本,相当于打开了script标签的defer属性

<script type="module" src="./foo.js"></script>
<!-- 类似于 -->
<script src="./foo.js" defer></script>
如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
<script type="module"> 
import utils from "./utils.js"; 
// other code // ... 
</script>

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

  1. 代码是在模块作用域之中运行,而不是在全局作用域运行
  2. 模块内部的顶层变量,外部不可见
  3. 模块脚本自动采用严格模式,不管有没有声明use strict
  4. 模块之中,可以使用import命令加载其他模块,也可以使用export命令输出对外接口
  5. 模块之中,顶层的this关键字返回undefined,而不是指向window
  6. 同一个模块如果加载多次,将只执行一次