游荡于六道众生之外的邪灵:闭包

  你以为它死了,其实,它只是换一种方式,在你看不到的地方活着。  --肖蜀黍,为闭包立题。
       它,就是javascript世界里,据说是反人类的一种设计:闭包。
  闭包就像僵尸,集天地之怨气、怒气、死气与晦气而生。不老不死不灭,被天地人三界摒弃在众生六道之外,浪荡无依,流离失所。
  

  任何懂OOP(面向对象的“变成死相”)的工程师都知道,调用完某个方法后,方法内部的变量就会被销毁,外部不可能访问到这个方法内的变量。然而,在javascript里,这条定律被颠覆了。

  请看下图:变量作用域+函数 -> 闭包。 有了这个东西,即使创建变量的上下文环境不复存在了,我们依然可以拿到这个变量。


 它是js里很难理解的一部分,然而却是十分重要。前端开发者们也许并不完全弄懂它的原理,然而很可能经常接触到它。它的定义如下:
    1. WIKI定义:函数闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
    2. MDN定义:闭包是指能够访问自由变量的函数。换句话说,定义在闭包中的函数可以“记忆”它被创建时候的环境。
    3. 百度百科:闭包是指可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)、
    4. 阮一峰定义:闭包就是能够读取其他函数内部变量的函数。打算

  如上四条定义的描述不一致,但都提到了一个东西,那就是“自由变量”。本文的第二张配图(来自网络)。这个灰色的东西,就是定义里提到的“自由变量”。

       我知道要用严谨的学术来讲清楚这个东西,一定会让人一脸懵逼啊。 那么咱就说用轻松愉快地聊天吧,别掀翻了友谊的小船才好……

  首先,什么是自由?文章的开篇说了个玩笑,就像僵尸,是死而复生的东西。换言之,不属于它原来的世界(创建它的环境,通常是内部函数),也不属于死后的极乐世界(被垃圾回收,再变成内存),但是它的精神是自由的(仍然被保存在栈内),可以出来危害人间(被外部函数引用)。

  然后,什么是变量?意思是能够随意变换(值或者是数据类型),并且保存运算的结果。

       最后,自由的变量是什么呢,用两张图来解释一下。

 

  (G:Globe/全局环境;F:Function/普通函数;N:Nested/内嵌函数;a,b,c:随意命名的变量,就像张三李四王五)

 

  如上图:G是全局环境,它有函数F和变量a;F有函数N和变量c。正常情况下是这样的规则:

  • G不可能访问到N内的变量c
  • N可以访问自己的变量c
  • N执行完后,变量c就会被销毁

 

  但是我们有一种方法,能让N内的c逃逸出来,变成这个样子:

 

  如上图,G调用了F,F执行完后变量b就死了,但是变量c带着它在N内的记忆(函数运算后的结果),逃逸出来啦!

     但是必须注意,尽管c来到了G的世界,但它毕竟是僵尸啊,所以跟a不是同一种变量,它有自己的特点:

  • G可以访问变量c,但必须通过F的实例
  • 变量c拥有在N内所有的记忆(保存N的计算结果)
  • 变量c还可以与F外的东西有交互(比如传参开始新的计算)

       那么问题来了,什么方法这么神奇呢? 简单的例子是酱紫的:

 function number(initial) {
        var sum = initial || 0;
        function add(addend) {
            sum += addend;//sum是自由变量
            return sum;
        }
        return add;//创造了闭包
    }
 var instance = number(1);//此时sum并没有死,它还能继续发挥作用
 console.log("add 2:" + instance(2));//3
 console.log("add 3:" + instance(3));//6
   函数number内定义一个函数add,add引用了它外部的变量sum,再由number把add函数返回。
那为什么sum仍然存在?首先得搞清楚,函数执行完后,这个函数的内部变量为啥会死?

世间的生灵都有生死轮回,人的一生就像个写好了的函数,执行到最后就会死掉,死了就会有黑白无常来收走灵魂。
javascript的世界同样如此,函数执行到最后,任务已经完成,那么变量就会被垃圾回收器(GC)收回去。
注意到没,“任务已经完成”才会被回收!用什么来判断任务已经完成呢,GC认为,变量不再被引用就回收了。
而自由变量sum,仍然被外部的变量instance引用,所以GC就会放过它,还把它留在内存栈,等待有缘人。
阳寿未尽,自由变量add借助引用者instance做相关的事,而且只有引用者才能看到它。一旦引用者也死了,自由变量自然会死。


再回到正题,存在即合理。那么闭包存在有啥作用呢?常常用来模拟私有变量,上一个MDN的例子:
    var Counter = (function() {
        var privateCounter = 0;
        function changeBy(val) {
            privateCounter += val;
        }
        return {
            increment: function() {
                changeBy(1);
            },
            decrement: function() {
                changeBy(-1);
            },
            value: function() {
                return privateCounter;
            }
        }
    })();

    console.log(Counter.value()); /* logs 0 */
    Counter.increment();
    Counter.increment();
    console.log(Counter.value()); /* logs 2 */
    Counter.decrement();
    console.log(Counter.value()); /* logs 1 */

  于是开发者就可以尽情玩OOP啦,封装是多么重要!(下一期我会整理关于对象模型的相关知识,还有与函数相关的原型链、第一公民,敬请期待 ^_^)

这类例子在javascript世界实在太多了,比如jQuery的源码,用的是IIFE(immediately-invoked function expression)语法,就是基于闭包原理。

当然,有利也有弊,常见的错误就是循环内被创建,出现意想不到的结果,比如事件监听(此外还有延时函数,在此不作赘述):
    var list = document.getElementsByTagName("li");
    function register() {
        for (var i = 0; i < list.length; i++) {
            console.log('register the li:' + i);
            list[i].onclick = function () {
                console.log("you click the list:" + i);//i是一个自由变量,由for循环创造
            };
            //变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。
            //不过,变量的生存期是可以很长,长到即使创建它的函数不在了,仍然可可以存在很长一段时间
        }
    }
    register();//每次点击一个Li结点,控制台都会输出同一个值(for循环的最大值)

 

   解决这个烦恼通常有如下三种办法:
  1. 独立保存循环变量的值
  2. 使用匿名函数隔离
  3. 新建函数作用域

      代码如下:

  /* 方法一:每个函数都有一个独立的上下文,所以解决这个办法最直接的是指定自己的上下文*/
  function register2() {
    for (var i = 0; i < list.length; i++) {
        console.log('register the li:' + i);
        list[i]._i = i ;
        list[i].onclick = function () {
            console.log("you click the list:" + this._i);//i是一个自由变量,由for循环创造
        }
    }
}
  //register2();


   /* 方法二:既然是要隔离上下文,那只需要一个匿名函数来隔离就行了*/
    function register3() {
        for (var i = 0; i < list.length; i++) {
            console.log('register the li:' + i);
            (function(i){
                list[i].onclick = function () {
                    console.log("you click the list:" + i);//调用的是自己上下文的i
                }
            })(i);//再嵌一个函数,把注册事件都隔离开来
        }
    }
    //register3();


    /* 方法三:顺着这个思路,匿名函数也可以抽出来*/
    function click(i){
        list[i].onclick = function () {
            console.log("you click the list:" + i);
        }
    }
    function register4() {
        for (var i = 0; i < list.length; i++) {
            console.log('register the li:' + i);
            click(i); //已经把i传递到另外一个函数啦
        }
    }
    register4();

 

      所以呢,闭包有风险,最好不要随便惹它。换言之,也是MDN的一句话作为结尾:

      在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

 

 

 

posted @ 2016-05-30 17:27  肖大叔的小巫  阅读(456)  评论(0编辑  收藏  举报