函数闭包机制

1.闭包理解

1.1 闭包机制

闭包是一种机制,是把函数中的声明的变量和函数定义时的作用域绑定到一起来解析函数的机制是闭包机制。

1.2 闭包表现形式

  1. 函数嵌套

  2. 内部函数持有外部函数作用域的引用 (不论内函数什么时候执行,只要内函数不解除对内函数对象的引用,外函数内的栈内存不会像外函数离开执行栈一样主动清除,外函数的作用域就会一直被内函数持有)

注: 为了方便本文使用,此处规定在一个一层嵌套模型中,函数内部嵌套的函数为内函数,内函数外部的函数叫外函数。

2.闭包产生原因

2.1 词法作用域

JavaScript使用词法作用域,函数执行时所使用的作用域是函数定义时的作用域,而不是执行时的作用域。

2.2 词法作用域实现原理

结合之前写的“JavaScript中的解释器和编译器”可知,实现原理是函数定义时会保存定义时所处作用域,相当于引用了所处作用域,使得其在垃圾回收时不会被销毁,函数执行时创建执行上下文中的词法环境和变量环境时会将保存的作用域添加进去,这样函数在执行时使用的就是定义时的作用域。

3.闭包应用

闭包的应用主要可以从闭包机制下函数的作用域来考虑。

3.1 作用域隔离特性

let name = "Danny"
;(function() {
    
    
    let name = "Lucy"
    console.log(name)
})();
console.log(name)

使用立即执行匿名函数创建一个函数作用域,可以将想要使用的变量与外界作用域进行隔离,提供了隔离机制。

3.2 作用域持久存在特性

<div id="container" style="height: 20px; width: 20px; background: black;"></div>
<script>
    function optimizer() {
      
      
        let lastTime = Date.now()
        let cnt = true
        return function() {
      
      
            if(Date.now() - lastTime < 10000 && cnt === false)
                return
            else {
      
      
                console.log('you are clicking')
                lastTime = Date.now()
                if(cnt)
                    cnt = false
            }
        }
    }
    document.getElementById("container").addEventListener("click", optimizer())
</script>

使用作用域持久存在性,来存储变量。上面实现了一个节流函数,要求点击按钮的时间间隔至少为10秒,其中使用lastTime来存储上一次点击时间。

3.3 作用域参数传递特性

let arr = []
for(var i = 0; i < 10; i++)
    arr[i] = () => console.log(i)
arr[1]()

// 解决arr[]()输出都是同一个值的问题
for(var i = 0; i < 10; i++) {
    
    
    ;(function (j) {
    
    
        arr[j] = () => console.log(j)
    })(i);
}
arr[1]()

上面使用了“作用域隔离”特性和“作用域参数传递”特性解决了在“JavaScript解释器和编译器”中提到的例题中的问题。循环创建的每个块级作用域中再创建一个函数作用域,var声明的i的会覆盖块级作用域中var声明的其它i,此时用函数作用域来进行隔离,并将i作为参数传递进函数作用域,JavaScript中函数传参都是传值,传进来的i会在函数作用域中重新开辟一块栈内存来存储。这种方法也可以说是运行中动态传值,可以参考关于“函数柯里化”的总结。

for (var i = 1; i <= 5; i++) {
    
    
    setTimeout(function timer() {
    
    
        console.log(i)
    }, i * 1000)
}

这个相似的例子可以使用函数作用域配合参数传递来进行解决,也可以利用let声明和块级作用域来解决,也可以依靠setTimeout的第三个参数直接传给timer函数来解决。

4.解决闭包造成的内存泄漏

4.1 内存泄漏的原因

闭包机制会造成在内函数解除对内函数对象的引用之前,外函数中栈内存和堆内存中的内存都不会回收。

4.2 内存泄漏的情况

结合之前写的“JavaScript中的垃圾回收”可知,垃圾回收针对的是堆内存,垃圾回收程序不会回收栈内存,因为执行上下文出栈时会自动清空栈内存。所以在内函数解除对内函数对象的引用前,外函数中的栈内存不会被回收,但是可以回收堆内存。闭包中的内存泄漏就是指不恰当的操作导致外函数中堆内存不会被回收。

情景 解决思路
栈内存未被内函数引用 无法回收
栈内存被内函数引用 不能回收
堆内存未被内函数引用 使用“解除引用”来回收
堆内存被内函数引用 1.将会用到的属性存进外函数栈内存 2.内函数引用这些新开辟的栈内存 3.使用“解除引用”来回收堆内存

4.3 内存泄漏举例

举例1—堆内存未被内函数引用

function outer() {
    
    
    let obj = {
    
    
        name: "Danny",
        gender: "man"
    }
    let msg = "hello"
    return function inner() {
    
    
        console.log(msg)
    }
    obj = null // 使用“解除引用”来回收堆内存
}

let func = outer()
func()

举例2—堆内存被内函数引用(借用<<JavaScript高级程序设计(第四版)>>的例子)

<!--未改进情况:造成内存泄漏-->
<div id="container"></div>
<script>
    function getElementId() {
      
      
        let element = document.getElementById("container")
        return function() {
      
      
            console.log(element.id)
        }
    }
    let getId = getElementId()
    getId()
</script>

<!--改进情况:避免内存泄漏-->
<div id="container"></div>
<script>
    function getElementId() {
      
      
        let element = document.getElementById("container")
        // 第一步:将要使用的对象中的感兴趣的属性存入栈内存。
        let id = element.id
        return function() {
      
      
            // 第二步:将内函数对堆内存中的对象的引用改为对栈内存中感兴趣的属性的引用。
            console.log(id)
        }
        // 第三步:手动解除堆内存的引用。由于内函数解除其函数对象的引用前,外函数不会清理栈内存中对堆内存的引用,所以必须手动清理才能使堆内存被清理。
        element = null
    }
    let getId = getElementId()
    getId()
</script>

说明:
举例2属于最后一种情况“堆内存被内函数引用”。如果内函数并不是用到堆内存中对象的所有属性,那么我们就可以采用上述方法回收堆内存中对象不使用的属性占用的内存。

Guess you like

Origin blog.csdn.net/qq_37464878/article/details/121418057