JavaScript的闭包

95 阅读7分钟

对于面试的高频知识点,一直以来闭包都是很多人学习的痛点,说难也不难,说简单也不简单,摸清楚其来龙去脉理解起来就容易很多了。我在看《JavaScript高级程序设计》与《JavaScript核心技术解密》以及《你不知道的JavaScript》三本书的过程中都有对闭包的讲解,我结合自己的理解对其进行了一定的融合,希望能够将闭包讲解清楚。

概念

在《JavaScript核心技术开发解密》里面对闭包的定义是这样的:闭包是一种特殊现象,它是由两部分组成,执行上下文和在上下文中创建的函数,当该函数执行时访问了执行上下文的变量对象中的值时,就产生了闭包。

简单地说就是在同一个执行上下文中函数访问了函数作用域外的变量对象的值就产生了闭包。

function bb(){
var a=2;
function cc(){
console.log(a);//2
}
return cc;//将cc函数对象本身当作一个返回值
}
var jj=bb();//bb()执行之后cc()的返回值就会被赋值给jj,调用jj();
jj();//2,直接console.log(jj())其值也是2,这就是闭包的效果;

实际上在考虑JavaScript引擎的执行顺序的时候,当bb();被执行完之后,按理说就会被“垃圾回收”掉,但是因为cc()在bb()的作用域内,cc()拥有涵盖bb()作用域的闭包,所以bb()不会被回收,而是一直存在以供cc()在任意时刻的使用。或者说一个函数内的变量可以在函数外被调用是闭包的效果,而这个通过各种手段将函数传递到其词法作用域外部,它都会持有对原作用域的引用,这个引用就叫闭包,无论在何处使用该函数都会调用其闭包。其最显著的表现就是使用回调函数。

循环与闭包

了解了闭包的概念,在for循坏中的使用是非常考究的。

for(var i=1;i<=5;i++){
(function(){//这个函数作用域里面是没有实际意义的,也就是i是不能在for循环的每一个迭代中被捕获的,输出的结果也是最后一个不满足条件的迭代i=6;
setTimeout(function timer(){
console.log(i);//将会输出5个6
},i*1000);
})();
}

上面的代码不会循环输出我们想要的1~5是因为for循环的大括号是不会产生自己的作用域的,那么在这个代码中其实是没有闭包产生的,每一个迭代都被封闭在一个共享的作用域里,因此实际上是只有一个i,setTimeout调用执行的变量对象其实是上面循环中第一个不再满足条件的值,即6。

想要循环输出1~5怎么办呢?为每一个迭代都设置一个闭包作用域,即在执行上下文中设置一个变量来传递每一个迭代的i.

for(var i=1;i<=5;i++){
(function(){
var j=i;//通过声明一个变量,传递i,使得上下文内有一个自己本身作用域的变量。
setTimeout(function timer(){
console.log(j);//1,2,3,4,5
},j*1000);
})();
}
//或者更加精简的写法
for(var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);//1,2,3,4,5
},j*1000);
})(i);
}

当然,ES6里let的出现就让一切都变得简单了。块级作用域与闭包强强联手,天下无敌了。

for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);//1,2,3,4,5
},i*1000);
}

单例模式与闭包

在JavaScript中有很多解决特定问题的编程思想(设计模式),例如工厂模式,发布订阅模式,观察者模式,单例模式等。其中单例模式是最常用的设计模之一,它的实现与闭包息息相关。

简单的介绍几种常用的单例模式:

1. 最简单的单例模式

对象字面量的方法就是最简单的单例模式。

var per={
name:'Jano',
age:21
getName:function(){
return this.name;
},
getAge:function(){
return this.age;
}
}

上面的代码块就是一个单例模式,也就是一个实例的创建。但是这种单例模式是很不安全的,其属性是可以被外部修改的(即per.name/age可以被引用重新赋值)。在很多场景中为避免出现意料之外的bug,一般情况下我们都希望定义的实例发生不可控的修改。

2. 私有方法/属性的单例模式

我们知道想要一个对象拥有私有方法属性,只需要创建一个单独的作用域将该对象与外界隔离即可,常用的就是借助匿名函数自执行的方法。

var per=(function(){
var name='Jano';
var age=21;

return{
getName:function(){
return name;
},
getAge:function(){
return age;
}
}
})();

//访问私有变量
per.getName();//Jano

私有变量的好处就在于对其的操作都是可控的,你可以提供一个getName方法让外部可以访问该name属性的值,也可以提供一个setName的方法让外部可以修改name属性的值。

3.调用时才初始化的单例模式

有时候也叫“懒汉单例模式”,即用到才创建实例,没用到就不创建。上面的两种单例模式也叫“饿汉单例模式”,即不管有没有用到都先创建一个实例。“饿汉单例模式”在代码运行时间上有一定优势(省去了判断的时间),相当于事先准备好食材了,用到的时候直接用就可以了,但是对于一些访问频率低的实例,会造成一定空间浪费。所以我们在实际使用过程中根据实例的使用频率来判断怎么创建一个实例。

var per=(function (){
//定义一个变量,用于保存实例
var instance=null;
var name='Jano';
var age=21;
//初始化方法
function inital(){
return {
getName:function(){
return name;
},
getAge:function(){
return age;
}
}

return{
getInstance:function(){
if(!instance){//判断是否存在
instance=inital();//创建一个实例
}
return instance;//返回该实例
}
}
}
})();
//使用到时获取实例
var p1=per.getInstance();
var p2=per.getInstance();
//因为在p1的时候已经创建了实例,所以在if判断时就已经判断到该实例的存在,
p2时就不会再去创建实例,而是直接使用之前的实例。

console.log(p1===p2);//true

模块化与闭包

模块化编程是现在最流行,也是必须要掌握的一种开发方式。模块化编程是建立在单例模式上的,一个单例就是一个模块。而单例模式又与闭包息息相关,无论是require还是ES6的modules,其核心思路都类似。

这里就以建立在函数自执行基础上的单例模式来介绍一下模块化编程的使用。

//创建一个专门用于管理全局状态的模块,该模块中的私有变量ststus保存了所有的状态值,并对外部提供了访问与设置的私有变量方法。
      var module_status=(function(){
          var status={
              number:0,
              color:null
          }
          var get=function(prop){
              return status[prop];
          }
          var set =function(prop,value){
              status[prop]=value;
          }
          return{
              get:get,
              set:set
          }
      })();
      //创建一个颜色的模块,负责背景颜色的显示
        var module_color=(function(){
          var state=module_status;
          var colors=['orange','#ccc','pinl'];
          function render(){
              var color=colors[state.get('number')%3];
              document.body.style.backgroundColor=color;
          }
          return{
              render:render
          }
      })();
      //创建创建管理状态的模块,将颜色的管理与改变方式进行定义绑定,
      使用时只需要调用render方法即可。
           var module_context=(function(){
          var state=module_status;
          function render(){
              document.body.innerHTML='this Number is'+state.get('number');
          }
          return{
              render:render
          }
      })();
//创建主模块借助功能模块,达到想要的效果
      var module_main=(function(){
          var state=module_status;
          var color=module_color;
          var context=module_context;
          setInterval(function(){
              var newName=state.get('number')+1;
              state.set('number',newName);
              color.render();
              context.render();
          },1000);
      })();

上面的代码可以看出,模块化最主要的一个特点就是将一个功能写在一个实力里面,这样在调用的时候就可以直接使用。

说到这里,关于闭包已经说的差不多的。实际上对于闭包,很重要的一点就是对对象作用域的理解,在《你不知道的JavaScript》中对象作用域的讲解还会具体说到词法作用域的概念,有兴趣的朋友也可以去了解一下。