JavaScript 闭包
JS的闭包一直是我比较头疼的问题。自从开始学习计算机后,闭包这个难题就一直围绕着我。离散数学中也存在闭包概念,但是和程序中的闭包还不一样,所以一直把我搞得一愣一愣的,今天抽出点时间,从闭包的历史开始学起。
我们在工作中,都会或多或少的在使用闭包,也许你不清楚每一个闭包作用,也许你用闭包实现了一个功能却不自知。没关系,闭包并不用过多深入,它就像空气,就像水,你明白它是H2O,不用搞懂它的分子结构,我们只需让它自然的出现在我们的代码中即可。读这篇文章,你可以不求甚解,当经验够了,你将自然而然明白。
为什么叫闭包(Closure)?
Closure词汇最早在数学中出现,是用来描述集合关系的。那它和我们计算机中的闭包有什么关系吗?
没有,计算机中的闭包(Closure)和其他所有闭包(Closure)都没有关系!那为什么要叫做闭包?这不是容易和其他概念弄混淆吗?
为了摆脱以前闭包概念的影响,我们要记下闭包的概念:闭包是函数和声明该函数的词法环境的组合。
闭包概念不止一个,每个人都可以用自己的话来描述,我这里再举几个概念:
闭包就是能够读取其他函数内部变量的函数
当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。
从概念上来说,JavaScript中的所有function都是闭包!
JavaScript 中的闭包形式
例子:
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
代码做了什么?
我们首先声明函数makeFunc()
,它含有内部变量与内部函数,接着我们返回了内部函数displayName
。那么,myFunc
变量也就是displayName
的引用。
例中,displayName()
我们称之为一个闭包,它是由displayName
本身+在创建displayName()
时能访问的作用域组成。
例子2:
(function (document) {
var viewport;
var obj = {
init: function(id) {
viewport = document.querySelector('#' + id);
},
addChild: function(child) {
viewport.appendChild(child);
},
removeChild: function(child) {
viewport.removeChild(child);
}
}
window.jView = obj;
})(document);
代码做了什么?
拆开看,(function (document) {})()
这个意思是,我们定义了一个匿名函数,并调用它。在其内部定义了两个变量,并将jView
,设置为全局变量。
乍一看,我们并没有使用到闭包啊?window.jView = obj;
这句话就是在使用闭包。它把内部变量引用到全局,这样js的GC无法回收,则jView可以使用viewport变量。
闭包的应用
闭包一般用于保存函数内部变量,在模块化编程流行的时候,闭包起到了很重要的作用。我们想要封装一个模块,如何保存我们的内部变量,而又不污染全局变量?闭包就是一个很好的解决方法。
为什么闭包可以保持函数内部变量?原因是JavaScript的垃圾回收机制,A返回B,C保存B,这样垃圾回收就不会回收B与C。
闭包的缺点
因为闭包会保持内部函数,所以在内部函数的引用没有被释放之前垃圾回收是不会回收内部函数状态的。这样容易造成内存泄漏。
闭包函数会访问上层作用域,所以当上层作用域是独立的父函数时,可能会存在误操作,把父函数中的状态改变。
练习
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
output:The Window
这一题考验的是作用域链,function(){return this.name;}
虽然是闭包,可以访问外层this,但是它自身就存在this变量,匿名函数的this用于指向window。
function foo(x) {
var tmp = 3;
return function (y) {
alert(x + y + (++tmp));
}
}
var bar = foo(2); // bar 现在是一个闭包
bar(10);
bar(10);
bar(10);
output:16 17 18
这题没什么好说的,因为闭包,所以保持函数内部状态。
for (var i = 0; i < 4; i++) {
setTimeout(function() {
console.log(i);
}, 300);
}
output: 4 4 4 4
这一题和事件队列有点关系,像深入的同学可以看我以前一篇彻底搞懂JavaScript异步机制。
简单说一下,在执行时,js主线程执行for循环,for循环内部是异步操作,直接扔到异步操作队列,等待主线程执行完毕,挨个执行异步回调,当执行异步回调时,i已经等于4了。所以最后输出4个4。