什么是闭包
从入行就有人跟我说闭包,到现在闭包也是前端躲不开的概念,每次面试的时候我也会问一下闭包相关的问题,今天针对闭包写一下我的理解
我想从闭包的表现形式、闭包的作用和闭包的原理说一下闭包
闭包的表现形式
闭包的表现形式就是一个函数中返回另一个函数,返回的函数中又引用了该函数作用域内的变量,另一种形式是函数作为参数传递也会行程闭包
function add(){
let a = 1
return function(b){
return a+b
}
}
const addfunc = add()
console.log(addfunc(10))
/* * @params n,m number型 * @return 返回两个数相加额结果 */
function add(n,m){
return n+m;
} //闭包
(function fn(f){
var n = 1
var m = 2
f(n,m); // 调用add函数
})(add); // add函数作为参数f传入
以上形式就是一个简单的闭包,函数内返回一个函数,返回的函数调用的时候能够获取到该函数中的变量a的值,至于为什么能够获取到a的值,后面原理再详细讲解,这步只要知道闭包的表现形式就行了
闭包的作用
闭包的作用是封装,封装的好处是可以避免全局污染和延长变量的生命周期,如何避免全局污染和怎么延长变量的使用周期的后面再讲
function test(){
var a = 0;
var b = 1;
function getA(){
return a
}
function setA(value){
a = value
}
function add(){
return a+b
}
return {
getA:getA,
setA:setA,
add:add
}
}
const testItem = test()
testItem.setA(10)
console.log(testItem.getA())
console.log(testItem.add())
上面的代码返回的对象中有函数使用到了test作用域内的变量,那么这个作用域在函数执行完毕之后就不会被清除,你也可以理解成因为这块作用域的变量在之后的代码运行中会被用到,所以这块作用域在函数执行完毕之后没有被清除反而被锁定了,也可以说是封装到一块内存中了,而且内部的变量只能通过返回的函数去操作,无法通过外部去修改也不会受到外部环境的影响
闭包的原理
闭包的原理要从js的垃圾回收机制和作用域讲起
作用域:在js中一个作用域是以函数的大括号为基本单位的,每个函数在执行的时候都会在内存中创建一块区域用来保存这个函数内所创建的变量
function parent(){
var a = 1
console.log("父作用域parent")
function child(){
var b = 2
console.log("子作用域child")
console.log(a+b)
}
child()
}
function parent2(){
var c = 3
console.log("parent2作用域")
}
parent()
parent2()
上边的代码执行的时候,内存中创建了三块作用域,分别是parent、child、和parent2,parent和child是嵌套的,又叫父子作用域,两个作用域形成了一个作用域链,用图片描述如下
顺便说一下,变量值获取会按照作用域链向上查询,child函数在执行的时候在本作用域下没有找到b变量就会去父作用域去找这个变量找到了就直接用,没找到就在向上去找,但是在parent中是无法直接获取到parent2中声明的变量c的
作用域和上下文的区别:
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。
要通过作用域对应的执行上下文环境来获取变量的值。
同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
垃圾回收机制:每个函数在执行的时候都会在内存中创建一块区域,一个函数在执行之前js会把函数内部变量存到内存中,然后再一行一行的执行函数内部的代码,当函数执行完毕后,这块内存会被回收,就是删除,这就是js的垃圾回收机制。
但是有一种情况是不会清除内存的,就是闭包,就像文章最上边的代码add函数执行完毕后返回一个函数,而这个函数用到了add作用域内的a变量,add虽然执行完毕了,但是如果这个时候清除内存那么当调用addfunc的时候他用到的a变量就获取不到了,所以js会在内存中保留这块区域,这也就是闭包可以延长变量声明周期的原因
举一个遇到闭包就会提到的例子:
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
如上代码,在执行的时候我们希望点击p标签的时候分别弹出相应的help,但是实际情况是点击p标签弹出的都是'Your age (you must be over 16)'
之前提过作用域是以函数的大括号为单位的,所以上边的代码在执行setupHelp函数的时候内存中创建了一块区域如下
input获取焦点时执行函数showHelp,showHelp中使用item的help值,为什么会每次help值都是一样的,问题就出现在item变量创建的位置,循环三次只不过是给item附值三次,最后的item赋的值是helpText[2],所以每次获取焦点时到内存中获取到的都是helpText[2].help,这个时候就应该使用闭包的形式解决,如下
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
这样在内存中的形式如下
循环的时候每次都执行了一次makeHelpCallback函数,每执行一次函数就会在内存中创建一块区域,这个区域执行完之后返回的函数使用到了helpText的数据,这个就形成了闭包,所以这块区域会被保存下来等待执行时获取变量的值,这样每个区域存的内容就是循环的时候当前item的值。input获取焦点的时候就会到相应的作用域内去获取item.help。
ES6新出的声明变量的方式let也可以解决上述问题,是因为let声明的变量只在他坐在的大括号内有效,无论是函数的大括号还是for循环if判断的大括号。
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
当然闭包也有缺点,因为形成闭包之后垃圾回收机制无法释放内存,所以大量使用闭包有造成内存溢出的风险。
以上就是我理解的js闭包,如果感兴趣的朋友可以到官网去详细的阅读一下官网的讲解