一.什么是闭包
1.产生闭包的条件
①函数嵌套
②内部函数引用了外部函数的数据(变量\函数)
③并且调用了外部函数
例子:写一个闭包函数并写出执行顺序:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
解析:函数bar的词法作用域可以访问foo的内部作用域,并且bar在被作为返回值赋值给baz执行时,bar函数在定义时的词法作用域以外的地方被调用,依然可以访问foo函数的内部作用域变量a,这就是闭包
运行结果:
2.常见的闭包
①将函数作为了一个函数的返回值
function foo() {
var a = 2;
function fn() {
a ++;
console.log(a);
}
return fn;
}
var fun = foo();
fun();
②将函数作为实参传递给另一个函数调用
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
5 -> 5 -> 5 -> 5 -> 5,即每个 5 之间都有 1 秒的时间间隔
扩展:
场景一:如果期望代码的输出变成:5 -> 0,1,2,3,4
1.闭包,使用匿名函数:
for (var i = 0; i < 5; i++) {
(function(){
setTimeout(function() {
console.log(new Date, i);
}, 1000);
})()
}
console.log(new Date, i);
运行结果:最开始先打印line9的console.log,然后每过一秒依次打印0,1,2,3,4
2.块级作用域:
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
运行结果:即使用 ES6 块级作用域中的 let 替代了 var,但是代码在实际运行时会报错,因为最后那个输出使用的 i 在其所在的作用域中并不存在,i 只存在于循环内部。
3.按值传递
var output = function (i) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 这里传过去的 i 值被复制了
}
console.log(new Date, i);
场景二:代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5
1.简单粗暴的方法:
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000 * j)); // 这里修改 0~4 的定时器时间
})(i);
}
setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
console.log(new Date, i);
}, 1000 * i);
2.使用promise+es6:
const tasks = [];
for (var i = 0; i < 5; i++) { // 这里 i 的声明不能改成 let,如果要改该怎么做?
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, j);
resolve(); // 这里一定要 resolve,否则代码不会按预期 work
}, 1000 * j); // 定时器的超时时间逐步增加
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000); // 注意这里只需要把超时设置为 1 秒
});
3.代码整洁更加有的优化
const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve();
}, 1000 * i);
});
// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000);
});
4.使用async await
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 声明即执行的 async 函数表达式
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();
二.闭包产生条件
1.函数嵌套
function A() {
function B() {
}
}
2.内部函数引用了外部函数中的数据(属性、函数)
function A() {
var a = "a";
function B() {
console.log(a);
}
}
3.执行外部函数(也可理解为定义内部函数)
function A() {
var a = "a";
function B() {
console.log(a);
}
}
A();
三.闭包的生命周期
产生:内部函数被定义执行时
死亡:内部函数不再被调用(成为垃圾对象)
四.闭包的优缺点
1.缺点:
(1)占用过多内存
由于闭包会携带包含它的函数的作用域,因此会比其他正常的函数占用更多的内存。mmp,比如相同体型的人,我比别人多一个啤酒肚,重量不重才怪。所以慎重使用闭包。
(2)闭包只能取到包含任何变量的最后一个值(重要)
function foo() {
var arr = new Array()
for (var i = 0; i < 10; i++) {
arr[i] = function(){
return i
};
}
return arr
}
var fn = foo()
for(var i = 0 ; i < fn.length ; i ++){
console.log(fn[i]())
}
上面的代码看上去,循环的每个函数都应该返回自己的索引值,即0 1 2 3 4 5 6 7 8 9 。但实际上确返回了10个10。
原因如下:
每个函数的作用域链中都保存了foo()函数的活动对象,所以,其实他们都引用了同一个变量 i,结果当foo()返回后,i的值为10,10被保存了下来,于是每个函数都引用着这个值为10的变量i,结果就如上面代码所示了。
解决的方法:
方法一、创建另一个匿名函数,强制达到预期效果:
function foo() {
var arr = new Array()
for (var i = 0; i < 10; i++) {
arr[i] = function(num){
return function(){
return num
}
}(i);
}
return arr
}
var fn = foo()
for(var i = 0 ; i < fn.length ; i ++){
console.log(fn[i]())
}
代码及运行截图:
如上面添加的代码,这里没有将闭包直接赋值给数组,而是定义了一个匿名函数,并将匿名函数的结果传给数组,在调用匿名函数的时候传入了i,由于函数是按值传递的,循环的每一个i的当前值都会复制给参数num,然后在匿名函数function(num)的内部,又创建并返回了一个访问num的闭包
。最终,arr数组中的每一个函数都有一个对应的num的副本,就可以返回各自不同的值了。。。。
这种说法好像不好理解,说直白一点,就是把每个i的值都赋给num,然后把所有的num的值放到数组中返回。避免了闭包只取到i的最后一个值得情况的发生。
方法二、使用let
因为es5没有块级作用域这一说法,在es6中加入了let来定义变量,使得函数拥有了块级作用域
function foo() {
var arr = new Array()
for (let i = 0; i < 10; i++) {
arr[i] = function(){
return i
};
}
return arr
}
var fn = foo()
for(var i = 0 ; i < fn.length ; i ++){
console.log(fn[i]())
}
代码及运行截图:
(3)闭包导致this对象通常指向windows
this是基于函数的执行环境绑定的,而匿名函数的执行环境具有全局性,因此this对象指向windows
解决办法,把外部作用域中的this对象保存在一个闭包也能访问到的变量里:
(4)内存泄漏
由于匿名函数的存在,导致外部环境的对象会被保存,因此所占用的内存不会被垃圾回收机制回收。
我们可以保存变量到一个副本中,然后引用该副本,最后设置为空来释放内存
注意一点:即使这样还是不能解决内存泄漏的问题,但是我们能解除其引用,确保正常回收其占用的内存
2.优点:
(1)模仿块级作用域
语法:
(function(){
//代码块
})()
例子:
function sum(num){
for(var i = 0 ; i < num ; i++){
console.log(i)
}
}
sum(5)
代码及运行截图
可以看到在for循环外还是能访问到i的
(2)在构造函数中定义特权方法,可以理解为可以用来访问私有变量的公有方法。
五.闭包的应用
利用闭包的方式得到当前li的索引号
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
闭包解决问题:
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}