为啥js中有闭包这个东西,其他后台语言里没有?
当一个函数调用时,内部变量的查找会按作用域链条来查找,按理说不会有啥特殊情况出现,js之所以会出现闭包这个现象,原因就是你调用的这个函数是另外一个函数的返回值。
说到函数能作为返回值,这是跟js中函数类型是第一类对象这种语言设计方式有关,我会先介绍第一类对象对js的编码风格的影响。当然你直接可以看第二部分,关于闭包的说明。
第一部分
啥是第一类对象呢,我帮你百度了。
第一类对象不一定是面向对象程序设计所指的物件,而可以指任何程序中的实体。一般第一类对象所特有的特性为:
可以被存入变量或其他结构
可以被作为参数传递给其他函数
可以被作为函数的返回值
可以在执行期创造,而无需完全在设计期全部写出
即使没有被系结至某一名称,也可以存在
在js中object类型就是第一类对象。你能怎么使用object类型。就怎么能使用function类型。
这里先说一下js中函数的四大作用
1.可以调用执行。程序世界里,函数的基本作用就是可复用代码段的封装,可以直接调用。
2.可以按照对象的使用方式来使用。原因就是js中函数类型是第一类对象。准确的来说js中函数本身就是对象。对象能做啥,他当然也能做。
3.可以提供作用域。js中没有块级作用域的概念。函数是提供作用域的最小单位。
4.可以作为构造函数。这一作用算是函数的特殊作用。可以作用生成其他对象的模板。也就是可以通过函数来模拟类的实现。
在讲闭包之前,先大致对函数的使用方式与对象和数组(数组本来就是对象,当然函数也是)做个对比。
1.关于字面量
对象的字面量"{}"
[code]var a = new Object();
a.b = "xxx";
//等同于
var a = {};
a.b = "xxx";
//或者
var a = {
b : "xxx"
};
[/code]数组的字面量"[]"
[code]var a = new Array('a','b','c');
//等同于
var a = ['a','b','c'];[/code]
那么函数呢,我们平常声明函数的方式,可以理解成是一种字面量(我没有说是)
[code]function a(x,y){
return x + y;
}
//相当于如下对象的字面量
var a = new Function(x,y,"return x + y;");[/code]
注意:上面所有a 都是对象的引用
2.关于匿名函数
同样也有匿名对象和匿名数组,我们先看看他们是怎么使用的
[code]var b = ({
a : "xxx"
}).a
alert(b) // "xxx"
for(var i = 0; i < [1,2,3].length; i++){
console.log(i);
}[/code]
同样函数也有匿名的
[code]funcion(){
alert("11");
}
//因为函数的最基本功能是调用,匿名函数也可以调用(我习惯称呼为函数自执行,一般书上都叫函数立即调用表达式)
(function(){alert(11)})();[/code]
3.可以存进变量或者其他结构。
因为数组元素中可以存入数组,当然也可以存入函数。对象也是,键值对的值可以存入任何东西,当然也可以存入函数,这时我们一般都用匿名函数,例如
[code]var a = function(){};//这种声明函数的方式也叫函数直接量。
var a = {
say : function(){//....}
};[/code]
4.可以做为参数,传入函数也就是平常我们说的回调函数。
众所周知函数有参数和返回值
对象和数组作为参数没得说,写下函数相关的例子
[code]var a = function(b){
b();
};
// 可以传入匿名函数,jquery中各种回调都是匿名的
a(function(){alert("123");});
//传入有名字的函数,跟c声明的位置无关,这里涉及到变量提升的问题以及函数优先初始化的问题。
a(c);
function c(){
alert("222")
} [/code]
广义的讲,当然了,回调函数,传入参数不一定非得函数变量,但是一定要包含函数的结构(例如数组、object对象、自定义对象),如下
[code]var a = function(object)
object.say();
}
a({x :"2222",say :function(){alert("xxxx")}});[/code]
5.作为返回值
对象和数组作为函数的返回值没得说,写下函数相关的例子
[code]function a(){
return function(){
alert("22222");
};
}
(a())();//alert "22222";[/code]
[attachimg][attachimg][attachimg]
第二部分
现在还是说说为啥出了个闭包这个东西,原因[color=Orange]就是你调用的那个函数是另一个函数的返回值,当外部调用时,会沿着这个返回值的函数作用域链条来找其内部相关变量的。[/color]
先大致说下作用域链条的问题。
函数中识别变量,是一层层向外找的,首先在函数内部找,看看是不是内部声明的,然后再到上一层找,没找到,再往上,直到全局作用域。如果全局页面都没声明,那浏览器就报错了。这一层层中的层是什么东西呢,就是函数,因为函数提供最小的作用域。看个例子
[code]var a = 3;
var b = 4;
function outer(){
var a = 5;
var c = 7;
var d = 8;
console.log(a);//5,outer内部的
console.log(b);//4,全局的
var inner = function(){
var c = 6;
console.log(b);//4,全局的
console.log(c);//6,inner内部的
console.log(d);//8,outer内部的
//console.log(e); //报错,没找到
b = 0 //找到全局的
d = "xxx";
}
inner();
console.log(b);//0,找全局的b
console.log(d);//"xxx", outer内部的
}
outer();[/code]
作用域链条我们明白了,然后咱再来看看闭包的情形
[code]//代码1
function a(){
var x = 0;
return function(){
x++;//此函数的作用域链能看到x
console.log(x);
}
}
var fun = a();//a返回的是个函数,保存起来没问题。
fun()//打印1
fun()//打印2[/code]
为啥打印2而不是1呢,原因是
[color=Orange]因为a中返回个函数,我们要调用这个函数,浏览器一看,你要运行的是函数,函数是有作用域链条的,哦,x我能找到,保证不报错的。里面的x当然也能自增加了[/color]
说的直白点就像如下代码一样
[code]//代码2
var x = 0;
var fun = function(){
x++;
console.log(x);
}
fun();//打印1
fun();//打印2[/code]
[color=Orange]补充:[/color]经网友提醒,闭包有占用内存的问题,这里说下,因为代码1中fun是一个函数的引用,浏览器对应的会对其作用域链条中的变量x做了保存,因而会占用内存。达到的效果就跟代码2中的x一样。
要释放其内存可以把其引用置空,使a返回的那个匿名函数无引用指向它,自然垃圾回收器会回收的。代码如下
[code]//代码3
function a(){
var x = 0;
return function(){
x++;//此函数的作用域链能看到x
console.log(x);
}
}
var fun = a();//a返回的是个函数,保存起来没问题。
fun()//打印1
fun()//打印2
//以后不再使用了,注意要释放内存
fun = null;
[/code]
注意:
[color=Orange]如果我换种调用方法呢
(a())();//打印1
(a())();//打印1
诶,为啥第二次不打印2了呢。原因很简单,因为两次调用返回的不是同一个函数引用,因此是两条作用域链条。[/color]
说的直白点就像如下的代码
[code]var x1 = 0;
(function(){
x1++;
console.log(x1);
})();//打印1
var x2 = 0;
(function(){
x2++;
console.log(x2);
})();//打印1[/code]
这种使用方式,就不会有出现闭包常驻内存的情况,因为每次使用都匿名的,当然了,也失去了闭包的意义。
大体闭包这种现象我是解释明白了。我没有给闭包下明确的定义,不同的书有不同的说法。
有的说,返回的那个函数是闭包,有的说返回的函数提供的作用域链条是闭包。有的甚至把其得到效果说是闭包,
大体是这么说的,通过这种方式,能访问某个部函数内部的私有变量,这种方式称为闭包。
不管怎么说都是跟函数的作用域链条相关的。更有甚者也有说所有函数都是闭包。
我个人觉得会出现闭包这个东西,主要原因就是跟js中函数是第一类对象有关,因为你调用的一个函数可能不是直接声明的,而是其他函数直接return的函数或者return某种结构中的一个函数。
关于是返回某种结构的中函数,举例如下
[code]function a(){
var x = 0;
var y = {name :"张三"};
var f1 = function(){
x ++;
}
var f2 = function(name){
y.name = name;
}
return [f1,f2];
}
var b= a()
b[0]();
b0;[/code]
再写个
[code]function a(){
var name = null;
var f1 = function(n){
name = n;
};
var f2 = function(){
return name;
};
return {
setName : f1,
getName : f2
}
}
var o =a();
o.setName("老姚");
var myName = o.getName();
console.log(myName);[/code]
如果在讲上述例子改写新的形式,把函数改成匿名的(有的人甚至觉得匿名函数是闭包,那样我会说,看来所有函数都是闭包了),就是一种设计模式:模块模式。
[code]var person = (function(){
var name = null;
var f1 = function(n){
name = n;
};
var f2 = function(){
return name;
};
return {
setName : f1,
getName : f2
}
}
)();
person.setName("老姚");
console.log(person.getName());[/code]
由此可以看出来应用闭包不只是简单的写个计数器啥的。
第三部分
下面也是一种闭包的应用。
原先是闭包,改后也也是闭包。达到的效果是,添加一层隔绝,来满足我们需要的结果。
有时我们本意不想用闭包的,
如下,我想弹出0,1,2的,结果都会弹出3.
[code]var fun = function(){
var a = [];
for(var i = 0;i<3;i++){
a.push(function(){
return i;
})
}
//console.log(i);//因为js中没有块级作用域,i最后变成3,而不是报错
return a;
}
var a = fun();
alert(a[0]());//3
alert(a[1]());//3
alert(a[2]());//3[/code]
可以改成
[code]var fun = function(){
var a = [];
for(var i = 0;i<3;i++){
a.push(function(j){
return function(){
return j;
};
}(i))
}
return a;
}
var a = fun();
alert(a[0]())//0
alert(a[1]())//1
alert(a[2]())//2[/code]
最开始的那个例子也可以避免闭包,改成
[code]function a(){
var x = 0;
return function(){
(function(y){//y写x也没问题,变量首先会在此作用找的
y++;
console.log(y);
})(x)
};
}
var fun = a();
fun();//输出1
fun();//输出1[/code]
[color=Red]注意:经网友指正,原文中的代码如下,是有问题的,原因就是第二个return,又创建了闭包。代码敲顺手了,还得细心验证才行。[/color]
此处要改成,“方式二”
[code]function a(){
var x = 0;
return function(x){
return function(){
x++;
console.log(x);
}
}(x);
}[/code]
还有一种情况也会出现闭包现象,把内部函数绑定了dom节点某种操作(onclick)的回调函数,没有写在return语句里。道理是一样的。写在return里,是return后调用,绑定到dom上,比如说触发点击事件后再调用,其道理是一样的,作用域链条该怎么找就怎么找。
闭包最起码的应用,就是我们可以把一些全局变量封装起来,通过这种方式来不污染全局,例如上面的模块模式例子。
最后:
“相信有很多想学前端的小伙伴,今年年初我花了一个月整理了一份最适合2018年学习的web前端干货,从最基础的HTML+CSS+JS到移动端HTML5等都有整理,送给每一位前端小伙伴,53763,1707这里是小白聚集地,欢迎初学和进阶中的小伙伴。”