深入理解闭包

闭包的定义

MDN 对闭包的定义:

  • 闭包: 能够访问自由变量的函数
  • 自由变量: 既不是函数参数也不是函数的局部变量

根据上面的例子,举一个例子:

var str = 'xiaoqi';
function getName() {
	console.log(str);	
}

getName()函数可以返回str这个变量,但str即不是getName函数的局部变量,也不是foo函数的参数,所以str就是自由变量。

这样函数getName就是一个闭包。

是不是觉得跟平时见到的闭包不一样?其实,从技术的角度上看,上面的例子就是一个闭包。

你是否要问:任何函数是否都有闭包,包括在全局范围内创建的函数?答案是肯定的。在全局范围中创建的函数也会创建一个闭包。但由于这些函数是在全局范围内创建的,因此它们可以访问全局范围内的所有变量,就无所谓闭包不闭包了。

实践上的闭包

上面说的是理论上的闭包,现在说一说实践角度的闭包:

我们通常见到的闭包:当内部函数被保存到外部时,才会真正涉及闭包,返回的函数可以访问自由变量。即做到下面两点:

  • 即使创建它的上下文已经销毁,它依然存在(比如从父函数返回的子函数)
  • 在代码中引用了自由变量

来看一个例子:

var str = 'xiaoqi';
function check() {
	var str = 'hhh';
	function getName() {
		console.log(str);
	}
	return getName;
}
var foo = check();
foo();

分析一下它的简要执行过程:

首先说明:首先,函数定义可以存储在变量中,函数定义在被调用之前对程序是不可见的; 其次每次调用函数时,就会临时创建一个本地执行上下文,当函数执行结束时,执行上下文就被销毁。

  • 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  • 全局执行上下文初始化
  • 执行check函数,创建check函数执行上下文,check函数执行上下文压入执行上下文栈
  • 定义getName函数时,保存了check函数的作用域链
  • check函数执行完毕,check函数执行上下文销毁
  • 执行getName函数,创建getName函数执行上下文,getName函数执行上下文压入执行上下文栈
  • getName函数初始化,创建变量对象等
  • getName函数执行完毕,getName函数执行上下文弹出

在getName函数执行时,check函数执行上下文已经被销毁,但因为getName保存了check函数的作用域链, 所以getName函数依然可以读到在check中定义的str(hhh)这个变量。

案例总结:上面的函数getName就是一个实践角度的闭包。其创建它的执行上下文销毁之后,它依然存在,并且它访问自由变量str(既不是getName中参数,也不是其局部变量)。

考题

了解闭包之后,我们来看一看相关考题:

题目1
var data = [];

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

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

上面提到过,函数在定义时,对程序是不可见的,当for循环执行之后,只会定义data[0]、data[1]、data[2]这些函数。
当执行这些函数时,才会创建执行上下文,会沿着作用域链向上访问,其作用域链是:

  Scope: [AO, globalContext.VO]

从全局作用域中访问到i。此时的i已经是3。所以结果都是3。

更改上面的例子,使用立即执行函数解决闭包问题(用闭包解决闭包)

var data = [];

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

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

更改之后,执行data[0]、data[1]、data[2]函数时,它们的作用域链:

 Scope: [AO, 匿名函数Context.AO globalContext.VO]

所以先会去匿名函数中查找,匿名函数的执行上下文:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        x: 0  
    }

在data[1]、data[2]中,其匿名执行函数的执行上下文的x分别是1、2。

所以最后的结果就是0、1、2

题目2

写出下面代码的执行过程,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?

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

console.log(new Date, i);

这段代码考察了异步、作用域、闭包。

  • for循环里设置了5个定时器,定时器是异步执行,循环之后的console.log(new Date, i);是同步执行,最先输出的应该就是console.log(new Date, i);这个同步代码。输出5。
  • 同步任务执行之后,开始执行异步任务,而这5个定时器都会在1秒之后触发。所以结果就是5->5,5,5,5,5
题目2- - -变式1

如果想让上述结果变为5->0,1,2,3,4,,应该如何改造代码? 出现上面的结果,就是由于闭产生的问题,我们也可以使用立即执行函数解决闭包问题。

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

console.log(new Date, i);

增补:有同学可能会想到es6的let ,这样该代码:

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

console.log(new Date, i);

使用let代替var,这样i只在本轮循环中有效,但是,最后console.log(new Date, i);输出的i不能访问,let只在自己的块级作用域中有效。代码最终会报错。

题目2 - - -变式2

如果将代码输出变成0 -> 1 -> 2 -> 3 -> 4 -> 5,并要求原有代码块中的循环和两处console.log不变,应该怎么改造代码?

我们可以使用es6中Promise方法。

const tasks = [];
for(var i = 0;i < 5;i++) {
	(function (j){
	     tasks.push(new Promise(function(resolve, reject) {
				    setTimeout(function(){
					    console.log(new Date, j);
					    resolve();
				},1000*j)   
		}))
	}(i))
}
Promise.all(tasks).then(function() {
        setTimeout(function(){
        console.log(new Date, i);
    },1000);
})
  • 定义一个数组用来存放Promise对象。

  • for循环中每一轮循环都会用立即执行函数push 一个Promise对象。每个promise对象里面有一个定时器,并且每一个定时器的延时时间都乘以相应的j值。

  • 将生成的5个Promise对象传给Promise.all(),这个方法会等传入的所有Promise对象状态都变成resolved后,在执行then方法。那么最后的定时器的延时只需要设置为1s。

  • 最后结果就是0 -> 1 -> 2 -> 3 -> 4 -> 5

题目2 - - -变式3

如果要使用async/await特性来更改上面的代码,该如何改?

const sleep = function(delayMs) {
	new Promise(function(resolve, reject) {
        setTimeout(resolve, delayMs);
    })
}
async function asyncPrint() {
	for(var i = 0;i < 5; i++){
		if(i > 0){
			await sleep(1000);
		}		
        console.log(new Date, i);
	}
	await sleep(1000);
	console.log(new Date, i);
};
asyncPrint();

在这里插入图片描述

题目3

下面的输出代码是什么:

function fun(n,o){
    console.log(o);
    return {
        fun:function(m){ 
            return fun(m,n); 
        }
    }
}

var a=fun(0); // undefined
a.fun(1);// 0 
a.fun(2); // 0
a.fun(3); // 0
var b=fun(0).fun(1).fun(2).fun(3);// undefined 0  1 2 
var c=fun(0).fun(1);// undefined 0
c.fun(2);// 1
c.fun(3);// 1

参考文章:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值