js闭包专题

150 阅读8分钟

什么是闭包?

闭包回答:

1:闭包具有封闭性不对外公开的结构,在js中函数可以构成闭包,根据作用域链规则 函数访问只允许外部数据,外部不能访问内部数据

2:闭包了解决外界允许访问,可以间接的通过一个函数返回新的函数或者对象的方法

代码举例

image.png

image.png

总结

闭包的实质是因为函数嵌套而形成的作用域链
比如说:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包
用途:使用闭包主要是为了设计私有的方法和变量
优点:可以避免变量被全局变量污染
缺点:函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
解决方法:在退出函数之前,将不使用的局部变量全部删除

答案:闭包是一个函数,什么函数呢?一个定义在函数内部的函数。例如:

var add = (function () {
    var counter = 0;
    return function () {
        return counter += 1;
    }
})();

复制代码在上面的例子中,函数内部return的这个函数function () {return counter += 1;},它就是一个闭包。我们直接把add在控制台打印出来:

我们可以看到,在这里全局定义的这个变量add,它实际上是一个函数,但是这个函数却是在另一个函数内部创建的,这个就是定义在函数内部的函数。

image.png

闭包有什么作用?

1、闭包可以访问到局部变量

即我们要解读这句话:能够读取其他函数内部变量的函数。
我们知道,在JavaScript中,有全局变量和局部变量。顾名思义,全局变量它是公有的、共享的,程序内的所有函数都可以直接调用这个全局变量,即它的作用域是全局性的;而局部变量,在函数内部声明的变量,它是私有的,只在函数内部起作用,在该函数之外的地方是无法被调用的,它的作用域是局部性的。

function add() {
    var counter = 0;
    return counter += 1;
}
console.log(add()); // 1
console.log(add()); // 1
console.log(add()); // 1

// 本意是想输出 3, 但事与愿违,输出的都是 1
复制代码以上代码将无法正确输出,每次调用add()函数,计数器都会设置为1。可以将它写成闭包的形式,使counter = 0只执行一次,并返回函数表达式,然后得到我们想要输出的结果。

function add() {
    var counter = 0;
    return function () {
        return counter += 1;
    }
}
var closure = add();
console.log(closure()); // 1
console.log(closure()); // 2
console.log(closure()); // 3

闭包它可以访问函数上一层作用域的局部变量,它使得函数拥有私有变量变成可能。

2、闭包可以保护里面的局部变量,使它们不会随着函数的结束而销毁

百度百科里面还有这么一段话:“闭包”一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。 这句话含有一个意思,就是在闭包内部的变量它是不会被销毁的,会一直存在内存中,并且不会受到外界的干扰。在上面的例子中counter的值就是这样受到闭包的保护,只能通过closure方法去修改。

使用闭包需要注意的点

1、闭包会使得函数中的变量都被保存在内存中,会增大内存的使用量,所以不能滥用闭包,否则会造成网页的性能问题,在IE9中可能会导致内存泄露。但这个导致内存泄露是IE早期的BUG,闭包本身并不会导致内存泄露。

因为,在JavaScript的垃圾自动回收机制中有这么一句话:从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。因为进入闭包的变量是有可能继续使用的,即它们不会被自动回收,这个时候需要我们手动回收清除。

2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。 最后,推荐观看下面关于闭包的这篇文章: developer.mozilla.org/zh-CN/docs/…

例子1

image.png

例子2

image.png

function foo () {
  var a = 2
  function bar () {
    console.log(a)
  }
  return bar
}
var baz = foo()
baz() //2
function test () {
  var result = new Array()
  for (var i = 0; i < 6; i++) {
    result[i] = function () {
      return i
    }
  }
   console.log(result)
  return result
}
test () //

微信图片_20210313171015.png

拓展

function test () {
  var result = new Array()
  for (var i = 0; i < 6; i++) {
    let a=function () {
      return i
    }
    result[i] = a()
  }
  return result

}
 test ()

image.png

例子:****

image.png

常见闭包场景:

判断公式:这个局部变量,是否直接 or 间接被 window 对象引用(牵连)?

常见案例分析:

1.定时器回调函数
(function(){
      var num = 0;      
      setInterval(function(){ //注意setInterval的回调函数,是注册在window下的
          console.log(++num)
      },500)
})()

//1.setInterval的回调函数 被 window 引用,所以不能回收
//2.回调函数执行引用匿名函数的num,所以匿名函数的局部变量对象,不能回收

2.事件处理函数
      var num = 0;
      document.onclick = function(){ //注意事件处理函数,注册在 window 下  
        console.log(++num)
      }
})()

num会同上述情况一样,无法被回收

3.利用闭包写法保存 变量 i
var arr1 = [] 
var arr2 = [] 

for(var i=0;i<10;i++){
    //不使用不闭包
    arr1[i] = ()=>{
        console.log(i) //这里的 i,在函数执行时,访问的是 10(循环条件i<10不满足,最后的保留值)
    }
    
    //使用闭包
    arr2[i] = ((i)=>{ //这里的 i 保存在 匿名函数的局部变量对象中
                 
        return ()=>{
            console.log(i) //访问 局部变量对象 保存的 i
        }    
    })(i)

}

arr1.forEach((f)=>{console.log(f())}) // 都是 10 ,因为访问的是全局的i,都指向一个值

arr2.forEach((f)=>{console.log(f())}) // 0~9 ,每次执行,访问的是自身所处作用域 专属的 i

关于闭包优化:

避开全局引用牵连: 在日常开发过程中,尽量避免把函数局部变量赋值给全局 window
主动释放局部变量: 对于已知体积较大的局部变量,必要时设置 null 清空其内存占用

    var responseData = [{}, ...] //例如这个变量存储了10万条数据
    var num = 0;    
                        
    setInterval(function(){ 
          console.log(++num)
          
          if(num === 8){//在确定业务代码完成任务,不再需要 responseDate 时
            responseData = null //解除内存占用
          }
    },500)

})()

回顾什么闭包

image.png

回顾闭包的作用

  • 可以读取函数内部的变量。看上面代码就是 c 可以操作 i
  • 让这些变量的值始终保持在内存中。i 一直在内存里
  • 避免变量污染全局
  • 把变量存到独立的作用域,作为私有成员存在
  • 模拟私有方法,模块封装

回顾闭包缺点

  • 增大内存消耗。闭包里的引用无法被垃圾回收,增大内存用量,使用不当会导致内存泄漏。
  • 对处理速度有影响。闭包的层级决定了引用在查找时经过的作用

回顾应用场景

应用场景一

典型应用是模块封装,在各模块规范出现之前,都是用这样的方法防止变量污染全局。

  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

对于模块封装有两个要点:

  1. 使用立即执行函数包裹,该函数必须被至少调用一次
  2. 必须返回至少一个函数形成闭包

回顾应用场景二

在循环中创建闭包,防止取到意外的值

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

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

破解前端面试闭包

题一

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

console.log(new Date, i);

只要你对 JS 中同步和异步代码的区别、变量作用域、闭包等概念有正确的理解,就知道正确答案是 C,代码的实际输出是:

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 5了,所以会输出一堆 5。

2017-03-18T00:43:45.873Z 5 2017-03-18T00:43:46.866Z 5 2017-03-18T00:43:46.868Z 5 2017-03-18T00:43:46.868Z 5 2017-03-18T00:43:46.868Z 5 2017-03-18T00:43:46.868Z 5

拓展1

接下来我会追问:如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?会有下面两种答案: 正确答案B

A. 60% 的人会描述为:5 -> 5 -> 5 -> 5 -> 5,即每个 5 之间都有 1 秒的时间间隔;
B. 40% 的人会描述为:5 -> 5,5,5,5,5,即第 1 个 5 直接输出,1 秒之后,输出 5 个 5;

这就要求候选人对 JS 中的定时器工作机制非常熟悉,循环执行过程中,几乎同时设置了 5 个定时器,一般情况下,这些定时器都会在 1 秒之后触发,而循环完的输出是立即执行的,显而易见,正确的描述是 B。

拓展2

如果这道题仅仅是考察候选人对 JS 异步代码、变量作用域的理解,局限性未免太大,接下来我会追问,如果期望代码的输出变成:5 -> 0,1,2,3,4,该怎么改造代码?

for (var i = 0; i < 5; i++) {
   //立即执行函数
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i); // 5

巧妙的利用 IIFE(Immediately Invoked Function Expression:声明即执行的函数表达式)来解决闭包造成的问题,确实是不错的思路,但是初学者可能并不觉得这样的代码很好懂,至少笔者初入门的时候这里琢磨了一会儿才真正理解。

有没有更符合直觉的做法?答案是有,我们只需要对循环体稍做手脚,让负责输出的那段代码能拿到每次循环的 i 值即可。该怎么做呢?利用 JS 中基本类型(Primitive Type)的参数传递是按值传递(Pass by Value)的特征,不难改造出下面的代码:

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);

拓展三 for 闭包结合es6 promise 方法

接着上文继续追问:如果期望代码的输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5,并且要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?新的需求可以精确的描述为:代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5(这里使用大概,是为了避免钻牛角尖的同学陷进去,因为 JS 中的定时器触发时机有可能是不确定的,具体可参见 How Javascript Timers Work)。

粗暴的方法(不友好)

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);

promise的方法(推荐的方法)

const array=[];
for(var i=0;i<5;i++){
 ((j)=>{
     let promise= new Promise((resolve)=>{
       setTimeout(()=>{
        console.log(new Date,j)
        resolve()
       },1000*j)
     })
     array.push(promise)
 })(i)
}

Promise.all(array).then((res)=>{
  setTimeout(()=>{
    console.log(new Date,i)
  },1000)
})

拓展四,结合es7 sync await 实现

,既然 Promise 已经被拿下,如何使用 ES7 中的 async await 特性来让这段代码变的更简洁?你是否能够根据自己目前掌握的知识给出答案?请在这里暂停 1 分钟,思考下。

    // 模拟其他语言中的 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);
    })();

经典回顾闭包面试题:

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

解决办法两种,第一种使用闭包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

第二种就是使用 setTimeout 的第三个参数

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}

第三种就是使用 let 定义 i 了

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

因为对于 let 来说,他会创建一个块级作用域,相当于

{ // 形成块级作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}