前端面试之闭包

70 阅读12分钟

闭包

闭包

函数内部的局部作用域变量 不能在函数外部直接调用 因为函数执行结束 执行空间就会被销毁 局部作用域变量也会被销毁释放

闭包就是生成一个 不会被销毁的执行空间

延长局部作用域变量的生存周期

在函数外部 可以 操作函数内部的局部作用域变量

为什么要用闭包

因为 JavaScript 存在 全局变量污染
也就是 全局变量 非常容易被其他程序(函数等)误操作

解决

为了保证数据的安全 往往需要将全局变量设定成 函数的局部变量

再通过闭包的语法 调用操作 最后变量数据

函数的执行原理

函数的封装过程

    1   在 堆 中 开辟一个的独立存储空间 准备存储 引用数据类型
        操作系统给这个存储空间 分配一个 内存地址   

    2   在 存储空间 中 以 字符串形式 存储 函数程序代码

    3   在 栈 中 存储 函数名称 
        函数名称中存储 函数存储空间的内存地址

函数的调用过程

    1   解析 栈 中 变量名称中存储的内存地址
        找到 堆 中 对应的存储空间 
        读取 存储空间中存储的函数程序

    2   形参赋值实参

    3   预解析/预解释/预编译

    4   执行函数程序

函数的执行原理 在 函数的存储空间中 再开辟一个独立的内存空间 专门存储函数中局部作用域变量 也就是 函数中的 变量和形参

函数执行结束 
这个空间就会被 销毁/释放

这个空间中存储的函数的局部作用域变量/形参 
也会被 销毁/释放

这个空间 称为 执行空间

JavaScript程序内存回收机制

函数执行时 函数内部的局部作用域变量需要参与程序的执行
函数执行结束 函数内部的局部作用域变量 就不再参与程序的执行

触发执行计算机程序的内存回收机制 
释放 函数执行空间 
释放 函数执行空间中存储的局部作用域变量
减少内存占用 提高程序的执行效率

生成不会被销毁的执行空间

如果 执行空间中的数据 正在被外部程序使用 执行空间不会被销毁释放

函数return的是引用数据类型 函数外有变量存储 函数的执行结果返回值 函数的执行空间不会被销毁

        function fun(){

            // 如果函数的返回值是基本数据类型
            // 函数外部的变量中存储的是 基本数据类型的数据数值
            // 赋值操作结束之后 变量和return的数据之间没有任何关系
            // return 基本数据类型

            // 如果函数的返回值是引用数据类型 
            // 函数外部的变量中存储的是 引用数据类型的内存地址
            // 相当于 函数中有数据的内存地址正在被函数之外的变量存储使用

            // 函数的返回值 是 引用数据类型(数组,对象,函数) 
            // 这个函数的执行空间 就不会被销毁
            // return 引用数据类型 ;
        }

闭包的基本语法

函数的返回值 是 另一个函数

在 函数A中 返回值是另一个函数B

在函数B中操作调用函数A中设定的局部作用域变量

在 函数A外 使用变量 存储 函数A的执行结果返回值 

也就是 存储的是 函数A return 的函数B

调用这个变量 就是 调用函数B 就是 操作 函数A的局部作用域变量

闭包的套路

function 函数a(){
    局部变量1
    局部变量2
    局部变量3
    .....


    return 匿名函数(){

        操作 函数a 的局部变量

    }

}

const 变量 = 函数a() ;
    变量中存储的是函数a return 的 匿名函数的内存地址

变量()
    变量的调用 就是 匿名函数的调用 就是 函数a局部变量的操作
        // 函数a
        function a(){
            // 函数a的局部作用域变量
            let a = 100 ;
            let b = 200 ;

            // return 的是 函数b
            return function(){

                // 函数b中设定要对函数a局部变量的操作
                console.log( a+b ); 

            }

        }

        // 在函数a外部 使用变量存储函数a的执行结果返回值
        // 变量res中存储的是函数a return 的 匿名函数的内存地址
        const res = a() ; 

        // 调用变量res 就是 通过变量res中存储的内存地址
        // 调用 函数a return 的匿名函数 
        // 匿名函数的程序 就是我们想要操作 局部变量a 局部变量b 执行的程序
        res() ;

已经弃用的闭包demo_ 点击事件

    <ul>
        <li>我是第一个li标签</li>
        <li>我是第二个li标签</li>
        <li>我是第三个li标签</li>
        <li>我是第四个li标签</li>
        <li>我是第五个li标签</li>
    </ul>

    <script>
        const oUlLis = document.querySelectorAll('ul>li');

        // 如果使用for循环绑定点击事件

        // var 关键词 声明的循环变量
        // // 整个循环只会生成一个循环变量
        // // 每次循环 对这个循环变量进行 重复赋值
        // // 循环结束 只有一个循环变量 存储 最终赋值的数据
        // for( var i = 0 ; i <= oUlLis.length-1 ; i++ ){
        //     // i 是 索引下标  oUlLis[i] 是 li标签对象
        //     oUlLis[i].addEventListener( 'click' , ()=>{
        //         console.log( i );
        //     })
        // }

        // // let 关键词 声明的循环变量
        // // 整个循环生成多个作用域 存储不同的循环变量 存储不同的数值数据
        // // 每次循环 生成一个独立的作用域 存储 独立的循环变量 独立的循环变量 存储独立的数值
        // // 循环结束 生成多个独立的作用域 存储 多个独立的循环变量 独立的存储变量存储的不同的数值
        // for( let i = 0 ; i <= oUlLis.length-1 ; i++ ){
        //     // i 是 索引下标  oUlLis[i] 是 li标签对象
        //     oUlLis[i].addEventListener( 'click' , ()=>{
        //         console.log( i );
        //     })
        // }


        // 使用闭包的语法 生成 多个作用域 存储 不同的循环变量


        //         for (var i = 0; i <= oUlLis.length - 1; i++) {
        //             // i 是 索引下标  oUlLis[i] 是 li标签对象

        //             // 将 函数fun() 调用执行 将 函数fun()的执行结果返回值 作为 点击事件的事件处理函数
        //             // 也就是 给 点击事件赋值的是 函数fun() 的执行结果返回值 也就是 return 的 匿名函数

        //             // 每次循环 调用 闭包函数fun 生成一个独立的作用域 使用变量index 存储赋值的 i 
        //             // 触发点击事件 调用的是 闭包函数fun return 的 匿名函数
        //             // 这个匿名函数 调用的是 对应的 执行空间中 存储的循环变量 i 的数值 


        // /*
        // 第一次循环 i 的数值是 0 
        //     oUlLis[i] 也就是 oUlLis[0] 对应的是第一个li标签 
        //     给 第一个li标签 绑定点击事件 
        //     绑定的事件是 fun(i) 的执行结果返回值 也就是 fun( 0 ) 执行结果返回值
        //     闭包函数执行的程序是
        //         function fun ( 0 ){

        //             var index = 0 ;

        //             return function(){
        //                 console.log( index 也就是 0 )
        //             }
        //         }

        //     也就是点击时 触发的程序是 闭包函数fun return 的匿名函数
        //         function(){
        //             console.log( index 也就是 0 )
        //         }

        //     也就是 比表函数fun 有一个独立的执行空间 存储 index = 0 

        // 第二次循环 i 的数值是 1 
        //     oUlLis[i] 也就是 oUlLis[1] 对应的是第二个li标签 
        //     给 第二个li标签 绑定点击事件 
        //     绑定的事件是 fun(i) 的执行结果返回值 也就是 fun( 1 ) 执行结果返回值
        //     闭包函数执行的程序是
        //         function fun ( 1 ){

        //             var index = 1 ;

        //             return function(){
        //                 console.log( index 也就是 1 )
        //             }
        //         }

        //     也就是点击时 触发的程序是 闭包函数fun return 的匿名函数
        //         function(){
        //             console.log( index 也就是 1 )
        //         }

        //     也就是 比表函数fun 有一个独立的执行空间 存储 index = 1 



        // */
        //             oUlLis[i].addEventListener('click', fun( i ) ) ;


        //         }

        //         function fun ( num ){

        //             var index = num ;

        //             return function(){
        //                 console.log( index )
        //             }

        //         }

        //         // 变量res中存储的是 函数fun return的匿名函数
        //         // 多次调用 就会生成多个执行空间 存储 函数fun中的局部作用域变量
        //         const res1 = fun( 100 ) ;
        //         const res2 = fun( 200 ) ;
        //         const res3 = fun( 300 ) ;


        // 最终的闭包的语法形式 绑定点击事件 
        for (var i = 0; i <= oUlLis.length - 1; i++) {

            // 使用 立即执行函数/自执行函数 在声明闭包函数的同时 调用闭包函数
            oUlLis[i].addEventListener('click', (function fun(num) {

                var index = num;

                return function () {
                    console.log(index);
                }

            })(i));

        }

闭包语法的基本总结

  1. 闭包语法的基本套路

函数return函数

使用变量存储 调用变量 就是调用return的函数

  1. 实际项目中一般使用立即执行函数的语法形式

     标签对象.addEventListener( 事件类型 , ( function(形参){
    
     let 变量 = 形参
    
     return function(){ console.log( 变量 ) };
    
     } )( 实参i ) )
    
  2. 闭包的优点缺点

(1)

优点 创建一个不会被销毁的存储空间

缺点 占用内存

(2)

优点 延长变量的生存周期

缺点 容易造成数据泄露

(3)

优点 保护变量不会受到全局变量污染 数据更安全

缺点 调用使用比较麻烦

单例模式

构造函数多次调用 只 生成一个实例化对象

不是每次调用生成不同的实例化对象

单例模式核心

之前设定给 实例化对象的属性属性值 现在设定成 函数方法的形参实参

参数少 程序简单的实例化对象更适合写成 单例模式

        // // 传统的构造函数
        class Fun{
            constructor( name , age ){
                this.name = name ;
                this.age = age ;
            }

            ff1(){ console.log( this.name ) };
            ff2(){ console.log( this.age ) };
            ff3(){ console.log( this.name , this.age ) };
        }

        const obj1 = new Fun( '张三' , 18 );
        const obj2 = new Fun( '李四' , 20 );
        const obj3 = new Fun( '王五' , 26 );

        console.log( obj1 );
        // obj1.ff1()

        console.log( obj2 );
        // obj2.ff1()

        console.log( obj3 );
        // obj3.ff1()


        // // 改造的构造函数
        class NewFun{
            // 构造器中 不存储 属性属性值
            constructor(){} ;

            // 函数方法中 以形参的形式 输入存储的数据数值
            ff1( name ){ console.log( name ) };

            ff2( age ){ console.log( age ) };

            ff3( name , age ){ console.log( name , age ) };
        }

        const obj4 = new NewFun();


        // obj.ff1( '张三' );
        // obj.ff1( '李四' );
        // obj.ff1( '王五' );

        // obj.ff2( 20 );
        // obj.ff2( 40 );
        // obj.ff2( 18 );

        // obj.ff3( '张三' , 18 );




        /*
            原来的构造函数 
                将 不同的数据 使用 键值对的形式 直接 存储到实例化对象的属性属性值 中

                有一组不同的数据 就需要创建一个 不同的实例化对象 存储这组数据

                多个实例化对象 只是 属性存储的属性值不同 其他一切都是相同的

            改造后的单例模式构造函数
                不再将 数据直接以属性属性值的形式 设定在对象中

                不同的数据 直接以 形参实参的形式 在调用函数时 直接赋值给 执行的函数程序

                一个 实例化对象 在调用函数时输入不同的数据
                就可以实现对应的功能需求 

        */

生成一个实例化对象的单例模式

多次调用只生成一个实例化对象的单例模式,需要防止全局变量污染

// 单例模式的构造函数
class Fun {
    // 构造器中 不存储 属性属性值
    constructor() { };

    // 函数方法中 以形参的形式 输入存储的数据数值
    ff1(name) { console.log(name) };

    ff2(age) { console.log(age) };

    ff3(name, age) { console.log(name, age) };
}



// // 直接多次调用构造函数 会 多次生成不同的实例化对象 
// const obj1 = new Fun();
// const obj2 = new Fun();
// const obj3 = new Fun();

// console.log( obj1 , obj2 , obj3 );

// console.log( obj1 === obj2 );
// console.log( obj2 === obj3 );
// console.log( obj3 === obj1 );

// 定义一个变量 存储 原始数值
let res = '原始值';

// 通过调用函数创建实例化对象 

/*
第一次调用函数 ff() 
变量res 存储的是 赋值的 原始值
执行程序 
    res = new Fun();
    return res;

变量res中 存储的是 调用构造函数 生成的实例化对象1 
return res 返回值 是 变量res中存储 实例化对象1 

变量obj1 中 存储的是 return 的 res 中 存储的 实例化对象1

*/ 
const obj1 = ff();


/*
第二次调用函数 ff()
变量res 存储的是 实例化对象1 

执行程序 
    return res ;

变量obj2 中 存储的是 res中存储的 实例化对象1


*/ 
const obj2 = ff();


const obj3 = ff();

console.log(obj1, obj2, obj3);

console.log(obj1 === obj2);
console.log(obj2 === obj3);
console.log(obj3 === obj1);

// 设定一个函数 
function ff() {
    /*
        第一次调用 
            变量res存储原始值 不是 实例化对象 
            调用 构造函数 生成 第一个实例化对象 
            存储到变量res中 
            作为 返回值 赋值给 第一个变量存储

        之后每次调用
            变量res存储 第一次生成的实例化对象 不是 变量res中存储的原始值
            不再调用构造函数 生成新的实例化对象 
            直接 return 变量res 
            也就是 继续 返回 第一个实例化对象
            之后的变量 存储的都是 第一个实例化对象 

        通过函数多次调用 始终return的都是第一次调用函数创建的实例化对象


    */

    if (res === '原始值') {

        res = new Fun();

        return res;

    } else {

        return res;

    }

}

闭包的单例模式

       // 使用 立即执行函数 
        // 在 设定函数的同时 执行函数
        // 变量createSP 中 存储的是 函数return 的 匿名函数 
        const createSP = (function() {
            // 为了方式 构造函数 和 变量res 被全局变量污染 
            // 将 构造函数 和 变量res 设定到函数程序中 防止全局变量污染
            // 以闭包的语法形式 操作 构造函数和变量res

            // 实际项目中不同的构造函数的单例模式 
            // 步骤套路都是相同的 只是 设定的构造函数不同而已

            // 定义好的构造函数
            class Fun {
                // 构造器中 不存储 属性属性值
                constructor() { };

                // 函数方法中 以形参的形式 输入存储的数据数值
                ff1(name) { console.log(name) };

                ff2(age) { console.log(age) };

                ff3(name, age) { console.log(name, age) };
            }

            // 相当于开关变量
            let res = '原始值';

            return function () {
                // 对 构造函数 和 变量res 的操作
                if (res === '原始值') {

                    res = new Fun();

                    return res;

                } else {

                    return res;

                }

            }

        })()

        // 调用 变量createSP 时 就是 在调用 匿名函数
        // 也就是 以 单例模式的形式 创建实例化对象
        const res1 = createSP();
        const res2 = createSP();

        console.log( res1 , res2 );

        console.log( res1 === res2 );