前端攻城狮查缺补漏系列-闭包

79 阅读4分钟

闭包

前言

看此篇前,请确保对作用域有一定的了解,不了解的异步至 作用域

闭包的概念

有权访问 另一个函数作用域 中变量的 函数,也就是个内部函数,我们可以在函数中或者 {} 中定义一个函数来创建一个闭包。

闭包的表现形式

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:55for循环
结束后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

实现1for (var i=0; i<5; i++>) {
  (function(j){
    setTimeout(function(){
      console.log(j)
    })
  })(i)
}
console.log(i) // 5 -> 0,1,2,3,4
实现2var 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)
})