对JavaScript闭包的一些个人见解

228 阅读8分钟

说到闭包,真的是很容易让新手程序员犯迷糊的一个概念,其实我自己对闭包的理解也非常的粗浅,同时我算是一个实用至上的人,百度的那些文章里面那些晦涩的概念看起来真的很容易让人头大,总是很干的来一句有权访问另一个函数内部作用域的函数就是闭包,而且闭包的写法也很多样化,真的很容易让新手犯迷糊,我这里尽量尝试最简单粗暴的方式讲一下我个人对闭包的理解以及我想到的一些简单的使用场景。

首先让我们忘掉什么鬼闭包,不要照着公式去写闭包,因为每个作者写出来的闭包可能都有点不一样,我们只需要把它当做一个普通的JS函数来对待就行了,这样也许反而更容易看懂。

首先我们来看一个超级简单的函数

function counter() {
    var count = 0;
    count ++;
    return count;
}
counter(); // 返回1
counter(); // 返回1
counter(); // 返回1

这个方法相信所有人都能够理解的吧,如果不能,建议从头开始学习JavaScript...这里我调用了counter()方法三次,每次都返回1,因为每次调用这个方法都会重新进入函数内部,从上到下执行,count变量都会被重新赋值为0,所以每次得到的结果都是1.

现在让我们来升级一下需求,假如我们想要count的值随着每次调用counter()方法都进行累加,该怎么去做呢?

var count = 0;
function counter() {
    count++;
    return count
}
counter(); // 返回1
counter(); // 返回2
counter(); // 返回3

将count设置为全局变量,然后在couter函数中对count进行计算,这次我们得到了123,。实际上我们在JavaScript基础中就学过,全局变量的执行环境是全局的,在全局任何地方改变它都会立即生效,同理任何全局变量自被改变开始,下一次改变的就不是它的初始值而是上一次改变的值。让我们再次升级下这个例子:

var count = 0;
function counter() {
    count++;
    return count;
}
counter() // 返回1
if(count >= 0) {
    counter()
}
counter() // 返回3

从上面的例子可以看出,全局变量的特点就是,无论在哪里、无论在何种作用域内,它的状态都是全局共享的,一般情况下,全局可访问&全局可修改。

完美解决了我们的需求有没有?是的,这么说也没错,但是这种方式的缺点实在是太过于明显了,为什么我们开发中要尽可能避免声明全局变量?原因实在是太多了,全局可任意修改数据,且前面的修改会直接影响后续代码的运行结果,如果你的代码中有多处地方使用全局变量且存在并行、异步调用的话,会导致太多太多不可预知的错误,所以无论什么时候,我们都应该尽量减少全局变量的定义!

现在让我们再次来升级下我们的代码,既然全局变量缺点太多,我们把它变成一个局部变量如何?

{
    var count = 0;
    function counter() {
        count += 1;
        return count;
    }
}

其实到这里,基本上闭包的雏形已经出来了,记住,如果想要想获得一个真正的闭包,你要做的无非是在以上代码的基础上多做两件事:1.将这个代码块转为一个函数。 2.将counter函数作为外部函数的返回值。就像这样:

function fn() {
    var count = 0
    function counter() {
        count += 1;
        return count;
    }
    return counter;
}
var getCounter = fn();
getCounter(); // 返回1
getCounter(); // 返回2
getCounter(); // 返回3

一个标准的闭包已经写出来了,虽然可能你到现在都还不清楚为什么这样写就实现了闭包,但是别急,很快我就会用最通俗的原理告诉你闭包的实现原理,相信看完之后你就会恍然大悟。

我们将这个闭包和我们最开始写的错误示例进行比较:

// 错误写法
function counter() {
    var count = 0;
    count ++;
    return count;
}
counter(); // 返回1
counter(); // 返回1
counter(); // 返回1
--------------------------------分割线----------------------------------
// 闭包
function fn() {
    var count = 0
    function counter() {
        count += 1;
        return count;
    }
    return counter;
}
var getCounter = fn();  // 这个步骤非常重要!!!
getCounter();

在这个错误例子中,因为count变量定义在函数内部,而我们每次调用的都是counter()函数,那么它就每次都会从上至下的执行内部代码,每次都会创建一个新的局部变量count并返回,所以无论调用多少次函数,我们都会得到同样的结果。

而闭包写法呢?我刚才标记了一行非常重要的代码var getCounter = fn(), 我们调用了fn()并把它的返回值赋值给了getCounter这个变量,从此以后,我们再也没有调用过fn()函数,因此自始至终fn()函数本身只执行过一次,那么fn()函数内部的count变量自始至终也只初始化过一次,同时因为fn()的返回值是一个函数,所以我们用getCounter接收了fn()的返回值之后,每次调用getCounter()函数调用的并不是fn(),而是fn()返回的counter()函数

到了这里,你起码应该对闭包的原理理解了一半了,那就是:闭包内定义的局部变量只会初始化一次,每次调用闭包方法访问的内部变量都是同一个(PS:这里存在歧义,如果多次声明同一个闭包函数但是赋值给不同的变量进行接收,它们之间的值是不可共享的,比如var demo1 = fn(), demo2 = fn();那么demo1和demo2内的count是不可共享的),如果你能明白我以上说了啥,那么恭喜你,胜利就在眼前了!

如果你已经明白了为什么每次调用闭包都是访问的同一个变量,那你可能还会疑惑,为什么闭包内部的变量可以持久化,这个就涉及到一点事件循环、变量生命周期以及内存方面的知识了,我在这里尽量通俗易懂的解释一下。

其实闭包中变量的持久化原理可以用内存机制也可以用eventLoop中的调用栈、事件队列等机制解释,只是分析机制时所站的角度略有不同。我们在这里用简单易懂的变量生命周期来解释一下。

全局变量会在window对象被销毁时一起销毁,而局部变量会在它的执行环境被销毁时一起销毁,一个函数声明但未调用时,并不会创建内部运行环境,我们用一个变量比如 var demo = fn() 来接收的同时就调用了fn()函数,那么现在在某个特定的运行环境中就初始化了一个变量count,并且因为demo变量接收了fn()返回的一个函数,这个函数可以用来访问那个特定运行环境中的count,所以每一次调用demo方法,我们都可以操作闭包内部的同一个count变量,直到demo变量的运行环境被销毁之前,demo变量都会存在(除非被改写或删除),结合上面说的闭包函数内的内部变量只会执行一次,所以我们就实现了闭包内部变量的持久化。

扩展:

1.为什么同一个闭包的多个实例之间的变量不可公用?

fn() {
    ...闭包代码...
}
var demo1 = fn(), demo2 = fn();

因为声明demo1和demo2的时候,都执行了一次fn()函数,因此demo1和demo2分别拥有了不同的运行环境,他们访问到的count并不是同一个count,如果想要保证全局访问的闭包内部变量都是同一个,就只调用一次闭包吧。

2.如何把闭包玩出花 我们已经知道了闭包的原理,但在实际运用中,一般不会用最基本的闭包,而是用一些进阶写法,比如:传参和差异化返回结果

比如:

function fn() {
   var a = 0, b = 1, c = 2;
   return function(type) {
       if(type == 0) {
           return a;
       } else if(type == 1) {
           return b;
       } else {
           return c;
       }
   }
}

var result = fn();
result(0); // 返回0
result(1); // 返回1
result(2); // 返回2

同样适用的还有我们平常用的非常多的定时器以及函数防抖节流等等,这里就不多做赘述了,大家可自行下去了解。

总结:

1.一个闭包实例因为只调用了一次闭包方法,而后调用的都是闭包返回值中的函数,所以一个闭包实例自始至终只对闭包中的内部变量进行了一次初始化。

2.因为我们用变量接收了闭包返回的函数,而这个函数有权访问闭包中的局部变量,所以我们可以一直访问这个闭包中的变量且改变该变量的值。

以上是个人对闭包的一些见解,有些地方为了让新手能够看懂所以写的不太准确,也有些地方可能确实存在理解上的错误,希望大家能帮忙指正!