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 打包,文件目录结构差不多是这样:
打包的代码生成在 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,全局都访问不到,不用担心污染全局。
把这段代码折叠起来看,就是这样,被折叠的这些逻辑会立即执行:
然后,原来的 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 需要导出的方法:
函数 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对应起来。
在外部调用函数o 和函数 n,因为闭包的原因,原来的变量 time,也就是被混淆后函数 o 和函数 n 里的变量 o,都是这两个函数的“私有变量”了,不会互相影响。
于是乎,我们就实现了各模块之间变量的隔离。
最后,打个断点看一下闭包中的这些变量吧,这些生成的变量 o、n和函数 r,都是匿名的立即执行函数闭包中的私有变量,不会影响全局。
拓展:从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 进阶的第一步。
如果我的文章对你有帮助,你的👍就是对我的最大支持^_^
你也可以关注《前端每日一问》这个专栏,防止失联哦~
我是阿林,输出洞见技术,再会!
上一篇:
下一篇: