第一章、浅探闭包:
大致意思,在w函数内部,定义一个b内部函数,并用return返回,那么b函数就是一个闭包函数。
function w(){
var c = "开创独立王国的闭包";
var n = 1;
function b() {
console.log(c+'----打印'+n+'次');
n++;
}
return b;
}
var f1 = w()
f1()//开创独立王国的闭包----打印1次
f1()//开创独立王国的闭包----打印2次
官方对闭包的定义:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
结合以上示例,分析官方定义:
1、环境的表达式(通常是一个函数)—指的是函数b;
2、b拥有很多变量:c、n…;
3、b绑定了这些变量,比如b绑定了n;每次通过f1执行完b后,b的执行上下文n不消失,可以每次叠加。
4、这些变量也是该表达式的一部分:c、n确实是b函数的一部分。
还有一种对闭包更直接明了的说法:闭包就是有权访问另一个函数作用域中变量的函数。
分析这句话:
1.闭包是定义在函数中的函数.
2.闭包能访问函数内的私有变量.
3.即使包含函数执行完了, 被闭包引用的变量也得不到释放.
闭包的意义在于:
js有两个作用域:全局作用域,函数的局部作用域;
局部作用域内可以访问全局作用域,但是全局作用域内却无法访问局部作用域。
var str = '我是全局作用域内的全局变量';
function f1(){
var idx = '我是函数f1局部作用域内的变量';
console.log('我在局部作用域内,访问了全局变量str:---'+str);
}
console.log('我在全局作用域内,访问了全局变量str:---'+str);
需求来了,我想在全局作用域内,访问f1作用域的变量idx,如何做到呢?
这时候闭包就出现了。
我们可以在f1内定义一个函数,通过此函数去访问f1;
var str = '我是全局作用域内的变量';
function f1(){
var idx = '我是函数f1局部作用域内的变量';
console.log('我在局部作用域内,访问了全局变量str:---'+str);
function funcb() {
console.log('我是用来访问idx的---'+idx)
}
return funcb
}
console.log('我在全局作用域内,访问了全局变量str:---'+str);
var cc = f1();
cc();//访问到了idx
看到没有,我们在全局作用域内通过闭包函数funcb访问到了局域变量idx;
这就是闭包的意义所在:访问函数的私有变量。
闭包就好比国与国、星球与星球之间的通信。这一点后面去讲解。
每个国与国之间都可以通过闭包的形式互相访问变量。
因此理解闭包一定要深刻理解全局作用域、局部(函数)作用域、(执行)上下文的概念。
还要知道一个重要概念:javascript除了全局上下文之外,只有函数可以创建的函数执行上下文。
如果你没有对以上四个概念有清晰的理解,请别说懂闭包。
闭包函数区别于普通函数的独有特性在于一下两点:
1、能访问母函数内的变量,其他函数无法访问母函数内变量。
例如:
function a() {
var num = 10;
return function (){
num++;
return num;
}
}
var b = a();
console.log(num);//Uncaught ReferenceError: num is not defined
2、先看一段代码:
//windows全局环境
var n=1;
function f1() {
console.log(n);
n++;
}
f1()//1
f1()//2
我们注意到,每次执行f1()后,n叠加1;再次执行f1(),n的值是上次函数执行的结果。也就是说n这个全局上下文不会被销毁。
这里啰嗦一下,为什么全局上下文永远不会销毁,只有关闭程序是才销毁呢。
这是js的特性:
上下文肯定是函数被执行时才产生的,那么全局上下文是谁创建的呢,我们可以理解为全局上下文是整个程序创建的,整个程序是最大的函数,犹如航空母舰,程序一旦执行,就会产生一个全局的上下文,只要程序处于执行中,这个全局上下文永远不会消失,直至结束整个程序。
参考1
参考2
与全局上下文一样,闭包函数的母函数执行后,母函数的上下文不销毁,会被存到js内存中。,此特性是闭包的核心特性,也是理解闭包的根本所在,如果对闭包的此特性不甚了解,那就请看第二章,第二章将用一整章篇幅去解读此特性。
function a() {
var num = 10;
return function (){
num++;
console.log(num);
}
}
var b = a();
b()//11 ---执行完后,母函数a的上下文num应该要销毁,重新回到num为10。
b()//12 ---??居然是12,说明母函数a的上下文没有被销毁,这样导致闭包函数每次执行时,其上下文都不一样。
闭包函数的使用
所以闭包函数的引用,必须先执行母函数,母函数执行完后母函数的上下文不消失,闭包函数的母函数的上下文不消失,从而导致闭包函数每次执行时在同一个作用域内(母函数作用域内),闭包函数的上下文每次可能都不同。
闭包函数长什么样?
闭包有两种设计定义方式,一种就是经典的return方式,上文讲的就是这种方式,一种就是new的方式。
- 闭包方式一:经典闭包写法–return方式
以上讲解的是最常用的经典闭包写法,具有闭包特性的函数(也就是闭包函数)是这样的:
1、函数必须含有return 或可以返回函数;
2、函数必须return的是一个函数 或可以返回函数;
标准闭包函数:
function a() {
var num = 1;
return function (){
num++;
console.log(num);
}
}
- 闭包方式二:new 方式的闭包写法
这种方式之所以被认定为闭包,是因为以下两点理由,下面代码中:
1、a是母函数
2、inc是a执行后返回,相当于return的函数,此函数绑定了a的私有变量n, 这是决定inc是否为闭包的重要依据。
function a(){
var n = 0;
this.inc = function () {
n++;
console.log(n);
};
}
var cc = new a();
cc.inc()//1
cc.inc()//2
如上,一般可通过return或new的方式创建闭包。
除了这两种方式,很多人认为自运行匿名函数和命名空间设计模式都是闭包,我觉得是不对的。
- 自运行匿名函数:
//这不是闭包
(function fn(){
var n = 8;
console.log(n) ;
})();
//这一种是闭包的设计,但并不是因为它是自运行匿名函数的原因,而是因为匿名函数内部return了一个函数的原因,这其实就是上面讲的两种闭包设计模式的第一种 return方式
(function fn(){
var n = 8;
return function(){
console.log(n) ;
}
})();
- js的命名空间写法
js的命名空间写法不能称之为闭包,它最多是使用了js关于引用对象一处改变,都受改变的特性。
//这是命名空间的写法,但不是闭包
var obj = {
n:8,
count:function(){
this.n++;
console.log(this.n);
}
}
其他还有一些把函数定义在原型上,这本质上也是运用了引用对象的特性,不是闭包:
//这不是闭包,是运用了引用对象的特性,才有对象元素值叠加的效果
function a(){
this.n = 8;
}
a.prototype.count=function(){
this.n++;
console.log(this.n);
}
var obj = new a();
obj.count()//9
obj.count()//10
如果稍微换一下,就行不通了
//这不是闭包
function a(){
this.n = 8;
}
a.prototype.count=function(){
this.n++;
console.log(this.n);
}
var newCount = (new a()).count;
newCount()//NaN
newCount()//NaN
闭包–国与国之间的通信或间谍
闭包的母函数a执行时,会创建一个函数w和一个母函数a上下文,因为a返回的是闭包函数,所以可以将w看作是闭包函数,把这个母函数a上下文比作一个小国, 把window全局上下文比作一个大国的话,那么这个函数w就是活动在大国里面的一个小国的间谍, 函数w看似生活在大国的环境里面,但实际上一切行动受小国控制, 所以函数w虽然活动于小国之外的大国window里面, 但实际上函数w只能算生活在小国内的,与小国内的其他函数无异; 因此函数w的最高级作用域其实是小国,然后是外层的大国; 函数w无论在小国内或者window内活动,都能改变这两国之间的变量或执行上下文; 所以闭包的设计原则目的之一是,通过闭包函数,游离于window环境当中,去遥控改变闭包的母函数小国
var rr = 2;
function a() {
var num = 10;
return function (){
num++;
rr++;
console.log(num);
}
}
var w = a();
w();//遥控改变小国内的num,当然也可以改变window大国内的rr
w();//12
补充
任何函数,要去创建这个函数的作用域取值,而不是“父作用域”。理解了这一点,对闭包的理解有帮助。
var max = 10;
var num = 125;
function fn(x) {
if(x>max){
console.log(x)
}
}
(function (f) {
var max = 100;
f(15)//15
console.log(++num)//126
})(fn)
把函数看出一个变量,如本例的num变量,它的值由创建它的地方确定,同样的fn的作用域也是创建它的地方去找。
王福朋写了另外一种闭包形式,写的比较精辟给出链接,可以了解
第二章、深探闭包:
整个js程序是最大的函数,程序一旦执行,就会产生一个全局的上下文,只要程序处于执行中,这个全局上下文永远不会消失,直至结束整个程序。
在上面我们说了,闭包的母函数执行时,母函数产生了一个执行上下文,这个上下文类似一个windows的全局上下文。
全局上下文只有整个js程序刷新或重载时才会销毁原来上下文,并生产新的上下文;
当程序一旦执行后,全局上下文不销毁。
正如全局上下文一样。
母函数执行时,就相当于程序装载,母函数执行生成上下文,就相当于整个程序装载时产生的全局上下文。
当母函数一旦执行后,母函数的上下文就不会被销毁。
为什么呢?
闭包函数绑架了母函数的上下文变量,从而导致了母函数的上下文不会被销毁。
你问我为什么会有这样的现象,我只能告诉你,这是js的游戏规则。
就好比打篮球,你上篮被打手了就会得到罚球一样,这是打篮球的规则;
这个规则也不是无中生有的,是合乎逻辑的。
你上篮上的好好的,别人打你的手,裁判判你一次罚球的机会,这合情合理吧。
回到js的游戏中:
母函数创建了一个闭包函数,闭包函数引用(也可以说成是绑定、绑架)了母函数内的变量,那么闭包函数每次执行的时候都需要引用母函数内的变量,这时候,js规定母函数的执行上下文不销毁,时刻准备给闭包函数创建执行上下文,这合情合理吧。
所以你要记住js的这条名为闭包的规则。
只需记住,闭包母函数的上下文永远不会被销毁,存在于内存当中。
让我们再次重温一次闭包的官方定义,是不是一下子觉得官方闭包的定义从未有过的贴切和准确:
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分
把闭包母函数的上下文看成全局上下文一样去理解,问题就简单了
javascript除了全局作用域之外,只有函数可以创建的作用域;
这是因为js的作用域不是块级作用域,而是函数式作用域。
js中与此类似的还有上下文的创建:
整个程序的执行产生全局上下文,局部函数的执行创建函数上下文;
相比全局上下文;
闭包函数的母函数产生的上下文也可看作一个全局上下文;
区别在于,
1、全局上下文是给所有函数用的;母函数上下文是给闭包函数用的;
2、全局上下文是js程序装载时创建;母函数上下文是母函数执行时创建;
除此之外母函数上下文几乎拥有全局上下文的一切属性,例如:
二者都具有,一旦创建永远不被销毁,只有程序停止运行时,才销毁。(母函数上下文也是永不销毁哦);
作用域内的函数执行的上下文都基于对应的全局或母函数上下文:
比如全局上下文,对应的是全局作用域,全局作用域内的所有函数的上下文都基于全局上下文;
比如母函数上下文,对应的是母函数作用域,其作用域内的闭包函数的上下文是基于母函数上下文。
我们把闭包母函数的上下文看成全局上下文一样去理解,问题就简单了。
了解了全局上下文的特质,就几乎相当于了解了母函数上下文;
得出结论
1、全局上下文永远不会消失,直至结束整个程序;
2、母函数上下文也永不消失,因为母函数也是程序的一部分,一旦母函数上下文被创建,上下文只会同母函数随整个程序结束而结束。
3、js中有两个全局上下文;window下的大的全局上下文;闭包函数母函数创建的小的全局上下文;
4、一个js程序,可以肯定会有一个全局上下文,可能没有或有一个或多个母函数上下文;
5、当js中有闭包函数母函数时,因为js中同时存在全局上下文和母函数上下文,因此会加重内存空间,影响性能;
小结语:
理解闭包函数的精髓全在于理解闭包母函数的上下文不销毁。
而不销毁的本质闭包函数绑定了母函数的域内变量,闭包函数的运行依赖于母函数的上下文。
然后是理解闭包函数执行上下文如何创建(压栈)、销毁(压栈)。
函数压栈出栈图示:
普通函数:
如图所示,普通函数每次从执行开始到最终结束都是重复如图所示的过程;
闭包函数:
如图所示,闭包函数每次从执行开始到最终结束都是重复如图所示红色矩形框内的过程;
由此可见闭包函数的执行和结束只与母函数上下文有关,与全局上下文无瓜葛。
容易误解地方
容易误解的地方,需要注意的是:
闭包函数的母函数上下文不销毁,
但闭包函数自己的上下文每次都是销毁的。
了解闭包,对上下文与作用域知识了解要透彻是关键,因此就写了第三章。
第三章 上下文与作用域
上下文、作用域基本概念的比较:
1、作用域是声明函数时就产生了,上下文是函数被执行才产生,并且函数执行完就消失。
2、作用域决定了上下文
3、作用域不会消失,无时不刻都存在着;上下文只有函数被执行时,才形成,函数执行完,上下文消失,但作用域没有消失的概念,它更像是一个地盘;
4、在同一个作用域下,函数被执行时,可能会产生不同的上下文;
5、上下文是函数被执行时才产生的【这也是为什么上下文也叫执行上下文了,要带执行二字】,执行完后一般会消失,但是闭包的函数父元素,执行完后,上下文不消失;
函数、上下文、作用域之间的联系:
1、函数只能改变上下文,而不能改变作用域范围;
2、由于函数的作用域是函数被定义时就确定了的,函数不可能改变作用域,
因此函数一辈子都是在跟上下文打交道,每次的执行都是在改变和影响上下文。
相比作用域而言,与函数关系更加紧密的是上下文。
3、
var str = 5;
上面这句代码,
变量str 是函数的作用域;
变量str 的值是函数的上下文;
函数的作用域永远都是str;
单str的值不一定永远都是5;
这就说明了函数的作用域从一开始就确定并且永远不会改变;
但函数的上下文却可能时刻改变着。
4、其实我们大可以将作用域和上下文具体化理解:
作用域就是函数要用的一个个变量。
上下文就是函数执行时,每个变量的具体值。
上下文与作用域误区
我们通常只关注作用域,但实际中,正如上面说的第二点,函数从被创建出来一直都是在依赖上下文,并改变和影响上下文。
所以相比作用域而言,函数与上下文关系更密切。
而我们往往只知道作用域,却不太理解上下文。
我们其实更应该去理解js的上下文。
第四章、闭包的几大特性:
本章讲述的这些特性,并非是都是闭包特有的,其他函数可能也有。
闭包函数是内部函数,因此继承了内部函数的一些特性:
1、可以访问父级函数的变量
上面的示例就是例子
2、闭包函数内的this指向window
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());//The Window
3、不能直接访问父级函数的this和arguments
闭包函数,也是一个内部函数,内部函数不能直接访问父级函数的this和arguments;可以通过在父级函数内将this和arguments赋值给变量,再通过访问父级函数变量的方式访问。
//不能直接访问父级函数的this
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());//The Window
//将父级函数的this赋值给that变量,通过that访问父级函数的this
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());//My Object
4、闭包函数使用的变量会被存入内存
闭包父级函数在被引用执行时,产生的上下文环境会被存入内存,不会随着执行结束而被销毁。
也就是说,上下文环境中包含的变量会被存入内存,不会随调用结束而被销毁,只有刷新或关闭浏览器才销毁。
上下文环境的理解,见链接
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();//父级函数被执行,产生上下文环境{n:999}
//父级函数被执行完,它产生的上下文环境{n:999}不销毁
result(); // 999
nAdd();//父级函数产生的上下文环境被改变,{n:1000}
result(); // 1000
第五章、难点闭包示例一:
function w(){
var c = 8;
console.log("w被执行了")
function b() {
console.log(c);
c++;
}
return b;
}
w()()//8
w()()//8
w()()//8
var d = w();
d()//8
d()//9
d()//10
将w()()拆开成var d = w();d();运行后,执行结果为什么不一样?
这个问题很简单,我们分析w()():
在w()()中,w()创建了上下文,这个上下文就是w()()的执行上下文,因此打印为8;
当执行到第二个w()()时,w()创建了上下文,这个上下文就是本行w()()的执行上下文,因此打印也为8;
我们分析:
var d = w();
d()//8
d()//9
d()//10
这里在第一句引用了w(),w()创建了上下文。
在下面几句中,d()是闭包函数的执行,并没有执行w();所以下面三句用的上下文环境都是var d = w()这句创造的同一个上下文环境。
综上:
将w()()拆开成var d = w();d();运行后,两种方式执行结果为什么不一样?
因为这两种方式是有区别的,第一种方式执行了三次w,第二种方式执行了一次w
第六章、难点闭包示例二:
var a =function(y){
var x=y;
console.log("匿名函数运行了")
return function(){
console.log(x);
x++;
console.log(y);
y--;
}
}(5);
a();//5 5
a();//6 4
a();//7 3
本例理解的难点是,示例中的匿名函数为什么会自运行了,原来
var a = function(x){}(n)
是匿名函数可以自运行的形式之一,相当于(function (x) {})(n)
关于匿名函数自运行的十三种方式,参考链接:
( function() {}() );
( function() {} )();
[ function() {}() ];
~ function() {}();
! function() {}();
+ function() {}();
- function() {}();
delete function() {}();
typeof function() {}();
void function() {}();
new function() {}();
new function() {};
var f = function() {}();//本例这里
1, function() {}();
1 ^ function() {}();
1 > function() {}();
有人觉得,可以上本示例的变量a改造成如下,注意这是一种错误的函数声明方法,这种方法不报错,但(5)在此无任何意义,不会被浏览器解析,不信,你可以打印a,打印出来的函数,没有(5):
function a(y){
var x=y;
console.log("匿名函数运行了")
return function(){
console.log(x);
x++;
console.log(y);
y--;
}
}(5);
为什么不能这样改造呢,其实示例中var a = 的右边是一个自运行的匿名函数,不是一个普通静态的匿名函数。