1-4 JS 模块化详解

101 阅读2分钟

背景

JS 最开始的定位为:简单的页面设计 - 简单动画 + 基本的表格提交(1995年网景耗时2周开发出来的)

并无模块化或命名空间的概念

后面前端发展越来越复杂,就对 JS 提出了“模块”的要求

模块化的阶段

幼年期:无模块化

通过多个 JS 文件来处理,写多个<script src="xxx"/> 来强行分隔

<!DOCTYPE html>
<html>
  <head>
    <title>xxx</title>
  </head>
  <body>
    <script src="jquery.js"></script>
    <script src="tool.js"></script>
    <script src="main.js"></script>
  </body>
</html>

存在的问题:

  • 污染全局作用域
    • 因为都在同一个 HTML 里面引入,将污染全局作用域,存在变量名冲突等问题
    • 不利于大型项目开发与多人团队共建

成长期:雏形-IIFE

IIFE:立即执行函数(语法侧优化),是模块化的基石

优势:作用域的封装,因为是函数所以有自己的作用域

(function(_w){
  console.log(_w) // 打印出来的是:传入的 Window 对象
})(window)

使用 IIFE 实现一个简单的模块

const module = (function(){
  let count = 0
  
  // 主流程功能
  const add = () => count++
  const reset = () => count = 0
  const get = () => count

  // 对外暴露接口
  const returnApi = {
    add,
    reset,
    get
  }

  return returnApi
})()

// module 为 {add: ƒ, reset: ƒ, get: ƒ}

成熟期:模块化爆发

CJS - CommonJS

来自于服务端(Nodejs)定义的模块化加载和导出规范(同步加载)

// 导出 exports
// tool.js
exports.add = (num) => num + 1
exports.reset = () => 0

// 加载 require
// main.js
const tool = require('./tool.js')
tool.add(5)
tool.reset()

其中通过require导入,通过exportsmodule.exports导出

其中的关系为:

require引入的是module.exports导出的

正常情况下exports === module.exports // true

异常情况

// 导出 exports
// tool.js
exports.add = (num) => num + 1
exports.reset = () => 0
module.exports = {
  log(num){
    console.log(num)
  }
}

// 加载 require
// main.js
const tool = require('./tool.js')
tool.add(5) // 报错
tool.reset() // 报错
tool.log(77) // 正常

优点:

  • 同步加载,易于理解代码执行

缺点:

  • 同步加载大量文件时,会阻塞
  • 因为是同步加载,所以无法按需异步加载
  • 没有语言层面的支持: CommonJS 不是 JavaScript 的语言层面的特性,而是一种规范。相比之下,ES6 模块是语言层面的特性,得到了更好的集成和支持。

AMD - Asynchronous Module Definition(异步模块定义)

针对浏览器端的模块加载规范,支持异步加载,不阻塞页面渲染

著名的框架为 require.js 

// 定义模块
define('moduleA',['dep1', 'dep2'], function(dep1, dep2){
  return {
    log(){
      dep1.a++
      dep2.b++
      console.log(dep1.a)
      console.log(dep2.b)
    }
  }
})

// 使用模块
require(['moduleA'], function(moduleA) {
  moduleA.log()
})

优点:

  • 异步加载
  • 提高了模块化开发

缺点:

  • 语法繁琐
  • 需要显示声明依赖

UMD - Universal Module Definition(通用模块定义)

UMD 是兼容多种模式(CJS/AMD 等)的模块加载器

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
      define(['dependency'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
      module.exports = factory(require('dependency'));
  } else {
      // 浏览器全局变量
      root.MyModule = factory(root.Dependency);
  }
}(this, function (Dependency) {
  // 模块的实际定义
  function MyModule() {
      // ...
  }

  // ...

  return MyModule;
}));

CMD - Common Module Definition(同步模块加载)

强调模块的加载与使用是同时的

著名框架 sea.js

define('module1', (require, exports, module) => {
	let $ = require('jquery.js') // 模块加载

  // $('.name').style.xxx 模块使用

  const getDom = (selector) => $(selector)

  module.exports = { getDom } // 模块 API 的暴露
})

优点:

  • 对依赖的加载可控,能达到“按需加载”,依赖就近

缺点:

  • 不支持异步加载

新时代:官方支持

ESM - ES6 module

ES6 提供的模块加载机制

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// main.js
import { add, subtract } from './math';

console.log(add(3, 4));      // 输出 7
console.log(subtract(8, 5)); // 输出 3
  1. 导出: 使用export关键字导出模块的功能。
  2. 导入: 使用import关键字导入其他模块的功能。
  3. 命名空间导入: 使用import * as aliasName from 'module';来导入整个模块的命名空间。
  4. 默认导出: 使用export default来指定一个模块的默认导出,可以使用import moduleName from 'module'; 进行导入。
  5. 动态导入: 使用import()来动态加载模块,返回一个 Promise

优点:

  • 模块文件有自己的作用域,不会污染全局
  • 默认使用严格模式

缺点:

  • 兼容性,低版本浏览器不一定兼容

模块化的目的

  • 隔离逻辑与作用域
  • 扩展协同的方便度

最终形成万物皆模块,作为前端工程化的基石

其他知识

严格模式

作用:使 JS 更安全,减少不确定性

使用:'use strict'

特点:

  1. 禁止使用未声明的变量: 在严格模式下,如果使用未声明的变量,将抛出 ReferenceError。
  2. this 的值为 undefined: 在严格模式下,如果函数不是作为对象的方法调用,this 的值将为 undefined。

调用栈

使用new Error().stack可以获取

function logCaller() {
    console.log(new Error().stack);
}

function outer() {
    logCaller();
}

outer();

// 打印结果:
// Error
//     at logCaller (<anonymous>:2:17)
//     at outer (<anonymous>:6:5)
//     at <anonymous>:9:1

组件库搭建所需

面试题

1. script 标签的参数:async、defer

都是用于控制脚本执行时机

  1. 加载行为:
    • async 和 defer 都不会阻塞页面渲染,允许页面继续加载。
  1. 执行时机:
    • async:脚本加载完成后立即执行,与页面加载和其他脚本执行顺序无关。
    • defer:按照它们在页面上出现的顺序执行,但会在文档解析完成后、DOMContentLoaded 事件触发前执行。
  1. 依赖关系:
    • async:适用于相互独立、无依赖关系的脚本。
    • defer:适用于有顺序依赖关系的脚本。

2. JQuery 源码-依赖处理

IFEE + 传参调配

(function(window, undefined ) {
   // 用一个函数域包起来,就是所谓的沙箱
   // 在这里边var定义的变量,属于这个函数域内的局部变量,避免污染全局
   // 把当前沙箱需要的外部变量通过函数参数引入进来
   // 只要保证参数对内提供的接口的一致性,你还可以随意替换传进来的这个参数
  "use strict";
  window.jQuery = window.$ = jQuery;
})( window );

3. 一行代码如何兼容 AMD、CJS?

AMD 关键:define

CJS 关键:module.exports

(factory => {
  
})(typeof module === 'objetc' && module.exports && typeof define === 'undefined' ?
cjsFactory : amdFactory)