之前做笔试题,好多关于JS闭包问题,就整理出来,分享一下。
首先,引用下所谓的“官方解释”:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
请注意加粗部分,会有益于你的理解。
来看例子1:
function a(){
var i = 1;
function b(){
alert(i);
}
return b();
}
var c = a(); c; // a();
例子2:
function a(){
var i = 1;
function b(){
alert(i);
}
return b;
}
var c = a(); c(); // a()();
上述两个例子的运行结果都是一样的,只不过应该会有很少人用例子2那样结尾a()()
那样的写法吧。嘻嘻,我也是今天才发现的。这样写的话,你是不是对函数后面的括号()
有了更深的理解呢?
没错,从上面我们可以看出来,函数声明定义时肯定少不了身份证()
,而当把一个函数名连带括号()
也赋给一个变量,这时的变量其实是指向了这个函数,也就是说把函数的指针赋给了此变量。像上面例子2结尾那样,a()()
中的前面部分a()
返回值(结果)是一个函数名(指向函数的指针),也就是说不会执行。所以要在a()
后面再加一个()
,才会执行,相当于b()
。因为a()
的返回值是b
,a()
()
也就相当于b
()
。
插播结束,返回来说“闭包”问题。
继续看例子:
function a(){
var i = 1;
alert(i);
}
function b(){
alert(i);
}
b();
上面的例子是会报错的:
现在返回去继续想“闭包”,你好惊奇的发现,例1和例2里,变量c竟然可以弹出了i
!它可是没有在变量c的同级作用域定义过啊!但是——因为“闭包”的存在:函数b()
以及函数a里的变量i
。
注意
上面提到的,“闭包”是包含着函数b()
以及变量i
,好多网上的文章只简单的提到了函数b()
,我认为,结合开篇中的“官方说法”,那种说法是不精确的。(变量也是表达式(闭包)的一部分)
JS闭包的有关概念。
首先应该知道的是JS中的作用域链(scope chain)概念:
(1)定义
每个人都有自己的生活环境,代码也一样,JS中的代码都有自己的“执行环境”。当代码在一个环境中执行时,就会生成变量对象的作用域链。这个链条,从当前环境中的变量对象开始,上溯至父环境(包含环境)中的变量对象,再到父环境的父环境中的变量对象···一直上溯到全局执行环境的变量对象。想象着一条锁链,从最下面开始爬,一直爬到最上顶端。每一个节点,代表着这一层级环境中的变量对象。
(2)作用
作用域链用来保证那些有权访问不同执行环境的代码,在对不同环境里的变量和函数访问时的有序性以及确定性。
(3)应用
当解析一个标识符时,是沿着作用域链一级一级地上溯(回溯)搜索标识符的过程。如果找不到标识符,就会产生错误。
还是直接看代码吧:
var color = "blue"; //全局作用域
function changeColor(){
if(color === "blue"){ //寻找`color`,此作用域(函数内)没有,则上溯到父级作用域,(这里是全局)找到了
color = "red";
}else{
color = "blue";
}
}
changeColor();
alert("Color is now " + color);
在这个例子中,函数changeColor的作用域链包含两个对象:它自己的变量对象(每个函数都有的arguments对象),以及全局环境中的变量对象。
再来看一个多层作用域链示例:
var weibing = "在"; //最外层守卫
var dajiangjun = "不在"; //2号大将军
function chengqiang(){ //城墙内
var dajiangjun= "在"; //1号大将军
function gongdian(){ //宫殿内
var zaixiang = "不在"; //宰相
if(zaixiang === "在")
console.log("把宰相找来!");
else if(dajiangjun === "在")
console.log("把大将军找来!");
else
console.log("把卫兵找来!");
}
return gongdian();
}
chengqiang();
结果可想而知:
想象这样一个场景:皇帝身居寝宫,寂寞了,想要招纳美女。那么首先想到的是宰相,他可以直接召见宰相,让宰相给自己办事。假如宰相不在,那么他可以召见大将军(职级低于宰相),让大将军给自己干;碰巧,大将军也去喝酒了,那就直接召见卫兵。在这个例子中,皇帝找到大将军了。但是有人会问,这段代码里,有两个dajiangjun
啊!对啊,可是,皇帝他老人家是从自己身边找人的。最外边的那个2号大将军,虽然也在,但是他没上朝,去考察去了。(没在城墙内),所以就召见1号大将军喽。而且自然而然,在城墙外边的人(例如普通老百姓),当然是见不到宰相和皇帝的啦。
例如在上面代码下面加如下代码:
function chixinwangxiang(){ //痴心妄想
alert("把"+zaixiang+"给爷叫出来!");
}
结果是:
在城墙外边,你肯定是见不到一人之下,万人之上的宰相大人啦! - - 到现在,闭包的面纱即将被揭开,
总结:
变量的寿命兼闭包的第一大作用:延长寿命
除了变量的作用域,另外一个和闭包有着亲密关系的就是变量的生存周期了。一般来说,全局变量的生存周期是永久的,直到我们主动销毁。而在函数内不用var
关键字声明的局部变量来说,当退出函数时,这些函数变量立即失去它们的价值,也就被垃圾回收机制销毁了,也算寿终正寝。可是在闭包中,却不是这样。
继续还是以代码说话:
var func = function(){
var a = 1; //退出后函数局部变量a直接被销毁
a++;
console.log(a);
};
func(); //2
func(); //2
func(); //依然是2
现在看看这段代码:
var func = function(){
var a = 1;
return function(){ //匿名函数
a++;
console.log(a);
}
};
var f = func(); //f是对func()的引用
f(); //输出2
f(); //输出3
f(); //输出4
f(); //输出5
由此可见,当退出函数后,局部变量a
并没有立即消失,一直存在,这样在第二次调用时a
才会是在 2的基础上加1,是3,以后每次调用也才会不断加1;这说明局部变量a一直存活着,寿命延长了!为什么呢?如果你看过我的上一篇文章,你就应该知道,在函数外边是不能访问函数(围墙)里面的变量的,而在这里,f
返回了一个匿名函数的引用,那f
就可以访问到func()
被调用时产生的环境,也就是func()
的生存空间,f
可以进去闲逛啦!那么想象一下,假如你是这个名叫"func"
的大观园的主人,你能不让随时都可能来的“刘姥姥”进去看看吗?既然让进,那么里面的亭台楼榭,一花一石恐怕都不能在园子一盖好,就把它们销毁了吧?
闭包的第二大作用:封闭变量
那么既然闭包可以有这样一个机制,我们可以用它来干什么呢?下面就来介绍介绍闭包这个特殊角色的奇技淫巧。其实上面已经有了第一大作用:延长寿命!现在来看一下他的第二个作用:封闭变量。其实这个也很好理解,闭包闭包,从字面上都可以理解有封闭作用啦。
继续例子:
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<script>
var nodes = document.getElementsByTagName('div');
for(var i = 0, len = nodes.length; i < len; i++){
nodes[i].onclick = function(){
alert(i);
}
};
</script>
</body>
</html>
测试这段代码,就会发现无论点击那个div
,最后弹出的结果都是5.这是因为div
节点的onclick
事件是被异步触发的,当事件被触发的时候,for
循环早已经结束,所以i
变量的值已经是5,在后续onclick
事件查找时肯定就是5啦。
那么怎么解决才能让div
返回如我们所愿?这时就是闭包大展身手的时候啦!思路是将每次循环的i
值封闭起来, 当沿着作用域链从内到外查找变量i
时,会先找到被封闭在闭包环境中的i
,这样的话,如果有5个div
,这里的i
就分别是0,1,2,3,4啦:
for(var i = 0, len = nodes.length; i < len; i++){
(function(i){
nodes[i].onclick = function(){
console.log(i);
}
})(i)
};
第三大作用:模拟面向对象
来看下面用面向对象实现的代码:
var extent = {
value:0,
call:function(){
this.value++;
console.log(this.value);
}
};
extent.call(); //输出:1
extent.call(); //输出:2
extent.call(); //输出:3
或者:
var Extent = function(){
this.value = 0;
};
Extent.prototype.call = function(){
this.value++;
console.log(this.value);
};
var extent = new Extent();
extent.call(); //输出:1
extent.call(); //输出:2
extent.call(); //输出:3
如果用闭包的方法,该怎么写呢?
var extent = function(){
var value = 0;
return {
call:function(){
value++;
console.log(value);
}
}
};
var extent = extent();
extent.call(); //输出:1
extent.call(); //输出:2
extent.call(); //输出:3
领略了这么多的奇技淫巧,是不是对JavaScript的闭包有了更深的理解?其实闭包的用途远不止这么多,更精彩的还需要大家在实践中多多发现。