• 让你弄懂js中的闭包


    闭包

    之前在我执行上下文执行上下文栈这篇文章中,出现了这样一个题目

    for (var i=0; i<10; i++){
        setTimeout(
            ()=>{
                console.log(i)  // 猜猜结果
            },
            2000
        )
    }
    

    题目答案是: 大约2s后输出10个10

    引发这个问题的原因恰恰就是因为var关键字没有块级作用域,当定时器异步执行时,同步执行的for早已经执行完毕了,此时i早已经变成了10了,所以最终10个定时器输出的i就全部都是10了

    我后来使用了一个方法解决了这个问题,这个方法就是本篇文章介绍的一个概念----闭包

    for (var i = 0; i < 10; i++) {  // for循环的i由于是var申明没有块级作用域,是全局变量
        (function (i) {     // 形参i是IIFE的局部变量
            setTimeout(
                () => {
                    console.log(i)
                },
                2000
            )
        })(i)   // 闭包产生
    }
    

    闭包如何产生

    我们先不管闭包是什么,有什么用。而是先研究一下闭包是如何产生的

    function func1 () {
        var a = 1   // 断点打在这
        function func2 () {
            console.log(a)
        }
        func2()     // 无需调用,闭包也已经产生了
    }
    
    func1() // 执行函数的时候,就会落在断点处
    

    到这,我们就能知道: 当两个函数互相嵌套,内部函数引用外部函数的变量时,就会产生闭包

    需要注意的是:

    • 变量值可以是对象也可以是普通类型的值
    • 内部函数不需要调用,只要引用了外部函数的变量,闭包就已经产生

    最后,我们总结一下闭包的产生, 总共需要两个条件:

    1. 函数嵌套
    2. 内部函数引用了外部函数的变量

    闭包是什么

    根据闭包是如何产生所需的两个条件,我们就能顺便说出闭包究竟是什么东西了---- 闭包就是那个包含被引用变量的对象

    常见的闭包

    常见的闭包有两种

    • 将内部函数作为外部函数的返回值
    • 将函数作为实参传递给另一个函数调用

    先看第一种:

    function func1() {
        var a = 1
        function func2() {
            a++
            console.log(a)
        }
        return func2
    }
    
    var getInnerFunc = func1() // 执行外部函数得到其返回值 ---- func2函数
    getInnerFunc() // 2
    getInnerFunc() // 3
    getInnerFunc() // 4
    

    这里插个题外话,如何计算闭包产生的个数?

    这就需要我们对前面闭包是如何产生的定义很清楚: 闭包是内部函数被定义的时候产生的(当然这个内部函数还要引用外部函数的变量)

    所以,闭包产生的个数就是外部函数被调用的次数,上面例子产生闭包个数是1个。

    在看第二种,这种方式可能对初学者比较劝退,python中装饰器概念对应着的就是这种方式:

    function fn1 (fn) {
        var MyName = 'Fitz'
        function wrapper () {   // 内部函数嵌套于外部函数
            return fn(MyName)   // 闭包产生
        }
        return wrapper
    }
    
    // 这个函数作为参数
    function fn2 (a) {
        console.log(a)
    }
    
    var decorator = fn1(fn2)
    decorator()
    

    闭包的作用

    讲了这么多,那闭包究竟有什么作用?,既然都是要在最外层(全局)中调用的,为什么不定义在全局中,而是这样多此一举呢?

    闭包能够使得函数内部的变量在函数执行完毕后,继续存活于内存中(延长局部变量的生命周期)

    function func1() {
        var a = 1
        function func2() {
            a++
            console.log(a)
        }
        return func2
    }
    
    var getInnerFunc = func1()
    getInnerFunc() // 2
    getInnerFunc() // 3
    

    根据执行上下文的相关概念: 函数执行上下文在函数调用时产生,函数内的语句执行完(函数调用完毕)后函数内申明的局部变量/函数将会被销毁(回收)

    但是上面这个例子很明显,在函数调用结束后,我们仍然能够访问函数内定义的局部变量(函数),这是为什么呢? 我来画图表示一下

    究其原因: 还是因为全局中仍然有变量关联着局部变量func2对应那个函数对象,所以能够通过全局变量getInnerFunc访问到这个函数对象

    由于func2这个变量对应的函数对象仍被引用着,所以当外部函数func1及其内部的局部变量被垃圾回收器进行回收时,这个被引用着的函数对象将不会被回收(注意:func1里面的局部变量a和func2都会被回收,但是func2变量指向的对象不会被回收),这将会导致下面介绍的内存泄露与内存溢出的问题

    闭包除了能够延长局部变量的申明周期,还能在对外部隐藏实现的情况下,让外部安全的操作函数内部的数据,这也是众多编程语言中gettersetter寄存器的基本原理

    function func1() {
        var a = 1
        function getter() {
            console.log(a)
        }
        function setter(val) {
            a = val
        }
        return {
            get: getter,
            set: setter
        }
    }
    
    var getInnerFunc = func1() // 执行外部函数得到其返回值 ---- func2函数
    getInnerFunc.get()  // 1
    getInnerFunc.set(666)
    getInnerFunc.get()  //666
    

    闭包的生命周期

    产生: 闭包是在函数定义的时候就产生,跟作用域一样,是静态的
    死亡: 当内部函数也成为垃圾对象的时候

    function func1 () {
        var a = 1
        function func2 () {
            console.log(a)
        }
        return func2
    }
    
    var f = func1()
    f()
    f = null // 这一步释放操作,让内部函数也成为垃圾对象,释放闭包
    

    闭包的应用

    介绍一大堆闭包相关的知识,那这个闭包它在实际中有什么用呢?

    闭包其中一个大的作用就是用于编写js模块,最典型的例子就是Jquery,看过Jquery源码的同学都会看到,Jquery是一个巨大的IIFE函数,这个函数里面向全局对象暴露$对象或者说JQuery对象

    基于闭包,我们也来简单的做一个js模块

    // 模仿jquery源码的方式
    // 我们来自定义一个数学工具方法
    (function myMath (globalObject) {
        var initVal = 1
        function add (val) {
            return initVal += val
        }
        function pow (val) {
            initVal = initVal ** val
            return initVal
        }
    
        globalObject.$ = globalObject.fakeJquery = {
            // es6的对象简写语法
            add,
            pow
        }
    })(window)
    
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
    </head>
    
    <body>
        <script src="./myModule.js"></script>
        <script>
            console.log(window)
            console.log($)
    
            console.log($.add(1))   // 2
            console.log($.add(2))   // 4
            console.log($.pow(2))   // 16
        </script>
    </body>
    </html>
    

    闭包的缺点

    闭包在实际运用很广泛,但是它有一个显著的缺点就是: 如果不及时释放闭包,就会造成内存泄露,内存泄漏到了一定程度,最终导致内存溢出

    内存泄露

    内存在使用完后没用被及时释放,一直被没用的东西占用着

    常见的内存泄露的情况

    意外的全局变量

    function test () {
        a = 'Fitz'  // 忘记使用关键字申明, 这里的a是全局变量
    }
    console.log(a)  // 'Fitz'
    

    没被及时清理的定时器

    setTimeout(()=>{
        console.log('hello')
    },1000)
    

    操作dom及其回调函数

    const btn = document.getElementById('btn01')
    btn.onclick = function () {
        alert('my name is Fitz')
    }
    btm = null  // 既要清空变量的应用
    document.body.removeChild('btn')    // 还要清空DOM的引用
    

    最后一种就是我们本篇介绍的闭包,导致的内存泄露了

    内存溢出

    内存占用超过了可用内存的总大小时,就会产生内存溢出

    闭包面试题

    题目1

    var name = 'the window'
    var object = {
        name: 'my object',
        getNameFunc: function () {
            return function () {
                return this.name
            }
        }
    }
    console.log(object.getNameFunc()()) // 'the window'
    

    解析: 这一题考查的是闭包中this的指向,闭包中的this可能令人有些迷惑,但是只要我们对this的知识比较扎实,就能不被表象所欺骗

    这里object.getNameFunc()()其实真正可以写成

    var innerFunc = object.getNameFunc()
    innerFunc() // this自然指向window
    /* 
        这其实属于this中的隐式绑定丢失的概念
    */
    

    那对于这道题目,如果我们一定要访问object中的name怎么办呢? 那我们就需要防止this的隐式绑定丢失,我们将object的this保存下来

    var name = 'the window'
    var object = {
        name: 'my object',
        getNameFunc: function () {
            var that = this
            return function () {
                return that.name
            }
        }
    }
    console.log(object.getNameFunc()()) // 'my object'
    

    接着是一道终极无敌蛇皮怪怪锤面试题,这题就是玩弄心态的,各位年轻人耗子尾汁

    上菜!

    function fun(n, o) {
        console.log(o)
        return {
            fun: function (m) {
                return fun(m, n)
            }
        }
    }
    
    var a = fun(0)
    a.fun(1)
    a.fun(2)
    a.fun(3)
    /* 
        输出结果是啥?
    
    */
    
    var b = fun(0).fun(1).fun(2).fun(3)
    /* 
        输出结果是啥?
    
    */
    
    var c = fun(0).fun(1)
    c.fun(2)
    c.fun(3)
    /* 
        输出结果是啥?
    
    */
    

    答案:

    function fun(n, o) {
        console.log(o)
        return {
            fun: function (m) {
                return fun(m, n)
            }
        }
    }
    
    var a = fun(0)
    a.fun(1)
    a.fun(2)
    a.fun(3)
    /* 
        输出结果是啥?
            - undefined
            - 0
            - 0
            - 0
    */
    
    var b = fun(0).fun(1).fun(2).fun(3)
    /* 
        输出结果是啥?
            - undefined
            - 0
            - 1
            - 2
    */
    
    var c = fun(0).fun(1)
    c.fun(2)
    c.fun(3)
    /* 
        输出结果是啥?
            - undefined
            - 0
            - 1
            - 1
    */
    

    解析:

    function fun(n, o) {
        console.log(o)
        return {
            fun: function (m) {
                return fun(m, n)
            }
        }
    }
    
    var a = fun(0)  // 由于没有实参给予o,所以o为undefined,输出undefined
    
    // 然后得到的一个对象赋值给变量a
    /* 
        a => {
            fun: function (m) {
                return fun(m, 0)    // function(m){...}是闭包
            }
        }
    */
    
    // 此时实参1赋值给形参m
    a.fun(1)  // 执行fun(n=1, o=0)  输出o=0
    // 此时实参2赋值给形参m
    a.fun(2)  // 执行fun(n=2, o=0)  输出o=0
    // 此时实参3赋值给形参m
    a.fun(3)  // 执行fun(n=3, o=0)  输出o=0
    /* 
        输出结果是啥?
            - undefined
            - 0
            - 0
            - 0
    */
    

    由于a.fun()是三次独立的调用,即产生的是不同的执行上下文,所以函数之间的变量是独立、没有记忆的

    接着

    function fun(n, o) {
        console.log(o)
        return {
            fun: function (m) {
                return fun(m, n)
            }
        }
    }
    var b = fun(0).fun(1).fun(2).fun(3)
    
    /* 
        fun(0):                         n=0 o=undefined     输出undefined 
        fun(0).fun(1):                  m=1 n=上次的n=0     输出0
        fun(0).fun(1).fun(2):           m=2 n=上次的m=1     输出1
        fun(0).fun(1).fun(2).fun(3):    m=3 n=上次的m=2     输出2
    */
    
    /* 
        输出结果是啥?
            - undefined
            - 0
            - 1
            - 2
    */
    

    由于是连续的调用,执行上下文对象始终是同一个,所以前一次调用后的变量/参数,会影响后一次的结果,是有记忆的

    最后

    function fun(n, o) {
        console.log(o)
        return {
            fun: function (m) {
                return fun(m, n)
            }
        }
    }
    
    var c = fun(0).fun(1)
    /* 
        fun(0):                         n=0 o=undefined     输出undefined 
        fun(0).fun(1):                  m=1 n=上次的n=0     输出0
    
        此时c是一个对象:
        c = {
            fun: function (m) {
                return fun(m, 1)
            }
        }
    */
    
    c.fun(2)
    /* 
        相当于执行:
            function (2) {
                return fun(2, 1)
            }
        输出1
    */
    
    c.fun(3)
    /* 
        相当于执行:
            function (3) {
                return fun(3, 1)
            }
        输出1
    */
    
    
    /* 
        输出结果是啥?
            - undefined
            - 0
            - 1
            - 1
    */
    

    这个例子是上面两种的结合,连续调用得到c对象,然后在对c对象进行独立的调用,考查的是执行上下文对象以及显而易见的闭包

  • 相关阅读:
    vue+element-ui商城后台管理系统(day01-day02)
    ssl证书-https重定向
    图形化编程娱乐于教,Kittenblock实例,空格键控制随机背景和角色
    图形化编程娱乐于教,Kittenblock实例,造型切换,制作雷电效果
    图形化编程娱乐于教,Kittenblock实例,飞行人特效,角色的运动和形状改变
    图形化编程娱乐于教,scratch3.0实例,人物造型改变,字幕效果
    图形化编程娱乐于教,scratch3.0实例,回答询问
    图形化编程娱乐于教, scratch3.0实例,猜水果,解读消息,变量的使用
    图形化编程娱乐于教,Kittenblock实例,自制演奏模块,调用各种乐器演奏
    图形化编程娱乐于教,Kittenblock实例,角色在街上找人问路
  • 原文地址:https://www.cnblogs.com/fitzlovecode/p/jsadvanced9.html
Copyright © 2020-2023  润新知