目录
什么是闭包?
闭包:闭包就是有权访问另一个函数作用域变量的函数
闭包简单定义为在一个函数内部的函数
形成闭包的原因:存在上级作用域的引用
初识闭包
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz=foo();
baz();//输出 2
复制代码
bar()
词法作用域能访问foo()
的内部作用域,将bar()
函数对象本身当作返回值。foo()
执行后,其返回值(也就是内部的bar()函数)赋值引用给变量baz
并调用baz()
,实际上只是通过不同的标识符调用了内部的函数bar()
.在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不在使用的内存空间.由于看上去foo()
内容不会再被使用,所以很自然地会考虑对其进行回收。
闭包的神奇之处正是可以阻止这件事情发生,事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域呢?原来是
bar()
本身在使用。bar()
拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
分析闭包经典使用场景
1. 使用return 返回函数
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //输出为2
复制代码
其实可以这样写,如下:
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
foo()();//输出为2
复制代码
bar
函数是一个闭包,它在foo
函数内部定义的函数
知识拓展--简化
- f()执行f函数,返回子函数
- f()()执行子函数,返回孙函数
- f()()()执行孙函数,返回重孙函数
注意:但注意,如果想这样执行,函数结构必须是这样,f的函数体里要return 子函数,子函数里要return 孙函数,如果没有return关键字,是不能这样连续执行的,会报错的。
function fun(){
return 5 //子函数
}
var a=fun
var b=fun()
console.log(a); //[Function:fun]
console.log(b);// 输出为5
复制代码
function fun(){
return k;
function k(){
return '555 '
}
}
var a=fun;
var b=fun();
var kk=fun()();
console.log(a); //[Function:fun]
console.log(b);// [Function:k]
console.log(kk); //555
复制代码
函数只要是要调用它进行执行的,都必须加括号。此时,函数实际上等于函数的返回值或者执行效果,当然,有些没有返回值,但已经执行了函数体内的行为,就是说,加括号的,就代表将会执行函数体代码。
2. 函数作为参数
var a = '函数外'
function fo(){
var a = ' fo 函数内'
function foo(){
console.log(a)
}
return foo
}
function f(p){
var a = 'f 函数内'
p()
}
f(fo())//输出为fo函数内
复制代码
使用
retuen foo
返回,foo()
是一个闭包,f(fo())执行的参数就是函数foo
,因为foo()
中console.log(a)
中的a
的上级作用域是函数fo
,所以输出的是fo
函数内部定义的a
的内容fo 函数内
。
思考
:如果将上面的f()函数改一下你是否能理解?如果你能理解那么上面的补充知识
你也懂了。
var a = '函数外'
function fo(){
var a = ' fo 函数内'
function foo(){
console.log(a)
}
return foo
}
function f(p){
var a = 'f 函数内'
p()()
}
f(fo)//输出为fo函数内
复制代码
3.IIFE(自执行函数)
var a = 2;
(function IIFE(){
console.log(a);
})() //输出为 2
复制代码
这样产生的是闭包
IIFE()
,但是严格来讲它并不是闭包,为什么?因为IIFE()
并不是在它本身的词法作用域以外执行的。自执行函数本身是没有变量作用域的,因此会使用外层函数的变量作用域。所以输出为2
.尽管IIFE
本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此IIFE
的确同作用域息息相关,即使本身并不会真的创建作用域。
4. 定时器setTimeout(回调函数都是闭包)
function wait(m){
setTimeout(function timer(){
console.log(m);
},1000)
}
wait("Hello");//1000毫秒后输出 Hello
复制代码
闭包
timer
传递给setTimeout()
。timer()
具有涵盖wait()
作用域的闭包,因此保存了对变量m
的引用。wait()
执行1000毫秒后,它的内部作用域并不会消失,timer()
函数依然保有wait()
作用域的闭包。
知识拓展-回调函数
一个函数被作为参数传递给另一个函数(在这里我们把另一个函数叫做“otherFunction”),回调函数在otherFunction中被调用。
注意回调函数都是闭包
- 将回调函数的参数作为与回调函数同等级的参数进行传递
- 回调函数的参数在调用回调函数内部创建
5.循环和闭包
思考下面的代码:
for(var i = 1;i<=5;i++ ){
(function(){
setTimeout(function timer(){
console.log(i);
},i*1000)
})();
}
复制代码
输出的答案是什么呢?
1,2,3,4,5?
错误正确输出是6,6,6,6,6,6
。为什么呢?解析一下6是从哪来的?相信你和我一开始一样都是千百个???
,而且循序的终止条件是i
不在是<=5
。条件首次成立时i
的值为6,因此,输出显示的是循环结束i
的最终值。延迟函数的回调会在循环结束时才会执行,不管定时器setTimeout
的时间是多少,所有的回调函数依然是在循环结束后才会被执行,因此每一次都是输出一个6来。
想要输出1,2,3,,4,5的结果,那么要如何修改我们的代码呢?
方法一(传参)
for(var i = 1;i<=5;i++ ){
(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000)
})(i);//传入参数 i
}
//输出1,2,3,4,5
复制代码
通过传参的方法,将
i
传递进去,将变量名取为j
来获取i
的值
方法二(定义变量接收i的值)
for(var i = 1;i<=5;i++ ){
(function(){
var j=i;
setTimeout(function timer(){
console.log(j);
},j*1000)
})();
}
//输出1,2,3,4,5
复制代码
通过定义变量的方法,将变量名取为
j
用来在每个迭代中存储i的值
方法三(将var改为let)
for(let i = 1;i<=5;i++ ){
(function(){
setTimeout(function timer(){
console.log(i);
},i*1000)
})();
}
//输出1,2,3,4,5
复制代码
for
循环头部使用let
声明,let
具有块级作用域,let
每次迭代都会声明,随后的每个迭代都是使上一个迭代结束时的值来初始化这个变量,迭代变量的作用域仅限于for循环块内部。let可以在任意代码块中隐式的创建或是劫持块作用域
;var
声明的其中块代码的作用域是全局的,所以当执行完循环之后运行setTimeout
中闭包之后,其中引用的i
就是全局作用域中的i
,然而let就不会。
知识拓展-- let
和var
的区别
for(let i = 0;i<=5;i++){
console.log(i);
}
console.log(i,'----');
//输出 i is not defined
复制代码
for(var i = 0;i<=5;i++){
console.log(i);
}
console.log(i,'++++');
//输出0,1,2,3,4,5,6 ++++
复制代码
var
在for循环
头部声明变量i
,在for循环
结束后i
会被暴露在全局作用域中,然而let
就不会,因为let可以在任意代码块中隐式的创建或是劫持块作用域
思考题
思考题1
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
//console.log(this);
return this.name;
};
}
};
console.log( object.getNameFunc()());
复制代码
输出的是
The Window
还是"My Object
?首先我们需要了解this.name
中this
指向的到底是什么?
思考题1解答
this
指向的是全局(window)因为上一级getNameFunc()
函数没有name
属性 ,因此就去找全局的name
属性,所以输出“The Window”
思考题2
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
console.log(object.getNameFunc()());
复制代码
输出的是
The Window
还是"My Object
?首先我们需要了解that = this
中this
指向的到底是什么?
思考题2解答
将
this
赋值给一个变量,内部函数是可以访问外部函数变量的.所以this
指向的是object
,由于this
关键字不是在包含的函数中引用的,而是通过that=this
这个调用的,所以这个this
不是在闭包内的,因此这个this
就不能调用函数体内的全局对象,而是他的局部对象object.name
,所以输出的是"My Object"
使用闭包注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。