闭包

闭包都被说烂了…,当做对自己学习的一个总结吧…jQuery(er)大哥写的《JavaScript ninja》里介绍的赶脚是比较清晰的,以下算是对原著翻译加读后感吧。前面理论的有个印象,后面的码才是重要的。

  • 什么是闭包
  • 闭包的意义
  • 闭包是怎么工作的
  • 闭包的应用场景

First Blood:What the fucking closure is?

当一个函数声明时,便会创建一个与此函数相关的scope,这个scope允许该函数获取或者操作函数外部的变量,换句话说,这个scope里包含了函数外部所有的变量。即通过这个scope,函数可以获取到函数声明时所处的作用域内所有变量,这个scope,就是闭包。

故理论上所有函数声明时就都会创建闭包,所谓的”函数外部所有的变量”,其实是指函数的上下文作用域链。但是实际上我们特指那些所处的上下文已被销毁(外部函数调用完毕),但内部函数依然存在,且引用了外部函数变量的情况。

double kill:what’s the meaning of closure?

  1. 函数被调用时,会创建出一个execution context(执行上下文),然后放到execution stack(执行栈)的顶端(入栈)。
  2. execution context分两个阶段,创建阶段和执行阶段。
  3. 创建阶段,JavaScript解释器首先创建一个activation object(活动对象),activation object包含了所有execution context内的变量函数声明arguments ,然后初始化scope chain(作用域链),scope chain中包含了execution stack(执行栈)里每一个execution context(执行上下文)。
  4. 至此创建阶段完成,开始进入执行阶段,执行阶段代码被解析执行。
  5. 此过程中函数局部变量的保存方式是通过动态分配内存实现的,如果有内部函数引用了该函数的局部变量,则将局部变量存至堆中,否则存在栈中,防止函数调用结束出栈所有局部变量被销毁。
  6. 内部函数声明时创建的闭包保存了外部函数上下文(内部函数本身也在闭包环境内),并保存在内存中(堆中),使得我们可以通过内部函数和它所在闭包,访问到外部函数中的信息,哪怕外部函数已经执行完毕。
  7. 内部函数调用完毕,JavaScript引擎认为可以清理时,被当做辣鸡回收,闭包被清除。

triple kill:How closure work?

先上马:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var beforeFun = "beforeFun";
var handler;
function foo(){
var inner = "inner";
function goo(){
return [beforeFun,inner,afterGoo,afterFun];
};
handler = goo;
var afterGoo = "afterGoo";
};
var afterFun = "afterFun";
foo();
handler();
// ["beforeFun", "inner", "afterGoo", "afterFun"]

我们以这个goo函数作为说明闭包的切入点,结合上面一套装逼理论,来说goo是如何通过闭包拿到那三个值的:

1
2
3
4
5
6
7
8
9
1,foo函数调用,创建foo的执行上下文,并将其放进执行栈,执行上下文创建过程中创建的执行对象包含了foo内的变量,函数声明,以及arguments,代码执行阶段使全局作用域中的handler引用了内部的函数goo;
2,foo的局部变量inner,afterGoo由于被goo引用,被分配至堆中,所以当foo执行完毕出栈时,inner,afterGoo没有同时被销毁。
3,goo声明时,产生了一个包含goo的scope(闭包),这个闭包保存了goo声明时所处的上下文内的变量,beforeFun,inner,afterGoo,afterFun都在这个闭包之中。
4,所以虽然foo执行完毕,但是由于内部函数goo所处的闭包存在,使得我们可以到foo的局部变量,并维持在内存中,直至goo执行完毕,被安全的当做辣鸡回收。
5,没有闭包的这种机制(动态分配内存,瞎编的...),foo执行完毕后,foo内部的变量随着foo被调用完毕被销毁就无法被拿到了,而由于闭包这种机制,我们在foo执行完毕后依然可以拿到并维持foo的内部数据。这大概就是闭包的意义...

闭包就像一个”气泡”,包裹了当前这个函数以及这儿函数以外的所有变量等信息,并保存在内存中,直到这个函数被javascript引擎清除,即垃圾回收,或者页面unload,闭包只是一个概念,没有专门的对象用来存储这些信息,但它是确实存在的

ultra kill:Putting closure to work

  • 获取私有化变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Ninja() {
    var feints = 0;
    this.getFeints = function(){
    return feints;
    };
    this.feint = function(){
    feints++;
    };
    }
    var ninja = new Ninja();
    ninja.feint();
    ninja.getFeints(); //1
    ninja.feints; //undefined

    我们把代码至于chrome的调试器中,并在两个匿名函数内打断点,执行并观察面板右侧chrome提供的闭包指示(只有在内部函数引用外部函数内资源,且在内部函数打断点时,chrome可以显示内部函数所处的闭包维护的变量);

closure

可以看到:

  1. ninja.feint指向的匿名函数内部的feints=0,即持有了Ninja函数的内部变量feints,执行后+1变为1;
  2. ninja.getFeints指向的匿名函数内部的feints=1,即与ninja.feint共同持有Ninja函数的内部变量feints;
  3. 通过闭包实现了面向对象的javascript编程,构造函数私有化变量,并通过内部函数所处的闭包,可以使实例能访问到构造函数的私有变量。
  • 在回调函数和timer中使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    示例1,
    $("button").click(function(){
    var $ele = $("#element");
    $ele.html("loading");
    jquery.ajax({
    url:"",
    success:function(data){
    $ele.html(data);
    }
    })
    });
    闭包在示例1中的意义:
    不通过闭包在html("loading")时和html(data)时每次都需要写$()去获取元素,而把元素保存在一个变量里,通过闭包,回调函数中直接拿变量就行了,无须再去$()获取元素,提高了速度。
    示例2,
    function animate(id){
    var ele = document.getElementById(id);
    var tick = 0;
    var timer = setInterval(function(){
    if(tick<10){
    ele.style.left = tick+"px";
    tick++;
    }else {
    clearInterval(timer);
    }
    },10);
    }
    闭包在示例2中的意义:
    假设ele与tick定义在全局作用域中,上述代码不会出什么问题。但是当再次调用animate(id2)时,问题便会出现,后面调用的animate中计数器受到影响。而如上所示的代码,不同的animate调用,即不同的匿名函数通过自身所在的闭包会维护自身的一份闭包环境,初始化的tick都是0,互不影响。
  • 绑定函数的上下文
    我们来看bind方法的简易实现,mdn有更为健壮的实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Function.prototype.bind = function(){
    var fn = this,
    arg = Array.prototype.slice.call(arguments),
    object = arg.shift();
    return function(){
    fn.apply(object,arg.concat(
    Array.prototype.slice.call(arguments)
    )
    )
    }
    };
    var str = "f**k";
    function anyFunction(arg){
    return arguments[0]+this+arguments[1];
    };
    var fun = anyFunction.bind(myObject,"suffix-");
    fun("-postfix"); //"pre-f**k-after"
    闭包在绑定函数上下文中意义:
    一眼看过去,bind函数返回一个引用了内部变量的函数,返回的函数内部通过闭包维持了bind函数内的两个局部变量。fn为被调用的函数自身,object为需要绑定的上下文(第一个参数),arg指除了第一个参数之外的预设定参数(函数柯理化)。
    闭包扮演的角色是bind函数返回的匿名函数内部总能通过闭包持有bind()中传递的object,作为返回函数的上下文,且这种预定于参数的方法可以实现延迟执行的效果(柯理化),虽然我并没有看出有什么卵用。
  • 为函数注册缓存方法
    对函数进行缓存处理,即首次调用执行函数并存储执行结果,重复调用则返回已经存储的结果,可以实现以空间换时间,提高数据处理的速度。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    Function.prototype.memorized = function(key){
    this.value = this.value || {};
    return this.value[key]!==undefined?
    this.value[key]:
    this.value[key] = this.apply(this,arguments);
    };
    //包装函数,调用函数时自动执行缓存方法memorized,而非显示的调用memorized
    Function.prototype.memorize = function(){
    var fn = this;
    return function(){ //匿名函数
    return fn.memorized.apply(fn,arguments);
    }
    };
    //判断是否为质数的函数
    var isPrime = (function(num){
    var prime = num !=1,i=2;
    for(;i<num;i++){
    if(num%i==0){
    prime=false;
    break;
    }
    }
    return prime;
    }).memorize();
    以上几个栗子基本上都是返回的匿名函数利用闭包来正确绑定函数的上下文,否则,返回的匿名函数默认上下文为全局对象(window或globle),匿名函数通过闭包进行上下文的修正。再多嘴一下,函数内部的上下文(context)指的是内部this的值。
  • 结合立即执行函数解决循环中计数器出现的问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    还记得那个典型的timer中计数器出现的问题么?
    for(var i=1;i<10;i++){
    setTimeout(function(){
    console.log(i);
    },500)
    }
    结果都是10,因为匿名函数所在的闭包内维护了一个i变量,timer使匿名函数异步执行,在循环后匿名函数才开始执行的,而此时的i已经变为10了;要解决这个问题估计大家也能查到方法:如下
    for(var i=1;i<10;i++){
    setTimeout((function(i){
    return function(){
    console.log(i);
    }
    })(i),500)
    }
    用立即执行函数包裹后,内部的匿名函数维护的i变量为立即执行函数提供的变量i,而这个立即执行函数不是异步执行的,而是每次循环的时候就会执行,所以每次立即函数执行时传入的i都是不一样的,这样导致每次内部匿名函数维护的i也是不一样的。核心在于那个立即执行函数是非异步执行的。

理论知识上当然细读ecmaScript规范所得到的理解是最准确的,但是规范总归是最难看懂的,在理解尽可能接近准确的前提下,代码表现上的正确可以作为接近理论准确的一个标准。

closure相关蚊帐;

未完待续…