什么是闭包
一、闭包的概念
闭包是指有权访问另一个函数作用域的变量的函数。创建闭包的常用方式,就是在一个函数内部创建另一个函数。
二、怎么实现闭包
闭包是指一个函数可以访问它定义时所在的词法作用域以及全局作用域中的变量。在JavaScript中,闭包可以通过函数嵌套和变量引用实现
function outerFun() {
let outerVar = '我在outer函数里!';
function innerFun() {
console.log(outerVar);
}
return innerFunction;
}
const innerFn = outerFun();
innerFn(); // 输出: 我在outer函数里!
在上面的代码示例中,innerFun引用了outerVar,因此JavaScript引擎会保留outerFun的作用域链,以便innerFun可以访问outerVar。
function a(){
function b(){
var bb = 666
console.log(aa); //输出:666
}
var aa = 333
return b
}
var demo = a()
demo()
在上面的代码示例中,a函数定义了一个名为aa的变量和一个名为b的函数,b函数引用了aa变量,因此JavaScript引擎会保留a函数的作用域链,b函数可以访问a函数的执行上下文,b函数内用到了外部函数a的变量aa,在a函数调用结束后该函数执行上下文会销毁,但会保留一部分留在内存中供b函数使用,这就形成了闭包。
具体来说,当内部函数引用外部函数的变量时,外部函数的作用域链将被保留在内存中,以便内部函数可以访问这些变量。
这种函数嵌套和变量共享的方式就是闭包的核心概念。当一个函数返回另一个函数时,它实际上返回了一个闭包,其中包含了原函数定义时的词法作用域和相关变量。
三、闭包的用途
1.封装私有变量
闭包可以用于封装私有变量,以防止其被外部访问和修改。封装私有变量可以一定程度上防止全局变量污染,使用闭包封装私有变量可以将这些变量限制在函数内部或模块内部,从而减少了全局变量的数量,降低了全局变量被误用或意外修改的风险。
在下面这个例子中,调用函数,输出的结果都是1,但是显然我们的代码效果是想让count每次加一的。
function addFn() {
let count = 1;
count++;
console.log(count);
}
addFn() //输出2
addFn() //输出2
addFn() //输出2
一种显而易见的方法是将count提到函数体外,作为全局变量。这么做当然是可以解决问题,但是在实际开发中,一个项目由多人共同开发时,不清楚别人定义的变量名称是什么,这么做有点冒险,而且也不建议定义全局变量,那有什么其他的办法可以解决这个问题呢?
function addFn(){
let count = 1
function a(){
count++
console.log(count);
}
return a
}
var acc = addFn()
acc () //2
acc () //3
acc () //4
答案是用闭包。上面示例代码中,addFn函数返回了一个闭包a,其中包含了count变量。由于count只在addFn函数内部定义,因此外部无法直接访问它。但是,由于a函数引用了count变量,因此count变量的值可以在闭包内部被修改和访问。这种方式可以用于封装一些私有的数据和逻辑。
2. 做缓存
函数一旦被执行完毕,其内存就会被销毁,而闭包的存在,就可以保有内部环境的作用域。
function fn1(){
var type ='JavaScript'
let tt1 = 1
const tt2 = 2
var innerBar={
getType: function(){
console.log(tt1);
return type
},
setType:function(newType){
type = newType
}
}
return innerBar
}
var bar = fn1()
console.log(bar.getType()); //输出:1 JavaScript
bar.setName('python')
console.log(bar.setType()); //输出:1 python
这里var bar = fn1() 执行完后本来应该被销毁,但是因为形成了闭包,所以导致fn1执行上下文没有被销毁干净,被引用了的变量type 、tt1 没被销毁,闭包里存放的就是变量type 、tt1,这个闭包就像是setType、getType的专属背包,setType、getType依然可以使用fn1执行上下文中的test1和type 。
3. 模块化编程(实现共有变量)
闭包还可以用于实现模块化编程。模块化编程是一种将程序拆分成小的、独立的、可重用的模块的编程风格。闭包可以用于封装模块的私有变量和方法,以便防止其被外部访问和修改。
const moduleFn = (function() {
let privateVar = '我是私有变量!';
function privateMethod() {
console.log(privateVar );
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
moduleFn.publicMethod(); // 输出: 我是私有的!
在上面的代码示例中,moduleFn 实际上是一个立即执行的匿名函数,它返回了一个包含publicMethod的对象。在函数内部,定义了一个私有变量privateVar和一个私有方法privateMethod。publicMethod是一个公共方法,它可以访问privateMethod,但是无法访问privateVariable。这种方式可以用于实现简单的模块化编程。
4、防抖、节流、函数柯里化
4.1防抖
//防抖 避免函数的重复调用 只会调用一次
function antishake(fn,wait){ //第一个参数是函数 第二个参数是毫秒值
let timer = null //声明一个变量来接收延时器 初始值为null
return function(){
clearTimeout(timer)
timer = setTimeout(() => {
fn() //调用这函数
}, wait);
}
}
let an = antishake(function(){ //用一个变量接收
console.log('555');
},2000)
document.querySelector('div').onmouseenter = ()=>{
an() //调用一次
}
4.2节流
function throttle(fn,wait){
let timer = null //节点闸
return function(){
if(timer) return //null false 不是null结果减少true 如果上传没有我就直接跳过 没有人我就上去
timer = setTimeout(() => { //上车了
fn()
timer = null //做完之后重新关闭节点闸
}, wait);
}
}
let throttle1 = throttle(()=>{
console.log('我上车了');
},2000)
document.querySelector('div').onclick = ()=>{
throttle1()
}
节流和防抖的区别
- 防抖避免重复执行 只执行一次
- 节流 减少执行次数 执行多次
4.3.函数柯里化
函数柯里化 其实就是函数颗粒化 将一个函数变成一个个颗粒可以组装,就是这个里面的多个参数 将他变成一个个的函数来传递这个参数。
简单柯里化函数:
function fnSum(a,b,c){//求和函数
return a+b+c
}
//简单柯里化 他就是使用了一个函数来改造原本的函数
function curry(fn){
return function(a){
return function(b){
return function(c){
return fn(a,b,c)
}
}
}
}
//调用 避免了多余的无用参数传递
let fnCurry = curry(fnSum) //函数
console.log(fnCurry(1)(2)(3));//6
改进后的柯里化函数:
function fnSum(a,b,c){//求和函数
return a+b+c
}
function curry1(fn){
//接收一个后面的参数 除了fn的参数
let args = Array.prototype.slice.call(arguments,1)//从下标1开始全部剪切 把Array里面的slice方法加给外部函数的arguments
return function(){
let newArg = args.concat(Array.from(arguments)) //将内部函数的参数和外部的参数合并
return fn.apply(this,newArg) //将内部函数自动指向 传入所有的参数
}
}
let fn2 = curry1(fnSum,1,2) //函数
console.log(fn2(3)); //6
最终改进的柯里化函数:可以任意组合传参 如果不满足就返回偏函数 如果满足就返回结果
function fnSum(a,b,c){//求和函数
return a+b+c
}
function curry2(fn){
//接收一个后面的参数 除了fn的参数
let args = Array.prototype.slice.call(arguments,1)
return function(){
let newArg = args.concat(Array.from(arguments)) //将内部函数的参数和外部的参数合并
if(newArg.length < fn.length){ //参数没有到三个 fn.length获取传递的函数的参数个数
return curry2.call(this,fn,...newArg) //又套了一个function 这个this指向这个function 如果没有到达会一直套这个方法
}else{
return fn.apply(this,newArg) //将内部函数自动指向 传入所有的参数
}
}
}
let fn3 = curry2(fnSum) //函数
console.log(fn3(1)()(2)()(3)); //6
console.log(fn3()(1)()(2)()()); //偏函数 function
四、闭包的缺点
闭包也存在着一个潜在的问题,由于闭包会引用外部函数的变量,但是这些变量在外部函数执行完毕后没有被释放,那么这些变量会一直存在于内存中,总的内存大小不变,但是可用内存空间变小了。 一旦形成闭包,只有在页面关闭后,闭包占用的内存才会被回收,就会造成的内存泄漏。
因此我们在使用闭包时需要特别注意内存泄漏的问题,可以用以下两种方法解决内存泄露问题:
- 1.及时释放闭包:手动调用闭包函数,并将其返回值赋值为null,这样可以让闭包中的变量及时被垃圾回收器回收。
- 2.使用立即执行函数:在创建闭包时,将需要保留的变量传递给一个立即执行函数,并将这些变量作为参数传递给闭包函数,这样可以保留所需的变量,而不会导致其他变量的内存泄漏。