故事的开始是闭包

123 阅读5分钟

——关于闭包的素质三连,什么是闭包?闭包的应用场景?闭包的缺点?通过这篇文章我讲一一阐述,这三个问题,习惯大家会了这个三连之后,也给我来一个素质三连哈(点赞,收藏,关注!🙏🙏🙏)

什么是闭包?

这里直接引用mdn 当中的定义:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

上一个简单的例子

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

如上述,一个函数 makeFunc,中定义一个函数以及一个变量name ,并将这个函数返回出去,由于作用域的影响(实质是函数执行上下文),函数外部如果想去访问内部的name变量只能通过返回的函数去访问。这就实现了变量私有化。

这就引入了闭包的使用场景,主要是为了变量的私有化。

使用场景

从模块化来说,common.js,ex module,原理会使用到闭包去实现模块化。

从性能优化上面来说,防抖,节流函数也会用到闭包。 具体可参考我这篇文章 浅谈——js函数防抖与节流

image.png

柯里化函数也会用到,其目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用

// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
    return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)

// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
    return height => {
        return width * height
    }
}

const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)

// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)

往设计模式上面去想,单例模式,也可以用闭包去保证实现,单例模式的应用 redux 等状态管理工具,为了保证单一的store 也是采用闭包实现。

function Singleton(name) {
    this.name = name;
}
// 原型扩展类的一个方法getName()
Singleton.prototype.getName = function() {
    console.log(this.name)
};
// 获取类的实例
Singleton.getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!this.instance) {
            this.instance = new Singleton(name);
        }
        return this.instance
    }        
})();

// 获取对象1
const a = Singleton.getInstance('a');
// 获取对象2
const b = Singleton.getInstance('b');
// 进行比较
console.log(a === b);

闭包的缺点

闭包的缺点就是内存泄漏,我们看一下下面的例子

function makeFunc() {
    var name = "Mozilla";
    var a = 1
    add=()=>{
    a++
}
//这里add ,相当于直接定义到window上面了。
    function displayName() {
        alert(a);
    }
    return displayName;
}

var myFunc = makeFunc();
add()
add()
myFunc();
//这里a输出3,可见a一直没有被销毁。就出现了内存泄露

那么这是怎么出现的呢?这里我要先介绍一下垃圾回收机制。

垃圾回收

js不同于c,c++等语言,它不用去手动申请空间,清除空间,这些操作交给了浏览器中渲染进程。同时他与该进程中的js 执行线程是相互影响的。

堆与栈

一般来说,js的数据类型存储在两种不同的空间,堆空间和栈空间。一般类型的数据boolean,string,number,等都存在栈空间中,函数执行时候也放入栈空间,而引用类型的变量地址放在栈空间中,实际存储在堆空间中,栈空间的大小是固定的,堆空间的大小是变化的。一般随着数据变化而变化。当函数执行完毕,执行栈弹出时候,里面的一般类型的数据没有了引用就会销毁。而对于一些引用类型的数据如上例的a,浏览器就要采用gc垃圾回收机制去清理。

堆内存分为新生代和老生代这些变量一般会先存在于一个名为新生代的空间中,新变量一般先放入新生代中,而新生代空间的清理也比较频繁的,它牺牲空间换取时间效率,所以它划分为两个空间,使用区域和空闲区,变量先放入使用区中,一段时间后出发垃圾回收机制时候,发现如果变量没有引用就会释放,剩余的移入空闲区,然后区域对调,等待下一次清理,经过多次清理还留在新生代的变量会移动到老生代中。

那么老生代里面怎么处理垃圾呢,有两种方式,引用计数与标记清理(现在大多数浏览器都采用标记清理),引用计数,就是浏览器判断该变量是否还存在引用,如果存在就不清理。但改方法的弊端就是如果存在重复引用,就没法清理,例如

let b ={}
let a = {}
b.obj = a;
a.obj = b;

这样的话即使a,b变量都不在使用,也无法即使清理。

标记清理的话,就是判断该变量能不能访问到,可以访问到就添加标记,如果后续不能访问到了就清理标记。

回到老生代,这里回收频率相对不高,每次会清理没有标记的变量,但是这样的话,会出现内存碎片化,所以,每隔一段时间会左移或者右移去整理内存。

如何去防止闭包带来的内存泄漏

1.就是我把变量myFunc 赋值为空,那么就无法访问到该变量。 2.暴露出一个方法去清理变量。

如果有帮助的话记得三连哈!!!家人们!