闭包的规范使用以及原理

在了解闭包之前先了解一下目前主流对闭包的定义:

1.闭包是指有权访问另一个函数作用域中的变量的 函数 。(Nicolas C.Zaka)
2.当函数可以记住并访问所在的词法作用域时,就产生了闭包,这个函 数持有对该词法作用域的引用,这个 引用 就叫做 闭包。(KYLE SIMPSON)

本人从理解角度来将更赞同后者的定义,从个人角度来讲,后者的定义能对于学习和应用者来说会更容易理解一点。
接下来首先了解下闭包中所涉及到的几个核心的知识点和概念,对于基础知识比较薄弱的开发者来说将会比较容易理解,涉及到一下几个概念:

1.JavaScript的作用域链和作用域。
2.JavaScript的标识符查找机制。
3.JavaScript的函数表达式和函数声明。
4.JavaScript的垃圾回收机制。

作用域链,作用域

作用域链就是指在调用变量或者函数时产生的搜索域的过程。
JavaScript的作用域就是词法作用域而不是动态作用域
词法作用域最重要的特征是它的定义过程发生在 代码的书写阶段。
动态作用域的作用域链是基于 调用栈的 词法作用域的作用域链是基于 代码中的作用域嵌套。

举例说明:
function fn1(){
console.log('我是函数1的');
console.log(infor);
}
function fn2(){
var infor = '我是函数2的';
fn1();
}
var infor = '我是全局的';
fn2();

这个会输出是么呢?
//我是函数1的
//我是全局的
当fn2执行的时候,会执行fn1,同时在fn2中定义一个局部变量infor,在执行fn1时,调用到输出变量infor时, 因为JavaScript是词法作用域,所以函数fn1执行时,会沿着定 义时的作用域链查找变量,而不是执行时, fn1函数定义在全局中,所以查找到了全局的infor, 输出了'我是全局的'而不是'我是函数2的'。

标识符查找机制

作用域链在本质上就是 指向变量对象的指针列表,只引用但是不实际包含变量对象,变量、函数等都存在于各自作用域的变量对象中,通过访问变量对象来访问它们。
只有在函数调用的时候,才会创建执行的环境和作用域链,与此同时每个相关环境都只能逐级向上搜索作用域链,来查询变量名和函数名等标识符,也就是在调用到一个变量或者函数的时候,会先在自己 的作用域中去查找,如果存在就使用,如果不存在就去自己 作用域的父作用域中去搜索,以此类推直到搜索到调用变量或者函数。

函数表达式,函数声明

var fn = function(){};//函数表达式
或者这样:
var fn = function hand(){};//函数表达式
两者的区别是前者是匿名的函数表达式,后者是具名的函数表达式。
function hand(){};//函数声明
还有一种大家常见的使用方法:
(function(){})();//这也是函数表达式,是立即执行函数。
函数声明和函数表达式的区别:
(1)函数表达式可以在后面加个‘()’立即执行,但是函数声明不可以。
(2)函数声明具有函数声明提升,而函数表达式不会。
注:关于函数和变量声明的提升不了解的可以查阅下相关资料。

垃圾回收机制

JavaScript最常用的垃圾收集方式是标记清除,垃圾收集器会给存储在内存中的所有变量都加 上标记,然后去除环境中的变量,以及被环境中的变量引用的变量的标记,说明这些变量还有 作用,暂时不能被删除,然后在此之后被加上标记的变量就是要删除的变量了,等待垃圾收集 器对他们完成清除工作。
对函数来说,函数执行完毕后,会自动释放掉里面的变量,可是如果函数内部存在闭包,它们 就不会被删除,因为这个函数还在被内部的函数所引用,所以他不会被加上标记,不会被清 除,而是会 一直存在内存中得不到释放! 除非使用闭包的那个内部函数被销毁,外部函数才能得 到释放
这也是闭包存在和使用所依赖的核心机制, 所以,虽然闭包强大,但是我们不能滥用它,且在没有必要的情况下尽量不要创建闭包,不然 将会有大量的变量对象得不到释放,过度占用内存。 下来举个例子说明:

闭包

前面的概念可能对于初学者和基础比较薄弱的人来说有点难理解,闭包其实可以简单的这样理解:闭包就是能够读取其他函数内部变量的函数。为什么这样说呢, 在我们的实际运用中,很多时候我们需要访问到函数内部的变量,也就是访问函数内部的局部变量,正常的情况下这种需求是办不到的,因为在JavaScript语言中, 父域中不允许访问子域的变量,但是子域可以访问到父域中的变量,那就只有通过闭包的方式实现了,
举例子说明。

function fn1(){
      var robot='闭包';
      function fn2(){
              alert(robot); // 闭包
        }
  }

简单的解释下这段代码:函数fn2在函数的fn1的内部,也就是说fn2内部的域是fn1的子域,那么fn1内部的所有局部变量对fn2都是可见的,也就是说fn2都是可以访问的,但是反过来就不行了,fn2内部的局部变量对于fn1是不可见的,也就是说fn1是访问不到的,这就是JavaScript语言特有的Chain Scope(链式作用域),子对象在调用非内部局部变量的时候,会一级一级向上寻找所有父对象的变量。因此,父对象的所有变量,对于子对象都是可见的,反之则不成立。
那么fn2可以访问fn1中的局部变量了,那么只要把fn2作为返回的值,那么在fn1的外部就可以访问fn1内部的变量了,这样问题就解决了,那么fn2函数就是闭包。

闭包的用处

闭包主要有两个用处:一个是可以访问函数内部的私有变量,另一个是让这些变量的值始终保持在内存中,不会被垃圾回收机制所清除。举例说明:

function fn1(){
      var myname='我叫张三!';
      opreat=function(){
                 myname = '我改名字了,叫李四';
               }
      function fn2(){
            console.log(myname);
          }
     return fn2;
  }
  var robot=fn1();
    robot(); // 我叫张三!
    opreat();
    robot(); // 我改名字了,叫李四

在这段代码里面,robot函数也就是fn1函数运行了两次,第一次运行输出是“我叫张三!”,第二次运行输出是“我改名字了,叫李四”,这就说明函数fn1中的私有变量myname一直在内存中保存,并没有在第一次运行结束后被垃圾回收机制所清除,这就是由闭包的引用函数内部私有变量引起的,fn1是fn2的父函数,但是fn2被赋值给了一个全局变量,这样就会使fn2始终保存在内存中,但是fn2又依赖于fn1的存在而存在,所以它们就不会被垃圾回收机制所清除。
还有其中的opreat函数,并没有使用var声明,所以就会默认是一个全局变量,再有就是opreat函数其实也是一个闭包,就是相当于一个在函数外面可以操作函数私有变量的函数,但是这样会导致在这一条作用域链上的函数和变量都不会被内存回收机制所清除,一直占用内存。那么问题来了,看下面这段代码会输出什么?

function fn1(){
     var myname='我叫张三!';
     opreat=function(){
             myname = '我改名字了,叫李四';
           }
     function fn2(){
          console.log(myname);
     }
     return fn2;
  }
fn1(); // ???
opreat();
fn1(); // ???

输出是:
//我叫张三!
//我叫张三!
这样一对比是不是就比较轻易理解了呢,其实就是没有被全局引用所以不会在内存中保存myname变量。

通过闭包实现模块模式

function showModule(){
   var someinfor="This is a module case.";
   function doSomething(){
     console.log(someinfor);
   }
   return {
     'doSomething':doSomething
   };
}
var obj=showModule();
obj.doSomething() //This is a module case.

我们通过调用showModule函数创建了一个模块实例,函数返回的这个对象,实质上可以看做是这个模块的公告API,是不是有些像其它面向对象语言中的class?(没有其他语言基础的人略过)

再来通过闭包实现一个单例模式:

var application = function(){
   var components=[];
   /*
   一些初始化操作
   */
   return{ //公共API
   getComponentCount:function(){
     return components.length;
   },
   registerComponent:function(component){
     components.push(component);
   }
   };
}();

这个例子创建了一个单例对象,函数里返回的对象字面量是这个单例模式的公共接口。 通过闭包实现模块模式,可以做到很多强大的事情,模块模式能成功实现,最关键的是返回的 API还能继续引用定义时所在的作用域,从而进行一些操作,也就是说,作用域并没有因为函 数执行后被销毁,也就是没有被内存回收,之所以没有被回收是因为闭包的存在和JavaScript 的垃圾回收机制。

闭包的注意事项

1)因为闭包使用过程中所引用到的变量都会被保存在内存中,所以对内存消耗很大,滥用会造成网页的性能问题,在IE中可能会导致内存泄漏,普遍的解决方法是在退出函数之前,将不使用的私有变量都删除掉。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包和循环

当循环和闭包结合在一起时,经常会产生让初学者觉得匪夷所思的问题。

function createFunction(){
   var result=[];
   for(var i=0;i<10;i++){
     result[i]=function(){
       return i;
     };
}

这个函数执行后,会创建一个 由十个函数组成的数组 , 并且产生十个互不相干的函数作用域 ,表面上看调用第几个函数就会输出几,但是结果并不是这样

var result=createFunction();
  result[0](); //10
  result[9](); //10

产生这种奇怪的现象的原因就是之前说的,createFunction的变量对象因为闭包的存在没有被释放,注意 闭包保存的是整个变量对象,而不是只保存只被引用的变量 ,在createFunction执行后,创建了十个函数,同时变量 i 没有被释放,依然保存在内存中,所以此时它的值保留为停止循环后的10。
当我们在外部调用函数时,函数沿着它的作用域链开始搜索所需要的变量,前面说过,JavaScript的作用域链是基于定义时的作用域嵌套,所以当我们调用某个函数比如 result[0]它就会首先在自己的作用域里通过RSH搜索 i ,显然 i 不存在这个作用域中,于是它又沿着作用域链向上一级作用域中搜索 i ,然后找到了 i ,但是此时createFunction函数已经执行,循环也已经执行完毕了, i 的值为10,所以获取到的i,值就为10,同理,其他的函数执行时,查找的i 也会是10, 所以每个函数执行结果都是输出10。
关键所在就是尽管循环中的十个函数是在各自的迭代中分别定义的,但是它们都处于一个共享的上一级作用域中,所以它们获取到的都是一个 i,所以解决此类问题的关键就是让函数查找i时,不找到createFunction的变量对象那一级 ,因为一旦向上搜索到createFunction那里,得到的就是10。所以我们可以通过一些方法在中间来截断本该搜索到createFunction变量对象的一次查找。

首先我们可以这样:

functioncreateFunction(){
   var result=[];
   for(var i=0;i<10;i++){
     (function(){
       result[i]=function(){
         return i;
       };
     })();
   }
   return result;
}

我们通过定义一个立即执行函数表达式,在result[i]函数上一级创建了一个块级作用域,如果我 们把这个块级作用域叫做 a,那么它查找i时是这样一条链 result[i]->a- >createFunction,之所以还会查找到createFunction中,是因为 a中没有 i这个变量,所以 我们需要做些什么,让它搜索到 a时就停下

function createFunctions(){
   var result=newArray();
   for(var i=0;i<10;i++){
     (function(i){
       result[i]=function(){
       return i;
     };
     })(i);
   }
   return result;
}

现在a这个块级作用域里定义了一个变量 i ,这个 i 与上级的 i 不会互相影响,因为它们存在各 自的作用域里, 同时我们将该次迭代时的 i 值赋给了 a这个块级作用域里的 i ,即a中的 i 保存了当次迭代的 i ,result[i]在外部执行时,是这样的调用链 result i -> a在a中就能找到需要 的变量,不需要再向上搜索,也不会查找到值为10的 i ,所以调用哪个result[i]函数,就会输出 哪个 i 。

在 ES6 中我们还可以使用 let 来解决此类问题
function createFunction(){
   var result=[];
   for(var i=0;i<10;i++){
     let j=i;
     result[i]=function(){
       return j;
     };
   }
   return result;
}
//输出一下
console.log(createFunction()[2]()); //2
let会创建一个块级作用域,并在这个作用域中声明一个变量。所以我们相当于在result[i]上套了 一层块级作用域
function createFunction(){
   var result=[];
   for(var i=0;i<10;i++){
   //块的开始
     letj=i;
     result[i]=function(){
     return j;
   };
   //块的结束
   }
   return result;
}
这种方式解决此类问题,与前面没有多大分别,总之就是为了不让函数调用时去查找到最上级 的那个 i 。

其实,如果在for循环头部来进行 let声明还会有一个有趣的行为:

function createFunction(){
   var result=[];
   for(let i=0;i<10;i++){ //每次迭代,都会声明一次i,总共声明10次
     result[i]=function(){
       return i;
     };
   }
   return result;
}
console.log(createFunction()[2]()); //2

这样在for头部使用 let声明, 每次迭代都会进行声明,随后每次迭代都会使用上一个迭代结 束时的值来初始化这个变量。

var 和 let 以及const声明的区别在后面章节会补充。

闭包和定时器(或者延时器)

模拟下场景,比如说现在需要第一秒输出1,第二秒输出2,第三秒输出3......等,这样的需求呢?

for (var i = 1; i <= 5; i++) {
   setTimeout(function timer() {
     console.log(i);
   }, i*1000);
}

会是理想的那样输出吗?
输出都是6,隔一秒输出一个,
为什么呢?
i是一个全局变量,在第一次输出的时候i就已经是6了,所以往后的都是6了,那可以修改下使之达到功能呢?


for (var i = 1; i <= 5; i++) {
   setTimeout(function timer(i) {
     console.log(i);
   }, i*1000);
}
这样子可以了吗?在我初学的第一感觉是,妥妥的,完美.我承认有这样的想法的确实还是有点功底的,但是没有分清立即执行函数和函数声明,改成这样就可以了:

for (var i = 1; i <= 5; i++) {
   setTimeout(function timer(i) {
     console.log(i);
   }(i), i*1000);
}
或者这样改动:
for (var i = 1; i <= 5; i++) {
   {
   let j = i;
   setTimeout(function timer() {
     console.log(j);
   }, i * 1000);
   }
}
利用let创建作用域块,就可以实现功能了.