[路飞]闭包的理解

103 阅读4分钟

闭包的理解

什么是闭包

闭包,当函数可以记住并访问其所在的词法作用域时,就产生了闭包。

闭包在 JavaScript 中随处可见,我更愿意把闭包看作 JavaScript 中的一种现象,如下列代码:

function foo() {
    var a = 1;
    
    function bar() {
        console.log(a)
    }
    
    bar()
}

foo();

在上面这段简单的代码中,产生了闭包,因为函数 bar 记住了其词法作用域(即函数 foo的作用域)的变量 a 并且访问了变量 a,当然这段代码我们看不出来函数 bar 对变量 a 的记忆现象,所以这段代码虽然产生了闭包,但是是一段没有什么实际意义的闭包,不能算我们认为的有用闭包,下面我们改进一下。

function foo() {
    var a = 1;
    
    function bar() {
        console.log(a)
    }
    
    return bar()
}

var fn = foo();
fn(); // 输出 1

我们将函数 bar 作为函数 foo返回值,然后在函数 foo 的词法作用域中调用,仍然能够访问到函数 foo 的内部作用域中的变量,ok,这就是我们所认为的有用的闭包,如果你还没有理解,或者认为闭包没有什么用处的话,我们再稍作改进。

假设有如下需求,我们需要完成一款小游戏中对游戏内金币的修改,但是为了维护金币的安全,我们希望将定义金币的变量设置的尽量私密,不能轻易被访问和修改,在需要修改金币数量的时候我们只能使用金币相关的接口方法去修改金币变量,这个时候我们可以借助闭包的特性,来处理。

(function(){
    var golds = 100;
    window.addGolds = function(){
        golds++
    }
    window.decreaseGolds = function(){
        golds--
    }
})()

通过这样处理,我们没有办法直接访问修改 golds 的值,只能通过我们定义的方法去修改,可以理解为将变量隐藏起来,做到了私有化,这也是闭包的运用场景之一。

闭包的运用场景

闭包的运用除了我们刚刚讲的,私有化变量,还有很多运用场景,列举如下:

  1. 私有化变量
  2. 循环赋值
  3. 单例模式
  4. IIFE模式
  5. 模块化
  6. 函数的珂里化
  7. ......

我们简单介绍几种。

循环赋值

let result = []
for(var i = 0; i < 6; i++) {
    result[i] = function() {
        console.log(i)
    }
}
result[2]() // 输出6

一个很常见的面试问题,这里我们希望输出的是2,然而由于var会造成变量提升,导致输出是6,我们可以通过闭包解决。

let result = []
for(var i = 0; i < 6; i++) {
    (function(i){
        result[i] = function() {
            console.log(i)
        }
    })(i)
}
result[2]() // 输出2

通过闭包我们解决了循环赋值的问题,但是我个人认为,能够解决这个问题不仅仅完全是因为闭包,这里是通过即时函数,在每次的迭代过程中,为result[i]所储存的函数创建了不同的词法作用域,从而达到了目的,很显然其实在第一段代码中也存在闭包的现象。

单例模式

var PersonSingleton = (function() {
    function Person(name){
        this.name = name
    }
    var instance
    return function(name) {
        if(instance) return instance
        return instance = new Person(name)
    }
})()
let lb = new PersonSingleton('lb')
let wyy = new PersonSingleton('wyy')
console.log(wyy) // Person { name: 'lb' }

IIFE模式

在jQuery中,就使用了IIFE模式对类库进行封装和模块化,起内部存在着闭包。

(function(){
    var $ = window;
    ...
    window.query = function(...){
        ...
    }
})(window)

模块化

var MyModule = function() {
    var name = 'my module'
    var getModuleName = function(){
        console.log(name)
    }
    return {
        getModuleName
    }
}

var myModule = MyModule()
myModule.getModuleName()

通过闭包的特性,我们可以封装模块,使得模块内部的变量可以与外部变量隔绝,不会造成变量污染,同时也可以方便模块内部的维护。

闭包的缺点

闭包可能会带来内存的泄漏,但是闭包不是一定会带来内存的泄漏,只有在内部函数对词法作用域中的变量一直存在引用关系时,才会导致内存无法释放,但是通常来时这种情况下,我们都会需要使用这些变量,从某种程度上来说,也不能算内存泄漏,只不过在开发中,我们不要过度使用闭包,否则会造成大量的内存无法释放。

总结

闭包其实是一个很简单的概念,但是因为在许多问题的解决中都存在的闭包的现象,我个人认为,大部分这些问题都不是由于闭包技术得以解决的,闭包本身也不是一个技术,在我看来只是js中的一种现象,一种特征,很多问题光靠闭包是无法解决的,也许当我们觉得闭包非常复杂的时候,可能是理解产生了偏差。