利用webpack理解CommonJS和ES Modules的差异区别

前言

问: CommonJS 和 ES Modules 中模块引入的区别?

CommonJS 输出的是一个值的拷贝;ES Modules 生成一个引用,等到真的需要用到时,再到模块里面去取值,模块里面的变量,绑定其所在的模块。

我相信很多人已经把这个答案背得滚瓜烂熟,好,那继续提问。

问:CommonJS 输出的值是浅拷贝还是深拷贝?

问:你能模拟实现 ES Modules 的引用生成吗?

对于以上两个问题,我也是感到一脸懵逼,好在有 webpack 的帮助,作为一个打包工具,它让 ES Modules, CommonJS 的工作流程瞬间清晰明了。

准备工作

初始化项目,并安装 beta 版本的 webpack 5,它相较于 webpack 4 做了许多优化:对 ES Modules 的支持度更高,打包后的代码也更精简。

$ mkdir demo && cd demo
$ yarn init -y
$ yarn add webpack@next webpack-cli
# or yarn add webpack@5.0.0-beta.17 webpack-cli

早在 webpack4 就已经引入了无配置的概念,既不需要提供 webpack.config.js 文件,它会默认以 src/index.js 为入口文件,生成打包后的 main.js 放置于 dist 文件夹中。

确保你拥有以下目录结构:

├── dist
│  └── index.html
├── src
│  └── index.js
├── package.json
└── yarn.lock

在 index.html 中引入打包后的 main.js:

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
 </head>
 <body>
  <script src="main.js"></script>
 </body>
</html>

在 package.json 中添加命令脚本:

"scripts": {
 "start": "webpack"
},

运行无配置打包:

$ yarn start

终端会提示:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

webpack 要求用户在打包时必须提供 mode 选项,来指明打包后的资源用于开发环境还是生产环境,从而让 webpack 相应地使用其内置优化,默认为 production(生产环境)。

我们将其设置为 none 来避免默认行为带来的干扰,以便我们更好的分析源码。
修改 package.json:

"scripts": {
 "start": "webpack --mode=none"
},

重新运行,webpack 在 dist 目录下生成了打包后的 main.js,由于入口文件是空的,所以 main.js 的源码只有一个 IIFE(立即执行函数),看似简单,但它的地位却极其重要。

(() => {
 // webpackBootstrap
})();

我们知道无论在 CommonJS 或 ES Modules 中,一个文件就是一个模块,模块之间的作用域相互隔离,且不会污染全局作用域。此刻 IIFE 就派上了用场,它将一个文件的全部 JS 代码包裹起来,形成闭包函数,不仅起到了函数自执行的作用,还能保证函数间的作用域不会互相污染,并且在闭包函数外无法直接访问内部变量,除非内部变量被显式导出。

var name = "webpack";

(() => {
 var name = "parcel";
 var age = 18;
 console.log(name); // parcel
})();

console.log(name); // webpack
console.log(age); // ReferenceError: age is not defined

引用 vs 拷贝

接下里进入实践部分,涉及源码的阅读,让我们深入了解 CommonJS 和 ES Modules 的差异所在。

CommonJS

新建 src/counter.js

let num = 1;

function increase() {
 return num++;
}

module.exports = { num, increase };

修改 index.js

const { num, increase } = require("./counter");

console.log(num);
increase();
console.log(num);

如果你看过前面叙述,毫无疑问,打印 1 1.

so why?我们查看 main.js,那有我们想要的答案,去除无用的注释后如下:

(() => {
 var __webpack_modules__ = [
  ,
  module => {
   let num = 1;

   function increase() {
    return num++;
   }

   module.exports = { num, increase };
  },
 ];

 var __webpack_module_cache__ = {};

 function __webpack_require__(moduleId) {
  // Check if module is in cache
  if (__webpack_module_cache__[moduleId]) {
   return __webpack_module_cache__[moduleId].exports;
  }
  // Create a new module (and put it into the cache)
  var module = (__webpack_module_cache__[moduleId] = {
   exports: {},
  });

  // Execute the module function
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  return module.exports;
 }

 (() => {
  const { num, increase } = __webpack_require__(1);

  console.log(num);
  increase();
  console.log(num);
 })();
})();

可以简化为:

(() => {
 var __webpack_modules__ = [...];
 var __webpack_module_cache__ = {};

 function __webpack_require__(moduleId) {...}

 (() => {
  const { num, increase } = __webpack_require__(1);

  console.log(num);
  increase();
  console.log(num);
 })();
})();

最外层是一个 IIFE,立即执行。

__webpack_modules__,它是一个数组,第一项为空,第二项是一个箭头函数并传入 module 参数,函数内部包含了 counter.js 中的所有代码。

__webpack_module_cache__ 缓存已经加载过的模块。

function __webpack_require__(moduleId) {...} 类似于 require(),他会先去 __webpack_module_cache__ 中查找此模块是否已经被加载过,如果被加载过,直接返回缓存中的内容。否则,新建一个 module: {exports: {}},并设置缓存,执行模块函数,最后返回 module.exports

最后遇到一个 IIFE,它将 index.js 中的代码包装在内,并执行 __webpack_require__(1),导出了 num 和 increase 供 index.js 使用。

这里的关键点在于 counter.js 中的 module.exports = { num, increase };,等同于以下写法:

module.exports = {
 num: num,
 increase: increase,
};

num 属于基本类型,假设其内存地址指向 n1,当它被 赋值 给 module.exports['num'] 时,module.exports['num'] 已经指向了一个新的内存地址 n2,只不过其值同样为 1,但和 num 已是形同陌路,毫不相干。

let num = 1;
// mun 相当于 module.exports['num']
mun = num;

num = 999;
console.log(mun); // 1

increase 是一个函数,属于引用类型,即 increase 只作为一个指针,当它被赋值给 module.exports['increase'] 时,只进行了指针的复制,是 浅拷贝(基本类型没有深浅拷贝的说法),其内存地址依旧指向同一块数据。所以本质上 module.exports['increase'] 就是 increase,只不过换个名字。

而由于词法作用域的特性,counter.js 中 increase() 修改的 num 变量在函数声明时就已经绑定不变了,永远绑定内存地址指向 n1 的 num.

JavaScript 采用的是词法作用域,它规定了函数内访问变量时,查找变量是从函数声明的位置向外层作用域中查找,而不是从调用函数的位置开始向上查找

function foo() {
 var x = 10;
 console.log(x);
}
function bar(f) {
 var x = 20;
 f();
}
bar(foo); // 10

调用 increase() 并不会影响内存地址指向 n2 的 num,这也就是为什么打印 1 1 的理由。

ES Modules

分别修改 counter.js 和 index.js,这回使用 ES Modules.

let num = 1;

function increase() {
 return num++;
}

export { num, increase };
import { num, increase } from "./counter";

console.log(num);
increase();
console.log(num);

很明显,打印 1 2.

老规矩,查看 main.js,删除无用的注释后如下:

(() => {
 "use strict";
 var __webpack_modules__ = [
  ,
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
   __webpack_require__.d(__webpack_exports__, {
    num: () => /* binding */ num,
    increase: () => /* binding */ increase,
   });
   let num = 1;

   function increase() {
    return num++;
   }
  },
 ];

 var __webpack_module_cache__ = {};

 function __webpack_require__(moduleId) {} // 笔者注:同一个函数,不再展开

 /* webpack/runtime/define property getters */
 (() => {
  __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/runtime/hasOwnProperty shorthand */
 (() => {
  __webpack_require__.o = (obj, prop) =>
   Object.prototype.hasOwnProperty.call(obj, prop);
 })();

 (() => {
  var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

  console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
  (0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();
  console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
 })();
})();

经过简化,大致如下:

(() => {
 "use strict";
 var __webpack_modules__ = [...];
 var __webpack_module_cache__ = {};

 function __webpack_require__(moduleId) {...}

 (() => {
  __webpack_require__.d = (exports, definition) => {...};
 })();

 (() => {
  __webpack_require__.o = (obj, prop) => {...}
 })();

 (() => {
  var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

  console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
  (0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();
  console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);
 })();
})();

首先查看两个工具函数:__webpack_require__.o 和 __webpack_require__.d。

__webpack_require__.o 封装了 Object.prototype.hasOwnProperty.call(obj, prop) 的操作。

__webpack_require__.d 则是通过 Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }) 来对 exports 对象设置不同属性的 getter

随后看到了熟悉的 __webpack_modules__,它的形式和上一节差不多,最主要的是以下这段代码:

__webpack_require__.d(__webpack_exports__, {
 num: () => /* binding */ num,
 increase: () => /* binding */ increase,
});

与 CommonJS 不同,ES Modules 并没有对 module.exports 直接赋值,而是将值作为箭头函数的返回值,再把箭头函数赋值给 module.exports,之前我们提过词法作用域的概念,即这里的 num() 和 increase() 无论在哪里执行,返回的 num 变量和 increase 函数都是 counter.js 中的。

在遇到最后一个 IIFE 时,调用 __webpack_require__(1),返回 module.exports 并赋值给 _counter__WEBPACK_IMPORTED_MODULE_0__,后续所有的属性获取都是使用点操作符,这触发了对应属性的 get 操作,于是执行函数返回 counter.js 中的值。

所以打印 1 2.

懂了词法作用域的原理,就可以实现一个”乞丐版“的 ES Modules:

function my_require() {
 var module = {
  exports: {},
 };
 let counter = 1;

 function add() {
  return counter++;
 }

 module.exports = { counter: () => counter, add };
 return module.exports;
}

var obj = my_require();

console.log(obj.counter()); // 1
obj.add();
console.log(obj.counter()); // 2

总结

多去看源码,会有不少的收获,这是一个思考的过程。
ES Modules 已经写入了 ES2020 规范中,意味着浏览器原生支持 import 和 export,有兴趣的小伙伴可以试试 Snowpack,它能直接 export 第三方库供浏览器使用,省去了 webpack 中打包的时间。

到此这篇关于利用webpack理解CommonJS和ES Modules的差异区别的文章就介绍到这了,更多相关webpack CommonJS和ES Modules 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • webpack4.x CommonJS模块化浅析

    先看下webpack官方文档中对模块的描述: 在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块. 每个模块具有比完整程序更小的接触面,使得校验.调试.测试轻而易举. 精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的. webpack 的核心概念之一就是一切皆模块,webpack 在项目中的作用就是,分析项目的结构,找到 JavaScript 模块以及其他一些浏览器不能直接

  • 利用webpack理解CommonJS和ES Modules的差异区别

    前言 问: CommonJS 和 ES Modules 中模块引入的区别? CommonJS 输出的是一个值的拷贝:ES Modules 生成一个引用,等到真的需要用到时,再到模块里面去取值,模块里面的变量,绑定其所在的模块. 我相信很多人已经把这个答案背得滚瓜烂熟,好,那继续提问. 问:CommonJS 输出的值是浅拷贝还是深拷贝? 问:你能模拟实现 ES Modules 的引用生成吗? 对于以上两个问题,我也是感到一脸懵逼,好在有 webpack 的帮助,作为一个打包工具,它让 ES Mod

  • 一步步教你利用webpack如何搭一个vue脚手架(超详细讲解和注释)

    Vue作为前端三大框架之一截至到目前在github上以收获44,873颗星,足以说明其以悄然成为主流.16年10月Vue发布了2.x版本,经过了一段时间的摸索和看官方的教程和api,才了解到2.0版本在1.0版本的基础上做了好多调整,废弃了好多api. 本文将详细介绍关于利用webpack搭一个vue脚手架的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 一.适用人群 1.对webpack知识有一定了解但不熟悉的同学. 2.女同学!!!(233333....) 二.目

  • 深入理解Commonjs规范及Node模块实现

    前面的话 Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javascript的,在javascript中的顶层对象是window,而在node中的顶层对象是global [注意]实际上,javascript也存在global对象,只是其并不对外访问,而使用window对象指向global对象而已 在javascript中,通过var a = 100:是可以通过w

  • electron-vue利用webpack打包实现多页面的入口文件问题

    项目需要在electron的项目中新打开一个窗口,利用webpack作为静态资源打包器,发现在webpack中可以设置多页面的入口,今天来讲一下我在electron中利用webpack建立多页面入口的踩坑经验. 1.webpack的核心概念 •Entry:入口,Webpack执行构建的第一步从Entry开始: •Module:模块,在Webpack里一切皆模块,一个模块对应着一个文件.Webpack会从配置的Entry开始递归找出所有依赖的模块. •Chunk:代码块,一个Chunk由多个模块组

  • 浅谈Node新版本13.2.0正式支持ES Modules特性

    在本月 21 日,即2019.11.21,Node.js 发布了 13.2.0 版本,更新了一些特性.其中最令人兴奋的莫过于正式取消了 --experimental-modules 启动参数.这说明Node.js 正式支持 ES modules.我们一起来看看. Stability Index说明 Stability Index,即 Api 的稳定指数说明.它包括3个值: Stability: 0 ,不推荐使用.表示该Api官方不推荐使用,该功能可能会发出警告.不能保证向后兼容. Stabili

  • 利用Java理解sql的语法(实例讲解)

    select 相当于 for 循环 select id from IDArray LinkedList a = new LinkedList(); for ( int i=0 ; i<tableA.length ; i++){ a.add(IDArray.get("id" ) ); } return a; 当执行子查询时,可以理解为 select id, ( select name from nameArray) as names ,from Idarray LinkedList

  • 详谈commonjs模块与es6模块的区别

    到目前为止,已经实习了3个月的时间了.最近在面试,在面试题里面有题目涉及到模块循环加载的知识.趁着这个机会,将commonjs模块与es6模块之间一些重要的的区别做个总结.语法上有什么区别就不具体说了,主要谈谈引用的区别. commonjs 对于基本数据类型,属于复制.即会被模块缓存.同时,在另一个模块可以对该模块输出的变量重新赋值. 对于复杂数据类型,属于浅拷贝.由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块. 当使用require命令加载某个模块时,就会运

  • 深入理解java中i++和++i的区别

    今天简单谈谈关于java的一个误区,相信很多刚开始学习java的朋友都会遇到这个问题,虽然问题很简单,但是经常容易搞混,说说java的i++和++i的区别. 先看一下代码: <span style="font-size:18px;">public class test { public static void main(String[] args) { int i = 0; for (int j = 0; j < 10; j++) { i=i++; } System.

  • 如何理解Vue中computed和watch的区别

    概述 我们在 Vue 项目中多多少少都会有用到 computed 和 watch,这两个看似都能实现对数据的监听,但还是有区别.所以以下通过一个小栗子来理解一下这两者的区别. computed 计算属性 计算属性基于 data 中声明过或者父组件传递的 props 中的数据通过计算得到的一个新值,这个新值只会根据已知值的变化而变化,简言之:这个属性依赖其他属性,由其他属性计算而来的. <p>姓名:{{ fullName }}</p> ... ... data: { firstNam

  • 深入理解JS中attribute和property的区别

    目录 attribute和property介绍 “脚踏两只船” attribute和property的取值和赋值 1.attribute取值 2.attribute赋值 3.property取值 4.Property赋值 更改property和attribute其中一个值,会出现什么结果 property 和 attribute非常容易混淆,两个单词的中文翻译也都非常相近(property:属性,attribute:特性),但实际上,二者是不同的东西,属于不同的范畴. property是DOM中

随机推荐