前言
闭包可以说是最最最最最最常见的面试题之一了,很多人面试的时候都被问到过闭包的知识,闭包的概念是什么?闭包的好处和坏处是什么?闭包有哪些应用场景了?
前两天朋友面试的时候也被问到过这个问题,网上的答案也有很多:有的说能够读取其他函数内部作用域的函数就是闭包,也有的说所有的函数都是闭包,哪怕是全局作用域上的,因为它能获取到全局作用域上的变量…答案也是很不统一,这就让还是新手的我对于答案有点举棋不定了,于是重温了一下《你不知道的javascript上卷》和《javascript高级程序第四版》,看了一下这两本书对于闭包的解释,记录一下相关知识,希望能够帮助到大家。
初始闭包
《你不知道的javascript》 javascript中闭包无处不在,你只需要能够识别并拥抱它。 闭包就是基于词法作用域的,你在书写代码的时候产生的一种现象。
当函数能够记住并访问自己所在的词法作用域的时候就产生了闭包
function foo(){
var a = 2;
function bar(){
console.log(a) //2
}
}
foo()
上述的这段代码中,bar可以说也是一个闭包,因为它能够访问到他所在的作用域里面的变量。但是他却是封闭在foo里面的,就相当于一个词法作用域的查找规则而已,自己的作用域没找到,去上一个作用域找。也是闭包的一部分
function foo(){
var a = 2;
function bar(){
console.log(a)
}
return bar
}
var baz = foo()
baz() //2 -----------这才是闭包的效果
我们将bar函数传递出去,然后调用foo函数的时候赋值给baz,调用baz就相当于调用了内部的bar函数了,而且bar相当于在自己所在的词法作用域之外执行了。foo函数执行之后,正常的话内部的作用域会被销毁,垃圾回收机制也会释放不再使用的内存空间,但是闭包可以阻止这种事情,让foo内部的作用域依然存在着。这样以供bar函数在后面的任何地方,任何时间引用
bar函数依然持有对这个作用域的引用,这个引用就叫闭包。
无论我们以何种的方式将bar这个函数传递出去
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
baz( bar);
}
function baz(fn) {
fn(); // 作为参数传递出去 这也是闭包
}
var fn;
function foo() {
var a = 2;
function bar() {
console.log( a );
}
fn = baz; // 直接赋值给全局变量fn
}
function bar() {
fn(); // 这也是闭包
}
foo();
baz(); // 2
上面这几组代码,有将函数作为参数传递出去的,也有将函数赋值给一个全局变量的,都让这个函数可以在词法作用域之外调用了,都是闭包。包括我们平时使用的一些计时器,定时器,事件监听器这些使用了回调 函数,都是闭包的应用
function getName(name){
setTimeout( function timer() {
console.log(name);
}, 1000 );
}
getName('小明')
//将一个内部函数timer传递给setTimeout,timer具有涵盖getName的闭包,还有这对name的引用
for循环与闭包
我们经常使用for循环
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
上述这段代码中,正常情况下我们想打印 1,2,3,4,5,但是事实上却以每秒一次的频率打印了五次6,6是从哪来的那?这个循环的终止条件是 i 不在<=5,所以首次成立条件的时候 i 就是6,打印的就是循环结束时候的值?这是为什么?
虽然这个timer函数在每次迭代的时候都定义一次,相当于定义了五个,但是都封闭在一个共享的全局作用域下面,就相当于只有一个i,根据js中setTimeout的运行机制来看,也是正常的。
那我们得如何处理那?我们可以在每次迭代的时候都创建一个作用域,这个样的话就不是在一个全局作用域了。
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
我们先尝试使用了一下自执行函数来创建作用域。
还是不行…
我们可以在每次迭代的时候创建一个块级作用域
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
我们使用到了es6 中的let定义变量,在每次迭代的时候都创建一个块级作用域,这样是可以的
闭包的应用场景
闭包有什么应用场景?比如定时器,回调函数,防抖函数....很多都是用到了闭包
我们还可以封装私有变量
(function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 构造函数
MyObject = function() {
};
// 公有和特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
var obj = new MyObject()
obj.publicMethod () //false
比如上面这种,我们使用一个自执行函数封装私有变量,创建一个构造函数表达式,我们没有用var定义,说明是一个全局的,在构造函数的原型上创建了一个publicMethod函数方法,这个函数就可以访问这个函数的词法作用域,就是一个闭包
我们也可以使用模块模式创建,返回一个单例对象
function singleton() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}
var foo = singleton();
foo.publicMethod() //false
singleton函数返回了一个对象,相当于一个模块实例,这个对象里面的publicMethod函数具有涵盖模块实例内部作用域的闭包,singleton函数也可以是自执行函数
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。
闭包也会出现也写问题,比如将把HTML元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。或者是你把函数return出来后 他就给 window了所以一直存在内存中。会造成内存泄露。
function Handler() {
let element = document.getElementById('element');
let id = element.id;
element.onclick = () => console.log(id);
element = null;// 将element设置为null,解除对这个对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。
}
参考资料:
《你不知道的javascript》上卷
《javascript高级程序设计第四版》
转载:https://blog.csdn.net/qq_42736311/article/details/115573668