ES6 模块化(Module)export和import详解

Stella981
• 阅读 768

ES6 模块化(Module)export和import详解

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

  1. // ES6模块

  2. import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。除了静态加载带来的各种好处,ES6 模块还有以下好处:
(1)不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点
(2)将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性
(3)不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供

1.严格模式(模块必须遵守,严格模式是ES5引入)
(1)严格模式主要有以下限制:
(2)变量必须声明后再使用
(3)函数的参数不能有同名属性,否则报错
(4)不能使用with语句
(5)不能对只读属性赋值,否则报错
(6)不能使用前缀0表示八进制数,否则报错
(7)不能删除不可删除的属性,否则报错
(8)不能删除变量delete prop,会报错,只能删除属性delete global[prop]
(9)eval不会在它的外层作用域引入变量
(10)eval和arguments不能被重新赋值
(11)arguments不会自动反映函数参数的变化
(12)不能使用arguments.callee
(13)不能使用arguments.caller
(14)禁止this指向全局对象
(15)不能使用fn.caller和fn.arguments获取函数调用的堆栈
(16)增加了保留字(比如protected、static和interface)

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

  1. //方式一

  2. export let name = '张三';

  3. //方式二(推荐)可以一起在模块结束位置输出

  4. let age = 12;

  5. let height = 14;

  6. //导出多个数据,单个数据也一样。

  7. export {age,height};

  8. //export age;或者export 12;都是错误的写法

  9. //方式三(输出函数)

  10. //1

  11. export let reduce1 = function reduce1(){

  12. console.log('reduce1函数减少');

  13. };

  14. export let add1 = ()=>{

  15. console.log('add1函数增加');

  16. };

  17. //2

  18. let reduce2 = function reduce2(){

  19. console.log('reduce2函数减少');

  20. };

  21. export {reduce2};

  22. function reduce2k(){

  23. console.log('reduce2k函数减少');

  24. };

  25. export {reduce2k};

  26. let add2 = ()=>{

  27. console.log('add2函数增加');

  28. };

  29. export {add2};

  30. //3

  31. export function reduce3(){

  32. console.log('reduce3函数减少');

  33. };

  34. //export function(){} 没有函数名,不能这么写

  35. //export ()=>{} 没有函数名,不能这么写

  36. //export add3(){} 不能这样写

  37. //方式四(类)

可以使用as关键字,重命名函数或者变量等,重命名后可以用不同的名字export多次输出

  1. export {

  2. age as AGE1,

  3. age as AGE2,

  4. height as HEIGHT

  5. };

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。这一点与CommonJS规范完全不同,CommonJS模块输出的是值的缓存,不存在动态更新。

  1. export var foo = 'bar';

  2. setTimeout(()=>{foo = 'baz';},500)

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

  1. import {name} from './Modules';

  2. console.log(name);

  3. import {age,height} from './Modules';

  4. console.log(age + "===" + height);

  5. import {reduce1,reduce2,reduce2k,reduce3,add1,add2} from './Modules';

  6. reduce1();

  7. reduce2();

  8. reduce2k();

  9. reduce3();

  10. add1();

  11. add2();

  12. import {foo} from './Modules';

  13. console.log(foo);

  14. setTimeout(()=>{console.log(foo)},500);

import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口的名称相同。可以使用as关键字,重命名函数或者变量等,重命名后可以用不同的名字import多次输入

  1. import {name as name1,name as name2} from './Modules';

  2. console.log(name1+"==="+name2);

export和import命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
export先申明后导出不会报错

  1. //导出多个数据,单个数据也一样。

  2. export {age,height};

  3. //方式二(推荐)可以一起在模块结束位置输出

  4. let age = 12;

  5. let height = 14;

export放到函数里面输出就会报错

  1. function A(){

  2. //导出多个数据,单个数据也一样。

  3. export {age,height};

  4. }

  5. //SyntaxError: 'import' and 'export' may only appear at the top level

import也是同理:

  1. //正确

  2. console.log(name);

  3. import {name} from './Modules';

  4. //错误

  5. function A(){

  6. import {name} from './Modules';

  7. }

export和import命令是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

  1. // 报错

  2. import { 'f' + 'oo' } from 'my_module';

  3. // 报错

  4. let module = 'my_module';

  5. import { foo } from module;

  6. // 报错

  7. if (x === 1) {

  8. import { foo } from 'module1';

  9. } else {

  10. import { foo } from 'module2';

  11. }

import语句会执行所加载的模块,因此可以有下面的写法,如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

  1. import './Modules';

  2. //错误import * from './Modules';

上面代码仅仅执行Modules模块,但是不输入任何值, 比如在某个模块中做了一些不需要输入值的操作,就可以使用,下面是使用import导入样式文件:

  1. import './style.css';//import导入样式

  2. 当然样式文件也可以使用一下方式使用样式

  3. import styles from './xx.css';

className="直接的样式名字"和className={styles.root},这里的className可以直接写样式的名字,也可以使用导入的名字点css的样式名字。

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

  1. //模块的整体加载

  2. import * as Modules from './Modules';

  3. console.log(Modules);

  4. console.log("模块的整体加载"+Modules.age + "=="+Modules.name);

5.export default 命令
使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

  1. export default class Person {

  2. getAge() {

  3. console.log('Person age');

  4. }

  5. };

  6. let weight = 100;

  7. export default weight;

  8. let width = 30;

  9. export {width as default};

export default命令只能使用一次,如果同一个文件中使用了多个export default,前面的将会被覆盖,但是使用export符合用法时,重复会报错。其他模块加载该模块时,import命令可以为该匿名函数指定任意名字,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

  1. import data from './Modules';

  2. console.log(data+"===");

export default命令用在非匿名函数前,也是可以的。下面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

  1. export default function foo() {

  2. console.log('foo');

  3. }

  4. //或者写成

  5. function foo() {

  6. console.log('foo');

  7. }

  8. export default foo;

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

  1. function add(x, y) {

  2. return x * y;

  3. }

  4. //export

  5. export {add as default};

  6. // 等同于

  7. // export default add;

  8. //import

  9. import { default as xxx } from 'modules';

  10. // 等同于

  11. // import xxx from 'modules';

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

  1. // 正确

  2. export var a = 1;

  3. // 正确

  4. var a = 1;

  5. export default a;

  6. // 错误

  7. export default var a = 1;

如果想在一条import语句中,同时输入默认方法和其他变量,可以写成下面这样

  1. import _,* as Modules1 from './Modules';

  2. console.log(_);

  3. console.log(Modules1);

  4. 或者

  5. import _, { name,age } from './Modules';

如果要输出默认的值,只需将值跟在export default之后即可

export default 12;

6.export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起部分输出

  1. //输出

  2. export {name,age} from './Modules';

  3. // 等同于

  4. import {name,age} from './Modules';

  5. export {name,age};

  6. //输入

  7. import {name,age} from './Tab';

  8. console.log(name + "Q"+age);

  9. 模块的接口改名和整体输出,也可以采用这种写法

  10. //输出

  11. export {name as firstName} from './Modules';

  12. //输入

  13. import {firstName} from './Tab';

  14. console.log(firstName+"===============");

整体输出

  1. //输出

  2. export * from './Modules';

  3. //输入

  4. import {name,age} from './Tab';

  5. console.log(name + "Q"+age);

  6. //或者

  7. import * as Tab from './Tab';

  8. console.log(Tab.name + "XXX" + Tab.age);

说明:整体输出和部分输出不能同时使用,这里的整体不包括默认。
默认接口的写法如下

  1. //输出

  2. export {default} from './Modules';

  3. //输入

  4. import TabDef from './Tab';

  5. console.log(TabDef);

说明:如果需要输出默认和整体,可以同时使用默认和整体两条输出命令。
具名接口改为默认接口的写法如下。

  1. export {height as default} from './Modules';

  2. // 等同于

  3. import { height } from './Modules';

  4. export default height;

同样地,默认接口也可以改名为具名接口。

  1. //输入

  2. export {default as defWidth} from './Modules';

  3. //输出

  4. import {defWidth} from './Tab';

  5. console.log(defWidth);

7.模块的继承
如果一个模块A继承了模块B,在模块A中可以使用export *命令整体的导入B中的变量、函数和Class等,export *会忽略B模块的default方法

  1. //TabParent.js

  2. export let w = 1;

  3. export let h = 2;

  4. //Tab.js

  5. export * from './TabParent';//Tab继承TabParent

  6. //TabMain.js

  7. import {w,h} from './Tab';

  8. console.log(w+"======="+h);

8.ES6模块加载的实质
ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。CommonJS模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

  1. //输入Tab.js

  2. let count = 1;

  3. function compute(){

  4. count++;

  5. }

  6. module.exports = {

  7. count:count,

  8. compute:compute

  9. };

  10. //输出TabMain.js

  11. let TabES5 = require('./Tab');

  12. console.log(TabES5.count);//1

  13. TabES5.compute();

  14. console.log(TabES5.count);//1

上面代码说明,Tab.js模块加载以后,它的内部变化就影响不到输出的TabES5.count了。这是因为TabES5.count是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

  1. let count = 1;

  2. function compute(){

  3. count++;

  4. }

  5. module.exports = {

  6. get count() {

  7. return count

  8. },

  9. compute:compute

  10. };

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

  1. //输入Tab.js

  2. let count = 1;

  3. function compute(){

  4. count++;

  5. }

  6. export {count,compute}

  7. //输出TabMain.js

  8. import {count,compute} from './Tab';

  9. console.log(count);//1

  10. compute();

  11. console.log(count);//2

由于ES6输入的模块变量,只生成一个动态的只读引用,所以这个变量是只读的,对它进行重新赋值会报错,只能改变对象属性的值,不能够改变引用。如果是一个基本数据类型,不是引用类型的数据,也不能改变他的值。

  1. //输入Tab.js

  2. export let obj = {};

  3. //输出TabMain.js

  4. import {obj} from './Tab';

  5. obj.len = 10;

  6. console.log(obj);

  7. //obj = {len:20};SyntaxError: "obj" is read-only

  8. 说明:如果在Tab.js中obj是10,在TabMain.js中也不能改变obj=20。

9.浏览器的模块加载
浏览器使用 ES6 模块的语法如下。

上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。浏览器对于带有type="module"的