全球共有10多个国家和地区1000多个平台,20万+商户使用
闭包?什么是闭包--JavaScript前端
作者: 何坤隆 发布时间: 2023-05-13 811 分类专栏: 前端开发
由于js中只有两种作用域,全局作用域和函数作用域(模块作用域和块级作用域的原理也是匿名函数作用域实现的),而在开发场景下,将变量暴露在全局作用域下的时候,是一件非常危险的事情,特别是在团队协同开发的时候,变量的值会被无意篡改,并且极难调试分析。这样的情况下,闭包将变量封装在局部的函数作用域中,是一种非常合适的做法,这样规避掉了被其他代码干扰的情况。
下面是一种最简单直接的闭包示例
//妈妈本体
function mother(){
//口袋里的总钱数
let money = 100
//消费行为
return function (pay){
//返回剩余钱数
return money - pay
}
}
//为儿子消费
let payForSon = mother()
//打印最后的剩余钱数
console.log(payForSon(5))
为了便于理解,我们将外部函数比喻为妈妈本体,里面保存着总钱数这个变量和消费这个行为,通过创建为儿子消费的这个行为对象,然后执行这个行为花费5元,返回剩余的95元。
这个就是为了将变量money保存在mother本体内而避免暴露在外部的全局环境作用域中,只能通过mother()创建消费行为来影响money这个变量。
由此可以归纳总结使用闭包的三个步骤
目的是为了形成一个专属的变量,只在专属的作用域中操作。
上述的闭包代码示例中,有一个缺陷的场景是,在后续不需要money变量的情况下,没有释放该变量,造成内存泄露。原因是payForSon这个函数的作用域链引用着money对象,解决的办法是将payForSon = null就可以释放方法作用域,进而解除对money的引用,最后释放money变量。
在开发的场景中,有时需要通过闭包来实现函数的柯里化调用。调用示例如下
alert(add(1)(2)(3))
这种连续的传参调用函数,叫做函数柯里化。
通过闭包的实现方式如下
function add(a){
//保存第一个参数
let sum = a
function tmp(b){
//从第二个函数开始递加
sum = sum + b
//返回tmp,让后续可以继续传参执行
return tmp
}
tmp.toString = function(){
return sum
}
//返回加法函数
return tmp
}
alert(add(1)(2)(3))
下面我们来一步步分析,
该例子的demo代码在我的github上,可以自行取阅
需求:在一个4*4的矩阵方块中,实现点击每个按钮时记录下各自的点击次数,相互之间互不干扰。
思路:在按钮事件中使用闭包,创建独立的存储变量空间。
注意:下列的方案1到方案3是逐次演进的优化方案,需要按照方案标号的次序逐层理解,更有利于理解最终的优化方案
<div id="container"></div>
...
let container = document.getElementById('container')
for (let r = 0; r < arr.length; r++) {
for (let c = 0; c < arr[r].length; c++) {
let cell = document.createElement('div')
cell.innerHTML = `(${r},${c})`
container.append(cell)
cell.onclick = (function () {
let n = 0
return function () {
n++
cell.innerHTML = `点${n}`
}
})()
}
}
在每个按钮上通过onclick绑定闭包方法,存储操作独立的n变量,这样就可以单独记录每个按钮的点击次数
缺点:这样做有一个不足的地方是,外部无法获取内部的n变量,不能实现与外部的交互,比如按钮间的相互影响。
为了改善方案1的缺点,我们引入外部数据arr来操作管控按钮点击数。 代码示例如下:
let arr = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
]
let container = document.getElementById('container')
for (let r = 0; r < arr.length; r++) {
for (let c = 0; c < arr[r].length; c++) {
let cell = document.createElement('div')
cell.innerHTML = `(${r},${c})`
container.append(cell)
cell.onclick = (function (r, c) {
return function () {
arr[r][c]++
cell.innerHTML = `点${arr[r][c]}`
}
})(r, c)
}
}
参照方案1 ,改动点包含两个
这样改进完以后,外部可以通过操作arr来与每个按钮的点击次数进行交互。
缺点:这样会将arr暴露在全局作用域下(可以在console控制台访问到),很容易被其他人或者模块误操作,也不利于封装
基于方案2的改进实现为,用一个立即执行的函数包裹住整个执行代码,这样就构建了一个函数作用域来封装arr变量为私有。代码如下:
(function () {
let arr = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
]
let container = document.getElementById('container')
for (let r = 0; r < arr.length; r++) {
for (let c = 0; c < arr[r].length; c++) {
let cell = document.createElement('div')
cell.innerHTML = `(${r},${c})`
container.append(cell)
cell.onclick = (function (r, c) {
return function () {
arr[r][c]++
cell.innerHTML = `点${arr[r][c]}`
}
})(r, c)
}
}
})()
这样一个相对完整的按钮点击次数的方案就完成了。
这个需要有call和bind的使用知识的前提,可以自行百度哈
废话不多说,直接上代码
Function.prototype.bind = function(obj){
console.log('调用自定义bind函数');
//保存当前函数对象
let fun = this
//去除第一个obj参数,并且转换为js数组
let outerArg = Array.prototype.slice.call(arguments,1)
return function(){
//将arguments转为js数组
let innerArg = Array.prototype.slice.call(arguments)
//汇总所有参数
let totalArg = outerArg.concat(innerArg)
//调用外部保存的函数,并且传参
fun.call(obj,...totalArg)
}
}
//调用示例
let zhangsan = {name:'wawawa'}
function total(s1,s2){
console.log(this.name + s1 + s2);
}
let bindTotal = total.bind(zhangsan,100)
bindTotal(200)
重写函数类的bind函数,
注意:
何坤隆
小当家ISV,重庆APP开发,小程序开发,软件系统开发 地址:重庆市南岸区南坪万达广场写字楼2栋19-6 联系电话:023-81361879
ICP备案号:渝ICP备15003473-1 增值电信业务许可证: 渝B2-15003473 渝公网安备 50010802005103号
友情链接: APP定制开发 小程序定制开发 MagicShop商城系统 酒类行业解决方案
重庆小当家互联网信息技术有限公司