一、闭包是什么
含义
闭包指的是:能够访问另一个函数作用域的变量的函数。清晰的讲:闭包就是一个函数,这个函数能够访问其他函数的作用域中的变量。
function outer() {
var a = '变量1'
var inner = function () {
console.info(a)
}
return inner // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}
实际上,闭包是站在作用域的角度上来定义的,因为inner访问到outer作用域的变量,所以inner就是一个闭包函数。
创建方式
在一个函数内创建另一个函数(只要使用了回调函数,实际上就是在使用闭包)。
目的
主要是为了设计私有的方法和变量。
优点
可以避免全局变量的污染。
缺点
- 闭包会常驻内存(闭包会携带包含它的函数的作用域),会增大内存使用量,使用不当很容易造成内存泄露。
- 闭包只能取得包含函数中任何变量的最后一个值。
特性
- 函数嵌套函数;
- 函数内部可以引用外部的参数和变量;
- 参数和变量不会被垃圾回收机制回收;
- 在执行环境中,闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域;
- 当函数返回了一个闭包时(闭包保存的是整个变量对象),该函数的执行环境的作用域链会被销毁,但是这个函数中的活动对象将会一直保存到闭包不存在为止。
- 闭包只能取得包含函数中任何变量的最后一个值。
二、闭包例子
第一个例子
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(i);
5
5,5,5,5,5
这个也是网上非常经典的一道题目了,可以这么去理解这道题,setTimeout 他存储的是一个函数地址,等到1000毫秒后,再真正去访问这个存储的函数,这个函数的作用域里没有找到变量i,所以会往上去查找,一层层去寻找这个变量。在这个例子中,i最后是5,所以console.log()出来的都是5。
把这个例子做个变型,便于更好的理解
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
console.log(i);
i = 10;
5
10,10,10,10,10
等到setTimeout里面的函数执行的时候,i已经变成了10,所以输出的5个数都是10
第二个例子
在上个例子中,我们知道setTimeout里输出的 i 之所以是错的,因为他在函数作用域里没找到变量i,所以要往上一层寻找,然而往上一层的变量i,在执行for循环之后已经变成了5。如果想防止这种情况出现,那么就要在存储函数的时候,给他在当前作用域中传入变量i,下面是传入变量的是三种方式。
输出 5 -> 0,1,2,3,4
第一种方式
for (var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
console.log(i);
第二种方式
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
console.log(i);
第三种方式
var output = function (i) {
setTimeout(function() {
console.log(i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i);
}
console.log(i);
第三个例子
输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5
和前两个例子不同的是,这次要再输出5
暴力解决
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000 * j);
})(i);
}
setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
console.log(i);
}, 1000 * i);
ES6 Promise
const tasks = [];
for (var i = 0; i < 5; i++) {
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(j);
resolve(); // 这里一定要 resolve,否则代码不会按预期 work
}, 1000 * j); // 定时器的超时时间逐步增加
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i);
}, 1000); // 注意这里只需要把超时设置为 1 秒
});
ES6 Promise 的模块化
const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(i);
resolve();
}, 1000 * i);
});
// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i);
}, 1000);
});
ES7 的 async/await
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 声明即执行的 async 函数表达式
for (var i = 0; i < 5; i++) {
if (i > 0) {
await sleep(1000);
}
console.log(i);
}
await sleep(1000);
console.log(i);
})();
三、在实际场景中使用
setTimeout
原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。
function f1(a) {
function f2() {
console.log(a);
}
return f2;
}
var fun = f1(10);
setTimeout(fun,1000); // 10
回调
定义行为,然后把它关联到某个用户事件上(点击或者按键)。代码通常会作为一个回调(事件触发时调用的函数)绑定到事件。
function changeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size12 = changeSize(12);
var size14 = changeSize(20);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-12').onclick = size14;
可以对应改变字体大小。
函数防抖
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
实现的关键就在于setTimeOut这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现。
/*
* fn [function] 需要防抖的函数
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timer){
clearTimeout(timer) //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
timer = setTimeOut(fn,delay)
}else{
timer = setTimeOut(fn,delay) // 进入该分支说明当前并没有在计时,那么就开始一个计时
}
}
}
封装私有变量
function f1() {
var value = 1;
var obj = {
getValue: function () {
return value;
},
addVlaue: function () {
value++;
}
};
return obj;
}
let result = f1();
console.log(result.getValue());//1
result.addVlaue();
console.log(result.getValue());//2
在返回的对象中,实现了一个闭包,该闭包携带了局部变量x,并且,从外部代码根本无法访问到变量x。
四、闭包常见的坑
内存泄漏
function showId() {
var el = document.getElementById("app")
el.onclick = function(){
aler(el.id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
}
}
// 改成下面
function showId() {
var el = document.getElementById("app")
var id = el.id
el.onclick = function(){
aler(id)
}
el = null // 主动释放el
}
引用的变量可能发生变化
function outer() {
var result = []
for (var i = 0;i<10;i++){
result[i] = function () {
console.info(i)
}
}
return result
}
this的指向
var object = {
name: "object",
getName: function() {
return function() {
console.info(this.name)
}
}
}
object.getName()() // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows
递归调用
这种写法会报错
function factorial(num) {
if(num<= 1) {
return 1
} else {
return num * factorial(num-1)
}
}
var anotherFactorial = factorial
factorial = null;
anotherFactorial(4);
这种写法就没问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial
var newFactorial = (function f(num){
if(num<1) {return 1}
else {
return num* f(num-1)
}
})
var anotherFactorial = newFactorial
newFactorial = null;
console.log(anotherFactorial(4)); // 24
参考链接