老实说关于闭包的文章看了不少篇,也看了不少遍,但是并不知道怎么用哇,不会用就老是会忘记这个知识点,看一次忘一次,忘一次看一次......长期以来,闭包对我来说就是神一样的玄乎的存在......
这篇文章呢,更多是我对闭包的一些个人体会,理解或许会有些偏差。博客这种东西嘛,更多的是分享自己的看法体会嘛。如果能和大家交流想法,那就更好了。
yoga at 2019/06/17.
目录
闭包的概念
闭包是什么?MDN官方解释——
闭包是函数和声明该函数的词法环境的组合。
官方解释的一个特点是:在你不懂这个东西的时候,你完全不知道这句话在说什么;当你懂了这个东西之后,会发现官方说明的语言真的非常精炼。
一步一步来,我们首先看下面这段代码:
function init() {
var name = "魏无羡";
function displayName() {
alert(name);
}
displayName();
}
init();
函数init()中定义了一个局部变量name和一个函数displayName()。displayName()在函数init()局部作用域内,可以访问到init()定义的局部变量name,因此运行该代码,我们会发现displayName()的alert()语句可以成功显示init()函数中定义的name的值。
进一步的,我们考虑这段代码:
function makeFunc() {
var name = "蓝忘机";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
同上一段代码一样,内部函数displayName访问了外部函数的局部变量。不同的是,外部函数makeFunc将displayName作为其返回值,因此,变量myFunc的值实际上是函数displayName的一个实例。
那么这段代码的运行结果,如我们所料,displayName()的alert()语句依然可以成功显示makefunc()中定义的name的值。结果或许有点让人费解,这个机制却是闭包很有意思的一点:
当我们执行var myFunc = makeFunc()语句的时候,将displayName函数实例的引用赋给了一个全局变量myFunc,这使得displayName实例存在与内存中。而因为JavaScript 中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里,即displayName实例的存在依赖于makeFunc,因此makeFunc也存在于内存中。这就导致我们看到的,myFunc一直可以访问到makeFunc的局部变量。
在上面的例子中,我们已经形成并使用了闭包。我们再回来看MDN的解释:闭包是由函数以及创建该函数的词法环境组合而成,这个环境包含了这个闭包创建时所能访问的所有局部变量。在上面的例子中,闭包不仅包含函数displayName(),还包含了它可以访问到的外部函数的局部变量name,我们称变量myFunc为一个闭包。
简单来说呢,可以将闭包理解为一个定义的函数内部的函数,这个函数可以访问外部函数的作用域中的变量。阮一峰老师说,从本质上说,闭包就是将函数内部和函数外部连接起来的一座桥梁。
为什么要使用闭包
找闭包相关资料的时候总能看到有人吐槽:把变量放到外面不行吗,用什么闭包,看又看不懂......
我们考虑用闭包实现一个计数器函数Counter,每次调用的时候,返回值比上一次增加1。代码如下:
function makeCounter() {
var count = 0;
function add() {
return ++count;
}
return add;
}
var Counter = makeCounter();
Counter(); // 1
Counter(); // 2
Counter(); // 3
Counter(); // 4
实际上,这个功能我们不用闭包也完全这个实现,将count设为全局变量,再用函数设置递增:
var count = 0;
function Counter() {
return ++count;
}
Counter(); // 1
Counter(); // 2
Counter(); // 3
Counter(); // 4
但是如果我们这么写了,那么count变量的值就变得不可控了,因为不仅Counter()函数可以访问它,页面上的任何脚本都能改变count的值。
可以说,闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。到这里,可以隐约看出来一点苗头:闭包的运用实际上体现了面向对象的编程思想。闭包机制使得变量可以一直保存在内存中的同时,不造成全局污染。
闭包应用实践举例
我不敢说闭包有什么什么样的用途,因为我觉得可能每个人用的方法都不一样,谈用途未免局限思维。这里只举几个闭包实践的例子。
斐波那契数列
这个是面试题来的:递归实现函数Fibonacci()满足斐波那契数列,即fibonacci(0)=1,fibonacci(1)=1,fibonacci(n)=fibonacci(n-1)+fibonacci(n-2),要求缓存。
我的代码:
function makeFibonacci() {
var fibonacciArr = [1, 1];
function fibonacci(n) {
if(fibonacciArr[n]) {
return fibonacciArr[n];
}
else {
fibonacciArr[n] = fibonacci(n-1) + fibonacci(n-2);
return fibonacciArr[n];
}
}
return fibonacci;
}
var Fibonacci = makeFibonacci();
Fibonacci(6); // output: 13
从实现功能的角度,fibonacciArr数组并不是非要放在闭包里面不可。但是题目要求是实现函数Fibonacci,那么fibonacciArr数组并不是外部需要关注的,不需要暴露给外界,并且需要在函数运行结束后仍然保存,因此我们用闭包来实现。更常用的写法可能是下面这样的:
var Fibonacci = (function() {
var fibonacciArr = [1, 1];
function fibonacci(n){
if(fibonacciArr[n]){
return fibonacciArr[n];
}
else{
fibonacciArr[n] = fibonacci(n-1) + fibonacci(n-2);
return fibonacciArr[n];
}
}
return fibonacci;
})();
Fibonacci(6); // output: 13
给多个Dom绑定事件
【本例来自佩吉秋: js闭包其实不难,你需要的只是了解何时使用它】
假设页面有n个按钮,需要给每个按钮绑定一个点击事件,点击时弹窗该按钮的索引编号。
按照一般的思路,我们可能会这么写:
var btns = document.getElementsByTagName('button');
for(var i = 0, len = btns.length; i < len; i++) {
btns[i].onclick = function() {
alert(i);
}
}
然后我们点击按钮的时候,就会发现不管点击哪个按钮,弹窗皆为5。那是因为,onclick事件是被异步触发的,当事件被触发的时候,for循环已经结束,此时变量i的值为5,onlick事件函数顺着作用域链从内向外查找变量i时,找到的值总是5。
那怎么能循环给button添加事件,并且还能alert出来不同的值呢?答案当然是:闭包!在闭包的作用下,定义事件函数的时候,每次循环的i值都被封闭起来,这样在函数执行时,会查找定义时的作用域链,这个作用域链里的i值是在每次循环中都被保留的,因此点击不同的button会alert出来不同的i。代码如下:
for(var i = 0, len = btns.length; i < len; i++) {
(function(i) {
btns[i].onclick = function() {
alert(i);
}
}(i))
}
闭包与类
正如我们前面所说,闭包体现了面向对象的编程思想。ES6之前,Js没有class(类)方法,我们可以用闭包来实现:
var Animal = function(bake) {
this.introduceSelf = function(name) {
console.log(bake + '~我的名字是' + name);
}
}
var Cat = new Animal('喵喵');
var Dog = new Animal('汪汪');
Cat.introduceSelf('布丁'); // 喵喵~我的名字是布丁
Dog.introduceSelf('佩琦'); // 汪汪~我的名字是佩琦
setTimeout实现倒计时
话不多说,代码如下:
var countdownTimer = (function() {
return function(n) {
for(var i = n; i > 0; i--){
(function(i){
setTimeout(function() {
console.log(i)
}, (n - i) * 1000)
})(i)
}
}
})();
countdownTimer(5); // 依次打印5、4、3、2、1,间隔1秒
关于内存泄漏的问题
很多关于闭包的文章都说闭包使用会导致内存泄露,实际上呢emmmmmmm......严格意义上这应该是代码没写好的问题。闭包或许容易造成滥用(循环创建闭包等等),因而造成内存泄露,但如果能够保证闭包在使用结束之后释放掉,基本上是不会有内存泄漏的。
总而言之就是,闭包的使用也应该遵循规范,科学正确地使用闭包,那内存的问题不大。
参考资料
- MDN: 闭包[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures]
- 阮一峰: 学习Javascript闭包(Closure)[http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html]
- 菜鸟教程: JavaScript闭包[https://www.runoob.com/js/js-function-closures.html]
- Raychan: 如何才能通俗易懂地解释JS中的的"闭包"?[https://www.cnblogs.com/wx1993/p/7717717.html]
- 佩吉秋: js闭包其实不难,你需要的只是了解何时使用它[https://www.jianshu.com/p/132fb6d485ee]
- Object_name: 对JS闭包的理解及常见应用场景[https://blog.csdn.net/qq_21132509/article/details/80694517]
- huansky: JS闭包原理与应用经典示例[https://www.jb51.net/article/153088.htm]
- 知乎: 关于js闭包是否真的会造成内存泄漏?[https://www.zhihu.com/question/31078912?sort=created]
- TiAnna501: JavaScript之详述闭包导致的内存泄漏[https://blog.csdn.net/u012876641/article/details/29185323]