闭包是什么?五分钟带你了解闭包

闭包

前言

闭包对每个前端来说都是一个绕不开的话题。学习之初也因为搞清闭包的概念耗费了不少精力,今天写一篇博客来记录本人对闭包的理解,笔者水平有限,若有疏漏及错误,愿不吝赐教。

什么是闭包?

你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的的表达式(通常是函数,因为ES6有了块级作用域的概念)。
可以先点击上面链接查看MDN对闭包的讲述。

闭包在JavaScript高级程序设计(第3版)中是这样描述:闭包是指有权访问另一个函数作用域中的变量的函数。

这样闭包的作用就很明显了

  • 可以在函数的外部访问到函数内部的局部变量。
  • 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。

我们来写一个简单的例子:

console.log('closure');
function add() {
    
    
  var count = 0;
  return function() {
    
    
    count += 1;
    return count;
  };
}
var a = add();
function fun() {
    
    
  alert(a());
}
fun();
fun();

我们来解释一下这段代码:
定义了add() 函数,函数内定义了count,返回值也是一个函数。子匿名函数引用外部变量count,因此形成了闭包。
当下面我们定义并赋值a = add()时,add()函数执行且仅执行一次。执行后a被赋值(匿名函数)了
上面代码中function() { count += 1; return count; };部分就形成了一个闭包。引用了函数外部变量count。因此count在函数执行结束也不会销毁,一直存在于内存中。然后每次调用a()函数时,返回的都是内层匿名函数(闭包)的执行结果,因为内层匿名函数在外层函数的内部,所以即使外层函数执行完了,第二个函数依然能访问count(JS设计的作用域链,当前作用域能访问上级的作用域)。

闭包经典案例

该案例是牛客网题库的一道题:

   <ul>
      <li>i</li>
      <li>i</li>
      <li>i</li>
      <li>i</li>
    </ul>

js实现:要求点击每个li标签,弹出对应的索引。(0,1,2,3)
实现1:

var elements = document.getElementsByTagName('li');
for (var i = 0; i < elements.length; i++) {
    
    
  elements[i].onclick = function () {
    
    
    console.log(i);
    alert(i);
  };
}

获取dom元素,进行遍历不就可以了吗?但是当我们自信的点击标签时候发现弹出都是4……。整个人都觉得不好了!

ps:有人疑惑不应该都是3吗?为什么是4? 这是因为for循环的执行顺序都是以i++结束的,最后一次不进入循环。但是var i 是在全局中的,因此i会被赋值为4.具体for循环知识可以自行搜索这里不做赘述。

下面是这个题目的解答:
每个li标签的onclick事件执行时,本身onclick绑定的function的作用域中没有变量i,i为undefined,则解析引擎会寻找父级作用域,发现父级作用域中有i,且for循环绑定事件结束后,i已经赋值为4,所以每个li标签的onclick事件执行时,alert的都是父作用域中的i,也就是4。这是作用域的问题。这里onclick事件绑定的函数形成闭包,但引用的变量i是全局的。
闭包真正的含义是,如果一个函数访问了此函数的父级及父级以上的作用域变量,就可以称这个函数是一个闭包。

这里其实是一个作用域问题,闭包中引用的变量不会消失,所以我们把每次的i都保存到变量中,就可以实现想要的结果。

for (let i = 0; i < elements.length; i++) {
    
    
  elements[i].onclick = function () {
    
    
    return (function (n) {
    
    
      console.log(n);
      alert(n);
    })(i);
  };
}

在这里插入图片描述
这样我们就使用闭包将i作为参数传递到匿名函数(闭包)中,每个点击事件都有了一个局部的num变量,保存的即相应的i值。

拓展:
我们可以直接将for循环的var改为let就可以实现想要的效果。let的到来,让在循环体内创建闭包具有了更好的可读性。可搜索学习let和const的相关知识。或亲自动手,将let通过babel编译查看相关结果进行研读。
有关let的知识点会在以后的博文中进行整理。

闭包造成的内存泄漏问题

闭包怎么引起内存泄漏?谁会存在内存泄漏?
我们借助垃圾回收机制的标记清除法来看一下:
示例:

function fn () {
    
    
	let count = 1
	function fun () {
    
    
		count ++
		console.log(`函数内部调用${
      
      count}`)
	}
	return fun
}
const result = fn();
result() //2
result() //3

我们来分析一下:

  1. result是一个全局变量,代码执行完毕不会立即销毁
  2. result可以找到fn()函数,fn()函数return fun(),因此 fun()也不会销毁。
  3. fun()引用到了父级作用域的count,因此,在引用的count便不会被垃圾回收机制回收。
    此时,闭包就引起了内存泄漏问题
    正常情况下,一般的局部变量函数执行完就会销毁,但是我们这里的result没有销毁,所以它引用到的count也会留存在内存当中。

闭包的应用

闭包的实际应用主要是函数工厂,以及模拟私有方法。

闭包实现函数工厂

举例:

function makeAdder(x) {
    
    
  return function(y) {
    
    
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

这是MDN官网的一个makeAdder(x)函数,接受一个参数x,并返回新的函数,返回的函数接受一个参数y,并返回x+y的值。

从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

应用案例:假如我们想在页面添加几个按钮用来调整页面字号。我可以给body元素设置font-size,然后通过em单位设置页面中其他元素的字号:

body {
    
    
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}
h1 {
    
    
  font-size: 1.5em;
}
h2 {
    
    
  font-size: 1.2em;
}

然后可以对body的font-size进行调整:

function makeSizer(size) {
    
    
  return function() {
    
    
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

这样,size12,size14,size16就可以分别将body字体调整为12,14,16px,再将它们添加到点击事件上。

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

这样就实现了想要的效果。这就是一个简单的闭包实现函数工厂的案例。

闭包模拟私有方法

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

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

在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.incrementCounter.decrementCounter.value

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

备注: 你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。

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

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

备注: 以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

闭包应用部分是引用MDN的案例,大家可以移步阅读(一定要读)。

今天的总结分享就到这里,继续努力搬砖!只作个人总结所用,有不足与疏漏请评论区补充。

猜你喜欢

转载自blog.csdn.net/hanyanshuo/article/details/127382363