闭包
前言
看此篇前,请确保对作用域有一定的了解,不了解的异步至 作用域
闭包的概念
有权访问 另一个函数作用域 中变量的 函数,也就是个内部函数,我们可以在函数中或者 {} 中定义一个函数来创建一个闭包。
闭包的表现形式
1:闭包是一个函数,而且是存放在另一个函数中。
2:闭包可以访问父级函数作用域的变量,并且这些被引用的变量不会被销毁。
下面这段代码,能体现上面说的两点。
functon animal () {
var age = 1
function cat () {
age ++
console.log(age)
}
return cat
}
//这里的newCat对象实际就是 animal函数返回的 cat函数。
var newCat = animal();
newCat()
// 执行newCat()输出2,没有问题,执行cat的时候,
//cat函数中没有定义age,于是通过作用域链找到了父函数作用域的age,并做了递加操作。
newCat()
//再执行newCat()输出3,问题来了,按道理来说,cat函数被执行时,找不到age变量就会往父级函数找,
//找到age等于1、递加后应该输出2呀。
> 这里就体现了闭包的核心功能,上一步执行后闭包内部的age变量并没有被销毁,
再执行cat函数时就不会往父级函数中查找了。
age等于2。所以递加后输出3
// 道理同上
newCat() // 4
闭包的原理
闭包的实现原理,其实是利用了作用域链的特性,作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条
闭包的优点
1:隐藏变量,避免全局污染
var age = 18;
function cat(){
age++;
console.log(age);// cat函数内输出age,该作用域没有,则向外层寻找,结果找到了,输出[19];
}
cat();//19
cat();//20
cat();//21
cat();//22
可以看到,age的值一直递增,如果程序还有其他函数,也需要用到age的值,则会受到影响,而且全局变量还容易被人修改,比较不安全,这就是全局变量容易污染的原因。下面使用闭包将变量封装起来:
var age = 8 //全局变量
functon animal() {
var age = 18 //局部变量
function cat () {
age++
console.log(age);
}
return cat
}
var newCat = animal();
newCat(); //19
newCat(); //20
newCat(); //21
newCat(); //22
console.log(age) // 18
这样每次调用不在经过局部变量age的初始值,这样就可以一直增加了,而且局部变量age在函数内部,不易修改和外泄,不会影响全部变量age,相对来说比较安全。
2:可以读取函数内部的变量,使用该特性可以缓存数据结果
当我们想把经过计算得到的结果缓存起来时,全局保存容易污染,可以使用闭包的原理封装缓存数据的方法。如下:
var stack = (function () {
var cache = {};
return {
get: function(key){
if (cache[key]) {
return cache[key]
}
},
set: function(key,data) {
cache[key] = data
}
}
})()
stack.set('data',{name:'张三',result: true})
stack.get('data') // {name:'张三',result: true}
闭包的缺点
不恰当的使用闭包可能会造成内存泄漏的问题,JS垃圾回收机制规定在一个函数作用域内,程序执行完以后变量就会被销毁,这样可节省内存使用闭包时,按照作用域链的特点,闭包(函数)外面的变量不会被销毁,因为函数会一直被调用,所以一直存在,如果闭包使用过多会造成内存销毁。
闭包金典面试题
下面代码输出了什么?
下面代码暂时不牵扯闭包
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
//Thu Oct 15 2020 13:46:55 GMT+0800 (中国标准时间) 5 这里打印的是最下面的console。
//Thu Oct 15 2020 13:46:56 GMT+0800 (中国标准时间) 5
//Thu Oct 15 2020 13:46:56 GMT+0800 (中国标准时间) 5
//Thu Oct 15 2020 13:46:56 GMT+0800 (中国标准时间) 5
//Thu Oct 15 2020 13:46:56 GMT+0800 (中国标准时间) 5
//Thu Oct 15 2020 13:46:56 GMT+0800 (中国标准时间) 5
输出原因简单概述:
for循环在极短的时间完成了循环操作并创建了5个异步setTimeout,待1秒后执行,
此时代码开始执行下面的console输出了当前时间Thu Oct 15 2020 13:46:55和for循环
结束后i的值(此时i变成了5,原因是js没有块级作用域,for循环执行完后i变量依然存在值为5)。
1秒后输出Thu Oct 15 2020 13:46:56和变量i(此时i变成了5,
原因是js没有块级作用域,for循环执行完后i变量依然存在值为5)
以上面代码为基础,使用闭包继续改造,需求是:代码执行立即输出i,1秒后输出,0,1,2,3,4
实现1:
for (var i=0; i<5; i++>) {
(function(j){
setTimeout(function(){
console.log(j)
})
})(i)
}
console.log(i) // 5 -> 0,1,2,3,4
实现2:
var output = function(i) {
setTimeout(function(){
cosnole.log(i)
})
}
for (var i=0; i<5; i++>) {
output(i)
}
console.log(i) // 5 -> 0,1,2,3,4
实现3:使用es6 let,此方法不完全解决问题,let申明了块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i); // undefined(此时的作用域访问不了i) -> 0,1,2,3,4
继续改造。需求:代码执行时,立即输出 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)
}, j*1000)
})(i)
}
setTimeout(function(){
console.log(new Date,i)
},5000)
实现2:使用ES6 promise 一下就骚起来了
var eventStack = []
for (var i = 0; i < 5; i++) { //创建5个异步,并保持到eventStack中
((j)=>{
var event = new Promise((resolve,reject) => {
setTimeout( () => {
console.log(new Date,j)
resolve();
}, j*1000)
})
eventStack.push(event)
})(i)
}
Promise.all(eventStack).then(()=>{ 使用.all方法,5个异步全部执行后回调
setTimeout(()=>{
console.log(new Date,i)
}, 1000)
})