javascript 闭包的概念,闭包的作用,闭包经典面试题详解(配图解)
函数作用域(闭包前置知识)
要彻底弄懂 闭包,必须先理解 JS的 变量作用域,变量分为: 全局变量 和 局部变量,JS的特殊之处在于:每个函数都会创建一个新的作用域,函数内部可以读取函数外部的变量,相反 函数外部无法读取内部变量。
var a = 123;
function fun(){
console.log(a);
}
fun(); // 输出123,函数内部的a在自身找不到,会跳到外部全局中去找。
function foo(){
var b = 345;
}
console.log(b);
// error, 全局作用域中并无 b 这个变量,b只产生于 foo 这个函数作用域中。
闭包的理解
所谓“闭包”,指的是一个父函数嵌套着另一个子函数,那么这个子函数也是这个父函数表达式的一部分,可以理解为嵌套函数和函数作用域链。
正常来说,一个嵌套函数,内部的子函数不能被外部作用域引用,但如果,把这个子函数作为一个返回值传给父函数,那么全局作用域就能执行这个子函数内的结果了。
闭包的用途
相同函数可以用多个相互独立的对象引用,避免代码冗余、相互污染。
否则同时调用一个函数,不仅会造成后面的函数不能正常使用,如果改动了全局变量,还会对前面正常的函数产生影响、造成污染。因此我们需要用到闭包。而且可以使代码简单化,体积小。
如何产生闭包?
在嵌套内部函数定于并引用父函数的属性时,就产生了闭包。
产生闭包必须要有嵌套函数,以及子函数引用父函数的属性或者变量
才能产生闭包。
function fn1(){
var a = 2; // 从第二行,到第五行结束,在这个定义过程,结束时经产生了闭包
function fn2(){
cosnole.log(++a);
} // 定义结束,产生闭包
return fn2;
}
var abc = fn1();
abc();
闭包的死亡时机:
在嵌套的内部函数成为垃圾对象时,闭包就死亡了,一般嵌套的内部函数之所以会变成垃圾对象,极大可能是程序猿手动把这个变量或者对象设置为null,这样这个对象就变成垃圾对象了,被浏览器执行回收。
闭包语法:
执行普通的嵌套函数的语法,是这这样的:
function fun(){
var a = 100;
function fn(){
console.log(++a);
}
fn();
}
fun(); // 101
fun(); // 101
fun(); // 101
在这个例子中,如果想在全局作用域下,调用这个 fn()内部的函数执行结果,是调用不出来的,因为他是个局部作用域,只能在本函数体内调用执行,调完即释放内存。
由于这个例子的 fn() 没有被持久化(每调完一次结束后释放内存),调用时只能直接调用外层函数 fun() 。此时内层的 fn() 每次被调用后就结束了生命周期,
相关的作用域也被销毁,因此 var a = 100 这个变量值也不会被保留引用,这样每次调用 fun() 时,都只会输出 101 。
执行闭包的写法
function fun(){
var a = 20;
return function fn(){ // 直接将执行结果返回给 fun,这个 fn就是闭包了。
console.log(++a);
}
}
var fns = fun(); // 接收这个返回结果
fns() // 输出21;
fns() // 输出22;
fns() // 输出23;
当 fn() 调用了上层函数内的 a 时,就已经产生了这个闭包行为了。 当每次执行 fn() 时,这个内存并不会被释放(因为它在调用外层的a,形成了闭包的依赖关系),
所以在 fn() 内的 a 的值内存,不会被释放,而是被保存下来。
所以每次调用这个返回值时,它总是以累加的形式输出,并不会被释放掉,生命周期比较长。
闭包实际上就是以某种方式持久化,并保留了对外层自由变量的引用的函数。
JS 中,通常一个函数执行完后,内部的整个作用域都会被销毁,被JS引擎的垃圾回收器回收,但闭包的出现阻止了这件事,上个例子中函数 fun() 的作用域就不会销毁,
因为它内部的作用域依然还存在,原来是本身在使用变量 a 的引用,而 fn 在 fun() 的作用域之外被执行,当每次调用 fns() 时,便又访问到函数 fun() 内部的变量 a() 。
简而言之,这个例子中的函数 fn() 就是一个闭包。
各种专业文献上的”闭包“(closure)定义的非常抽象,晦涩难懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在 Javascript 语言中,只有函数内部的子函数才能读取该函数的局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以在函数外部读取内部的变量,另一个就是让这些变量的值始终保持在内存中。
闭包的现实中的应用
定义JS模块
具有特定功能的JS文件
将所有的数据和功能都封装在一个函数内部(私有)
只向外部暴露一个包 n个方法的对象或者函数
模块的使用者,只需要通过模块暴露的对象,调用这个方法来实现对应的功能
方法一:
function fun(){ // JS 模块,并且有特定的功能,转换大小写
var msg = "my name is a juzheng"; // 私有属性
// 操作数据的函数
function f1(){
console.log("输出小写" + msg.toLowercase()); // 调用了上层的局部变量属性
}
function f2(){
console.log("输出大写"+ msg.toUpperCase());
}
// 向全局暴露两个对象,需要一个对象容器来保存
return {
one:f1,
two:f2
}
}
// 接收返回值
var abc = fun(); // 通过暴露对象,接收数据
abc.one(); // 接收容器的对象
方法二:
(function fun(window){ // JS模块,匿名函数自调用, 并且具有转换大小写功能
var msg = "my name is a juzheng";
function f1(){
console.log("输出小写" + msg.toLowerCase()); //调用上层变量属性
}
function f2(){
console.log("输出大写" + msg.toUpperCase());
}
window.myModel = { 将容器对象,转为 window全局对象,将容器对象暴露出来,
one:f1,
two.f2
}
})(window) // 推荐函数自调用时,形参和实参写上 window, 这样可以实现压缩代码
// 调用暴露对象
myModel.one();
myModel.two();
// 不需要用一个变量来接收,因为不是用return方法,而是在闭包中用window属性将暴露对象给定义好了,
所以在全局作用域下,只需要调用即可。
闭包的缺陷:
函数执行完后,函数内的局部变量没有释放,占用内存时间变长,容易造成内存泄漏
解决方法:
让内部函数变成垃圾对象,赋值为null,及时释放,让浏览器回收闭包。
内存溢出
一种程序运行出现的错误,当程序运行需要的内存超出了剩余内存时,就抛出了内存溢出的错误,浏览器直接奔溃。
var obj = {};
for(var i = 0; i<1000;i++){
obj[i] = new Array(10000000) // obj是伪数组,有下标
} // 一直输出,占用浏览器大量内存,导致浏览器原地奔溃
内存泄漏
占用的内存没有及时释放,内存泄漏积累多了容易导致内存溢出
常见的内存泄漏:
意外的全局变量,没有及时清理的计时器或者回调函数,闭包。
1:全局变量引起的内存泄漏
function fn(){
a = 10; //a 是全局变量,当函数执行完后并不会自动释放内存
console.log(a);
}
fn();
console.log(a);
2:计时器未结束引起的内存泄漏
setInterval(function fn(){ // 计时器没有定义结束条件,会一直无限执行
console.log("aaaaaa");
},1000);
// 正确做法
var stop = setInterval(function fn(){
console.log("aaaaaa");
},1000);
clearInterval(stop); //结束定时器
3:闭包引起的内存泄漏
function fun(){
var a = 10;
function f1(){
console.log(++a);
}
return f1;
}
var f = fun();
f();
// 由于闭包是会一直引用着函数内的局部变量,所以闭包内的变量
并不会被浏览器所释放,导致内存一直被占用着
闭包的两道经典题
题目一:
var name = "the window";
var obj = {
name : "myObject", // 属性
getNameFunc:function(){ // 方法
return function(){
return this.name;
};
}
};
alert(obj.getNameFunc()()); // 输出了 “the window”;
答案: 输出了全局 the window,为什么不是对象属性 myObject 呢?
解法: 形成一个闭包环境,需要两个条件:函数嵌套,子函数引用父函数局部变量,但是上面的例子,我只看到了函数嵌套,但没看到子函数引用父函数局部变量,所以这个函数嵌套根本就产生不了闭包环境,所以他不是个闭包。
调用 alert(obj.getNameFunc()()),注意,他有两个 () () ,调用第一个 () 时,实际上就是执行了getNameFunc()这个函数,但这个函数又嵌套了 return function(){ return this. name },
所以执行第一个括号时,就是执行了 return function(){ return this. name },到执行第二个 () 时,才是真正执行到了第二个函数的内部了,也就是 return this.name 了,但由于这个函数方法,他不是个闭包环境,所以 this 指向了 window 了,也就是 var name = “the winodw” ;
图解程序执行顺序
题目二:
var name2 = "the window";
var obj2 = {
name2:"my name is a obj",
getNameFunc2 :function fn1(){
var that = this;
return function(){
return that.name2;
};
}
}
alert(obj2.getNameFunc2()()); //输出 my name is a obj
这次是输出的是 对象内的属性了,因为这次的才是真正的闭包环境,有函数嵌套,有父函数变量给子函数引用,形成依赖关系,产生闭包环境。
父函数明确定义了局部变量,var that = this;然后这个 that 呢,他又被子函数所引用,当 alert(obj2.getNameFunc()()) 执行时,就是执行子函数内部的 return that.name,
先在自身查找有没有 that.name,没有就返回上一层父函数,找到了 var that = this 了,这个就是闭包的环境。当然,两个函数执行域内都没有这个变量属性时,再往上一层,
也就是obj2对象内的属性,有个 name,终于在对象的执行域找到了,那就输出了 对象name的属性值。