模块
在 JS 中,一个 JS 文件就是一个模块,可以使用export关键字暴露本模块的函数或变量给其它模块使用:
export function sayHello(name) {
console.log(`hello ${name}`);
return `hello ${name}`;
}
如果其他模块需要使用,可以使用import关键字导入:
import {sayHello} from './hello.js'
sayHello('Tom');
如果是浏览器加载的 Html 文件,需要在代码片段<script>标签的属性中标记这里是模块后才能使用导入:
<html>
<head></head>
<body></body>
</html>
<script type="module">
import {sayHello} from './hello.js';
document.body.innerHTML = sayHello('John');
</script>
可以使用 VSCode 的 live server 插件加载 Html 文件进行验证。
模块级范围
每个模块中的顶级变量都只能在模块自己的范围内使用(访问)。比如:
// \src\module\user.js
let userName = 'Tom';
// \src\module\hello.js
function sayHello(name) {
console.log(`hello ${name}`);
}
sayHello(userName);
<!DOCTYPE html>
<script type="module" src="./user.js"></script>
<script type="module" src="./hello.js"></script>
这里有两个 JS 文件,都是以模块的方式加载到 Html 中,此时它们中的顶级变量都只存在于模块自己的作用域,因此hello.js中的sayHello(userName)会报错,因为找不到userName这个变量。
正确的方式是使用导入导出分享模块之间的变量(或函数):
// \src\module3\user.js
export let userName = 'Tom';
// \src\module3\hello.js
import { userName } from "./user.js";
function sayHello(name) {
console.log(`hello ${name}`);
}
sayHello(userName);
<!DOCTYPE html>
<script type="module" src="./hello.js"></script>
模块只有在首次导入时才会执行
模块只有在首次导入时才会被评估和执行:
// \src\module4\random.js
export let randomNum = Math.random();
// \src\module4\a.js
import { randomNum } from './random.js'
console.log('a', randomNum);
// \src\module4\b.js
import { randomNum } from './random.js'
console.log('b', randomNum);
<!DOCTYPE html>
<script type="module" src="./a.js"></script>
<script type="module" src="./b.js"></script>
可以看到控制台输出的两个随机数是相等的,因为random.js只执行了一次。
这种行为和所有支持导入的脚本语言是一致的,比如 PHP 中的require_once。
可以利用这种行为实现一些特殊用途,比如预留模块中的一些变量,交给其他模块进行初始化:
// \src\module5\config.js
export let config = {
userName: undefined
};
export function getUserName() {
return config.userName;
}
// \src\module5\init.js
import {config} from './config.js'
config.userName = '张三';
// src\module5\index.js
import './init.js';
import {getUserName} from './config.js';
console.log(getUserName());
执行时机
以模块方式加载的 JS 脚本,会在 Html 被完全加载后加载,这和普通的 JS 脚本加载时机不同:
<html>
<body>
<script type="module">
console.log(typeof button);
// object
</script>
<script>
console.log(typeof button);
// undefined
</script>
<button id="button">Button</button>
</body>
</html>
这里先加载普通的 JS 脚本<script>,再加载 Html DOM 元素<button>,最后加载模块<script type="module">,因此控制台会先打印undefined再打印object。
Async
可以用async标记模块片段:
<!DOCTYPE html>
<html>
<head></head>
<body>
<button onclick="showCount()">show count</button>
<script async type="module">
import { inc } from './counter.js';
inc();
console.log('第一个模块加载完成');
</script>
<script async type="module">
import { inc } from './counter.js';
inc();
console.log('第二个模块加载完成');
</script>
<script type="module">
import { get } from './counter.js';
function showCount() {
alert(get());
}
window.showCount = showCount;
</script>
</body>
</html>
被async标记的模块片段会立即加载,不会像普通模块那样等待 HTML DOM 完全加载完毕后再加载。这通常会用在某些不依赖于 HTML DOM 或其他模块的模块。比如上面的示例中,两个异步加载的模块仅打印内容以及让模块加载计数器+1。
导出和导入
导出
可以导出变量、函数或类定义:
export let count = 0;
export function add(a, b){
return a + b;
}
export class User{
constructor(name, age){
this.name = name;
this.age = age;
}
}
除了分别导出,还可以单独导出:
let count = 0;
function add(a, b){
return a + b;
}
class User{
constructor(name, age){
this.name = name;
this.age = age;
}
}
export {add, User, count};
导入 *
可以按照需要分别导入:
import {count, User, add} from './export.js';
console.log(add(1, 2));
console.log(count);
console.log(new User('Bob', 20));
也可以使用import * as导入所有:
import * as util from './export.js';
console.log(util.add(1, 2));
console.log(util.count);
console.log(new util.User('Bob', 20));
如果导入的标识符与当前命名空间冲突,可以使用as重命名标识符:
import {count as myCount, User as MyUser, add as myAdd} from './export.js';
console.log(myAdd(1, 2));
console.log(myCount);
console.log(new MyUser('Bob', 20));
类似的,也可以在导出的时候使用as重命名标识符:
let count = 0;
function add(a, b){
return a + b;
}
class User{
constructor(name, age){
this.name = name;
this.age = age;
}
}
export {add as myAdd, User as MyUser, count as myCount};
import * as export3 from './export3.js';
console.log(export3.myAdd(1, 2));
console.log(new export3.MyUser('张三', 18));
console.log(export3.myCount);
默认导出
有时候一个模块中只会定义一个类或函数,此时使用默认导出更为方便,比如:
export default class{
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `${this.name} ${this.age}`;
}
}
注意这里的类定义没有类名,因为在这个模块里不需要使用该类定义的名称,而其他模块导入类定义时可以指定类名:
import User from './user.js';
let user = new User('Mike', 20);
console.log(user.toString());
与之前的导入相比,导入默认导出时不需要使用大括号({...})。
理论上导入默认导出时,可以指定任意名称,但通常来说是导入模块的文件名,比如user.js就命名为User。
default
某些情况下可以使用default关键字指代默认导出,比如:
class User{
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `${this.name} ${this.age}`;
}
}
export {User as default}
这和export default class User{...}是等价的。
虽然不建议这么做,但默认导出和普通导出是可以同时存在的,比如:
export default class User{
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `${this.name} ${this.age}`;
}
}
export function compare(user1, user2){
if(user1.age > user2.age) return 1;
if(user1.age < user2.age) return -1;
return 0;
}
如果要同时导入默认导出和普通导出:
import {default as User, compare} from './user.js';
let user1 = new User('Mike', 20);
let user2 = new User('Jane', 18);
console.log(compare(user1, user2));
也可以使用*导入所有内容:
import * as user from './user.js';
let User = user.default;
let compare = user.compare;
let user1 = new User('Mike', 20);
let user2 = new User('Jane', 18);
console.log(compare(user1, user2));
导入对象的default属性就是默认导出的内容。
重新导出
假设我们要开发一个 JS 工具包,我们希望所有客户端程序都通过一个统一入口index.js导入工具类/函数等:
// login.js
export function login(userName, password) {
if (userName === 'admin' && password === '123') {
return true;
}
return false;
}
export function logout() {
return true;
}
// index.js
import {login, logout} from './login.js';
export {login, logout};
这里index.js导入需要提供给客户端程序的工具函数,并重新导出。客户端程序通过index.js即可完成导入:
import {login, logout} from './index.js';
console.log(login('admin', '123'));
console.log(logout());
对于这种重新导出,有一个更简便的写法:
// index.js
export {login, logout} from './login.js';
动态导入
可以使用import方法动态导入模块,该方法返回的是一个Promise对象:
// user1.js
export default class{
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `Name:${this.name}, Age: ${this.age}`;
}
}
// user2.js
export default class{
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `姓名:${this.name},年龄: ${this.age}`;
}
}
async function test(){
let User;
if(Math.random() > 0.5){
let module = await import('./user1.js');
User = module.default;
}
else{
let module = await import('./user2.js');
User = module.default;
}
let user = new User('张三', 18);
console.log(user.toString());
}
for(let i = 0; i < 10; i++){
test();
}
// Name:张三, Age: 18
// Name:张三, Age: 18
// Name:张三, Age: 18
// Name:张三, Age: 18
// Name:张三, Age: 18
// Name:张三, Age: 18
// 姓名:张三,年龄: 18
// 姓名:张三,年龄: 18
// 姓名:张三,年龄: 18
// 姓名:张三,年龄: 18
The End.
本文的完整示例可以从获取。

文章评论