「前端每日一问(35)」webpack 模块化打包是如何用闭包实现的?

1,358 阅读7分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

本题难度:⭐ ⭐ ⭐

一般这道题的问法是:闭包有什么应用场景?

答:

实现模块化是闭包的一个应用场景。

我们用得最多的打包工具 webpack,就是用闭包实现的模块化,打包之后各个模块的变量不会相互污染。

比如,现在有一个 index.js,里面引入了两个模块 a 和 b,代码如下:

// a.js
module.exports = function funcA() {
  var time = 'funcA!' + Date.now()
  console.log(time)
}
// b.js
module.exports = function funcB() {
  var time = 'funcB!' + Date.now()
  console.log(time)
}
// index.js 里引入模块 a 和模块 b
const funcA = require('./a.js')
const funcB = require('./b.js')

var time = Date.now()
console.log('index.js' + time)

funcA()
funcB()

在模块 a,模块 b 和 index.js 里,都定义一个测试用的变量 time,来看一下打包之后这个 time 变量是如何被各个模块隔离的。

注意,time 变量之所以用 Date.now(),是为了好做演示,因为定义普通类型数据的话,webpack 打包之后变量就没了,就直接变成值了。

使用 webpack 打包,文件目录结构差不多是这样:

image.png

打包的代码生成在 dist 目录下的 main.js 中,长这个样子:

// main.js
;(() => {
  var o = {
    85: o => {
      o.exports = function () {
        var o = 'funcA!' + Date.now()
        console.log(o)
      }
    },
    326: o => {
      o.exports = function () {
        var o = 'funcB!' + Date.now()
        console.log(o)
      }
    }
  },
  n = {}
  function r (e) {
    var t = n[e]
    if (void 0 !== t) return t.exports
    var s = (n[e] = { exports: {} })
    return o[e](s, s.exports, r), s.exports
  }
  ;(() => {
    const o = r(85),
    n = r(326)
    var e = Date.now()
    console.log('index.js' + e), o(), n()
  })()
})()

首先,这块代码整体是一个立即执行函数,代码会立即执行,代码里定义的变量o、n 和函数 r,全局都访问不到,不用担心污染全局。

把这段代码折叠起来看,就是这样,被折叠的这些逻辑会立即执行:

image.png

然后,原来的 a 和 b 两个模块里的逻辑变成了方法 85 和 326,定义到了对象 o 上面。

var o = { // o 就是模块的集合
  85: o => { // a 模块,内部实现 a.js 里的逻辑,
    o.exports = function () {
      var o = 'funcA!' + Date.now() // 变量名由 time 变成 o,变短了,代码体积缩小了
      console.log(o)
    }
  },
  326: o => { // b 模块,内部实现 b.js 里的逻辑
    o.exports = function () {
      var o = 'funcB!' + Date.now()
      console.log(o)
    }
  }
},

注意这里的 85 和 326,是代码丑化混淆的结果,有两个作用:

  • 防止代码逻辑被看出
  • 减少代码体积

你可以把他们当成两个代号,85 代表了模块 a 的逻辑,326 代表了模块 b 的逻辑。

接着分析,代码块里定义了一个对象 n,一个函数 r,然后又立即执行了一段逻辑:

n = {} // 模拟 exports 对象
function r (e) { // 模拟 require 方法
  var t = n[e]
  if (void 0 !== t) return t.exports
  var s = (n[e] = { exports: {} })
  return o[e](s, s.exports, r), s.exports
}
;(() => { 
  const o = r(85), 
    n = r(326) 
  var e = Date.now() 
  console.log('index.js' + e), o(), n()
})()

对象 n 模拟了各个模块里的 exports 对象,在函数 r 里把 n 打印出来看一下,其实就是模块 a 和 模块 b 需要导出的方法:

image.png

函数 r 模拟了引入时的 require 方法。

const funcA = require('./a.js')
funcA()

相当于

const o = r(85) // 85 就是模块 a 的内容,内容放到前面的集合 o里
o()

最后的立即执行函数,立即执行 index.js 里的代码逻辑:

;(() => { // 立即执行函数,执行 index.js 里的代码逻辑
  const o = r(85), // 85 号方法,就是模块 a 里的逻辑
    n = r(326) // 326 号方法,就是模块 b 里的逻辑
  var e = Date.now() // index.js 里的逻辑,变量 time 也被混淆了。
  console.log('index.js' + e), o(), n()
})()

把原来的代码拿过来对比一下,混淆后的代码还是很直观的。

const funcA = require('./a.js')
const funcB = require('./b.js')

var time = Date.now()
console.log('index.js' + time)

funcA()
funcB()

执行混淆后的函数 o,就是执行模块 a 里的逻辑,执行混淆后的函数 n,就是执行模块 b 里的逻辑。

把函数 o 和 n 打印出来看一下,其实就是把模块 a 和模块 b里的代码逻辑抽出来,封装成函数,再放到模块集合 o 里面,与混淆后的代号85、326对应起来。

image.png

在外部调用函数o 和函数 n,因为闭包的原因,原来的变量 time,也就是被混淆后函数 o 和函数 n 里的变量 o,都是这两个函数的“私有变量”了,不会互相影响。

于是乎,我们就实现了各模块之间变量的隔离。

最后,打个断点看一下闭包中的这些变量吧,这些生成的变量 o、n和函数 r,都是匿名的立即执行函数闭包中的私有变量,不会影响全局。

image.png

拓展:从require的写法理解闭包

平时引入某某某模块时,都是用一个变量来接收,然后再执行,为啥要这么写呢?

const moduleXXX = require('xxx.js')
moduleXXX()

很简单啊,因为 require 函数返回的就是一个函数,当然要这么写咯。

一个比较简单的函数嵌套函数实现闭包,我们是这么写的:

function fn () { 
  var i = 100
  return function () {
    console.log(i)
  }
}

const tempFn = fn() // 用一个临时函数来接收,再执行这个临时函数

tempFn() // 100

和 require 的写法对比起来理解,它们俩的写法完全一样啊:

const moduleXXX = require('xxx.js')
moduleXXX()

// 这俩写法完全一样,都是用一个变量函数来接收,再执行这个函数

const tempFn = fn() 
tempFn()

与本文的模块化例子相结合,打包出来的函数 r,模拟了 require 方法,r 方法返回的就是一个函数。

var o = {
    85: () => { ... } // 模块 a 的逻辑
    326: () => { ... } // 模块 b 的逻辑
}

function r(e) {
  return o(e) // 返回 o(85)或者 o(326) 都是函数
}

;(() => {
  const o = r(85)  // 85 就是模块 a 的内容,r方法返回一个函数,用 o 来接收
  o()
  const n = r(326) // 326 就是模块 b 的内容
  n()
})()

总结一下:require 函数的实现,其实也是用到了闭包。

拓展:模块变多、引用关系变复杂的情况

模块再多,引用关系再复杂,其实打包出来的代码还是和上文的示例差不多。

比如,我们再添加一个模块 C,模块 A 会引用模块 C,再把模块 B 写得更复杂,导出多个方法:

// moduleA.js 引入模块 C
const funcC = require('./moduleC.js')

module.exports = function funcA() {
    var time = 'funcA!' + Date.now()
    console.log(time)
    funcC()
}
// moduleB.js 多导出几个方法
function funcB1() {
  var time = 'funcB1!' + Date.now()
  console.log(time)
}

function funcB2() {
  var time = 'funcB2!' + Date.now()
  console.log(time)
}

function funcB3() {
  var time = 'funcB3!' + Date.now()
  console.log(time)
}

module.exports = {
  funcB1,
  funcB2,
  funcB3
}
// moduleC.js 模块 C 不变
module.exports = function funcC() {
  const timec = Date.now()
  console.log('funcC!' + timec)
}
// index.js
const funcA = require('./moduleA.js')
const { funcB1, funcB2, funcB3 } = require('./moduleB.js')

var time = Date.now()
console.log('index.js' + time)

funcA()
funcB1()
funcB2()
funcB3()

最终打包出来的代码如下:

// main.js
;(() => {
  var n = {
      664: (n, o, c) => { // 模块 A 里,函数c(require函数)可以通过参数传递进来
        const t = c(761) // 模块 A 里执行模块 C 的逻辑
        n.exports = function () {
          var n = 'funcA!' + Date.now()
          console.log(n), t()
        }
      },
      300: n => {
        n.exports = { // 模块 B 里的多个方法,全部挂载到 n.exports 上
          funcB1: function () {
            var n = 'funcB1!' + Date.now()
            console.log(n)
          },
          funcB2: function () {
            var n = 'funcB2!' + Date.now()
            console.log(n)
          },
          funcB3: function () {
            var n = 'funcB3!' + Date.now()
            console.log(n)
          }
        }
      },
      761: n => {
        n.exports = function () {
          const n = Date.now()
          console.log('funcC!' + n)
        }
      }
    },
    o = {}
  function c (t) {
    var e = o[t]
    if (void 0 !== e) return e.exports
    var r = (o[t] = { exports: {} })
    return n[t](r, r.exports, c), r.exports
  }
  ;(() => {
    const n = c(664),
      { funcB1: o, funcB2: t, funcB3: e } = c(300)
    var r = Date.now()
    console.log('index.js' + r), n(), o(), t(), e()
  })()
})()

理解了本文的第一个例子,这个例子分析起来是没有难度的,模块再多,引用关系再复杂,核心的思想还是用闭包来模拟 require 函数。

结尾

看了这个例子,你会发现,闭包不是八股文,闭包是充斥在我们 JS 的各种应用里的,理解闭包,是迈向 JS 进阶的第一步。

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(34)」闭包和循环陷阱

下一篇:

「前端每日一问(36)」闭包在类库封装中的应用