JS模块化—CJS&AMD&CMD&ES6-前端面试知识点查漏补缺

本文从以时间为轴从以下几个方面进行总结JS模块化。从无模块化 => IIFE => CJS => AMD => CMD => ES6 => webpack这几个阶段进行分析。
历史 幼年期:无模块化
方式
  1. 需要在页面中加载不同的js,用于动画,组件,格式化
  2. 多种js文件被分在了不同的文件中
  3. 不同的文件被同一个模板所引用

此处写法文件拆分是最基础的模块化(第一步)
* 面试中的追问 script标签的参数:async & defer

总结
  • 三种加载
  • 普通加载:解析到立即阻塞,立刻下载执行当前script
  • defer加载:解析到标签开始异步加载,在后台下载加载js,解析完成之后才会去加载执行js中的内容,不阻塞渲染
  • async加载:(立即执行)解析到标签开始异步加载,下载完成后开始执行并阻塞渲染,执行完成后继续渲染
JS模块化—CJS&AMD&CMD&ES6-前端面试知识点查漏补缺
文章图片

  • 兼容性:> IE9
  • 问题可能被引导到 => 1. 浏览器的渲染原理 2.同步异步原理 3.模块化加载原理
  • 出现的问题
  • 污染全局作用域
成长期(模块化前夜) - IIFE(语法测的优化)
JS模块化—CJS&AMD&CMD&ES6-前端面试知识点查漏补缺
文章图片

作用域的把控
let count = 0; const increase = () => ++count; const reset = () => { count = 0; }

利用函数的块级作用域
(() => { let count = 0; ... }) //最基础的部分

实现一个最简单的模块
const iifeModule = (() => { let count = 0; const increase = () => ++count; const reset = () => { count = 0; } console.log(count); increase(); })();

  • 追问:独立模块本身的额外依赖如何优化
优化1:依赖其他模块的传参型
const iifeModule = ((dependencyModule1,dependencyModule2) => { let count = 0; const increase = () => ++count; const reset = () => { count = 0; } console.log(count); increase(); ...//可以处理依赖中的方法 })(dependencyModule1,dependencyModule2)

面试1:了解jquery或者其他很多开源框架的模块加载方案 将本身的方法暴露出去
const iifeModule = ((dependencyModule1,dependencyModule2) => { let count = 0; const increase = () => ++count; const reset = () => { count = 0; } console.log(count); increase(); ...//可以处理依赖中的方法 return increase,reset } })(dependencyModule1,dependencyModule2) iifeModule.increase()

=> 揭示模式 revealing => 上层无需了解底层实现,仅关注抽象 => 框架
  • 追问:
  • 继续模块化横向展开
  • 转向框架:jquery|vue|react模块细节
  • 转向设计模式
成熟期
JS模块化—CJS&AMD&CMD&ES6-前端面试知识点查漏补缺
文章图片

CJS (Commonjs)
node.js指定
特征:
  1. 通过module + exports对外暴露接口
  2. 通过require去引入外部模块,参考 前端进阶面试题详细解答
main.js
const dependencyModule1 = require('./dependencyModule1') const dependencyModule2 = require('./dependencyModule2')let count = 0; const increase = () => ++count; const reset = () => { count = 0; } console.log(count); increase(); exports.increase = increase; exports.reset = reset; module.exports = { increase, reset }

exe
const {increase, reset} = require(./main.js)

复合使用
(function(this.value,exports,require,module){ const dependencyModule1 = require('./dependencyModule1') const dependencyModule2 = require('./dependencyModule2') }).call(this.value,exports,require,module)

追问:一些开源项目为何要把全局、指针以及框架本身作为参数
(function(window,$,undefined){ const _show = function(){ $("#app").val("hi zhuawa") } window.webShow = _show; })(window,jQuery)

阻断思路
  • 一. window
    1. 全局作用域转换为局部作用域,window是全局作用域,如果不转成局部作用域会有一个向上查找知道全局的过程,提升执行效率
    2. 编译时优化:编译后会变成(优化压缩成本,销毁)
    (function(c){})(window) // window会被优化成c //window在里面所有别的执行所有的变化都会随着执行完毕都会跟着c一起被销毁

  • 二. jquery
    1. 独立定制复写和挂载
    2. 防止全局串扰
  • 三. undefined
    防止改写:在执行内部这段代码的时候保证undefined是正确的,不会被改写,如在外部定义一个undefined =1
    undefined对jquery本身是一个很重要的一个存在
优缺点
  • 优点:CommonJS率先在服务端实现了,从框架层面解决了依赖,全局变量未然的问题
  • 缺点: 针对服务端的解决方案,异步拉取,依赖处理不是很友好
=> 异步依赖的处理
AMD
通过异步执行 + 允许指定回调函数
经典实现框架:require.js
新增定义方式:
//define来定义模块 define(id, [depends], callback) //require来进行加载 reuqire([module],callback)

模块定义的地方
define('amdModule',[dependencyModule1,dependencyModule2],(dependencyModule1,dependencyModule2) => { //业务逻辑 let count = 0; const increase = () => ++count; module.exports = { increase } })

引入的地方
require(['amdModule'],amdModule => { amdModule.increase() })

面试题:如果在AMDModule中想兼容已有代码,怎么办?
define('amdModule',[],require => { const dependencyModule1 = require('./dependencyModule1') const dependencyModule2 = require('./dependencyModule2') //业务逻辑 let count = 0; const increase = () => ++count; module.exports = { increase } })

面试题:手写兼容CJS&AMD
//判断的关键: 1. object还是function 2. exports ? 3. define(define('AMDModule'),[],(require,export,module) => { const dependencyModule1 = require('./dependencyModule1') const dependencyModule2 = require('./dependencyModule2')let count = 0; const increase = () => ++count; const reset = () => { count = 0; } console.log(count); export.increase = increase(); })( //目标:一次性区分CJS还是AMD typeof module === 'object' && module.exports && typeof define !== function ? //CJS factory => module.exports = factory(require,exports,module) : //AMD define )

优缺点
  • 优点:适合在浏览器中加载异步模块的方案
  • 缺点:引入成本
CMD
按需加载
主要应用框架:sea.js
define('module',(require,exports,module) => { let $ = require('jquery') let dependencyModule1 = require('./dependencyModule1') })

优缺点
  • 优点:按需加载,依赖就近
  • 缺点:依赖打包,加载逻辑存在于每个模块中,扩大了模块体积,同时功能上依赖编译
ES6模块化
新增定义:
  • 引入:import
  • 引出:export
面试:
  1. 性能 - 按需加载
// ES11原生解决方案 import('./esMModule.js').then(dynamicModule => { dynamicModule.increase(); })

【JS模块化—CJS&AMD&CMD&ES6-前端面试知识点查漏补缺】优点:
通过一种统一各端的形态,整合了js模块化的方案
缺点:本质上还是运行时分析
解决模块化新思路 - 前端工程化 遗留
根本问题:运行时进行依赖分析
解决方案:线下执行
编译时依赖处理思路
define('a', () => { let b = require('b') let c = require('c') })

完全体:webpack为核心的前端工程化 + mvvm框架的组件化 + 设计模式

    推荐阅读