闭包

日期:2020年07月22日 阅读次数:2528 分类:javascript

一、什么是闭包?

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

1、函数嵌套函数
2、内部函数可以引用外部函数的参数和变量
3、参数和变量不会被垃圾回收机制所收回(即:变量长期驻扎在内存当中)

垃圾回收机制

function aaa () {
  var a = 1;
}
aaa();
// 当函数aaa执行后,函数内的a变量就会被垃圾回收机制收回。节约内存。

闭包中的情况

function aaa(){
  var a = 5;
  function bbb () {
    alert(a);
  }
  return bbb;
}
var c = aaa();    //aaa返回的就是bbb,所以现在c就是bbb
c();              //这里c()就相当于是bbb()。

// 因为闭包中参数和变量不会被垃圾收回机制所收回,所以这里函数bbb中的a是5。最终会弹出5。

【包中的参数和变量不会被垃圾回收机制所收回的原因】:
当外部函数被执行完后,外部函数的参数和变量还在内部函数中在使用,所以不会被垃圾回收机制所收回。

二、闭包与作用域链

想要理解闭包,先要了解作用域链至关重要。

闭包只能取得包含函数中任何变量的最后一个值。如下例:

例一:

function fn () {
  var result = new Array();

  for (var i=0; i<10; i++) {
    result[i] = function () {
      return i;
    }
  }

  return result;
}

var arr = fn();

arr[0]() // 这里返回的是10,而不是0
arr[1]() // 这里返回的是10,而不是1

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值。但实际上,每个函数返回的都是10。

【解析】因为每个函数的作用域链中都保存着fn()函数的活动对象(可以理解为是包含了fn的局部作用域变量的对象)。也就是每个函数中返回的i其实就是fn局部作用域中的变量i。因为fn执行完成后,i已经是10。所以所有函数返回的都是10。

我们可以再创建另一个匿名函数来让闭包的行为符合预期。如下例

例二:

function fn () {
  var result = new Array();

  for (var i=0; i<10; i++) {
    result[i] = (function (n) {
      return function () {
        return n;
      }
    })(i)
  }

  return result;
}

var arr = fn();

arr[0]() // 这里返回的是10,而不是0
arr[1]() // 这里返回的是10,而不是1

【解析】
这时每个函数的作用域链中都保存着1、fn()函数的活动对象;2、自执行的匿名函数的活动对象。
而每个函数返回的n就是自执行的匿名函数的入参,而每个函数对应的匿名函数的实参就是当时匿名函数执行时的对应的索引。所以结果是符合预期的。

总结

以上两个例子的区别在于作用域链不同

例一:数组中的每个函数的返回结果指向的是fn中的i,而i在fn执行完成后已经是10。
例二:数组中的每个函数挂架的结果指向的是fn执行过程中for循环中对应匿名函数中的参数n,而循环过程中每个实参刚好是对应的索引。

三、闭包有什么好处?

1、希望一个变量长期驻扎在内存当中。
2、私有成员的存在(成员私有化)
3、避免全局变量的污染。?

四、应用在哪里?

4.1、模块化代码

正常情况下尽量减少全局变量的出现,所以可以使用闭包的方法来把全局变量改成局部变量。

如下是一个传统的闭包。先执行一次外部函数,然后把返回值赋给一个新变量去执行。

function aaa () {
  var a = 1;
  return function () {
    a++;
    alert(a);
  }
}
var b = aaa();

b();//2
b();//3

可以改写如下:

var aaa = (function () {
  var a = 1;
  return function () {
    a++;
    alert(a);
  }
})();

aaa();//2
aaa();//3
// 这时aaa就是return返回的匿名函数(即:内部函数)

//把一个【函数声明】放在()中,就成了【函数表达式】(函数声明和函数表达式的区别见对应笔记)
// 这边形式也叫模块化代码

4.2、在循环中直接找到对应元素的索引

var aLi = document.getElementsByTagName('li');

for(var i=0;i<aLi.length;i++){
  aLi[i].onclick = function () {
    alert(i);
  }
}
// 想要的效果是弹出对应的索引值,实际都是aLi.length的值。比如有3个li,那就是全弹2.
// 原因是,i是全局变量,当点击的时候,for已经结束,i的值已经是2

用闭包解决一

var aLi = document.getElementsByTagName('li');

for(var i=0;i<aLi.length;i++){
  (function(i){
    aLi[i].onclick = function () {
      alert(i);
    }
  })(i)
}

用闭包解决二

var aLi = document.getElementsByTagName('li');

for(var i=0;i<aLi.length;i++){
  aLi[i].onclick = (function (i) {
    return function (){
      alert(i);
    }
  })(i)
}

用let解决

var aLi = document.getElementsByTagName('li');

for(let i=0;i<aLi.length;i++){
  aLi[i].onclick = function () {
    alert(i);
  }
}

let支持块级作用域。上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是各自对应的索引值。

五、闭包需要注意的地方

1、因变量会长期驻扎在内存,会产生内存的浪费

2、IE下会引发内存泄漏(在浏览器关闭前一直驻扎在内存中,无法释放)

window.onload = function() {
  var oDiv = document.getElementById('div1');

  oDiv.onclick = function() {
    alert(oDiv.id);
  }
  //以上代码会发现内存泄漏
  //触发条件:事件触发的函数中引用外部的内容时。

  //以下是解决方法
  //onunload 事件在用户退出页面时发生。
  window.onunload = function() {
    oDiv.onclick = null;
  }
}

//或者改成如下
window.onload = function() {
  var oDiv = document.getElementById('div1');
  var id = oDiv.id;//外部先定义一个变量

  oDiv.onclick = function() {
    alert(id);//然后内部使用这个变量
  }

  oDiv = null; //设置为null
}
文章标签: