前端基础 | 神一样的闭包

老实说关于闭包的文章看了不少篇,也看了不少遍,但是并不知道怎么用哇,不会用就老是会忘记这个知识点,看一次忘一次,忘一次看一次......长期以来,闭包对我来说就是神一样的玄乎的存在......

这篇文章呢,更多是我对闭包的一些个人体会,理解或许会有些偏差。博客这种东西嘛,更多的是分享自己的看法体会嘛。如果能和大家交流想法,那就更好了。

yoga at 2019/06/17.

目录

闭包的概念

为什么要使用闭包

闭包应用实践举例

        斐波那契数列

​        给多个Dom绑定事件

​        闭包与类

        setTimeout实现倒计时

关于内存泄漏的问题

参考资料


闭包的概念

闭包是什么?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......严格意义上这应该是代码没写好的问题。闭包或许容易造成滥用(循环创建闭包等等),因而造成内存泄露,但如果能够保证闭包在使用结束之后释放掉,基本上是不会有内存泄漏的。

总而言之就是,闭包的使用也应该遵循规范,科学正确地使用闭包,那内存的问题不大。

 

 

参考资料

  1. MDN: 闭包[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures]
  2. 阮一峰: 学习Javascript闭包(Closure)[http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html]
  3. 菜鸟教程: JavaScript闭包[https://www.runoob.com/js/js-function-closures.html]
  4. Raychan: 如何才能通俗易懂地解释JS中的的"闭包"?[https://www.cnblogs.com/wx1993/p/7717717.html]
  5. 佩吉秋: js闭包其实不难,你需要的只是了解何时使用它[https://www.jianshu.com/p/132fb6d485ee]
  6. Object_name: 对JS闭包的理解及常见应用场景[https://blog.csdn.net/qq_21132509/article/details/80694517]
  7. huansky: JS闭包原理与应用经典示例[https://www.jb51.net/article/153088.htm]
  8. 知乎: 关于js闭包是否真的会造成内存泄漏?[https://www.zhihu.com/question/31078912?sort=created]
  9. TiAnna501: JavaScript之详述闭包导致的内存泄漏[https://blog.csdn.net/u012876641/article/details/29185323]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值