闭包作用域

541 阅读4分钟

一、闭包机制

我们函数执行产生的两大机制“保存/保护”,称之为闭包机制!!

【保护】:保护里面的私有变量不受外界的干扰. 防止全局变量的污染

【保存】:一但这个上下文形成不销毁的作用域 不被释放,里面的私有变量和值,就保存起来了,可以供其“下级”上下文中调取使用

二、闭包在实战中的应用

【1】闭包之私有变量的保护应用

团队协作开发中,A/B共同开发一个页面,最后要把代码合并在一起,为了防止全局变量的冲突污染,我们建议每个开发者,都把自己的代码放置到一个闭包中(自执行函数执行即可,这样就是私有的上下文)保护起来

// A的代码
(function anonymous() {
    /* 自执行函数执行,会形成一个私有的上下文,在这里声明+定义的变量或者函数都是私有的 */
    var x = 100,
        y = 200;
    function func() {
        // ...
    }
})();
// B的代码
~ function anonymous() {
    // console.log(anonymous); //=>函数本身 
    //=>匿名函数设置的函数名只能在函数里面应用,函数外面是无法访问的
    var x = 200,
        n = 0;
    function func() {
        // ...
    }
    function handled() {
        // ...
    }
}();
// console.log(anonymous); //=>Uncaught ReferenceError: anonymous is not defined

【jquery】 通过window添加属性暴漏到全局

(function(){
    function jquery(){
    }
    //把jquery 这个方法通过window添加属性暴漏到全局
    window.jquery=window.$=jquery;
})()

在使用的时候: jquery() 或者$()

【zepto】 把自执行函数的通过return把返回结果在外面用一个变量进行接收

var zepto=(function(){
             return {
                fn:function(){},
                ..... 
                 }
           })()
           
 // 在使用的时候:zepto.fn          

【2】私有变量之保存机制-选项卡案例再忆、

【选项卡案例原版】

还写了还几个不同版本的 戳这里 戳这里

<!DOCTYPE html>
<html lang="en">

<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">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        ul,
        ol {
            list-style: none;
        }

        .main {
            width: 500px;
            margin: 0 auto;
        }

        ul>li {
            width: 100px;
            height: 40px;
            line-height: 40px;
            text-align: center;
            border: 1px solid #333;
            margin-right: 10px;
            display: inline-block;
            position: relative;
            top: 1px;
        }

        .main>div {
            height: 300px;
            line-height: 300px;
            border: 1px solid #333;
            text-align: center;
            display: none;
        }

        .main li.current {
            background: darkcyan;
            border-bottom-color: darkcyan;

        }

        .main div.current {
            background: darkcyan;
            display: block;
        }
    </style>
</head>

<body>
    <div class="main" id="main">
        <ul>
            <li class="current">音乐</li>
            <li>电视</li>
            <li>综艺</li>
        </ul>
        <div class="current">音乐内容</div>
        <div>电视内容</div>
        <div>综艺内容</div>
    </div>
</body>

</html>
<script>
    var main = document.getElementById("main");
    var lis = main.getElementsByTagName("li");
    var divs = main.getElementsByTagName("div");

    for (var i = 0; i < lis.length; i++) {
        lis[i].index = i;
        lis[i].onclick = function () {
            var index = this.index;
            change(index);
        }
    }

    function change(index) {
        for (var i = 0; i < lis.length; i++) {
            lis[i].className = "";
            divs[i].className = "";
        }
        lis[index].className = "current";
        divs[index].className = "current";
    }
</script>

三、回忆当初里面的i 为啥是3

1、【作用域方式去思考】

当我们触发点击事件的时候,这个函数执行,形成私有上下文, 在这个私有上下文里面,并没有私有变量i,所以就会向上级上下文进行查找,此时上级作用文就是全局上下文里面的i,当我们发生点击事件的时候,此时for 循环早已完成,i早就是3

上下文:

  • window 全局上下文 EC(G)
  • 函数执行形成私有上下文
 for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = function () {
           // 这里的i为啥会变成3?当我们触发点击事件的时候,这个函数执行,形成私有上下文,
           // 在这个私有上下文里面,并没有私有变量i,所以就会向上级上下文进行查找,此时上级上下文就是全局
           // 而全局作用上下里面的i,当我们发生点击时间的时候,此时for 循环早已完成,i早就是3
            change(i);
        }
    }

2、【同步异步事件去思考】

  • 【同步事件】:当一件事件做完之后,再继续下一件事情
  • 【异步事件】:当一件事件还没有做完,不再等待,直接去做下一个事件。所有的事件都是异步编程。
 for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = function () {
            change(i);
        }
    }

for循环是同步事件,执行完i=3;点击事件是异步事件,当我们点击页面上的按钮的时候,这个for循环早已经执行完了。

lis[0].onclick = function () {
            change(i);
}

lis[1].onclick = function () {
            change(i);
}
lis[2].onclick = function () {
            change(i);
}

解决方法:参照选项卡文档中选项卡解决方案

练习题

let x = 5;
const fn = function fn(x) {
    return function (y) {
        console.log(y + (++x));
    }
};
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x); 

闭包.png

四、引申思考

下面代码是否可以,每隔1000MS依次输出 0 1 2 3 4 5 ?

如果不可以,说明为啥?

以及如何解决?

// 不行:基于var在循环中声明的变量“i”是全局变量
//   第一轮循环 全局i=0 设置第一个定时器{1000} 给定时器设置的回调函数并没有执行,等待1秒后执行,循环继续
//   第二轮循环 全局i=1 设置第二个定时器{2000}
//   ...
//   第五轮循环 全局i=4 设置第五个定时器{5000} i++,让全局的i=5,循环结束
// ->每一个定时器设置的回调函数,只有等到循环结束后,到了等待的时间,才会触发执行
for (var i = 0; i < 5; i++) {
    setTimeout(function () {
        /!*
         * EC(AN)
         *   
         * 作用域链:<EC(AN),EC(G)>
         * 形参赋值:--
         * 变量提升:--   
         *!/
        console.log(i); //i不是自己私有的,是全局的,但是此时全局的i已经是循环结束后的5了
    }, (i + 1) * 1000); 
}

五、解决方案

利用闭包机制解决 用了保存的应用

1. 自执行函数

循环结束:设置了五个闭包,每一个闭包中都有一个自己的私有变量“i”,存储的值分别是0~4;全局i是5;

for (var i = 0; i < 5; i++) {
            //手动设置个闭包 
            (function (i) {
                setTimeout(function () {
                    console.log(i); 
                }, (i + 1) * 1000);
            })(i)
 }

2. 大函数返回小函数手动生成闭包

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

3. 使用forEach

// new Array(5).fill(null) 创建一个长度为5的数组,每一项填充null,把其变为密集数组,这样可以forEach
//   “_”就是形参占位,原本应该有个形参变量,但是我不想用,所以我占个位即可
new Array(5).fill(null).forEach(function (_, index) {
    // 第一轮  EC(AN1) index=0  设置定时器{1000}  闭包
    // 第二轮  EC(AN2) index=1  设置定时器{2000}  闭包
    // ...
    setTimeout(function () {
        console.log(index);
    }, (index + 1) * 1000);
});

4.let

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

5.终极方案:基于定时器传参「核心:闭包」

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

六、闭包内的值暴露给外面使用的两种方法

需求:我们需要把某一个闭包(私有上下文)中的值暴露到外面

1、基于window.xxx=xxx暴露到全局

这种办法虽然可以实现,但是也会存在冲突,如果我们每一个版块都需要暴露更多的方法,同时都基于这种方法暴露到全局对象GO上,也可能导致方法之间的冲突

(function anonymous() {
    function queryURLParams() {
        // ...
    }

    function sum() {
        // ...
    }

// 想暴露到外面使用,可以暴露到全局上(赋值给全局对象GO =>window)
    window.queryURLParams = queryURLParams;
    window.sum = sum;
})();
queryURLParams(); //=>window.queryURLParams() 

2、JS中的设计模式:单例设计模式

每一个版块暴露到全局下只有一个变量而已,所有需要供别人调取的方法都在对象中(这样暴露一个或者多个都无所谓)

  • 避免了全局变量的污染,也同时实现了不同闭包之间方法的公用性
var utils = (function anonymous() {
    // queryURLParams:获取URL地址参数信息
    function queryURLParams() {
        // ...
    }

    // sum:实现任意数求和
    function sum() {
        // ...
    }

    // 把需要供外面访问的变量和方法,赋值给一个对象,最后返回(外层基于VAR utils定义变量来接收即可)
    return {
        queryURLParams: queryURLParams,
        sum: sum
    }; //=>return AAAFFF000;
})();
// console.log(utils); //=>{queryURLParams:函数,sum:函数}
utils.queryURLParams();
utils.sum(); 

utils对象中包含了需要供别人调取使用的方法;此时我们把utils称之为“命名空间”,而对象就是一个空间,空间中包含了当前版块中的内容,或者是把当前版块中的内容按照命名空间进行了分组,每一个分组都是一个单独的个体

命名空间

  • 把每一个对象值 赋值给变量 =>可以理解为,给每一个对象的堆内存空间起了个变量名,所以此时可以把这个变量称为 “命名空间”

  • 当前对象中的键值对都是 命名空间 中私有的键值对