前言
之前对于的概念,一直是刷刷面试题随便看看,争取面试的时候能说上原理即可,也没有深入了解。日常写业务代码可能有用到但是自己不了解,今天在看《JavaScript设计模式与开发实践》一书中有关
的章节,在此做个笔记。
一、闭包的概念
从两个地方着手了解,闭包的形成与变量的作用域,以及变量的生存周期密切相关。
1.1 变量的作用域
顾名思义,就是指变量的有效范围。我们经常说到的就是在函数中声明的变量作用域
当在函数声明一个变量的时候,如果该变量前面没有带上关键字,这个变量就会成为全局变量,这很容易造成命名冲突。
另一种情况是用关键字在函数中声明变量,这时候的变量即是全局变量,只有在改函数内部才能访问到这个变量,在函数外面是访问不到的。
var foo = function() {
var a = 1;
console.log(a); // 1
}
foo();
console.log(1) // Uncaught ReferebceError: a is not defined
JavaScript 中函数可以用来创造函数作用域,函数里面可以获取外部的变量,但是函数外面无法获取到函数里的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么这次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止,变量的搜索是从内到外的。比如
var a = 1;
var foo = function() {
var b = 2;
var foo2 = function() {
var c = 3;
console.log(b); // 2
console.log(a); // 1
}
foo2();
console.log(c); // 报错 c is not defined
}
foo();
1.2 变量的生存周期
对于全局变量来说,生存周期是永久的,除非我们主动销毁这个全局变量。
而对于在函数内部用关键字声明的局部变量来说,当退出函数时,这些局部变量失去了它的价值,它们都会随着函数调用的结束而被销毁。
var func = function() {
var a = 1;
console.log(1); // 退出函数后局部变量 a 将被销毁
}
func();
再看这段代码:
var func = function() {
var a = 1;
return function() {
a++;
console.log(a);
}
}
var f = func();
f(); // 2
f(); // 3
f(); // 4
当执行时,
返回了一个匿名函数的引用,它可以访问到
被调用时产生的环境,而局部变量
一直处在这个环境里。既然局部变量所在的环境还能别外界访问,这个局部变量就有了不被销毁的理由。这里就产生了一个
结构,局部变量的生命看起来被延续了。
一个闭包的经典应用
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<script>
var nodes = document.getElementsByTagName('div');
for(var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function() {
console.log(i)
}
};
</script>
</body>
</html>
无论点击哪个div,最后打印的都是5。这是因为div节点的是被异步触发的,for循环早已结束,变量i为5。
解决办法是在的帮助下,把每次循环的i的值都封闭起来,当事件函数中顺着作用域链中从内到外查找变量i时,会先找到闭包环境中的i
var nodes = document.getElementsByTagName('div');
for(var i = 0; i < nodes.length; i++) {
(function(i) {
nodes[i].onclick = function() {
console.log(i)
}
})(i)
};
二、闭包的作用
2.1 封装变量
- 闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。一个计算乘积的简单函数
const mult = (function() {
let cache = {};
let calculate = function() { // 封闭 calculate函数
let a = 1;
for(let i = 0; i < arguments.length; i++){
a*= arguments[i]
};
return a;
};
return function() {
var args = Array.prototype.join.call(arguments, ',');
if(args in cache) {
return cache[args];
};
return cache[args] = calculate.apply(nll, argumemts);
}
})()
- 延续局部变量的寿命
img对象经常用于数据上报,如:
var report = function(src) {
var img = new Image();
img.src = src
}
report('http://xxx.com/getUserInfo');
但是在某些低版本浏览器中使用会丢失30%的数据。原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,此时或许还未来得及发出HTTP请求,所以此次请求会丢失掉。
现在我们把img变量用闭包封闭起来,便能解决请求丢失的问题
var report = (function() {
var imgs = [];
return function(src) {
var img = new Image();
imgs.push(img);
img.src = src;
};
})();
三、闭包和面向对象设计
使用闭包实现一个面向对象系统
这是一段跟闭包相关的代码
var Extent = function() {
var value = 0;
return {
call: function() {
value++;
console.log(value);
}
}
};
var extent = Extent();
extent.call(); // 1
extent.call(); // 2
extent.call(); // 3
换成面向对象写法就是:
var Extent = {
value: 0;
call: function() {
this.value++;
console.log(this.value);
}
}
extent.call() // 1
extent.call() // 2
extent.call() // 3
或者:
var Extent = function() {
ths.value = 0;
};
Extent.prototype.call = function() {
this.value++;
console.log(this.value);
};
var extent = new Extent();
extent.call() // 1
extent.call() // 2
extent.call() // 3
四、用闭包实现命令模式
先用面向对象的方法编写一段命令模式的代码
<!DOCTYPE html>
<html>
<body>
<button id="execute">点击我执行命令</button>
<button id="undo">点击我撤销命令</button>
<script>
var Tv = {
open: function() {
console.log('打开电视');
},
close: function() {
console.log('关上电视');
}
};
var OpenTvCommand = function(receiver) {
this.receiver = receiver
};
OpenTvCommand.prototype.undo = function() {
this.receiver.close(); // 撤销命令,关闭电视
};
OpenTvCommand.prototype.execute = function() {
this.receiver.open(); // 执行命令,打开电视
};
var setCommand = function(command) {
document.getElementById('execute').onclick = function() {
command.execute(); // 输出:打开电视
};
document.getElementById('undo').onclick = function() {
command.undo(); // 输出:关闭电视
}
};
setCommand(new OpenTvCommand(Tv));
</script>
</body>
</html>
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行前,可以预先往命令对象中植入命令的接收者。
但是在JavaScript中,函数本身就是一等公民,可以四处传递,用函数对象而不是普通函数来封装请求显得更加自然和简单。下面是闭包版本的代码
var Tv = {
open: function() {
console.log('打开电视');
},
close: function() {
console.log('关上电视');
}
};
var createCommand = function(receiver) {
var execute = function() {
return receiver.open(); // 执行命令,打开电视机
};
var undo = function() {
return receiver.close(); // 执行命令,关闭电视机
};
return {
execute: extcute,
undo: undo
}
};
var setCommand = function(command) {
document.getElementById('execute').onclick = function() {
command.execute(); // 输出:打开电视
};
document.getElementById('undo').onclick = function() {
command.undo(); // 输出:关闭电视
}
};
setCommand(new createCommand(Tv));
五、闭包与内存管理
闭包是一个非常强大的特性,但人们对其也有诸多误解,有一种耸人听闻的说法就是闭包会造成内存泄漏,要尽量减少闭包的的使用。
局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包确实会是一些数据无法被及时销毁。
但是我们使用闭包的一部分原因就是主动把一些变量封闭在闭包中,因为可能在以后还要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一样的。这里不能说是内存泄漏。如果将来要回收这些变量,我们可以手动把这些变量设置为null。
跟闭包和内存泄漏有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄漏。
但这本身并非闭包的问题,也并非JavaScript的问题,在IE浏览器中,由于BOM和DOM中的对象是引用C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄漏在本质上也不是闭包造成的。
同样,如果要解决循环引用带来的内存泄漏问题,我们只需要把循环引用中的变量设置为null即可,将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收他们占用的内存。