目录
什么是闭包?
闭包的产生
闭包中的变量
自动形成的闭包与内存占用
手动形成闭包
闭包的优点/特点
对闭包的小结:
闭包经典面试题一
闭包基础面试题二
闭包经典面试题三
闭包经典面试题四
真题解答
需要先掌握的知识
1.javascript中的作用域与作用域链
2.Javascript中的垃圾回收
闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包。
而作用域链,正是实现闭包的手段。
闭包的产生:执行函数时,使用了函数外部的数据,闭包就产生了。
function a(){
var i = 10;
console.log(i);
}
a();
此时不会产生闭包,因为此时a函数没有使用外部数据,对于a函数来说,i在自己的函数作用域中。
var i = 10;
function a(){
console.log(i);
}
a();
此时,会产生闭包,因为a函数在自己的作用域中中找不到i 这个变量了,引用了外部数据,就会创建闭包。通过作用域链可知,它会顺着作用域链一层一层的往上找。
i就会被放到闭包中。
var i = 10;
function A(){
var j = 20;
var k = 30;
function b(){
var x = 40;
var y = 50;
function c(){
console.log(i, j, k, x);
}
c();
}
b();
}
A();
被放入闭包中的变量有:i , j, k,x;y没有被放进去,因为y没有被引用。
这里一共创建了三个闭包:
全局闭包里面存储了i的值,
闭包a中存储了变量j, k,
闭包b中存储了变量x,
通过javascript的垃圾回收可知:只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
一个数据要不要放入闭包取决于该变量有没有被引用。
只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
那么产生一个新的问题:那么多闭包,那岂不是占用内存空间么?
实际上,如果是自动形成的闭包,是会被销毁掉的。
var i = 10;
function A(){
var j = 20;
var k = 30;
function b(){
var x = 40;
var y = 50;
function c(){
console.log(i, j, k, x);
}
c();
}
b();
}
A();
console.log(k);
尝试打印输出变量 k,显然这个时候是会报错的,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。
function eat(){
var food = "鸡翅";
console.log(food);
}
eat(); // 鸡翅
console.log(food); // 报错
我们声明了一个eat函数,并对它进行调用。
JavaScript引擎会创建一个eat函数的执行上下文,并且给food进行赋值
当eat方法执行完后,上下文被销毁,food变量也会跟着消失,因为food是eat函数的局部变量,它作用于eat函数,会随着eat的创建而创建,销毁而销毁。所以当我们再次打印 food 变量时,就会报错,告诉我们该变量不存在。
手动形成闭包:
function eat(){
var food = '鸡翅';
return function(){
console.log(food);
}
}
var look = eat();
look(); // 鸡翅
look(); // 鸡翅
eat返回一个函数,并在在这个函数内部访问food这个局部变量。
调用eat函数,将其结果赋值给look,look指向eat的内部函数,然后调用它,并最终输出food的值。
这里之所可以访问到food,是因为垃圾回收器只回收没有被引用的变量,但是一旦一个变量被引用着,垃圾回收器就不会回收此变量。
在上面示例中,eat函数执行完,照理说应该被销毁,但是向外部返回了eat内部的匿名函数,而这个匿名函数中又引用food,所以垃圾回收器不会对food变量进行回收
通过此特性,我们可以解决一个全局变量污染的问题, 早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量 有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题。
var name = "GlobalName";
// 全局变量
var init = (function () {
var name = "initName";
function callName() {
console.log(name);
// 打印 name
}
return function () {
callName();
// 形成接口
}
}());
init(); // initName
var initSuper = (function () {
var name = "initSuperName";
function callName() {
console.log(name);
// 打印 name
}
return function () {
callName();
// 形成接口
}
}());
initSuper(); // initSuperName
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
//请写出打印的结果:三个4
setTimeout()
的意思是设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
setTimeout
函数会优先执行之前的事件,最后再执行后续的事件。而之前的事件是i
循环,显然得等i
循环执行完毕再执行打印出i
的这个函数,当i
等于3时,因为小于等于3会再执行一次,直到i
等于4,由于闭包的原因,所以它们仍然能访问到变量 i,不过此时 i 变量值已经是 4 了。一共执行了3次,根据前面所诉会打出3个4。
要解决这个问题,我们可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:
for (var i = 1; i <= 3; i++) {
(function (index) {
setTimeout(function () {
console.log(index);
}, 1000);
})(i)
}
当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字。
它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解。
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
function fun(){
var count = 1;
return function (){
console.log("jsfhjd")
console.log(count);
console.log(count++);
console.log(count);
}
}
fun();
var fun2 = fun();
fun2();
console.log("sjfhe");
fun2();
fun2();
fun();这里什么都不会打印
var fun2 = fun(); fun2指向fun内部的匿名函数
执行fun2函数时,就会调用这个匿名函数,就会执行,会打印出jsfhjd,1,1,2
console.log("sjfhe");打印出sjfhe
fun2();jsfhjd,2,2,3
fun2();jsfhjd,3,3,4
第二次执行闭包起作用,第一次为被销毁
我们可以看出只要count这个变量一直被使用(或者说后续会被使用到),所以不会被回收,我理解的是:这个变量不再使用了,就会被垃圾回收器回收,释放内存。
- 公司简介
- 联系我们
- 营销网络
打印的结果为:kzh, 0,kzh,1,kzh,2——>点击之后打印3,jhg,
因为i是贯穿整个作用域的,而不是给每一个li分配一个i,点击事件使异步,用户一定是在for运行完了以后,才点击,此时i已经变成3了。
那么怎么解决这个问题呢,可以用立即执行函数,给每个li创建一个独立的作用域,在立即执行函数执行的时候,i的值从0到2,对应三个立即执行函数,这3个立即执行函数里边的j分别是0,1,2所以就能正常输出了,看下边例子:
- 公司简介
- 联系我们
- 营销网络
打印结果:kzh,jh,0,kzh,jh,1,kzh,jh,2——>点击事件:0,1,2
function fun(n,o){
console.log(o);
return{
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
var b = fun(0).fun(1).fun(2).fun(3)
var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
输出结果为:
undefined,0,0,0
undefined,0,1,2
undefined,0,1,1
解析:函数内部有函数就形成了闭包
fun(0);输出o为undefined:fun(0)只传一个参数赋值给n,o输出为undefined {n = 0}
a部分:
a.fun(1):a.fun代表内部函数的fun,执行内部函数,m = 1=>n,n=>o从内部向外部执行,n获取上次闭包值为0对应o,输出 o = 0 {m = 1}
a是返回对象 fun:function(m){ return fun(m,n) },闭包保存在a 中,闭包又是用来存储介质对的,认为key是n,存储的值是0,也就是说a 里面有一个闭包n值为0,所以每次传值不论传什么值进去,都是复制给m了,但通过闭包n值始终不变,n=0恒成立,所以输出为:undefined,0,0,0
b部分:
由上述推导知:fun(0) = undefined
fun(0).fun(1)相当与a.fun(1),结果为0,返回值是个对象,n = 0
fun(0).fun(1).fun(2),返回值是个新的对象,内部存储的闭包也是新的,和上面不同,n = 1
fun(0).fun(1).fun(2).fun(3),n = 2
总结:
当前传入的参数为多少并不重要,重要的是上一步给闭包中传入的key为多少
c部分:
输出依次:undefined/0/1/1
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
使用闭包可以解决一个全局变量污染的问题。
如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。