模块化

模块化

8.1 概述

在 ES6 之前,有两种模块化标准:

  1. Node → CommonJS
  2. 浏览器 → AMD

符合 AMD 标准的工具比如有 require.js, 现在已经逐渐被抛弃,因为 AMD 异步加载的特性,模块依赖关系比较难以维护。

ES6 提出 ES6 Module 标准,目的是称为 Node 和浏览器模块化的通用方案,并且其编译时加载的特性,让模块依赖关系更具明确。

8.2 CommonJS

8.2.1 概述

CommonJS 模块特性:

  1. 所有代码运行模块作用域,不会污染全局作用域
  2. 模块可以被多次加载,但只有第一次加载才会执行,后续的加载只读取缓存结果
  3. 模块的加载是同步执行的,所以加载顺序就是 require 执行顺序
  4. 导出的值是一份拷贝,修改后不会影响模块内部的变量

示例如下:

// 定义模块 a.js
let a = 5;
let fun = ()=>console.log(++a);

module.exports = {
    fun: fun
    a: a
}

// 引用模块 a.js
let obj = require('a.js');
obj.fun(); // 6
console.log(a) // 5

8.2.2 module 对象

Node 内部提供一个 Module 构造函数,所有的 module 模块都是 Module 的实例:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
}

而在 CommonJS 模块内部,你也可以访问到 module 对象,它代表当前模块,它具有以下属性:

  • module.id → 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename → 模块的文件名,带有绝对路径。
  • module.loaded → 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent → 返回一个对象,表示调用该模块的模块。
  • module.children → 返回一个数组,表示该模块要用到的其他模块。
  • module.exports → 表示模块对外输出的值。

8.2.3 module.export 属性

module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports 变量。

为了方便,Node 给模块提供了一个 exports 变量,指向 module.exports。等同于 Node 在每个模块头部执行了这样一句代码:

var exports = module.exports;

所以,在模块里不能将 exports 变量指向一个新的值,这样会切断它和 module.exports 的联系

exports = a; // 无效

另外,直接给 module.exports 赋值的话,前面通过 exports 变量导出的数据也会失效。

exports.a = xxx;
module.exports = b; // 导出的 a 无效

建议放弃 exports,只使用 module.exports。

8.2.4 require 命令

CommonJS 通过 require 命令来加载模块文件,即读入并执行一个 js 文件,并返回它的 export 对象。

另外,js 文件被第一次 require 的时候,里面的 js 代码会被执行,返回 module.exports 结果。然后,module.exports 会被缓存起来,下次再 require 这个文件,直接返回其 module.exports 的缓存,而不会再次执行。

另外,被 require 的值,本质是 module.exports 的一个拷贝,对 module.exports 的输出进行修改,不会影响到模块内部的值。这一点和 ES6 Module 的值映射不一样。

require 命令是来自 module.require,它也具有一些辅助属性和方法:

  1. require(): 加载外部模块
  2. require.resolve():将模块名解析到一个绝对路径
  3. require.main:指向主模块
  4. require.cache:指向所有缓存的模块
  5. require.extensions:根据文件的后缀名,调用不同的执行函数

8.3 ES6 Module

8.3.1 概述

特性如下:

  1. 兼容 Node 和浏览器的模块化语法
  2. 静态化,在代码编译时就可以确定模块依赖关系
  3. 导出的值是一份映射,只读不可修改,可以调用模块方法去修改,修好后再读得到是最新的映射
  4. 自动采用严格模式,不管你有没有在模块头部加上"use strict"

示例:

// 定义模块 a.js
export let a = 5;
export let fun = ()=>a++;


// 引用模块 a.js
import {a, fun} from 'a.js'

fun();
console.log(a); // 6

8.3.2 export 命令

导出写法1:

export let a = 5;
export let fun = ()=>a++;

导出写法2:

let a = 5;
let fun = ()=>a++;
export {
	a,
	fun
}

上面两种写法效果都一样,导出的都是值的映射。

错误写法:

export 1; // 报错

var m = 1;
export m; // 报错

8.3.3 import 命令

正常加载:

import {a, fun} from 'a.js'
fun();
console.log(a) // 6

整体加载:

import * as obj from 'a.js'
obj.fun();
console.log(obj.a) // 6

// 整体加载后,obj 的属性不允许修改
obj.a = 99; // 报错
obj.fun = null; // 报错

8.3.4 export default 命令

通过 export 导出的想要 import 引入,你必须要知道 export 了什么。而有的时候我可能不想知道到底 export 了什么,就可以使用 export default

// 定义模块 a.js
export default function() {
	console.log()
}
// 引用 a.js
import fun from 'a.js'
fun();

本质上,export default 等同于定义并输出了一个 default 的变量。

// 定义模块 a.js
let fun = ()=>{};
export default fun
// 等同于 export {fun as default};

// 引用 a.js
import fun from 'a.js' 
// 等同于 import {default as fun} from 'a.js';

也因为 export default 等同输出 default 变量,所以不能在后面再定义变量

export default var a = 99; // 报错
export default 99; // 正确

export var a = 99; // 正确
export 99; // 报错

并且要区分 export default 和 export 输出一个对象时。

let a = 99;
export {a}; 
// a 是值映射,只读不可改

export default {a} 
// {a} 是 default 的值映射,default 不可改,但 default.a 可改,且不会影响真正的 a

总结:

  1. export default a 对应 improt b form 'a.js'
  2. export var a = 1 或 export {a} 对应 import {a} from 'a.js' 或 improt * as obj from 'a.js'

所以,export default 和 export 也可以混用

// a.js
export default 1;
export var b = 2;


// 引用 a.js
import a, {b} form 'a.js'
console.log(a, b); // 1 2

8.3.5 export 与 import 的复合写法

// 正常写法
export { foo, bar } from 'my_module';
// 可以理解为
import {foo, bar} from 'my_module';
export {foo, bar}

// 接口改名
export {fun1 as fun2} from 'my_module';

// 整体输出
export * from 'my_module';

// 默认接口
export {default} from 'my_module'

// 具名接口改为默认接口
export {fun as default} from 'my_module'

8.3.6 import()

ES62020 引入 import() 函数,和 import 语句的编译时加载不同,import 方法是运行时加载,和 Node 的 require 方法类似,参数可以是一个变量,主要区别是 require 是同步加载而 import 是异步加载并返回一个 Promise 对象

另外,import() 函数与所加载的模块没有静态连接关系,通过 Promise 回调参数拿到的值,是可修改的,且修改后不会影响模块内部变量的值。

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

因为 import 的动态性,它可以用来实现按需加载、动态路径加载。

8.4 ES6 Module 和 CommonJS 的区别

  1. 加载时机不同:
    • CommonJS 是运行时加载,必须到代码执行才能确认模块依赖关系,所以 webpack 无法进行 tree-shaking 去除死代码。
    • ES6 Module 是编译时加载,能在编译时确认模块依赖关系,以便 webpack 进行 tree-shaking 去除死代码;
      • import 的参数必须是字符串常量;require 的参数可以是变量
      • import 和 export 语句必须写在顶层作用域;require 语句可以写在任意地方
  2. 导出机制不同:
    • CommonJS 的导出是值的拷贝,修改后不会影响模块内部变量;
    • ES6 Module 的导出是值的映射,只读不可修改,但可以调用模块内部方法进行修改,修改后再读就可以得到最新的映射值。
  3. 顶层作用域的 this 不同
    • ES6 Module 默认使用严格模式,所以顶层 this 只会是 undefined;
    • 而 CommonJS 如果不指明使用严格模式,顶层 this 会是 window
posted @ 2020-07-16 10:09  树干  阅读(229)  评论(0编辑  收藏  举报