前言
其实这章我本来主要想写的是关于闭包的实际运用,但我发现能够和前面章节的知识点都关联起来,帮助大家更深入地理解闭包,当然,是在深刻理解作用域的基础之上。
我想,大概真正掌握一门技术,是能够将所有知识点都串联起来吧。
就像脑子里面自动构建一个很完整的知识体系。
还木有那么深的功力啦...QAQ
so今天就来做一个简单的知识串讲吧。
从闭包回归块级作用域
来看一段代码:
for(var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
预期是,每秒从开始依次递增打印出1~5。
但实际上:
每秒每次打印的都是6,打印了5个6。
乍一看,这不合理呀。
但是,前一章说过的吧。setTimeout 是个异步任务。
那么 setTimeout 的回调函数 timer 就会延迟执行。
也就是说,等到循环已经走完了,i这时候都已经等于6了,才会执行每一轮的 timer。
因为 i 在这里,是全局共享变量。
var 定义的 i 会导致变量提升。
所以才变成全局变量。
此时循环中的5个函数都在共用这个i。
然而想要达到预期效果,我们肯定需要每一轮都有自己的作用域。
那这个时候就能运用闭包的原理来解决问题了。
回顾且牢记:
当函数可以记住并访问所在的词法作用域时,就产生了闭包。
用IIFE来创建闭包:
for(var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000);
})();
}
这样每次循环的时候,依托IIFE都会立即创建并执行里面的匿名函数。
每个迭代都会生产一个新的作用域,相当于创建一个新的闭包。
然后我们用一个变量 j 将每次迭代的 i 存储起来,再在 timer 中打印出来。
这样能保证 timer 记住并访问它所在的作用域,由此形成一个闭包,而闭包会保持对这个变量 j 的引用。
利用IIFE的特性优化一下:
for(var i=1; i<=5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000);
})(i);
}
我们知道,用IIFE的目的是为了创建新的作用域。
我们也知道,IIFE自身存在很多缺陷。
那有没有什么替代IIFE的好的方案呢?
get!
块级作用域,对吧。
前面几章我们提到的块作用域,能够很好地解决这个问题。
本质上这是将一个块转换成一个可以被关闭的作用域。
for(var i=1; i<=5; i++) {
let j = i; // 用 let 声明就行啦!
setTimeout(function timer() {
console.log(j);
}, j*1000);
}
那不妨这样:
for(let i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
简直绝了。
第一次看完的时候,我直呼牛皮🥹🥹🥹
改动的最终结果很简单,但是推理过程耐人寻味呀。
从闭包到模块
你一定很好奇这里为什么会提到模块。
闭包实际上可多地方用到了呢!
举个例子:
function person() {
var age = 18;
var sex = '女';
function printAge() {
console.log(age);
}
function printSex() {
console.log(sex);
}
}
定义一个 person 函数,里面有 age 和 sex 这两个私有属性,还有 printAge 和 printSex 俩函数。
我们让这个模块暴露出去:
function person() {
var age = 18;
var sex = '女';
function printAge() {
console.log(age);
}
function printSex() {
console.log(sex);
}
return {
printAge,
printSex
};
}
var p = person();
p.printAge();
p.printSex();
我们调用内部函数,看打印效果:
可以正常打印出来!
在上面这段代码中,printAge 和 printSex 函数具有涵盖模块实例内部作用域的闭包。
小结
闭包到这里就算结束啦。
到此,《你不知道的JavaScript》(上)就已经过半了。
日后也还要继续努力~