0%

js-module

JavaScript一直没有模块(module)体系,无法将一个大程序拆分成相互依赖的小文件,再用简单的方法拼装起来,这对开发大型的、复杂的项目形成了巨大的障碍。在ES6之前,社区制定了一些模块化加载方案,最主要的有CommonJSAMD两种,前者用于服务器,后者用于浏览器,ES6在语言标准的层面上,实现了模块功能,完全可以取代CommonJS和AMD规范,成为浏览器和服务器通用的解决方案。

ES6模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入输出变量。CommonJS和AMD模块都只能在运行时确定这些东西,比如CommonJS的模块就是对象,输入时必须查找对象属性:

1
2
3
4
5
6
// CommonJS模块
let {stat, readFile} = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let readFile = _fs.readFile;

上面实质上是整体加载fs模块(即加载fs的所有方法),生成一个对象,然后再从这个对象上读取三个方法。这种加载称为“运行时加载”,因为只有在运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入:

1
import {stat, readFile} from 'fs';

上面实质上是从fs模块中加载两个方法,其他的不加载,这种加载称为“编译时加载”或“静态加载”,即ES6可以在编译时就完成模块加载,效率比CommonJS模块的加载方式更高,也使得静态分析成为可能。

ES6的模块自动采用严格模式,不管你在模块头部有没有加上"use strict";。严格模式的主要限制有:

  • 变量名必须声明后使用
  • 函数参数不能有同名属性
  • 不能使用with语句
  • 禁止this指向全局,顶层的this指向undefined

ES模块功能主要由两个命令构成:

  • export 规定模块的对外接口
  • import 输入其他模块提供的功能

export命令

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

1
2
3
4
 // test.js
export const firstName = 'Michael';
export const lastName = 'Jackson';
export const year = 1958;

或者可以等价写为:
1
2
3
4
const firstName = 'Michael';
const lastName = 'Jackson';
const year = 1958;
export {firstName, lastName, year};

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

特别注意:**exprt命令规定的事对外的接口,必须与模块内部的变量建立意义对应的关系。**

1
2
3
4
5
6
export 1; // 错误直接输出1,而不是接口
var m = 1;
export m; // 错误直接输出变量,而不是接口
export var m = 1; // 正确
export {m}; // 正确
export {m as n}; // 正确

同样的,functionclass的输出也必须遵守这样的写法:

1
2
3
4
function f () {};
export f; // 错误;
export {f}; // 正确
export function f(){}; // 正确

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

1
2
export let foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面输出变量foo,值为bar,500毫秒之后变为baz

在CommonJS中,模块输出的是值的缓存,不存在动态更新。

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

import 命令

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

import命令接受一对大括号,里面指定要从其他模块中导入的变量名,大括号里面的变量名,必须与被导入模块的对外接口的名称相同(可以使用as关键字将输入的变量进行重命名):

1
2
// 从test模块导入定义的三个变量
import {firstName, lastName, year as y} from './test';

import命令输入的变量都是只读的,因为它的本质是输入接口。不允许在加载模块的脚本里面改写接口:

1
2
import {firstName, lastName, year as y} from './test';
y = {}; // 错误,不可修改

但是如果导入的是一个对象,那么修改对象的属性是允许的,但是不建议改,都应该当做只读,否则会出现bug很难查错。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js可省略。如果只是模块名,不带有路径,那么必须通过配置,告诉引擎怎么取到这个模块。

import命令具有提升效果,会提升到整块的头部,首先执行。因为本质是,import命令是编译阶段执行的,在代码之前。并且,由于import是静态执行的,所以不能使用表达式和变量。

最后,import语句会执行所加载的模块,如果多次重复执行同义句import语句,那么只会执行一次,不会执行多次。

模块的整体加载

除了制定加载某个输出值,还可以整体加载,即用*指定一个对象,所有输出值都加载在这个对象上面。例如:

1
2
import * as test from './test';
test.foo();

但是要注意的是:模块整体加载所在的对象上面,应该是可以静态分析的,所以不允许运行时改变。

export default 命令

前面看了,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,也为了方便某些框架自动加载,可以使用export default命令,为模块指定默认输出:

1
2
// test.ts
export default {firstName, lastName, year, foo};

这样,其他模块加载该模块时,import命令可以指定为任何名字:

1
2
import test from '../interface/test';
test.foo();

上的import命令可以使用任意名字指向模块的输出,这时就不需要知道原模块的输出名字是什么,需要注意的是:使用export default后,导入命令import就不需要使用大括号了

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此,export default命令只能使用一次。所以import命令后面才不用加大括号,因为只能唯一对应export default命令。

export defaultexport不同,正因为export default命令其实输出一个叫default的变量,所以它后面不可跟变量声明语句,这正好和export相反:

1
2
3
4
5
6
export var a = 1; // 正确
export default var a = 1; // 错误
var a = 1;
export a; // 错误
export default a; // 正确
export default 1; // 正确

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成:

1
2
3
4
5
6
7
// test.js
const firstName = 'Michael';
const foo = (t) => {
console.log(t);
};
export {firstName, lastName, year, foo};
export default foo;
1
2
3
// 使用
import test, {firstName} from '../interface/test';
test(firstName);

总结

在写angular的时候,知道要export对应的class,在写eggjs的时候,有export default,但是不知道为啥这么用,通过学习ES6模块这部分,来细致了解下。

参考:http://es6.ruanyifeng.com/#docs/module

码字辛苦,打赏个咖啡☕️可好?💘