Javascript中,闭包无疑是重要的一个环节,入门时多半会被弄晕。
下面先从一个例子入手:
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
alert(f1 === f2); // false
本以为,f1
和f2
完全是两个完全一样的函数,但是实际上lazy_sum
返回给f1
和f2
的两个匿名函数(参考本文第三部分)并不一样,即使看到“字面的表达式”是一样的,其实这涉及到更关键的一点是对于Javascript的“对象”的理解,这一点或许将来值得单独拿出来写篇文章。
一、闭包的定义
以上的lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种程序结构就被称为“闭包(Closure)”。(廖雪峰的定义)
在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。(阮一峰的的定义)
二、变量的作用域
要理解闭包,首先必须要明确的是Javascript特殊的变量作用域,也即全局变量和局部变量,这一点不难,可以简单地按照是否使用var声明变量来加以区分。
与变量的作用域相关的还有一个this
方法,以及that
指向。下面两段代码,在看完本文后不妨回来思考下如何解释两段代码的结果。
第一段代码:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
第二代代码:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
三、匿名函数
另一个需要明确的,就是匿名函数,举几个栗子:
//普通函数
function box() { //函数名是 box
return'Lee';
}
//匿名函数
function(){ //简单的匿名函数,单独使用会报错
return'Lee';
}
//通过表达式自我执行
(functionbox() { //封装成表达式,单独使用会报错
alert('Lee');
})(); //()表示执行函数,并且传参
//把匿名函数赋值给变量
var box=function(){ //将匿名函数赋给变量
return'Lee';
};
alert(box()); //调用方式和函数调用相似
//函数里的匿名函数
function box() {
return function(){ //函数里的匿名函数,产生闭包
return'Lee';
}
}
alert(box()()); //调用匿名函数
四、闭包与内存
接下来就是闭包最让人疑惑的地方。
闭包最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
如何理解呢?从一个简单计数器入手:
function getCounter(){
var counter = 1;
var add = function() {
counter++;
}
var desc = function() {
counter--;
}
var get = function() {
return counter;
}
return {
add: add,
desc: desc,
get: get
}
};
var counter1 = getCounter();
var counter2 = getCounter();
console.log(counter1 === counter2) //false
console.log(counter1.get()) //1
console.log(counter2.get()) //1
counter1.add()
counter1.add()
counter1.add()
counter2.desc()
counter2.desc()
console.log(counter1.get()) //4
console.log(counter2.get()) //-1
counter1 = null;
counter2 = null;
以上的例子可以看到,闭包内的函数add
、desc
可以反复调用,而父函数getCounter
里的变量值则是在不断累加的。也就是说函数counter1
及counter2
中定义的父函数getCounter
中的,由变量及子函数等一系要素构成该父函数的“运行环境”,一直停留在内存中,因而出现了累加的效果。
在JS的垃圾回收机制(garbage collection)中,如果一个变量发生过一次及以上的引用,那么这个变量就将停留在内存中,如以上代码,console.log(counter1.get())
即是一次对counter1
引用,所以这个getCounter()
将会作为counter1
的值一直停留在内存中,直到重新给counter1
赋值,例如counter1 = null;
就是一次清理内存的操作。在目前的小型脚本中,是不太会显示出内存占用问题的,而且现在新型的浏览器都有一定的内存控制机制,但是在古董级的IE浏览器中,可能就会面对严重的内存泄漏问题。所以及时清理无用内存是一个很好的习惯。
五、正确地使用闭包
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];
console.log(f1()); // 16
console.log(f2()); // 16
console.log(f3()); // 16
在这样一个程序中,我们期望得到的是[1,4,9]
,但是最终得到的却是[16,16,16]
。为什么呢?
因为内存停留的原因啊,f1
、f2
、f3
每一次赋值,返回的函数都引用了i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。
所以返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
那么如何解决呢?
再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i)); //所谓“绑定”,即是通过`i`传参,每次返回函数时初始化
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
console.log(f1()); // 1
console.log(f2()); // 4
console.log(f3()); // 9
六、大佬是这样玩的
几十年前,一位美国数学家阿隆佐·邱奇(Alonzo Church,1903年6月14日-1995年8月11日),提出了一套名为λ演算(Lambda calculus)的计算机运算逻辑,最简单的部分,不使用加减乘除以及数字,来进行四则运算。对的,如果让你算1+1,你可以伸出两个指头,让你算9x9,你要说你背得乘法口诀表,但是这些方法计算机并不理解啊,所以需要有一个方法可以让计算机实现以上得运算,于是λ演算就解决了这个问题,也因此今天你得以在计算器上执行各种复杂得运算,你以为显而易见的1+1等于2,其实背后有一套严密的运算逻辑(不要跟我讲哥德巴赫猜想,拒绝?)。
那么我们用JavaScript来演示一下λ演算的逻辑吧:
'use strict';
// 定义数字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');
}))();
其实这并不是真正意义上的加减乘除运算,而是通过循环引用制造的“计算假象”罢了 :P