闭包

173 阅读7分钟

首先,闭包是函数的高级应用方式,函数内部的函数就叫做闭包

在线代码示例:codesandbox.io/s/practical…

函数的两个步骤

  1. 函数定义

函数的内容分为两部分,函数名和函数体。把函数名当作变量存储到栈中(这个变量指向存储函数体的空间地址),把函数体内的代码以字符串的形式存储到堆中。

函数名存到栈中(基本数据类型),函数体存到堆中(复杂数据类型)

  1. 函数调用

首先根据栈中的地址找到函数体,然后在调用栈里开辟一个函数执行空间,在函数执行空间内进行形参赋值和预解析,然后把堆中的函数体代码复制一份到函数执行空间中来,最后执行这些代码,执行完毕后销毁这个函数执行空间。

如果多次调用,那么必然是调用一次执行完毕销毁,再次调用会再次开辟出新的函数执行空间,然后再次执行,执行完毕后销毁……

注意:定义在函数内部的变量,会随着函数执行空间的销毁而销毁,并不会保存上一次执行后留下的值,一切都会重新开始。

不会被销毁的函数执行空间

如果想要一个函数执行完后不会被销毁,那么需要满足两个条件:①函数内部返回一个复杂数据类型;②函数外部有变量接收。如果满足这两个条件,那么这个函数执行空间不会被销毁。

function fun () {
    console.log('hello world')
    return {}
}
const res = fun()
// 执行 fun() 且返回一个 空对象 {}
console.log(res); // hello world {}
// 创建了一个新的函数,此时返回的是新对象
const res2 = fun()
console.log(res2) // hello world {}
console.log(res == res2) // false

以上代码和函数的两个步骤一样,就是在调用栈中开辟的函数执行空间中执行代码的时候,多了一句retrun {} 这句代码会在调用栈中创建一个存储空间存储 {}并且返回,在内存的栈中有一个obj变量用来存储这个{}的地址。那么为了这个存储{}的变量可以访问到,为了保证这个{}的存在,函数执行空间就不会被销毁。

用处:延长了变量的生命周期

闭包

形成闭包的条件:

  1. 一个不会被销毁的函数执行空间
  2. 函数内部直接或者间接的返回一个函数
  3. 内部函数操作(访问,赋值等)外部函数的变量 当三个条件都满足的时候,内部函数就被称为外部函数的闭包函数
function fun () {
    const num = 100
    return function () {
        return num
    }
}
const res = fun() // res 就等于 return function () {} 是 fun 的闭包函数
console.log(res()); // 100

上述代码的执行过程:

  1. 在栈内存中存储 fun 变量,在堆内存中存储 fun 的函数体,fun 变量存储的是 fun 函数体的地址,指向 fun 函数体
  2. 在调用栈中开辟出一个函数执行空间,执行函数 fun
  3. 在执行函数 fun 时,首先定义了一个 num 的变量,然后 return 了一个函数,这个函数又被存储在函数执行空间中,且在栈内存中存储这个函数的地址 res,也就是接收 fun() 的返回值,就是这个函数,被 res 接收。

闭包.png

闭包的作用:

  1. 保护变量私有化,函数内部的变量就是私有变量
  2. 在函数外部,访问函数内部的私有变量res()可以拿到函数内部的num

闭包的优缺点:

  1. 保护变量私有化,优点:不污染全局;缺点:外部不能访问,需要闭包函数
  2. 可以在函数外部访问函数内部的变量,优点:不局限于私有变量;缺点:外部访问需要闭包函数
  3. 变量的生命周期,优点:变量的生命周期被延长了;缺点:一个不会被销毁的函数空间

因为不会被销毁,所以会占用内存,导致内存泄露

销毁闭包空间

如果想要销毁闭包,只要让不销毁的空间不存在了,闭包就没了。也就是可以让外部不再有变量接收,或者让外部接收的变量重新赋值。

function fun() {
    const num = 100;
    return function () {
        return num;
    };
}
// fun(); // 没有变量接收,那么就不会使用 return 的闭包函数
let res = fun(); // res 是 fun 的闭包,如果把 res 赋值给一个基本数据类型,那么就会销毁闭包函数
res = 100;

闭包的应用 - 沙箱模式

设计模式:为了解决特定问题给出的简洁而优化的解决方案

沙箱模式:为了解决变量私有化以后的访问和操作,在外部可以利用闭包函数访问或设置函数内部的私有变量或方法

使用:

  1. 利用闭包,把所有的函数或者属性都放在一个函数内部
function fun() {
    let num = 100;
    const str = "hello world";
    function inner1() {
        console.log("我是 inner1!");
    }
    // 间接返回一个函数
    return {
        // 获取 num
        getNum: function () {
            return num;
        },
        // 把接收到的形参赋值给 num
        setNum: function (val) {
            num = val;
        }
    };
}
const res = fun(); // res 接收的是一个对象,对象里面有一个函数,使用着外部函数的变量
console.log(res.getNum()); // 100
res.setNum(10);
console.log(res.getNum()); // 10

使用自执行函数向外暴露属性和方法

// 定义一个自执行函数 ()(),外部使用一个变量接收这个函数执行的返回值,也就是内部 return 出的 test 函数
const utils = (function () {
    function test() {
        console.log("test");
    }
    return {
        test
    };
})();

闭包的语法糖

它就是一个特殊的语法,getter 获取器和 setter 设置器

  1. 形成闭包
  2. 返回一个对象
  3. 在对象里以 getter 和 setter 的形式返回函数,语法如下
get 函数名 () {}
set 函数名 () {}

举例 - 使用普通写法

function fun() {
    let num = 100;
    return {
        getNum: function () {
            return num;
        },
        setNum: function (val) {
            num = val;
        }
    };
}
const res = fun();
console.log(res.getNum()); // 100
res.setNum(200);
console.log(res.getNum()); // 200

使用语法糖 getter 获取器和 setter 设置器写法

function fun1() {
    let num = 100;
    return {
        // 此时 getNum 存储的是函数的返回值,而不是这个函数了
        get getNum() {
            return num;
        },
        // 此时 setNum 赋值的时候,就是在调用这个函数
        set setNum(val) {
            num = val;
        }
    };
}
const res1 = fun1();
console.log(res1.getNum); // 100
res1.setNum = 200; // 设置 num 变量的值
console.log(res1.getNum); // 200
// 此时的 getNum 和 setNum 都已经不是函数了,而是一个对象的成员,它们相当于对象的 key,而调用它们得到的是对象的 value

getter和setter.png

function fun1() {
    let num = 100;
    return {
        // 可以直接写成 num
        get num() {
            return num;
        },
        // 同上
        set num(val) {
            num = val;
        }
    };
}
const res1 = fun1();
console.log(res1.num); // 100
res1.num = 200; // 设置 num 变量的值
console.log(res1.num); // 200

函数柯里化

函数柯里化,是一种函数的封装形式,把一个函数的两个参数拆开成为两个函数,每个函数一个参数,当有多个参数的时候,把第一个参数提取出来。

示例:封装使用正则去验证用户名

不使用柯里化写法

function testName (reg, name) {
    return reg.test(name) // 返回验证结果
}
const reg = /[^_]\w{5,11}$/
const res = testName(reg, 'zuo12345')
console.log(res); // true

使用柯里化写法

function testName1(reg) {
    return function (name) {
        return reg.test(name);
    };
}
const reg1 = /[^_]\w{5,11}$/;
const res1 = testName1(reg1)
console.log(res1('zuo12345')); // true

使用闭包形式的好处是,封装的时候,可以直接导出 res1,使用的时候,只用传递一个参数 name即可,当多处使用这个正则或者这个方法的时候,如果遇到需要修改正则,可以只用修改这里的一处正则,就修改了所有使用它的地方,便于维护。

循环绑定事件

<!DOCTYPE html>
<html>
    <head>
        <title>Parcel Sandbox</title>
        <meta charset="UTF-8" />
    </head>

    <body>
        <button>1</button>
        <button>2</button>
        <button>3</button>
        <script src="src/index7.js"></script>
    </body>
</html>
var btns = document.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = (function (i) {
        return function () {
            console.log(i + 1); // 每次点击 按钮,会输出对应的索引 1 2 3
        };
    })(i);
}

点击 btns[i] 真正执行的函数是闭包函数,把 i 变成了一个私有变量,每次点击都会执行一遍

另一种形式

var btns = document.querySelectorAll("button");
var handler = function (j) {
    btns[j].onclick = function () {
        console.log(j + 1);
    };
};
for (var j = 0; j < btns.length; j++) {
    handler(j);
}

小测试

function fun (i) {
    return function (n) {
        console.log(n + (--i))
    }
}
var fn = fun(1)
fn(2) // 2
fun(5)(3) // 7
fun(2)(5) // 6
fn(9) // 8