很多人学JS刚学到闭包的时候会比较懵,特别是从强类型语言(如Java)转而学JS的人,更是觉得这都啥跟啥呀。本文也就只针对这些刚学的新手,所以不会去谈闭包的原理,只谈闭包的基本使用,新手可以放心食用。只有在知道如何使用之后,你再深入了解就会得心应手,在用都不知道用的情况下就想对一个知识点了解的很透彻,这是不可能的。
了解闭包的使用之前,先得捋清一下一些基本的知识点,咱们一个知识点一个知识点慢慢往下捋,到最后你就会发现你已经知道如何使用闭包了。
思路梳理
JS中,函数内声明的变量其作用域为整个函数体,在函数体外不可引用该变量,听起来很玄乎,一看代码大家就很清楚了:
function outter() {
// 在函数内部声明一个变量
let x = 3;
// 在函数体内使用这个变量,这个肯定没有什么问题
console.log('我在本函数里使用变量:' + x);
}
outter(); // 输出内容:我在本函数里使用变量:3
console.log(x); // 报错,因为函数外部这样拿不到函数内部的变量
复制代码
这个知识点相信大家都可以理解,我在这里说的再浅显一些,函数内部声明的变量,就只能在声明该变量的大括号内使用,大括号外就用不了。所以看函数内的变量作用域,直接找大括号就好了。
我们再继续前进,由于JS的函数可以嵌套,此时内部函数可以访问外部函数定义的变量,反过来则不行:
// 外部函数
function outter() {
let x = 3;
// 内部函数
function inner() {
let y = x + 1; // 内部函数可以访问外部函数的变量x
}
let z = y + 1; // 报错,外部函数访问不了内部函数的变量y
}
复制代码
这一点也很好理解,和前面一个知识点是完全一致的,内部函数inner()
因为在外部函数outter()
的大括号内,当然就可以使用变量x,而外部函数outter()
在内部函数inner()
的大括号外面,自然就用不了变量y。
了解上面的基本知识点后就可以开始了解闭包了。假设现在我们有一个需求,我就是想在outter()
外面拿到变量x怎么办? 好办呀,直接在outter()
里将x当做返回值返回就好了:
function outter() {
let x = 3;
return x;
}
let y = outter(); // 3
复制代码
OK,这样是拿到了变量x,但是,严格的来说这只是拿到了变量的值,并没有拿到变量。啥意思呢,就是说你无法对变量x的值进行修改,如果我想将变量x的值自增1呢?你是无法修改的,你就算修改变量y的值,x的值也不会被改变:
function outter() {
let x = 3;
return x;
}
let y = outter(); // 3
y++;
console.log(y); // 4, y的值确实被修改了
console.log(outter()); // 3, 函数内部x并没有被修改
复制代码
有可能你会想到,那我在函数内部将x自增,然后再返回不就可以了?
function outter() {
let x = 3;
x++;
return x;
}
console.log(outter()); // 4
复制代码
OK,没问题,但是我想每次调用函数的时候,x都会自增,就像一个计数器一样,x的值会随着我的调用次数动态增加。我们可以按照上面的代码来演示一下看能否达到要求:
function outter() {
let x = 3;
x++;
return x;
}
console.log(outter()); // 4
console.log(outter()); // 4, 但我想要的是5
console.log(outter()); // 4, 但我想要的是6
复制代码
会发现每次调用都是4,因为当你调用outter()
的时候,x在最开始都会被重新赋值为3然后自增,所以每次拿到的值都是固定的,并不会动态增加。那这时该咋办呢? 这里闭包就能派上用场了!
闭包的最基本演示
还记得之前所说的吗,内部函数可以调用外部函数内声明的变量,我们先看一下在内部函数操作一番后,我们能否拿到x的值
// 外部函数
function outter() {
let x = 3;
// 内部函数
function inner() {
// 在内部函数操作x
x++;
}
// 调用一次内部函数,将x进行更新
inner();
// 最后将x进行返回
return x;
}
console.log(outter()); // 4
console.log(outter()); // 4
console.log(outter()); // 4
复制代码
这样是可以获得x的值,但这样还是达不到我们计数器的要求,因为每次调用outter()
时,x的值都会被重新赋值为3。 我们应当绕过outter()
函数重新赋值的步骤,只需要获得x自增的操作就可以了。 怎么只获取自增的操作呢,现在自增的操作是在内部函数inner()
里,我们能否只拿到内部函数?当然可以啦!!
JS是一个弱类型语言,并且支持高级函数。就是说,JS里函数也可以作为一个变量来进行操作!我们在外部函数outter()
里将内部函数作为变量进行返回,就可以拿到内部函数了 。接下来要仔细理解代码,这种操作就是闭包:
// 外部函数
function outter() {
let x = 3;
// 内部函数
function inner() {
// 在内部函数里操作x
x++;
// 每次调用内部函数的时候,会返回x的值
return x;
}
// 将inner()函数作为变量返回,这样当别人调用outter()时就可以拿到inner()函数了
return inner;
}
let fun = outter(); // 此时拿到是函数inner(),就是说fun此时是一个函数
// 我们接下来调用fun函数(就等于在调用inner函数)
console.log(fun()); // 4
console.log(fun()); // 5
console.log(fun()); // 6
复制代码
可以看到上面代码完美拿到了内部函数inner()
,并实现了需求。内部函数对外部函数的变量(环境)进行了操作,然后外部函数将内部函数作为返回值进行返回,这就是闭包。上面代码的思路步骤就是:
调用外部函数outter()
---> 拿到内部函数inner()
---> 调用inner()
函数 --- > 成功对outter()
函数内的变量进行了操作。
看到这有人可能会说,我为啥要多一节步骤,要先拿到内部函数,再对变量进行操作啊?不能直接在外部函数里对变量进行操作,省了中间两个步骤吗? 哥,之前不是演示了吗,如果直接从外部函数操作,变量值是“死”的,你是无法实现动态操作变量的。 因为外部函数每次调用完毕后,会销毁变量,如果再重新调用则会重新为变量开辟空间并重新赋值。内部函数的话则会将外部函数的变量存放到内存中,从而实现动态操作
通过闭包实现装饰模式
上面演示的是内部函数可以操作外部函数的变量,其实不仅仅是某个变量这么简单,内部函数可以操作外部函数所拥有的环境,并可以携带整个外部函数的环境。这句话如果不能理解也完全没关系,丝毫不影响你平常使用闭包,使用的多了自然而然就会明了。我们接下来继续演示闭包,更加加深理解:
现在我有一个需求,我想让一些函数运行的同时并打印日志。这个打印日志的操作并不属于函数本身的逻辑,需要剥离开来额外实现,这种“扩展功能”的需求就是典型的装饰模式。我们先来看一下普通的做法是怎样的:
function fun() {
console.log('fun函数的操作');
}
fun(); // fun函数的操作
复制代码
我们要对fun()
函数进行扩展功能,最直接的办法当然是修改fun()
函数的源代码咯:
function fun() {
console.log('额外功能:在运行函数前打印日志');
console.log('fun函数的操作'); // fun()函数本身的功能
console.log('额外功能:在运行函数后打印日志');
}
复制代码
这样是达到了需求,但是如果我有几十个函数需要扩展功能呢,岂不是要修改几十次函数源代码?上面只是为了做演示,将扩展功能写的很简单只有两句代码,可往往很多扩展功能可不止几行代码那么简单。况且,很多时候就不允许你修改函数的源代码!所以上面这种做法,是完全不行的。
这时候,我们就可以用到闭包来实现了。函数可以当做变量并进行返回,那么函数自然也可以当做变量作为参数进行传递。这就非常非常灵活、方便了。我将需要扩展的函数当做参数传递进来,然后在我的函数里进行额外的操作就可以了:
// 需要被扩展的函数
function fun() {
console.log('fun函数的操作');
}
// 闭包的外部函数,需要接收一个是函数的参数
function outter(f) {
// 此时f就是外部函数的一个成员变量,内部函数理所应当的可以操作这个变量
function inner() {
console.log('额外功能:在运行函数前打印日志');
f(); // 调用外部函数的变量f,也就是说调用需要被扩展的函数
console.log('额外功能:在运行函数后打印日志');
}
// 外部函数最后将内部函数inner返回出去
return inner;
}
// 调用外部函数,并传递参数进去. 这样就可以拿到已经扩展后的函数:inner()
let f = outter(fun);
f(); // 此时f函数已经将原来的fun()函数功能扩展了,就相当于是inner()
// 一般装饰模式都是将原函数给覆盖:
fun = outter(fun);
fun(); // 此时再调用原函数的话,其实就是在调用inner(),是包含了扩展功能的
/*
输出内容:
额外功能:在运行函数前打印日志
fun函数的操作
额外功能:在运行函数后打印日志
*/
复制代码
通过闭包就完美实现了装饰模式,如果还有其他函数需要扩展的话,直接调用outter()
函数即可,简单方便。如果上面这个操作看不明白,千万不要想复杂了,第一个闭包演示是操作变量x,这个闭包演示也是操作变量,只不过这个变量f
是一个函数罢了。本质没有任何区别。
闭包的总结
现在我们来对闭包进行总结一下,原理方面就不谈了,就只谈使用。
使用的思路是:
调用外部函数outter()
---> 拿到内部函数inner()
---> 调用inner()
函数 --- > 成功对outter()
函数内的变量(环境)进行了操作。
闭包是啥呢 ?就是将内部函数作为返回值返回,内部函数则对外部函数的变量(环境)进行操作。
为啥要通过内部函数这一步骤呢?因为内部函数可以将外部函数的环境存放到内存里,从而实现提供了更为灵活、方便的操作。
闭包的使用不难,当你使用熟练之后,再去了解背后原理就会非常轻松了。
小扩展
本文只终于讲解闭包的基本使用,其他稍微深一点的东西就不讲了,有兴趣的可以去扩展一下:
- 在面向对象(OOP)的设计模式中,比如Java,装饰模式是需要通过继承和组合来实现,装饰者和被装饰者必须都继承了同一个抽象组件。 而JS中,则通过闭包非常灵活的实现了装饰模式,任何函数都可以被装饰从而扩展功能。不过这还不算最方便,在python里直接是从语法层面提供了装饰模式,即装饰器。 JS在ES6也通过语法层面实现了装饰器,不过和python的有些不太一样,有兴趣的可以去了解一下。
- 内部函数可以操作外部函数的变量,上面样式的那些变量都是固定的值,如果变量是一个引用的值(比如引用了外面的一个数组,在外部函数的外面也可以直接对数组进行修改),会产生什么后果。
- 运用闭包的好处上面已经演示了,那闭包的坏处是什么?提示:内存