闭包

作用域

作用域表示的是一个变量的可用范围,其实它是一个保存变量的对象。

函数作用域

用函数形式以function(){……}类似的代码包起来的(省略号……)区域,即函数作用域。

es6引进块级作用域

ES6规定:
在某个花括号对{ }的内部,用let关键字声明的变量和函数拥有块级作用域。
这些变量和函数只能被花括号对{ }的内部的语句使用,外部不可访问。
块级作用域和函数作用域也可以统称为局部作用域。

为什么引入块级作用域?

  1. 解决声明提前的问题
  2. var声明的变量有污染
    for循环里面的i在循环完毕后就没用了,但没被回收掉,而是一直存在的“垃圾”变量。
    而用let声明变量,事后这种垃圾变量会很快被回收掉。

什么是闭包

闭包是一个函数,它有权访问外部作用域内的变量/参数。

为什么会形成闭包

存在对上级作用域内变量的引用。
在JS中,变量的作用域属于函数作用域。
在函数执行后,作用域就会被清理、内存被回收。
但是由于闭包是建立在一个函数内部的子函数,它可访问上级作用域,即使上级函数执行完毕,作用域也不会随之销毁,这时的子函数(也即闭包),便拥有了访问上级作用域中的变量的权限。

应用场景

各种回调: 一个Ajax请求的成功回调,一个事件绑定的回调方法,一个setTimeout的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。

无论何种方式,
只要是: 对函数类型的值进行传递,当函数在别处被调用时都有闭包的身影

举例说明

一、reutrn 一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dog(age) {
var name = "Bob";
var age = age;
<!-- addAge() 就是一个闭包,存在对上级作用域的引用-->
return function addAge() {
age++;
console.log(name,age);
}
}

let dogA = Dog(8);
for(var i=0;i<3;i++) {
dogA();
}

二、循环赋值

1
2
3
4
5
6
7
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}

因为存在闭包,形成了10个互不干扰的私有作用域,上面能依次输出0~9。
若将外层的自执行函数去掉,就不存在外部作用域的引用了,输出的结果就是连续的 10。
连续输出10,因为 JS 是单线程的,
遇到异步的代码不会先执行(会入栈),等到同步的代码执行完( i++ 到 10时),异步代码才开始执行此时的 i=10 输出的都是 10。

上述问题用let也可解决,let具有块级作用域,形成了10个互不干扰的私有作用域

1
2
3
4
5
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}

三、函数防抖思路

例如连续点击按钮,只有最后一次点击完成,才执行回调函数。
<button onclick="foo()">foo按钮</button>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明防抖函数debounce
function debounce(fn, delay = 500) {
let timer;
console.log('立即执行debounce,参数是', fn);
return function closure() {
if (timer) clearTimeout(timer);
timer = setTimeout(fn, delay);
console.log('timer号码', timer);
}
}

// 注意!!!给变量foo赋的值是debounce的函数执行结果,
// 因为debounce是一个函数,现在后面加上()和实参,就是在执行。
const foo = debounce(() => {
console.log('foo按钮的最后一次点击');
})

闭包体现在:
foo按钮绑定的foo变量,指向一个函数,即debounce防抖函数执行的结果,也就是closure函数。
当后面再点击foo按钮时,不会再执行debounce函数,而是执行变量foo指向的closure函数。
而closure函数操作的timer要从它的上级作用域,即debounce的作用域中查找,也就形成了闭包。
timer变量如果放在内部函数(即closure函数),那么每次调用都会创建一个新的timer变量,无法实现防抖。
timer变量放在上级作用域,这样closure函数每次操作的timer是同一个timer,
即debounce中的timer。

在全局声明timer也可以做到,但利用闭包实现有好处:
多个按钮复用debounce函数时,会为每个按钮创建函数作用域,每个函数作用域内的timer互不干扰。
声明全局变量时,命名不重复,才能不相互影响。

四、函数节流

1
2
3
4
5
6
7
8
9
10
11
12
// 节流
function throttle(fn, timeout) {
let timer = null;
return function (...arg) {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arg)
timer = null
}, timeout)
}
}

无论是实现防抖还是节流,都需要利用闭包的特性,来记录timer的状态,让每一次事件触发时,操作的是同一个定时器,以此来实现防抖、节流功能。

五、点击事件后的回调函数需要传参

1
<button id="test">test double</button>
1
2
3
4
5
6
7
8
9
10
11
function Test(value) {
return function() {
<!-- 在这个闭包函数内,处理参数 -->
let sum = Number(value) + Number(value);
console.log(sum);
}

}

$("#test").on('click',Test(10));

六、封装变量

利用闭包定义的函数可以访问父级函数的私有变量和私有函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var counter = (function(){
var privateCounter = 0; //私有变量
function change(val){
privateCounter += val;
}
return {
increment:function(){ //三个闭包共享一个词法环境
change(1);
},
decrement:function(){
change(-1);
},
value:function(){
return privateCounter;
}
};
})();
counter.value();//0
counter.increment();
counter.increment();//2

创建一个匿名函数,并立即执行。
函数体内定义局部变量,局部函数。
匿名函数返回一个对象,其中包含了三个函数,这三个函数可以访问和改变局部变量。
counter指向匿名函数的返回值,即包含了三个公共函数的对象(在内存中的地址)。
这样就只能通过暴露的三个公共函数来访问匿名函数内部的局部变量。

作用

保护函数的私有变量不受外部干扰,当希望重用一个对象,又想保护对象不被随意篡改时。

闭包的缺点

过度使用闭包会导致内存占用过多,容易内存泄漏。
因为JS在进行垃圾回收时,不会将它引用的上级作用域(即父函数)的变量释放掉,内存不会被回收。
可以手动置空闭包函数。
例如:

1
2
3
4
5
6
7
8
var counter = (!function(){
var num = 0;
return function(){ return ++num; }
}())
var n = counter(); // 传递函数类型的值
n(); // 调用传递的函数, 形成闭包
n();
n = null; // 置空闭包本身,而不是释放闭包内部的变量
查看评论