# 闭包
闭包是函数和声明该函数的词法环境的组合。

在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
- 闭包
- 形成三要素
- 函数嵌套
- 内层函数使用外层变量
- (被变量调用整个函数嵌套体)
- 闭包作用
- 保护需要保护的数据不被定义在外面被轻易修改
- 避免无效闭包
- 循环引用
- for循环中异步操作
- 代码优化:写在原型链上的方法优于写在构造函数上
- 形成三要素
可打断点在浏览器source中看到闭包的信息
闭包,指的是词法表示包括不被计算的变量的函数,也就是说, 函数可以使用函数之外定义的变量。
- 闭包是一个函数
- 闭包可以使用在它外面定义的变量
- 闭包存在定义该变量的作用域中
在Js中,只有函数内部的子函数才能读取局部变量,因此可把闭包简单理解成"定义在一个函数内部的函数"。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
# 闭包用途
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get (key) {
return data[key]
}
}
}
const c = createCache()
c.set('a', 100)
c.set('a', 160)
console.log( c.get('a') )
/**
* 设置鼠标样式
* @param updateTableCol 选中列
* @param updateTableCell 在单元格内部左侧
* @param updateLeftStaple 在表格左侧 正文左侧
* @returns
*/
const updateCursor = (function () {
// 如果这个换成全局变量,其实也可以,找的时候都可能有些麻烦。
let selectTableCol = false
return function (updateLeftStaple: boolean) {
// xxx处理
// selectTableCol= xxxx
}
})()
# 变量生存周期
全局变量,生命周期是永久的。局部变量,当定义该变量的函数调用结束时,该变量就会被垃圾回收机制回收销毁 再次调用该函数时又会重新定义了一个新变量。
var func = function(){
var a = 'linxin';
var func1 = function(){
a += ' a';
console.log(a);
}
return func1;
}
var func2 = func();
func2(); // linxin a
func2(); // linxin a a
func2(); // linxin a a a
可以看出,在第一次调用完 func2 之后func中的变量a变成 'linxin a',而没有被销毁。因为此时func1形成了一个闭包,导致了a的生命周期延续了。
提示
- 闭包使用其他函数定义的变量, 使其不被销毁。比如上面 func1 调用了变量 a
- 闭包存在定义该变量的作用域中,变量 a 存在 func 的作用域中,那么 func1 也必然存在这个作用域中。
- 在另一个函数内部定义的函数,初始化时会将包含它的函数(外部函数)的活动对象添加到他们作用域中。外部函数执行完毕后,活动对象不会被销毁,因为匿名函数或者说闭包的作用域仍然在引用这个活动对象。所以对于外部函数,仅仅是销毁了它的作用域链 。直到闭包被销毁,外部函数的活动对象才会被销毁。
# 在循环中创建闭包:一个常见错误
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
运行这段代码后,无论焦点在哪个input上,显示的都是关于年龄的信息。
原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。
解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
这段代码可以如期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,help 指向 helpText 数组中对应的字符串。
另一种方法使用了匿名闭包:
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 马上把当前循环项的item与事件回调相关联起来
}
}
还可以用let关键词代替var声明变量,每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
# 内存管理
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,当前作用域都会主动的进行释放和销毁。但当 遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。
在闭包中调用局部变量,会导致这个局部变量无法及时被销毁,相当于全局变量一样会一直占用着内存。如果需要回收这些变量占用的内存,可以 手动将变量设置为null。
function f2(){
var n=22;
var nAdd=function(){n++};
return function(){
return {
n:n,
nAdd:nAdd
}
}
}
//result2就是创建了一个匿名函数
var result2=f2();
//调用函数
console.log(result2());//{n: 22, nAdd: ƒ}
result2().nAdd();
console.log(result2());//{n: 23, nAdd: ƒ}
result2().nAdd();
console.log(result2());//{n: 24, nAdd: ƒ}
console.log(result2)//f
//解除对匿名函数的引用,以便释放内存
result2=null;
console.log(result2)//null
console.log(f2) //f2()
TIP
在使用闭包的过程中,比较容易形成 JS对象和DOM对象的循环引用,就有可能造成内存泄露。这是因为浏览器的垃圾回收机制中,如果两个对象之间形成了循环引用,那么它们都无法被回收。
两个对象:JS对象test,DOM对象简称为'testId' 在func方法中,test引用了DOM对象testId, 然后以test为中转,给testId这样一个DOM对象的onclick事件绑定了一个闭包。如果在被绑定闭包中,访问test这个JS对象。 那么,就形成了互相依赖:
- test依赖testId(引用)
- testId(的点击事件handler)依赖test(在绑定事件中的handler中操作了test)
function func() {
var test = document.getElementById('test');
test.onclick = function () {
console.log('hello world');
}
}
在上面例子中,func 函数中用匿名函数创建了一个闭包。变量 test 是 JavaScript 对象,引用了 id 为 test 的 DOM 对象,DOM 对象的 onclick 属性又引用了闭包,而闭包又可以调用 test ,因而形成了循环引用,导致两个对象都无法被回收。要解决这个问题,只需要把循环引用中的变量设为 null 即可。
function func() {
var test = document.getElementById('test');
test.onclick = function () {
console.log('hello world');
}
test = null;
//再给test赋值onclick的时候点击事件已经存在dom上了,test只是一个暂存dom元素的变量
//这仅仅断开了局部变量 test 和 DOM 元素之间的引用,但并没有改变 DOM 元素本身或它的任何属性,包括 onclick 事件处理器。,如果是要解绑点击事件,需要把dom上的onclick设置为null
// document.getElementById('test').onclick = null;
}
提示
干掉作为“中转变量”只是为了绑定click handler的test,打破循环。(类比死锁解决方案中的检测与解除--剥夺)
function func() {
var test = document.getElementById('test');
test.onclick = funcTest;
}
function funcTest(){
console.log('hello world');
}
打破闭包,使click的handler无法再满足闭包条件,来访问func内部变量,从根源(闭包的产生的三条件)上杜绝相互依赖的发生。(类比死锁解决方案中的预防--破坏形成死锁的先决条件)
# 性能考量
如果不是某些特定任务需要使用闭包,在其它函数中创建函数不明智,闭包在 处理速度 和 内存消耗 方面对脚本性能有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。
# 闭包应用场景
- 运用场景:封装变量 收敛权限 模拟私有属性、方法
//封装变量 收敛权限
//记录是否存在,不存在录入,存在则返回false,如果不用闭包,就需要暴露一个数组,容易被人为修改
function quanxian(){
let arr=[]
return function(val){
if(arr.indexOf(val)>=0){
console.log(false)
return false
}else{
arr.push(val)
console.log(true)
return true
}
}
}
let ceshi=quanxian()
ceshi(1)//true
ceshi(1)//false
ceshi(2)//true
- 抖动和节流
<html>
<style>
#test{
width:400px;
height:400px;
border:1px solid red
}
</style>
<body class="m-2">
<div id="test">1111</div>
<div id="test">1111</div>
<div id="test">1111</div>
<div id="test">1111</div>
<div id="test">1111</div>
<div id="test">1111</div>
<div id="test">1111</div>
<div id="test">1111</div>
<script>
function throttle(fn, delay) {
// last为上一次触发回调的时间, timer是定时器
let last = 0, timer = null
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last < delay) {
// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
</script>
</body>
</html>
# 闭包面试题
var a=0,
b=0;
function A(a){
A=function(b){
console.log('function A')
console.log(a+b++)
}
console.log('function B')
console.log(a++)
}
A(1)//function B 1
A(2)//function A 4
const foo = (function() {
var v = 0
return () => {
return v++
}
}())
for (let i = 0; i < 10; i++) {
foo()
}
console.log(foo)
// () => {
// return v++
// }
console.log(foo()) // 10
在循环执行时,执行 foo(),这样引用自由变量 10 次,v 自增 10 次,最后执行 foo 时,得到 10。(自由变量是指没有在相关函数作用域中声明,但是使用了的变量。)
const foo = () => {
var arr = []
var i
for (i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
return arr[0]
}
foo()() // 10
var fn = null
const foo = () => {
var a = 2
function innerFoo() {
console.log(a)
}
fn = innerFoo
}
const bar = () => {
fn()
}
foo()
bar() // 2
解析
正常来讲,根据调用栈的知识,foo 函数执行完毕之后,其执行环境生命周期会结束,所占内存被垃圾收集器释放,上下文消失。但是通过 innerFoo 函数赋值给 fn,fn 是全局变量,这就导致了 foo 的变量对象 a 也被保留了下来。所以函数 fn 在函数 bar 内部执行时,依然可以访问这个被保留下来的变量对象,输出结果为 2
var fn = null
const foo = () => {
var a = 2
function innerFoo() {
console.log(c)
console.log(a)
}
fn = innerFoo
}
const bar = () => {
var c = 100
fn()
}
foo()
bar() // 报错
在 bar 中执行 fn() 时,fn() 已经被复制为 innerFoo,变量 c 并不在其作用域链上,c 只是 bar 函数的内部变量。因此报错 ReferenceError: c is not defined。
# 内存管理基本概念
内存空间可以分为栈空间和堆空间,其中
- 栈空间:由操作系统自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。
- 堆空间:一般由开发者分配释放,这部分空间就要考虑垃圾回收的问题。
var a = 11
var b = 10
var c = [1, 2, 3]
var d = { e: 20 }
对于分配内存和读写内存的行为所有语言都较为一致,但释放内存空间在不同语言之间有差异。例如,JavaScript 依赖宿主浏览器的垃圾回收机制,一般情况下不用程序员操心。但这并不表示万事大吉,某些情况下依然会出现内存泄漏现象。
内存泄漏是指内存空间明明已经不再被使用,但由于某种原因并没有被释放的现象。这是一个非常「玄学」的概念,因为内存空间是否还在使用,某种程度上是不可判定问题,或者判定成本很高。内存泄漏危害却非常直观:它会直接导致程序运行缓慢,甚至崩溃。
内存泄漏场景举例 我们来看几个典型引起内存泄漏的例子:
var element = document.getElementById("element")
element.mark = "marked"
// 移除 element 节点
function remove() {
element.parentNode.removeChild(element)
}
上面的代码,我们只是把 id 为 element 的节点移除,但是变量 element 依然存在,该节点占有的内存无法被释放。
需要在 remove 方法中添加:element = null,这样更为稳妥。
function foo() {
var name = 'lucas'
window.setInterval(function() {
console.log(name)
}, 1000)
}
foo()
这段代码由于 window.setInterval 的存在,导致 name 内存空间始终无法被释放,如果不是业务要求的话,一定要记得在合适的时机使用 clearInterval 进行清理。
闭包使用不当,极可能引发内存泄漏,需要格外注意。
function foo() {
let value = 123
function bar() { alert(value) }
return bar
}
let bar = foo()
这种情况下,变量 value 将会保存在内存中,如果加上:
bar = null
这样的话,随着 bar 不再被引用,value 也会被清除。