闭包是什么
闭包是个让人很迷惑的东西,特别是初学的时候,各种类型的函数都写不明白,接触到闭包确实有些难懂,这篇文章就彻底弄清楚闭包。
前置知识:作用域和作用域链
JS有三种作用域,全局作用域、函数作用域、块级作用域(ES6)新增。这里主要讨论全局作用域和局部作用域,因为块级作用域和闭包关系不大。
浏览器环境下,全局作用域就是window.代码执行过程中,会先创建一个全局的VO,里面存放了函数的地址,比如fn(){}:0Xb00。当函数被创建的时候,会再堆中开辟一块内存空间,就是函数变量,函数变量里存放的了他的父级作用域,函数变量和VO之间形成了引用。
在内层作用域中,可以访问外层作用域的变量
所以在函数中可以访问到其父级作用域中的变量,这就形成了作用域链,定义了变量了一套访问规则。
闭包的产生场景
下面是一个最简单的闭包的例子,闭包也可以理解成一种现象。
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
}
}
```
let counter = createCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3
```
一句话说明白:内部的匿名函数可以访问到createCounter函数的私有(局部)变量count,而由于JS中函数是一等公民可以作为参数被返回,外面通过调用createCounter函数可以拿到内部函数,从而通过内部函数来实现对createCounter函数的私有(局部)变量的访问!就实现了在函数作用域外部访问函数内部变量。
几个经典问题的探讨
1.闭包造成内存泄漏
下面以outer作为外部函数,inner作为内部函数来描述。
闭包造成内存泄漏的原因是因为全局VO对象存在对inner函数的函数对象的引用(这里不懂的可以补一下VO,AO的知识),而inner函数的父级作用域与outer函数的AO对象形成了循环引用,导致两者都无法被释放,即outer函数的私有变量和inner函数看起来造成内存泄漏。
但是这就造成了内存泄漏吗?因为外面在接收到了outer函数,后续可能需要连续使用outer函数,这是外面的需求,但是一旦后续不再需要通过inner函数获取outer函数的私有变量,记得将接收内部函数的变量置为null,不然就会造成内存泄漏,所以说滥用闭包会造成内存泄漏!
2.闭包有“记忆性”并且接收到的函数相互独立
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
let counter1 = createCounter();
let counter2 = createCounter();
console.log(counter1()); // 输出: 1
console.log(counter1()); // 输出: 2
console.log(counter2()); // 输出: 1
上面的代码有两个意思:
1.接收到inner函数的参数可以反复调用,count会改变,有记忆性。
2.如果用两个不同的参数接收inner函数,那么他们的调用彼此独立。
闭包的使用场景
1.模拟私有变量与方法
let counter = (function () {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
})();
console.log(counter.value()); // 输出: 0
counter.increment();
counter.increment();
console.log(counter.value()); // 输出: 2
counter.decrement();
console.log(counter.value()); // 输出: 1
在这个例子中,privateCounter变量和changeBy函数都是在立即执行函数表达式(IIFE)内部声明的,所以它们只能在该函数内部访问。但是,我们可以通过返回一个对象,该对象包含一些方法来访问和修改这些私有变量和方法。
柯里化(实现逻辑的复用)
function curry(fn) {
if (typeof fn !== "function") {
throw new Error("error");
}
function fn1(...args1) {
if (fn.length <= args1.length) {
return fn.apply(this, args1);
} else {
function fn2(...args2) {
return fn1.apply(this, args1.concat(args2));
}
return fn2;
}
}
return fn1;
}
function sum(a, b, c) {
return a + b + c;
}
let curried = curry(sum);
console.log(curried(1, 2)(3));
console.log(curried(1)(2)(3));
console.log(curried(1, 2, 3));`
上面是柯里化的实现方案,主要就是为了实现逻辑的复用,这个要会。