闭包--js(二十一)

94 阅读5分钟

一、函数创建与定义的过程

  • 函数定义阶段
    1. 在堆内存中开辟一段空间
    2. 把函数体内的代码一模一样的的存储在这段空间内
    3. 将这个空间的地址, 赋值给函数名(栈内存中)
  • 函数调用阶段
    1. 按照变量名内的存储地址找到堆内存中对应的存储空间
    2. 在调用栈中开启一个空间 (我们叫做执行空间)
    3. 在执行空间中进行形参赋值
    4. 在执行空间中进行预解析
    5. 在执行空间中完整执行一遍函数内的代码
    6. 开辟的函数执行空间销毁

二、不会销毁的函数执行空间

  1. 当函数内返回一个复杂数据类型
  2. 并且函数外部有变量接收这个复杂数据类型
  3. 函数执行完毕的函数执行空间不会被销毁
  4. 如果以后不需要这个执行空间了, 那么只需要将外部的变量更改一个引用地址就行
function fn() {
    const obj = {
        a: 1,
        b: 2
    }
    return obj
}

const res = fn()
console.log(res)

// 如果后续不需要这个空间了, 只需要让 res 指向别的位置即可
res = 100

三、认识闭包

  • 需要一个不会被销毁的函数执行空间
  • 需要 直接 或 间接的返回一个函数
  • 内部函数, 需要访问外部函数内创建的局部变量
  • 概念: 函数里的函数
  • 优点:
    1. 可以在函数外面访问到函数内部的变量
    2. 延长了变量的生命周期
  • 缺点
    1. 每一个闭包都会创建一个不会销毁的内存空间,如果闭包书写的太多, 那么这个不会被销毁的空间就越来越多,就可能会造成页面/程序的卡顿,大量使用会造成内存溢出
function outer () {
    let a = 100
    let b = 200

    // 我们说 inner 是 outer 的闭包函数
    function inner () {
        /**
         * 我使用了一个 a 变量, 但是 inner 自己没有
         * 所以我用的是 外部函数 outer 内部的变量 a
        */
        // console.log(a)
        return a
    }
    return inner
}

// 我们说 res 是 outer 的闭包函数
let res = outer()
console.log(res)//ƒ inner() {
                /**
                 * 我使用了一个 a 变量, 但是 inner 自己没有
                 * 所以我用的是 外部函数 outer 内部的变量 a
                */ 
               //   return a  }
console.log(res())//100

四、沙箱模式

  • 是 JS 中 利用 闭包 完成的一个设计模式
    • 设计模式: 为了解决某一类问题的最优化的写法, 但不是万能
  • 利用了 函数内 "间接" 返回一个函数
  • 外部函数 返回一个对象, 对象内书写多个函数
 function outter() {

            let a = 100
            let b = 99

            const obj = {
                getA() {
                    return a
                },
                setA(val) {
                    a = val
                },
                getB() {
                    return b
                },
                setB(val) {
                    b = val
                }
            }

            return obj
        }

        /**
         *  此时调用函数会开启一个执行空间 (XF666)
         *      内部有对应的局部变量 a 和 b
         * 
         *  所以我们现在 res_1 使用的变量是来自 执行空间 XF666
        */
        const res_1 = outter()
        console.log(res_1.getA())   // 100
        res_1.setA(999)
        console.log(res_1.getA())   // 999


        /**
         *  此时调用函数会开启一个执行空间 (XF777)
         *      内部有对应的局部变量 a 和 b
         * 
         *  所以我们现在 res_2 使用的变量来自 执行空间 XF777
        */
        const res_2 = outter()
        console.log(res_2.getA())   // 100

1、沙箱模式小案例

<button class="sub">-</button>
<input class="inp" type="text" value="1">
<button class="add">+</button>

<br>

<button class="sub1">-</button>
<input class="inp1" type="text" value="1">
<button class="add1">+</button>
// 准备一个沙箱
function outer() {
    let a = 1
    return {
        getA() {
            return a
        },
        setA(val) {
            a = val
        }
    }
}

// 0. 获取元素
const subBtn = document.querySelector('.sub')
const addBtn = document.querySelector('.add')
const inp = document.querySelector('.inp')

// 0. 准备变量
// let count = 1
let res = outer()
subBtn.onclick = function () {
    let count = res.getA()
    res.setA(count - 1)
    inp.value = res.getA()
}
addBtn.onclick = function () {
    // count++
    let count = res.getA()
    res.setA(count + 1)
    inp.value = res.getA()
}

// 0. 获取元素
const subBtn1 = document.querySelector('.sub1')
const addBtn1 = document.querySelector('.add1')
const inp1 = document.querySelector('.inp1')

// 0. 准备变量
let res1 = outer()
subBtn1.onclick = function () {
    let count = res1.getA()
    res1.setA(count - 1)
    inp1.value = res1.getA()
}
addBtn1.onclick = function () {
    let count = res1.getA()
    res1.setA(count + 1)
    inp1.value = res1.getA()
}

五、沙箱模式的语法糖

  • 尽可能的简化沙箱模式的语法
  • 利用的是 getter 和 setter 来进行操作数据
  • 语法糖:
    • 再不影响功能的情况下, 提供一点更适合操作的语法
    • 一碗水, 喝了能让我们补充水分, 水里加点糖, 喝完能让我们补充水分, 并且更好喝了
// 基础版
        function fn1() {
            let a = 100
            
            return {
                getA () {
                    return a
                },
                setA (val) {
                    a = val
                }
            }
        }

        const res1 = fn1()
        console.log(res1.getA())    // 100
        res1.setA(999)
        console.log(res1.getA())    // 999

// 语法糖 优化版
        function fn2() {
            let a = 1
            let b = 'QF001'

            return {

                /**
                 *  将来对象中如果有人访问了 属性 a, 那么就会触发 get a 这个方法
                 *  如果有人修改了 属性a, 那么就会自动触发 set a 这个方法
                */

                get a () { return a },
                set a (val) { a = val },

                get b () { return b }
            }
        }
        const res2 = fn2()

        // 用了语法糖之后的简化写法
        console.log(res2.a) // 1
        res2.a = 666
        // 用了语法糖之后的简化写法
        console.log(res2.a) // 666

        console.log(res2.b) // 'QF001'
        res2.b = 'QF999'    // 当前没有给 属性b 设置 set 方法, 所以修改是无效的
        console.log(res2.b) // 'QF001'

六、闭包的面试题

function fun(n, o) {
    console.log(o)

    const obj = {
        fun: function (m) {
            return fun(m, n)
        }
    }

    return obj
}

var a = fun(0)    // undefined
a.fun(1)    // 0
a.fun(2)    // 0
a.fun(3)    // 0

/**
 *  var a = fun(0)
 *  a.fun(1)
 *  a.fun(2)
 *  a.fun(3)
 *
 *  1. var a = fun(0)
 *          调用 fun(QF001) 函数(QF001) 传递一个 参数 0
 *              全局函数 fun (QF001) 的 形参 n == 0     形参 o == undefined
 *          调用 fun 函数后, 会返回一个对象 存储在 变量 a 中, 这个对象内部有一个属性叫做 fun, 属性值为 一个函数(QF002),
 *              所以我们可以通过 a.fun()   去调用这个函数
 *
 *  2. a.fun(1)
 *      2.1 调用这个函数 会 return 一个函数 fun (为全局函数 QF001) 的调用结果,   
 *      2.2 调用全局函数 fun(m, n)       m 此时 传递的是 1, n 传递的是 0
 *      2.3 执行全局函数 fun(m, n) 内部会输出第二个形参
 *
 *  3. a.fun(2)
 *      2.1 调用这个函数 会 return 一个函数 fun(为全局函数 QF001) 的调用结果
 *      2.2 调用全局函数 fun(m, n)  m 此时传递的是 2, n 传递的是 0
 *      2.3 执行全局函数 fun(m, n) 内部会输出第二个形参
 *
*/