你越是认真生活,你的生活就会越美好
——弗兰克·劳埃德·莱特
《人生果实》经典语录
什么是闭包
MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数
。
那什么是自由变量
呢?
自由变量是指在函数中使用的
,但既不是函数参数也不是函数的局部变量的变量
。
由此,我们可以看出闭包共有两部分组成
:
闭包 = 函数 + 函数能够访问的自由变量
另一种说法如下
js的作用域
分两种全局和局部,基于作用域链相关知识
在js作用域中
访问变量的权利由内向外
,内部作用域可以获得当前作用域下的变量,也可以访问包含当前作用域的外层作用域下的变量
反之则不能,也就是说在外层作用域下无法访问内层作用域下的变量
,同样在不同的函数作用域中也是不能相互访问彼此变量的,我们想在一个函数内部也能访问另一个函数内部的变量
该怎么办呢?
闭包就是用来解决这一需求的,闭包的本质
就是在一个函数内部创建另一个函数
。
闭包有3个特性:
- 函数嵌套函数
- 函数内部可以引用函数外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
闭包的两种主要形式
函数作为返回值
function a() {
var name = 'hong';
return function() {
return name
}
}
var b = a()
b() // "hong"
在这段代码中,a()
中的返回值是一个匿名函数,这个函数在a()作用域内部
,所以它可以获取a()作用域下变量name的值
,将这个值作为返回值赋给全局作用域下的变量b
,实现了在全局变量下获取到局部变量中的变量值
再来看一个闭包的经典例子
function fn() {
var num = 3
return function() {
var n = 0;
console.log(++n)
console.log(++num)
}
}
var fn1 = fn()
fn1() // 1 4
fn1() // 1 5
一般情况下,在函数fn执行完
后,函数作用域里的变量会被销毁
,但是在这个例子中,匿名函数
作为fn的返回值
被赋值给了fn1
,这相当于fn1=function(){var n = 0 ... }
,并且匿名函数内部引用着fn函数里的变量num
,所以变量num无法被销毁
,
而变量n
是每次被调用时会重新创建,所以每次fn1
执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后剩下孤零零的num,这里产生了内存消耗
的问题
再来看一个经典例子-定时器与闭包
写一个for循环,让它按顺序打印出当前循环次数
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
console.log(i) // 5
// for循环会打印五次5
按照预期它应该依次输出0 1 2 3 4,而结果它输出了五次5,这是为什么呢?
由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行
,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i
的值也已经编程5,所以打印出来五个5,
我们为了实现预期结果应该怎么改这段代码?(ps:如果把for循环里面的var变成let,也能实现预期结果
)
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(() => {
console.log(i)
}, 100)
})(i)
}
console.log(i) // 5
// for循环依次打印 0 1 2 3 4
引入闭包来保存变量i
,将setTimeout
放入立即执行函数
中,将for循环中的循环值i
作为参数传递,100毫秒后同时打印出0 1 2 3 4
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
console.log(i) // Uncaught ReferenceError: i is not defined
// for循环依次打印 0 1 2 3 4
在ES5当中使用for循环都是采用var,而在ES6中都是采用的let,并且我们更推荐于let,
在js语言中,if和for是没有作用域
,只有function
才拥有,var
是全局变量,let
是块级作用域,const
是常量(不可改变的变量称为常量);可以把let看做成更完美的var
es5中for循环使用var定义
的,就相当于定义了一个全局的i
,每次循环i++
都可以进行改变i
的值,
var
进行变量提升和代码预解析后,var是在全局最前面的,es5的解决方案是采用闭包
;
函数具有块级作用域
,let具有块级作用域的,每次循环执行都相当于新创建了块级作用域
// 那如果我们想实现每隔100毫秒分别依次输出数字,又该怎么改呢?
for (var i = 1; i < 6; i++) {
(function(i) {
setTimeout(() => {
console.log(i)
}, i * 100)
})(i)
}
console.log(i) // 6
// for循环依次打印 1 2 3 4 5
在这段代码中,相当于同时启动5个定时器,i*100是为5个定时器分别设置了不同的时间,同时启动,但是执行时间不同,每个定时器间隔都是100毫秒,实现了每隔100毫秒就执行一次打印的效果。
闭包作为参数传递
var num = 15
var fn1 = function(x) {
if (x > num) {
console.log(x)
}
}
void function(fn2) {
var num = 100;
fn2(30)
}(fn1)
// 30
在这段代码中,函数fn1作为参数
传入立即执行函数中,在执行到fn2(30)
的时候,30作为参数
传入fn1
中,这时候if(x>num)中的num取的并不是立即执行函数中的num
,而是取创建函数的作用域中的num
,这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值15,即30>15,打印30
闭包面试题
// 分别打印出什么
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
闭包的利弊
好的地方
- 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
- 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
- 匿名自执行函数可以减少内存消耗
不好的地方
-
其中一点上面已经有体现了,就是
被引用的私有变量不能被销毁
,增大了内存消耗,造成内存泄漏
,解决方法是可以在使用完变量后手动赋值为null
; -
其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
推荐阅读
谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强