闭包原理

在了解JAVA闭包的过程中偶然读到JS的闭包文章,讲解的很明了,所以就先从JS的闭包入手学习。文章出自廖雪峰的网站

#高价函数
JS中函数可以通过变更引用,当把函数的变量引用作为参数传递给其他参数时就构成了高价函数。比如以下add方法,将其中f指定为一个函数如Math.abs,则方法体中的f(x) + f(y)则可以转换成Math.abs(y) + Math.abs(y)。当x=-1,y=2时,add方法体计算过程为Math.abs(-1) + Math.abs(2)=1+2=3。

1
2
3
function add(x, y, f) {
return f(x) + f(y);
}

##map/reduce
map/reduce方法是Array类中的方法,其中map方法需要传入一个函数,他作用于数组中的每一个元素。比如定义pow函数,计算一个数的平方,传入map后,map方法会针对每个元素调用该pow函数,并将结果返回

1
2
3
4
5
6
7
function pow(x) {
return x * x;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

reduce方法同样需要传入一个函数,比如我们要对数组元素作累加操作,定义一个函数f = function(x,y){return x+y},函数的返回值与下一个元素合作为两个参数再将调用函数f。下边的例子可以分解成[1, 2, 3, 4].reduce(f) = f(f(f(1, 2), 3), 4)= 10。有点儿递归的意思

1
2
3
4
5
6
var arr = [1,2,3,4];
var f = function (x, y) {
return x + y;
}
var results = arr.reduce(f); // 10
console.log(results);

reduce方法的完整定义如下(参考文档):

1
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
  • previousValue
    上一次调用回调返回的值,或者是提供的初始值(initialValue)
  • currentValue
    数组中当前被处理的元素
  • index
    当前元素在数组中的索引
  • array
    调用 reduce 的数组
  • initialValue
    作为第一次调用 callback 的第一个参数。

回调函数第一次执行时,previousValue 和 currentValue 可以是一个值,如果 initialValue 在调用 reduce 时被提供,那么第一个 previousValue 等于 initialValue ,并且currentValue 等于数组中的第一个值;如果initialValue 未被提供,那么previousValue 等于数组中的第一个值,currentValue等于数组中的第二个值。

如果数组为空并且没有提供initialValue, 会抛出TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供initialValue, 或者有提供initialValue但是数组为空,那么此唯一值将被返回并且callback不会被执行。

更多的使用就不作展开了,这里帖下polyfill,可以更好的理解其中的处理机制

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
if(!Array.prototype.reduce){
Object.defineProperty(Array.prototype, 'reduce', {
value: function(callback/*, initialValue*/){
if(this === null) { //判断是否是是null调用的reduce函数
throw new TypeError('Array.prototype.reduce called on null or undefined')//使用===能判断出undefined?
}
if(typeof callback !== 'function') {//判断出入的参数是否是函数
throw new TypeError(callback + 'is not a function' )
}

// 1. let o be ?? ToObject(this value)
var o = Object(this) // 生成了一个数组o //得到数组的副本

// 2. let len be ? toLength( ? get(0, "lenght"))
var len = o.length >>> 0 //获取数组的长度

var k = 0 //当前索引
var value //最终返回的值

if(arguments.length >= 2) { //如果有第二参数 (初始值), 从数组的第一个元素开始遍历
value = arguments[1] //将其赋值给value, 在做累加
}else { //没有初始值
while (k < len && !(k in o)){ //当数组是稀疏数组时, 判断数组当前是否有元素, 如果没有就索引加一
k++
}

//3. if len is 0 and initialValue is not present //如果数组为空, 且初始值不存在。抛出一个错误
if( k >= len) {
throw new TypeError('Reduce of empty array with no initial value')
}
//把第一个非空元素的值给value, 并索引加一
value = o[k++]
}
//以上都是为了获得value的初始值。 以便一下累加

// 8. Repeat, while k < len
while (k < len){
// a. let Pk be ! ToString(k)
// b. let Kpresent be ? HasProperty(0, Pk)
// c. if kPresent is true, then
// i. let kValue be ? Get(0, Pk)
// ii. let accumulator be ? call(
// callbackfn, undefined,
// << accumulator, kValue, k, o >>).
if( k in o) {//如果当前索引处有值, 调用函数,累加
value = callback(value, o[k], k, o)// 传入4个参数, 第一个是value, 第二个当前值, 当前索引, 当前数组
}
k++
}
return value

}
})
}

/**
* 整体说一下该polyfill函数的思路
* 1. 如果javascript引擎不支持reduce函数, 那么久在Array的原型上定义了一个reduce的函数, 不过为啥要通过Object.defineProperty()来定义呢? 可以直接添加一个这样的函数就是了嘛
* 2. 在添加的函数中,判断是否是null调用。
* 3. 判断第一个参数是是否为函数, 如果不是抛出错误
* 4. 复制这个数组
* 5. 得到数组的长度
* 6. 设置当前索引
* 7. 定义value变量
* 8. 给value设置初始值, 如果传入了第二参数, 就将其设置为value, 否则找到第一个非空元素设置其为value的值
* 9. 循环调用callback累加, 其中会通过in 判断是否为空, 如果为空跳过调用callback、
* 10. 返回value
*/

同时,Array还有其他很多的高阶函数,比如filter(),sort(),every(),find()等等。

#闭包

上边介绍了高阶函数,即把函数当作参数传递给方法。而闭包则是方法返回函数。先看下reduce求和的例子

1
2
3
4
5
6
7
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}

sum([1, 2, 3, 4, 5]); // 15

当执行sum([1, 2, 3, 4, 5]); 时,则立即计算出结果,如果把sum定义成一个函数,当执行sum()时才进行计算结果,这时需要怎么做呢?操作如下:

1
2
3
4
5
6
7
8
9
10
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
var f = lazy_sum([1, 2, 3, 4, 5]);
f();

当我们调用lazy_sum时返回的是一个函数,并没有真正的进行计算,只有当调用f()函数才开始执行,计算求和。在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中这就构成强大的闭包。这里特别注意的sum函数的定义使用到了其作用域外的变量,即arr,当我们调用lazy_sum方法时返回的f函数,其内部保存了传递的参数[1, 2, 3, 4, 5]。换个角度说当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1(); // 16
f2(); // 16
f3(); // 16

count方法返回一个数组,每个数组元素是一个函数,这个函数引用了外部变量i,当count方法执行完毕后,i变成了4,这个时候再调用f1时,这个比引用的变量i已经变成了4,所以执行结果为16,其他元素也一样。并不是期许的1、4、9。如果说要达到1、4、9的效果需要怎么做呢?再看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function count() {
var arr = [];
var ft = function(n){
return function () {
return n * n;
}
}

for (var i=1; i<=3; i++) {
arr.push(ft(i));
}
return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

console.log(f1());
console.log(f2());
console.log(f3());

ft函数返回一个函数类型的返回值,这个函数引用的变量是ft的参数n。当执行for循环时,每次push进去的分别是ft(1)、ft(2)、ft(3),所以当调用f1()、f2()、f3()分别返回的是1、4、9。这里为啥i的变化没有影响到(传递给)求积函数呢?是因为i作为变量传递给了方法ft,ft重新分配内存,生成临时变量,这个临时变量被求积函数引用。而上一个例子中每次push进去的求积函数引用的变量i都是同一个,当for循环完了他们共同引用的变量已经变成了4,所以每个执行结果都是16。
这里有两个点需要注意:

  1. 闭包最大的特点就是返回函数引用了外层变量,所以当外层函数执行完,变量并没有销毁,还在被返回函数持有,或者说被返回函数hang住了,这就是闭包最厉害的地方。
  2. 返回函数直接引用的外层变量,外层变量发生变化,对于返回函数来说做不了任何事情,他既不能记录外层变量的变化状态,也不能持有某一时刻该变量的值,他只能眼吧吧的看着。只有,当,返回函数被调用执行时他才获取并使用此刻的外层变量,这时的外层变量是啥就是啥。
  3. 当返回函数改变外层变量时,当前的返回函数变量是可以感知的,例子如下(为啥c2的执行不是4、5、6?因为c2是重新执行函数create_counter()返回的对象,与c1无任何关系):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function create_counter(initial) {
    var x = initial || 0;
    return {
    inc: function () {
    x += 1;
    return x;
    }
    }
    }
    var c1 = create_counter();

    console.log(c1.inc());//1
    console.log(c1.inc());//2
    console.log(c1.inc());//3

    var c2 = create_counter();

    console.log(c2.inc());//1
    console.log(c2.inc());//2
    console.log(c2.inc());//3

#脑洞大开
很久很久以前,有个叫阿隆佐·邱奇的帅哥,发现只需要用函数,就可以用计算机实现运算,而不需要0、1、2、3这些数字和+、-、*、/这些符号。

JavaScript支持函数,所以可以用JavaScript用函数来写这些计算。来试试:

// 定义数字0:
var zero = function (f) {
    return function (x) {
        return x;
    }
};

// 定义数字1:
var one = function (f) {
    return function (x) {
        return f(x);
    }
};

// 定义加法:
function add(n, m) {
    return function (f) {
        return function (x) {
            return m(f)(n(f)(x));
        }
    }
}

// 计算数字2 = 1 + 1:
var two = add(one, one);

// 计算数字3 = 1 + 2:
var three = add(one, two);

// 计算数字5 = 2 + 3:
var five = add(two, three);

// 你说它是3就是3,你说它是5就是5,你怎么证明?

// 呵呵,看这里:

// 给3传一个函数,会打印3次:
(three(function () {
    console.log('print 3 times');
}))();

// 给5传一个函数,会打印5次:
(five(function () {
    console.log('print 5 times');
}))();

``

*上边说了半天闭包,但是正式的概念不好给出,是一种设计模式?还是语法特性?还是什么?所以也就不知道整体叫闭包函数,还是返回函数叫闭包函数,因此本文描述中使用了外层变量、返回函数这样的字眼*