对es6模块的一点理解

194 阅读5分钟

本文主要是对阮一峰老师es6入门,关于模块一章的理解加示例

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

// index.js
import {a} from './demo1';
console.log('index,js');
console.log('demo1---a', a);

// demo1.js
let a = 10;
console.log('demo1.js');
export { a };

// webpack打包后
var __webpack_modules__ = ({
 "./src/demo1.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { \"a\": () => (a)\n});\nlet a = 10;\nconsole.log('demo1.js');\n\n\n//# sourceURL=webpack://web/./src/demo1.js?");
 }),
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);var _demo1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./src/demo1.js\");\n\n\nconsole.log('index.js');\nconsole.log('demo1---a', _demo1__WEBPACK_IMPORTED_MODULE_0__.a);\n\n//# sourceURL=webpack://web/./src/index.js?");

})
});

分析webpack打包后的文件

  1. 入口文件是./src/index.js,会执行./src/index.js对应的eval,之后给_demo1__WEBPACK_IMPORTED_MODULE_0__变量赋值,值就是执行__webpack_require__函数的结果,参数是./src/demo1.js。这个函数的作用之一就是读取demo1.js中的代码。这就是所谓的编译时加载或者静态加载

截屏2022-02-2609.30.47.png

二、export命令除了输出变量,还可以输出函数或类(class)

常用的export形式为

export var firstName = 'Michael';
export var lastName = 'Jackson';
export function multiply(x, y) {
  return x * y;
};

var year = 1958;
export { year };

三、export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值,这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新

这一点es6上关于《Module 的加载实现》一章有详细的例子,这里想补充两点,一是这里的值需要区分基本类型和引用类型,如果CommonJS模块输出的值是引用类型,那么值可能会发生变化 二是webpack对于两者这点差别的实现方式

// index.js
import './demo1.js';
require('./demo2.js');

// webpack打包后
({"./src/demo1.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// "use strict";
eval("__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__, {\"a\": () => (a)});let a = 10;\n\n\n//# sourceURL=webpack://web/./src/demo1.js?");
}),
 "./src/demo2.js":
((module) => {
eval("let b = 2;\nmodule.exports  = { b }\n\n//# sourceURL=webpack://web/./src/demo2.js?");
})})

如果使用import导入,会使用__webpack_require__.d对导出的值进行一层封装

__webpack_require__.d = (exports, definition) => {
  for(var key in definition) {
    if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
	Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
   }
 }
};

可以看出,__webpack_require__.d就是将导出的值变为getter,setter,这样之后作为属性读取的时候可以读取到最新的值。这里感觉和vue实例中的data原理类似

四、export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import命令也是如此

// demo.js
var m = 10;
console.log('100');
function f1() {
  // 不可以
  import './index.js'
}
// 可以
import './index.js'

五、如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次,commonJS中的require每次都会执行,但是这里的执行不是说每次文件都会运行,而是在第一次加载的时候运行一次,生成一个对象,以后用到的时候,会到这个对象的exports上取值

// index.js源码
import { a } from  './demo1';
import { a as a1 } from './demo1';
const b1 = require('./demo2');
const c1 = require('./demo2');
console.log(a, a1, b1, c1);

// webpack打包后的index.js
eval("__webpack_require__.r(__webpack_exports__);var _demo1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/demo1.js\");const b1 = (__webpack_require__("./src/demo2.js\").b);const c1 = (__webpack_require__(./src/demo2.js\").c);console.log(_demo1__WEBPACK_IMPORTED_MODULE_0__.a, _demo1__WEBPACK_IMPORTED_MODULE_0__.a1, b1, c1);//# sourceURL=webpack://web/./src/index.js?");

可以看到打包后的index.js中,只有一次import,但是有两次require,而且利用import静态分析的特性,配合webpack的配置,可以做到Tree Shaking

六、ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。

首先,就是this关键字。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。

七、循环引用

commomJS中,require命令第一次加载脚本,就会执行整个脚本,然后在内存生成一个对象,以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。 一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出

// index.js
var a = require('./a.js');
var b = require('./b.js');

// 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 执行完毕');

在b当中,require('./a.js')会到缓存中取值,这时a.js只执行到了exports.done = false;所以返回的a.done为false

ES6模块是动态引用,如果从一个模块加载变量,模块会成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值

// demo1.js
let foo = 'foo';
import { bar } from './demo2';
console.log('demo1.js');
console.log(bar);
export { foo };

// demo2.js
let bar = 'bar'
import { foo } from './demo1';
console.log('demo2.js');
console.log(foo);
export { bar };

截屏2022-02-2611.43.27.png 由于let不会进行变量提升,所以foo未定义,想要不报错,可以使用var变量,但并不是我们想要的效果

解决这个问题的方法,就是让demo2.js运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决,因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了

// demo1.js
import { bar } from './demo2';
console.log('demo1.js');
console.log(bar());
function foo() { return 'foo' }
export { foo };

// demo2.js
import { foo } from './demo1';
console.log('demo2.js');
console.log(foo());
function bar() { return 'bar' }
export { bar };

// webpack打包后的
var __webpack_modules__ = ({
 "./src/demo1.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__, { \"foo\": () => (foo)});var _demo2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/demo2.js\");\n\nconsole.log('demo1.js');\nconsole.log((0,_demo2__WEBPACK_IMPORTED_MODULE_0__.bar)());\nfunction foo() { return 'foo' }\n\n\n//# sourceURL=webpack://web/./src/demo1.js?");
}),

"./src/demo2.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__, {\"bar\": () => (bar)\n});var _demo1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/demo1.js\");\n\nconsole.log('demo2.js');\nconsole.log((0,_demo1__WEBPACK_IMPORTED_MODULE_0__.foo)());\nfunction bar() { return 'bar' }\n\n\n//# sourceURL=webpack://web/./src/demo2.js?");
}),

以上就是对es模块化的一些理解,下面是针对上述知识点的一些问题

问题一:以下代码的执行过程

// index.js
import './demo1';
import './demo2';

// demo1.js
foo();
console.log('demo1');
import { foo } from './demo2';

// demo2.js
export function foo() {
  console.log('foo');
}

问题二--四:两次log的结果分别是?

// demo1.js
import { num } from './demo2';
console.log('num', num);
setTimeout(() => {
  console.log('1000s num', num);
}, 1000);

// demo2.js
export function addNum() {
  num++;
}
setTimeout(() => {
  addNum();
});
export let num = 0;

问题三

// demo1.js
const num = require('./demo2');
console.log('num', num);

setTimeout(() => {
  console.log('1000s num', num);
}, 1000);

// demo2.js
function addNum() {
  num++;
}
setTimeout(() => {
  addNum();
});
let num = 0;
module.exports = num;

问题四

// demo1.js
const obj = require('./demo2');
console.log('num', obj);

setTimeout(() => {
  console.log('1000s obj', obj);
}, 1000);

// demo2.js
function addNum() {
  obj.a = 10;
}
setTimeout(() => {
  addNum();
});
let obj = { a: 1 };
module.exports = obj;