什么是模块体系(module)
开发大型的、复杂的项目时,把所有代码都写在一个文件里,会显得很傻X,后期维护某个功能时会忍不住砸键盘;
如果把大程序里每个小模块给单独拆分出来,然后集中拼接到总程序里,这样写起来能省很多麻烦,后期维护特定功能也方便,这样搞其实就是模块体系;
而且有了模块体系后,可以提高代码复用性,可以用别人的模块,来节省自己造轮子的时间;
很多语言都有这个功能,Ruby有require,Python有import,连CSS都有@import,但是很长一段时间内,JS都是没有这方面的支持的;
JS模块体系的发展
JS火了以后,用的人越来越多,项目越来越复杂,慢慢就发现不对劲:JS也得来一个模块功能才行,不然写大项目太折磨人了;
于是ES6之前,社区就制定一些模块加载方案,主要有CommonJS和AMD两种方案;
CommonJS是针对服务器的方案,AMD是针对浏览器的方案;
ES6出来后,实现了模块功能,而且实现的很好,所以可以替代CommonJS和AMD两种方案,成为浏览器和服务器通用的模块解决方案;
ES6中关于模块的设计思想
ES6希望模块实现尽量的静态化,让各个模块在编译的时候就能确定依赖关系、输出和输入的变量;
而CommonJS和AMD方案,都只有在运行模块的时候,才能确定这些东西:
// CommonJS模块
let { writeFile,readFile } = require('fs'); //析构赋值
// 等同于:
let fsObj = require('fs');
let writeFile = fsObj.writeFile;
let readFile = fsObj.readFile;
CommonJS模块看作对象,引入时要去对象上一个个查找属性;上面代码中加载fs模块,就是加载fs的所有方法,然后生成一个对象fsObj,然后从这个对象上读取两个方法。
这种加载称为运行时加载,只有在运行的时候才能得到对象,没办法在编译的时候做静态优化;
ES6的模块就不是对象,而是通过export命令直接把要导出的东西给输出出去,别的文件通过import命令直接引入就可以得到相应内容了,不用挂载到对象上,再从对象上去找;
// ES6模块
import {writeFile,readFile} from 'fs';
上面的代码是从fs模块直接加载两个方法,其他的一律不加载,也不管;这种加载方式叫做编译时加载或者静态加载,在编译的时候就完成了模块加载,效率更高;
一个模块就是一个文件,文件里的变量其他文件是获取不到的,除非是用export导出才行;
具体语法
注意:这里不用webpack等任何工具,只用浏览器原生的解析JS文件;
// 准备三个文件:index.html、main.js、module.js
// index.html
<body>
<script src="./main.js" type="module"></script>
<script src="./module.js" type="module"></script>
</body>
// module.js
export let firstName = 'Vincent';
export let lastName = 'Liu';
// main.js
import { firstName,lastName } from './module.js';
console.log('Name is ' + firstName + lastName);
// Name is VincentLiu;
需要注意的地方
因为这里没用webpack等工具,所以在html文件里引入时,要加type="module",不然会报错:
// 引入module.js时不加:
<script src="./module.js"></script>
<script src="./main.js" type="module"></script>
// 结果:
// Uncaught SyntaxError: Unexpected token 'export' VM2319 module.js:2
Name is VincentLiu //但是还是可以运行的;
// 引入main.js时不加:
<script src="./module.js" type="module"></script>
<script src="./main.js"></script>
// 结果:
// Uncaught SyntaxError: Cannot use import statement outside a module main.js:1
// 运行不了了
单单从模块的角度看,引入顺序是不影响结果的;先引入module.js也行,先引入main.js也行;
错误情况
// 不能直接导出一个值:
export 1;
// 报错;
// 导出时忘了加 {} 括号:
let m = 1;
export m;
// 报错
// 应该加括号,导出函数、类、对象等内容时同理
let m = 1;
export {m};
引入的时候注意后缀名
// module.js
export let firstName = 'Vincent';
export let lastName = 'Liu';
// main.js
import { firstName,lastName } from './module'; /*注意这里没加后缀*/
console.log('Name is ' + firstName + lastName);
// GET http://127.0.0.1:5500/module net::ERR_ABORTED 404 (Not Found)
// 因为没有使用工具,只用原生的浏览器解析,所以不加后缀名就出错了
定义时就导出变量
// module.js
export let firstName = 'Vincent';
export let lastName = 'Liu';
// main.js
import { firstName,lastName } from './module.js';
console.log('Name is ' + firstName + lastName);
// Name is VincentLiu;
先定义变量,然后统一导出
// module.js
let firstName = 'Vincent';
let lastName = 'Liu';
export {
firstName,
lastName
}
// main.js
import { firstName,lastName } from './module.js';
console.log('Name is ' + firstName + lastName);
// Name is VincentLiu;
在定义时就导出函数
// module.js
export function sayHi(){
console.log('hi');
}
// main.js
import {sayHi} from './module.js';
sayHi();
// hi
先定义函数,然后导出
// module.js
function sayHi(){console.log('hi');}
export {sayHi}
// main.js
import {sayHi} from './module.js';
sayHi();
// hi
导出时给函数或变量重命名
使用as关键字可以对导出或引入的东西进行重命名
导出时重命名
// module.js
function func1(){console.log('你执行了 func1');}
function func2(){console.log('你执行了 func2');}
let m = 1;
export {
func1 as f1,
func2 as f2,
m as n
}
// main.js
import { f1,f2,n } from './app.js';
f1(); //你执行了 func1
f2(); //你执行了 func2
console.log(n); //1
引入时重命名
// module.js
function func1(){console.log('你执行了 func1');}
function func2(){console.log('你执行了 func2');}
let m = 1;
export {
func1,
func2,
m
}
// main.js
import { func1 as f1,func2 as f2,m as n} from './app.js';
f1(); //你执行了 func1
f2(); //你执行了 func2
console.log(n); //1
导出类
// module.js
export class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
sayHi(){
console.log(this.name + ' said: hi!');
}
}
// 或这样导出:
class Person{...}
export { Person }
// main.js
import {Person} from './module.js';
let person1 = new Person('Jack',18);
person1.sayHi();
导出对象
// module.js
export let a = {
name:'Vincent',
age:18
}
// main.js
import {a} from './module.js';
console.log(a.name);
// Vincent
动态导出内容
export导出的其实是一个输出的接口,与其对应的是动态绑定的关系,所以通过这个接口,我们可以取到模块内部实时的值;
// module.js
export let a = 1;
setTimeout(() => {
a = 2
},1000);
// main.js
import {a} from './app.js';
console.log(a); //1
setTimeout(() => {
console.log(a);
},1000)
// 一秒后输出为2
export位置的问题(作用域)
export可以放在模块文件里的任何位置,放在开头也行,放在末尾也行,放在中间也行;
但是要注意,不能放在块级作用域内!
// module.js 第一种情况
function fn() {
export let a = 1;
}
fn();
// module.js 第二种情况
let x = 1;
if(x === 1){
export let a = 1;
}
// main.js
import {a} from './app.js';
console.log(a);
// 报错: Uncaught SyntaxError: Unexpected token 'export' module.js:
export如果处于作用域内,就会报错,因为处于条件代码块里,就没办法静态优化了,这就违背了ES6的设计理念了;
import位置的问题(作用域)
export要注意作用域的问题,import自然也要注意作用域的问题,因为都是ES6模块里的东西,自然理念是一样的;
本质上还是因为import是静态执行的,所以不能放在代码块里,因为代码块只有在运行时才能得到结果,这种结构是不符合静态编译的理念的;
// module.js
export function sayHi(){console.log('hi')}
// main.js
let x = 1;
if(x === 1){
import {sayHi} from './module.js';
}
// 报错!
import具有提升效果
import命令自带提升效果,会把引入的东西自动提升到头部,首先执行;
// module.js
export function sayHi(){console.log('hi');}
// main.js
sayHi();
import {sayHi} from './module.js'
// hi
// 故意把import语句放在下面,先运行函数,也是同样的效果
两次或多次相同的import只会触发一次效果
// module.js
export function sayHi() {
console.log('hi');
}
sayHi()
// main.js
import './module.js';
import './module.js';
import './module.js';
...
// 你引入一万次,也只会执行一次,输出一个hi
模块的整体加载
逐个加载的坏处在于:如果需要载入的东西多了,一个一个载入会累到爆炸;
// module.js
function f1() {console.log('你执行了f1');}
function f2() {console.log('你执行了f2');}
function f3() {console.log('你执行了f1');}
...
export {f1,f2,f3,...}
// main.js
import {f1,f2,f3,...} from './module.js';
f1();
f2();
f3();
...
所以这时我们可以使用整体加载:
// module.js
function f1() {console.log('你执行了f1');}
function f2() {console.log('你执行了f2');}
function f3() {console.log('你执行了f1');}
...
export {f1,f2,f3,...}
// main.js
import * as funcs from './module.js'; //注意*不用加括号{}
funcs.f1();
funcs.f2();
funcs.f3();
...
这样其实是在加载模块的时候,用星号*指定一个对象,然后把所有的导出值都挂载到这个对象上;
这样乍得一看和CommonJS又很像了,但是本质上并不相同,一个是直接引入一整个对象,然后从对象上找属性,可能会造成很多不用的也给引进来;而这里是先引入规定好的导出的东西,再把这些东西挂载到一个对象上;
另外,引入的函数、变量等内容,在运行时不允许被改变,因为是只读属性:
// module.js
function f1() {console.log('你执行了f1');}
let b = 1;
export { f1,b }
...
import * as funcs from './module.js';
funcs.f1 = () => {
console.log('guoyin');
}
// 报错:Uncaught TypeError: Cannot assign to read only property 'f1' of object '[object Module]'
funcs.b = 2;
// 报错:Uncaught TypeError: Assignment to constant variable.
// 不能设置只读属性
但是有个特殊的,需要注意的,就是可以改导出的对象或数组里的值:
// module.js
export let a = [1,1,1];
export let b = {name:'Jack'}
// main.js
import {a,b} from './module.js';
a['0'] = 888;
console.log(a); //[888, 1, 1]
b.name = 'Tom';
console.log(b.name); //Tom
export default 命令
从之前的例子可以看出来,想import一个变量或者函数之类的东西,必须要知道名字才行。但是假如我不想去慢慢看文档,了解每个属性和方法名字叫啥咋办?或者说就想导出、引入一个匿名函数咋办?
这时就需要用到 export default 命令了:
// module.js
export default function(){
console.log('hi');
}
// main.js
import f1 from './module.js'; /* 注意f1两边没加花括号{} */
f1();
//hi
你可能想问这个f1名字是哪儿来的,其实这个名字是可以随便起的,只要你高兴,叫f2也行,叫f3也行; 但是前提是,必须要 export default 才行;
当然,具名函数也可以用 export default 导出:
// module.js
export default function sayHi(){
console.log('hi');
}
// 或者这样写:
function sayHi(){
console.log('hi');
}
export default sayHi;
// main.js
import f1 from './module.js'; /* 注意f1两边没加花括号{} */
f1();
//hi
虽然sayHi是一个具名函数,但是用 export default 导出了,所以在外面引用时依然可以随便起名字;
export default 和 花括号 {}
使用了 default 导出时,引入时就不用加花括号;
没使用 default 导出时,引入时就要加花括号;
// module.js
export default function f1() { /* 注意有default */
console.log('执行了f1');
}
export function f2(){ /* 注意没有default */
console.log('执行了f2');
}
// main.js
import fn from './module.js'; /* 注意没有花括号 */
fn();
import {f2} from './module.js'; /* 注意有花括号 */
f2();
export、import 和 as 一起用
export 和 as 一起用:
// module.js
function func1(){
console.log('执行了f1');
}
export {func1 as default} // 等同于 export default func1
// main.js
import fn from './module.js'; /* 因为作为default导出了,所以可以随便取名字 */
fn();
import 和 as 一起用:
// module.js
function func1(){
console.log('执行了f1');
}
export {func1 as default}
// main.js
import fn from './module.js'; /* 因为作为default导出了,所以可以随便取名字 */
fn();
// 等同于这样写:
import {default as fn} from './module.js';
import命令和import()函数
import命令会被JS引擎静态分析,不能放在代码块中或者函数中,这样虽然编译效率提高了,却也导致了一个问题:无法在运行时加载模块,也就失去了动态加载的功能;
Node.js里的require是有这个功能的,想要import也实现,于是ES2020提案加了一个 import()函数,来支持动态加载模块功能;
注意区别:一个是import命令,一个是import()函数;
// html
<body>
<button id="btn">click me</button>
<script src="./main.js"></script>
</body>
// 注意:没有引入module.js文件,而且main.js引入时也不用写 type="module"!
// module.js
function sayHi() { // 这里没写export
console.log('hi');
}
sayHi();
// main.js
let btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
import('./module.js')
})
点击按钮时动态载入module.js,控制台输出hi;
也可以按条件加载:
if(x === 1){
import('module-1.js');
}else{
import('module-2.js');
}
其实上面的省略性的用法,import()函数返回的是一个Promise对象:
import('module.js').then((...)=>{...}).catch((err)=>{...})
本篇仅为整理笔记,关于import()的更多用法不多细述,有兴趣的可以看阮一峰老师的ES6教程