你对闭包怎么理解

一、什么是闭包?
一个函数和对其周围状态( lexical environment ,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包( closure )。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。 – 摘自 MDN
大白话简单理解闭包 : 能够访问其他函数内部变量的函数,被称为闭包。
下面我们通过代码来直观的理解下 :

function closure() {
    // name 是一个被 makeFunc 创建的局部变量
    var name = 'Mozilla';
    // displayName() 是内部函数,一个闭包
    function displayName() {
        // 使用了父函数中声明的变量
        alert(name);
    }
    return displayName;
}
var myFunc = closure();
myFunc();

如上代码在浏览器中运行之后 displayName() 函数内的 alert() 语句成功显示出了变量 name 的值 , 原因在于JavaScript 中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。 在本例子中, myFunc 是执行closure 时创建的 displayName 函数实例的引用。 displayName 的实例维持了一个对它的词法环境(变量name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到 alert 中。
二、闭包的应用场景有哪些 ?
1. 事件函数的封装 :
假设我们有如下需求 :
通过页面元素点击事件修改页面字体大小
代码实现如下 :

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>Document</title>
	<style>
		body {
			font-size: 12px;
		}

		h1 {
			font-size: 1.5em;
		}

		h2 {
			font-size: 1.2em;
		}
	</style>
</head>

<body>
	<p> 这是 p 元素的文案 </p>
	<h1> 这是 h1 元素的文案 </h1>
	<h2> 这是 h2 元素的文案 </h2>
	<a href="#" id="size12">12</a>
	<a href="#" id="size14">14</a>
	<a href="#" id="size16">16</a>
	<script>
		// 获取页面元素
		const size12Btn = document.getElementById('size12');
		const size14Btn = document.getElementById('size14');
		const size16Btn = document.getElementById('size16');
		// makeSizer 返回事件响应函数
		const makeSizer = size => () => {
			document.body.style.fontSize = size + 'px';
		};
		// 页面元素绑定事件
		size12Btn.onclick = makeSizer(12);
		size14Btn.onclick = makeSizer(14);
		size16Btn.onclick = makeSizer(16);
	</script>
	1
</body>

</html>

在本例子中我们通过 makeSizer 高阶函数 ( 返回值是函数的函数 ) 给我们的页面元素绑定相应的事件响应 , makeSizer 的返回值便是我们的响应函数 ( 在 makeSizer 函数中它便是闭包 ) 。这种方式极大的优化了我们的代码量,提高了整个代码的可读性。 

2. 用闭包模拟私有方法
javascript 没有 java 中那种 public private 的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:

const makeCounter = function() {
	let privateCounter = 0;
	function changeBy(val) {
		privateCounter += val;
	}
	return {
		increment() {
			changeBy(1);
		},
		decrement() {
			changeBy(-1);
		},
		value() {
			return privateCounter;
		},
	};
};
const Counter1 = makeCounter();
const 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 */
Counter2.increment();
Counter2.increment();
console.log(Counter2.value()); /* logs 2 */
console.log(Counter1.value()); /* logs 1 */

注意两个计数器 Counter1 和 Counter2 它们都保持各自的独立性。每个闭包都是引用自己作用域内的变量 privateCounter, 每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

3. 在循环中给页面元素绑定事件响应函数

在循环中给页面元素绑定事件响应函数是我们 js 面试中的非常基础且常见的考题。现有如下场景 :

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>Document</title>
</head>

<body>
	<div>0</div>
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<script>
		var nodes = document.getElementsByTagName('div');
		var length = nodes.length;
		for (var i = 0; i < length; i++) {
			nodes[i].onclick = function () {
				alert(i);
			};
		}
	</script>
</body>

</html>

这段代码运行之后无论点击哪个 div 最后弹出的结果都是 5, 这是因为 div 节点的 onclick 事件是被异步出发的 , 当事件被触发的时候, for 循环早就结束了,这个时候变量 i 的值已经是 5 。 解决这个问题的方法之一我们可以利用闭包 , 把每次循环的 i 值都封闭起来。 

<html lang="en">

<head>
	<meta charset="UTF-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>Document</title>
</head>

<body>
	<div>0</div>
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<script>
		var nodes = document.getElementsByTagName('div');
		var length = nodes.length;
		for (var i = 0; i < length; i++) {
			(function (i) {
				nodes[i].onclick = function () {
					alert(i);
				};
			})(i);
		}
	</script>
</body>

</html>

这样当在事件函数中顺着作用域链从内到外查找变量 i 时,会先找到被封闭在闭包环境中的 i 。所以最终我们用闭包改造后的代码每次点击按钮的时候会分别弹出 0 , 1 , 2 , 3 , 4 。

三、闭包存在什么问题 ?
有一种耸人听闻的说法是闭包会造成内存泄漏 , 造成内存泄漏的原因其实与闭包毫无关系。闭包跟内存泄漏有关的地方是,使用闭包的同时比较容易循环引用,如果闭包的作用域中保存着一些 DOM 节点,这时候可能会造成内存泄漏,但从本质上来讲这并非闭包的问题,在 IE 浏览器中由于 BOM 的 DOM 中的对象是使用 c++ 以 COM 对象方式实现的 , 而 COM 的垃圾回收机制使用的是引用计数策略,也就是说如果两个对象之间形成的循环引用 , 那么这两个对象都无法被回收,从本质上来讲这并非闭包的锅。
但是闭包本身会造成常驻内存 , 来看下面一段代码

function foo() {
    var a = 3;
    function result() {
        console.log(a);
    }
    return result;
}
var test = foo();
test();

上述代码中,理论上来说, foo 函数作用域隔绝了外部环境,所有变量引用都在函数内部完成, foo 运行完成以后,内部的变量就应该被销毁,内存被回收。然而闭包导致了全局作用域始终存在一个 test
的变量在引用着 foo 内部的 result 函数,这就意味着 foo 内部定义的 result 函数引用数始终为 3 ,垃圾运行机制就无法把它销毁 . 这就是我们说的 闭包本身会造成内部变量常驻内存。

四、面试中遇到该题目 ?
讲清楚如下三点 :

  • 什么是闭包?

        大白话 : 能够访问其他函数内部变量的函数,被称为闭包。

  • 闭包有哪些实际的使用场景 ?

        a 、事件函数的封装
        b 、用闭包模拟私有方法
        c 、在循环中给页面元素绑定事件响应函数

  • 闭包存在什么问题 ?

        闭包本身会造成内部变量常驻内存

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值