一、什么是闭包?
一个函数和对其周围状态( 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 、在循环中给页面元素绑定事件响应函数
- 闭包存在什么问题 ?
闭包本身会造成内部变量常驻内存