Js基础之闭包

概念

之前在作用域那篇博客中说作用域链时提到一个叫闭包的概念,在MDN中解释是:闭包是函数和声明该函数的词法环境的组合,而Js高程里的解释是:有权访问另一个函数作用域中的变量的函数。其实两个解释都是同一个意思,后者可能更好理解。

闭包要解决什么问题?
1.局部变量无法长久保存(一般来说,当函数执行完毕后,局部变量会被销毁。闭包除外),而全局变量可能造成变量污染,所以闭包这种机制可以长久的保存变量,又不会造成全局污染。
2.局部变量无法共享,闭包可以间接访问。

然而也带来了一些问题,闭包引用的作用域不会被垃圾回收处理,因此不合理使用会消耗内存。

举例分析

首先举一个简单的例子,若我们想要设置或者获取一个函数内部的变量值,应该怎么做?

function foo () {
   var name = "张三";
   var age = "18";
   return {
      getName: function() {
        return name;
      },
      setName: function(value) {
        name = value;
      },
      getAge: function() {
        return age;
      },
      setAge: function(value) {
        age = value;
      }       
   }
}

var person = foo();
console.log(person.getName()); // 张三

相当于foo通过返回一个key是函数名,value是函数的对象,来对外提供设置、获取foo局部变量的方法。而这一切的实现,依赖于foo中的四个闭包。大家是否发现,这个方法实现了修改、查询,跟jquery的 .data()方法有相似之处,data功能多了删除、增加,其实现要相对复杂。大家有兴趣可以参考JQuery数据缓存源码解析jQuery 源码系列(八)data 缓存机制

下面再来看看稍微复杂一点的例子。

这是前段时间一个朋友参加招聘,给我发的一题校招题。这段代码执行会在界面显示0-9共计10个button,但是点击每个button后alert的都是”You are on Cloud -1”。

首先,我们可以看到最外层是一个立即执行函数(IIFE),里面的循环跑了10次后,我们点击button后,click触发的回调函数会在它的作用域中寻找i变量,而i变量并不在回调函数的变量对象中,所以它会沿着作用域链去搜索,回调函数的上级作用域就是IIFE的作用域,在该作用域中找到了i这个变量,此时i的值为-1(i是IIFE下变量,最后一个值将之前的0、1、2…9覆盖),所以点击每个button都会alert -1。

为何IIFE执行完毕后,其作用域中的i变量不销毁呢?
这是由于i被回调函数引用着,必须等到这些回调函数执行完毕后才会销毁。

此时,我们发现了问题所在,我们想要到达Cloud9,就必须让click触发的回调函数有自己的局部变量,那么闭包此时就能解决我们的问题。

具体做法:

(function(){
       var i;
       for (i = 9; i >= 0; i--) {
          var button = document.createElement('button');
          button.innerHTML = 'Take your career to Cloud' + i;

          button.addEventListener('click', (function (j) {
                return function(){
                    alert("You are on Cloud " + j);
                }
          })(i));

          document.body.appendChild(button);
       }
})();

便于大家理解,也可以这样写:

(function(){
       var i;
       for (i = 9; i >= 0; i--) {
          var button = document.createElement('button');
          button.innerHTML = 'Take your career to Cloud' + i;
          var foo = function (j) {
                return function(){
                    alert("You are on Cloud " + j);
                }
          }
          button.addEventListener('click', foo(i));

          document.body.appendChild(button);
       }
})();

为何加入闭包(foo)以后就可以alert You are on Cloud 9了呢?

因为foo函数的作用域中存在j这个变量(函数的形参也会存放在变量对象里),所以在循环跑完的时候,点击button,click触发的对应foo函数可以在其对应的变量对象中找到对应的j的值

既然已经有了ES6标准,我就来说说如何用ES6来解决这个问题。

(function(){
       for (let i = 9; i >= 0; i--) {
          var button = document.createElement('button');
          button.innerHTML = 'Take your career to Cloud' + i;

      button.addEventListener('click', function () {
            alert("You are on Cloud " + i);
          });

          document.body.appendChild(button);
       }
})();

let声明的变量只在所在代码块内有效,每个闭包都绑定了块作用域的变量,而且特殊的是for循环中循环的那部分是个父级作用域,所以for循环中每一个i的值都是一个新的变量,这在阮一峰老师ES6入门中有介绍。所以我们点击button触发对应click回调后去对应for的作用域(子作用域)搜索的时候,会得到与click回调绑定的对应i值

题外话

ES6这套标准让JS慢慢向着规范化靠拢,我也在ES6中慢慢前行,我发现写博客是一个知识总结的过程,把工作中用到的、学习中了解到的,不熟悉、模糊的地方,能通过写博客慢慢总结、然后翻看各种资料,最后明朗化。这就是知识吸收的过程。在此建议没有写博客的同学马上上手搭建一个,并定期更新。关键搭建的教程,在我的第一篇博客中有详细介绍。-)-