0. 前言

近来发觉coffeescript很好玩,以前在chrome中debug js的时候总会遇到写在chrome内部的coffee script,这也是偶然接触它的动机。

1. 闭包(closure)是什么?

可以在c++ 11,python,js等等中接触到的一种类似于函数式编程的一种函数宣言模式。
在c++中是函数中的lambda,在python和js是函数中的函数。
我用coffee script举个例子。

c = ->
  count = 0;
  -> console.log ++count;

它编译出来是这样子的(省略头尾的function(){})

  var c;

  c = function() {
    var count;
    count = 0;
    return function() {
      return console.log(++count);
    };
  };

这就是一个典型的闭包。

2. coffee中的闭包(closure in coffeescript)

我们会注意到,coffee编译出来的结果是这样的。

a = -> console.log "hello world";
a();
// Generated by CoffeeScript 2.3.2
(function() {
  var a;
  a = function() {
    return console.log("hello world");
  };
  a();
}).call(this);

仔细一看,这难道不是满足函数中的函数的定义了么?这是闭包吗?
答案是no
我们可以做一个典型的闭包测试,来验证这一结果。

3. 闭包的一个基本特性 – 延迟代入

闭包经常会造成的一种编程错误,就是闭包中的函数,调用外部函数的变量(var)值。
比如下面的这个coffeescript。

nums = [1,2,3,4,5]
func = -> 
  for num, i in nums
      nums[i] = -> num*num;
func()
console.log num() for num in nums

编译出来是这样的。

// Generated by CoffeeScript 2.3.2
(function() {
  var func, j, len, num, nums;

  nums = [1, 2, 3, 4, 5];

  func = function() {
    var i, j, len, num, results;
    results = [];
    for (i = j = 0, len = nums.length; j < len; i = ++j) {
      num = nums[i];
      results.push(nums[i] = function() {
        return num * num;
      });
    }
    return results;
  };

  func();

  for (j = 0, len = nums.length; j < len; j++) {
    num = nums[j];
    console.log(num());
  }

}).call(this);

可能有的人会认为这段的输出应该是

1
4
9
16
25

然而输出是这样的

25
25
25
25
25

原因很简单。因为在闭包(closure)中,数值的带入是被延迟的。
也就是说,在for loop中,数值是在for loop结束之后,以for loop结束时候的数值被代入到闭包的函数中的。
在上面的例子中,num结束的数值是5,所以最后运行起来的结果,就是25。

4. 闭包的延迟小解析

当然,我想不难发现,这个本身就应该是一个错误,而不是闭包的运行错误。
因为我们是定义了一个未运行的函数,未运行的函数中,有在运行时候,就已经取消了它的定义的变量(var),所以这个变量在实际运行时应当是未定义的。所以它的定义应当是遵从编译器设计。

可能这么讲起来比较绕口。
还是这个函数的例子。

1 nums = [1,2,3,4,5]
2 func = -> 
3   for num, i in nums
4       nums[i] = -> num*num;
5 func()
6 console.log num() for num in nums

在第4行定义的函数,不过在第五行的时候,仍旧未被运行。
我们在内存管理的heap和stack中已经知道,在第4行末尾for文结束之后,num这个变量已经结束了它的生命周期,它已经在内存中被抹除。
而此时,我们仍旧还没有使用num。
所以理论上,当我们在第五行实际运行完毕for文之后,num应当是未定义的。
那么在第6行,我们实际上运行num[i]这个函数的时候,调用的明显是一个未定义的变量num。
那么num不会按照1,2,3,4,5的被调用也属于正常现象。
那么这里就应当遵从编译器的规则。
编译器在这里定义的num,在闭包中,是延迟定义的。
也就是说,是按照num的最后一次的数值所调用。
这也是产生5个25的原因。

5. coffee中的闭包(closure in coffeescript)

那么好了,我们来测试,在coffee中,是不是所有函数都是闭包呢。
同样是这个函数,我们写下以下的代码。

1 nums = [1,2,3,4,5]
2 for num, i in nums
3  nums[i] = -> num*num;
4 console.log num for num in nums
5 console.log num() for num in nums

编译出来的结果是这样的

// Generated by CoffeeScript 2.3.2
(function() {
  var i, j, k, l, len, len1, len2, num, nums;

  nums = [1, 2, 3, 4, 5];

  for (i = j = 0, len = nums.length; j < len; i = ++j) {
    num = nums[i];
    nums[i] = function() {
      return num * num;
    };
  }

  for (k = 0, len1 = nums.length; k < len1; k++) {
    num = nums[k];
    console.log(num);
  }

  for (l = 0, len2 = nums.length; l < len2; l++) {
    num = nums[l];
    console.log(num());
  }

}).call(this);

运行出来的结果是这样的。

[Function]
[Function]
[Function]
[Function]
[Function]
NaN
NaN
NaN
NaN
NaN

很显然和我们的预期,5个25是明显不同的。

这里并没有进行延迟调用,而是简单直接的给出了真正的“期待”答案:
因为num是未定义的(coffee版的第3行),所以给出的结果也是未定义。
未定义乘以未定义,输出的结果就多少会不可预料(笑)。

6. 下次内容

那么为什么这个函数中的函数,就不会是闭包呢,

(funciont() {}).call(this);

唔…这个就需要接下来的继续调查了。XD

7. 后记

打算把这篇整理之后发到qiita上面去。

ref: [1]クロージャ
[2]Do we have closures in C++?
[3]You Don’t Know JS: Scope & Closures
[4]クロージャ(Closure) by N-qiita


Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)