深入理解js中的闭包

写在前边:
我们知道,当函数执行时,会形成自己的执行期上下文,并把它挂到自己的作用域链上,当函数执行完之后,它的执行期上下文就会被释放。所以,一般情况下,在函数外部访问函数内部的变量需要特殊的方法才能解决,这个特殊的方法就是闭包。

在理解闭包前,我建议你先了解下js的作用域。
理解js中的作用域

闭包的概念

闭包:闭包指的是在函数的外部可以访问函数内部的变量。函数没有被释放,整条作用域链上的局部变量都将得到保留。创建闭包的一般方法是在函数内部返回一个新函数。

通俗的理解:
闭包:顾名思义,是一个封闭的包,但是这个包露出内部的一条线,这条线就是闭包内部返回函数的作用域链,它上面挂载了这个函数以及他的所有父级函数的变量,我们可以通过这条线访问到函数内的变量,这就是闭包。

闭包的形成机制

我们知道:
当一个函数被定义时,它的scope属性会指向他的父级的scope的引用
当一个函数执行时,会形成它自己的执行期上下文(AO),并把它挂载到他的作用域(scope chain)的最顶端,它的父级的scope依次下移。
当函数执行完毕后,他自己的执行期上下文(AO)会被销毁
关于scope、AO详情见理解js中的作用域

这样,当我们在函数内部返回一个函数并在其外部被一个变量接收时,它的作用域链上存的是它的父级的作用域链,只要这个函数存在则它的作用域链就会一直存在,这样它的作用域链上的变量得不到释放,即能在函数外部访问作用域内部的变量。

为了便于理解,我们举个简单的例子:

function test(){
	var a = 100
	function b(){
		a++
		console.log(a)
	}
	return b
}
var global = 100
var c = test()
c() // 101
c() // 102 

1.当定义并执行test函数时,它的作用域链指向它的AO以及全局的GO
在这里插入图片描述
2.当函数b被返回时,b的作用域链上挂载这它的父级的作用域链,即test执行时的作用域链
在这里插入图片描述
3.此时,虽然test()函数执行完毕,它的执行期上下文被销毁(注意这里的销毁指的是指向被销毁,即图中箭头消失)。但是此时返回的函数b的scope上拥有b的父级的所有作用域链。此时,又将return 的b 赋值给c。所以,当执行c函数时,它会在b的作用域链上寻找所需要的变量,这样就实现了闭包。所以当c执行两次时,结果分别是101、102

闭包的作用

闭包的作用一般有两个:
1.可以在函数外部,使用用函数内部的变量
2.函数内部的变量不会被释放。

闭包的缺陷

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,

关于闭包的常见面试题

第一题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

输出结果都是3。为什么?

1.当执行完for循环后,此时的全局执行期上下文为

GO:{
	data:[...],
	i:3
}

2.当执行data[0]时,它产生它自己的执行期上下文(AO),此时他的作用域链为

scope:[AO,GO]

此时,它的AO上没有i变量,就向它的上一级的执行期上下文中找,即以上的GO,所以输出结果为3

其他两个执行结果同理。

当我们将其修改为闭包时,即如下代码

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (j) {
        return function(){
            console.log(j);
        }
  })(i)
}

data[0](); // 0
data[1](); // 1
data[2](); // 2

此时,他的执行结果分别为0,1,2

当执行完for循环时,此时的GO为

GO:{
	data:[...],
	i:3
}

当data[0]执行时,此时他的作用域链为

scope:[AO,匿名函数的AO,GO]

而此时匿名函数的AO为

AO:{
	i:0
}

data[0]的AO中没有变量i,所以它沿着作用域链向上寻找,找到匿名函数的AO,即此时i为0.

执行data[1],data[2]同理

第二题


for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

console.log(i);

以上这道题是在面试中经常问到的问题,那么它输出的是什么呢?相信大多数朋友都可以知道,最后他的结果为
5,5,5,5,5,5。
只要了解了js的运行机制、以及同步异步的问题,我们横容易知道第一个5是立即输出,之后的5在1s后同时输出。

那么我们将它改造为闭包。

for (var i = 0; i < 5; i++) {
	(function(i){
		setTimeout(function() {
	        console.log(i);
	    }, 1000);
	})(i) 
}
console.log(i);

它的结果为5,0,1,2,3,4
我们分析下它的作用域。

首先,先定义了5个立即执行函数,然后执行循环外部的console,此时的GO为{i:5},所以先输出5
1s后5个立即执行函数同时执行,此时定时器内部的i为其外部函数(即立即执行函数)的i,此时i分别为0,1,2,3,4,所以输出为5,0,1,2,3,4

想要那么有没有什么其它方法来改造呢?答案是有的,es6里提供了一个叫let的东西,他会形成块级作用域。

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

console.log(i);  

以上代码会报错,因为最后的i是不存在的,因为let形成了块级作用域,只在for循环内部起作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值