JS模块

在ES6中,我们可以使用 import 关键字引入模块,通过 export 关键字导出模块,功能较之于前几个方案更为强大,也是我们所推崇的。

ES6 的模块化汲取了 CommonJS 和AMD 的优点,拥有简洁的语法和异步的支持。并且写法也和 CommonJS 非常的相似。

关于 ES6 模块的基本用法相比大家都比较熟悉了。这里我们主要和 CommonJS 对比学习。

与 CommonJS 的差异

两大差异:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

值拷贝&值引用

js 复制代码
// lib/counter.js
var counter = 1;function increment() {
  counter++;
}

function decrement() {
  counter--;
}

module.exports = {
  counter: counter,
  increment: increment,
  decrement: decrement
};// src/main.jsvar counter = require('../../lib/counter');counter.increment();
console.log(counter.counter); // 1

在 main.js 当中的实例是和原本模块完全不相干的。这也就解释了为什么调用了 counter.increment() 之后仍然返回1。因为我们引入的 counter 变量和模块里的是两个不同的实例。

所以调用 counter.increment() 方法只会改变模块中的 counter .想要修改引入的 counter 只有手动一下啦:

javascript 复制代码
counter.counter++;
console.log(counter.counter); // 2

而通过 import 语句,可以引入实时只读的模块:

javascript 复制代码
// lib/counter.js
export let counter = 1;
export function increment() {
  counter++;
}
export function decrement() {
  counter--;
}// src/main.js
import * as counter from '../../counter';console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2

加载 & 编译

因为 CommonJS 加载的是一个对象(module.exports),对象只有在有脚本运行的时候才能生成。而 ES6 模块不是一个对象,只是一个静态的定义。在代码解析阶段就会生成。

ES6 模块是编译时输出接口,因此有如下2个特点:

  • import 命令会被 JS 引擎静态分析,优先于模块内的其他内容执行
  • export 命令会有变量声明提升的效果,所以import 和 export 命令在模块中的位置并不影响程序的输出。
javascript 复制代码
// a.js
console.log('a.js')
import { foo } from './b';// b.js
export let foo = 1;
console.log('b.js 先执行');// 执行结果:
// b.js 先执行
// a.js
// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
  console.log('bar2');
}
export function bar3() {
  console.log('bar3');
}// b.js
export let foo = 1;
import * as a from './a';
console.log(a);// 执行结果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js

循环加载的差异

"循环加载"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

javascript 复制代码
// a.js
var b = require('b');// b.js
var a = require('a');

循环加载如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

在 CommonJS 中,脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

javascript 复制代码
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

输出结果为:

javascript 复制代码
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

从上面我们可以看出:

  • 在b.js之中,a.js没有执行完毕,只执行了第一行。
  • main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行

ES6 处理"循环加载"与 CommonJS 有本质的不同**。ES6 模块是动态引用**,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

javascript 复制代码
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

运行结果如下:

JavaScript 复制代码
b.mjs
ReferenceError: foo is not defined

上面代码中,执行a.mjs以后会报错,foo变量未定义.

具体的执行结果如下:

  • 执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs
  • 执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。
  • 执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。

解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。

javascript 复制代码
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

最后执行结果为:

javascript 复制代码
b.mjs
foo
a.mjs
bar

特点

  • 每一个模块加载多次, JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例,或者说就是一个对象
  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。不会污染全局作用域;
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的
相关推荐
你也向往长安城吗5 分钟前
推荐一个三维导航库:three-pathfinding-3d
javascript·算法
karrigan14 分钟前
async/await 的优雅外衣下:Generator 的核心原理与 JavaScript 执行引擎的精细管理
javascript
wycode22 分钟前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode1 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏1 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
我是哈哈hh2 小时前
【Node.js】ECMAScript标准 以及 npm安装
开发语言·前端·javascript·node.js
张元清2 小时前
电商 Feeds 流缓存策略:Temu vs 拼多多的技术选择
前端·javascript·面试
pepedd8643 小时前
浅谈js拷贝问题-解决拷贝数据难题
前端·javascript·trae
@大迁世界3 小时前
useCallback 的陷阱:当 React Hooks 反而拖了后腿
前端·javascript·react.js·前端框架·ecmascript
小高0073 小时前
📌React 路由超详解(2025 版):从 0 到 1 再到 100,一篇彻底吃透
前端·javascript·react.js