essay | tech | year-summary | about
日期:2018-12-12T00:00:00Z
近来发觉coffeescript很好玩,以前在chrome中debug js的时候总会遇到写在chrome内部的coffee script,这也是偶然接触它的动机。
可以在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);
};
};
这就是一个典型的闭包。
我们会注意到,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。
我们可以做一个典型的闭包测试,来验证这一结果。
闭包经常会造成的一种编程错误,就是闭包中的函数,调用外部函数的变量(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。
当然,我想不难发现,这个本身就应该是一个错误,而不是闭包的运行错误。
因为我们是定义了一个未运行的函数,未运行的函数中,有在运行时候,就已经取消了它的定义的变量(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的原因。
那么好了,我们来测试,在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行),所以给出的结果也是未定义。
未定义乘以未定义,输出的结果就多少会不可预料(笑)。
那么为什么这个函数中的函数,就不会是闭包呢,
(funciont() {}).call(this);
唔...这个就需要接下来的继续调查了。XD
打算把这篇整理之后发到qiita上面去。
ref:
[1]クロージャ
[2]Do we have closures in C++?
[3]You Don't Know JS: Scope & Closures
[4]クロージャ(Closure) by N-qiita