案例 | 闭包作用:保护和保存

javascript/jquery

浏览数:125

2020-5-26

文 /
景朝霞

来源公号 /
朝霞的光影笔记

ID /
zhaoxiajingjing

图 /
自己画

点个赞,让我知道你来过~

前情提要:

  1. 题目 | let和var的区别(一、二)
  2. 图解 | let和var的区别(一、二)
  3. 题目 | 带VAR和不带VAR的区别
  4. 图解 | 带VAR和不带VAR的区别
  5. 总结 | LET和VAR区别(三、四)
  6. 图解 | 作用域和作用域链
  7. 练习题 | 作用域和作用域链
  8. 图解 | 理解闭包

0 / 什么是闭包?

来~大声朗读3遍 or 写一遍吧!

<u>函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就称为闭包</u>

var i = 0;
function A(){
    var i = 10;
    function x(){
        console.log(i);
    }
    return x;
}
var y = A();
y();
function B(){
    var i = 20;
    y();
    i++;
}
B();

△全图在文末~

【图一】

(1)浏览器会开辟一个栈内存作为全局执行上下文/全局作用域。

(2)在全局执行上下文中有VO(G)变量对象用于当前作用域下变量存储和值存储。

(3)在代码执行之前,会有变量提升:找var/function。var:只会提前声明变量名;function:提前声明+定义。

如:

var i; 带var的只提前声明

function A(){...}引用数据类型:

  • 浏览器开辟一块16进制地址的堆内存
  • 把函数体中的代码以代码字符串格式进行存储
  • 并把16进制地址赋值给变量名A
  • 函数在哪创建,在执行时需要查找的上级作用域就是哪里
    【PS:在哪里出生,他爹妈就在哪里,它的老家就在哪里~~我滴老家就在这个屯儿~我是这个屯里土生土长的人儿】

var y;带var的只提前声明

(4)在代码自上而下执行时,对于已经进行变量提升的用删除线表示。

i = 0;已经声明过变量
i,此处进行等号赋值:把变量
i与值0进行关联。

function A(){…} 已经声明+定义了

y = A();已经提前声明了变量y,此处进行赋值

y = A();执行这句代码的意思:

  • 在当前作用域下查找到函数A=>AF0
  • 浏览器开辟一个全新的栈内存,形成一个全新的私有作用域
  • 执行函数A,即把AF0里面的代码字符串拿出来执行
  • 如果函数A有return xxx;,则把return后面的东西赋值给变量y
  • 如果函数A没有return,则函数的返回值为undefined赋值给变量y

(5)执行函数A,会开辟一个全新的栈内存/执行上下文/作用域,并把函数的返回值赋值给变量y

函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包

  • 当前作用域中this指向的是window
  • 作用域链scopeChain:<AO(A), VO(G)>
  • 在函数里面有AO(A)活动对象进行:变量存储+值存储
  • 形参赋值&变量提升&代码执行
  • 函数A有返回值return x;,即:把函数x的堆内存地址BF0返回去,赋值给变量y

函数A执行完后,该栈内存/执行上下文/作用域EC(A),不销毁:当前作用域内有东西被外界占用了。

此作用域不销毁,里面的变量i和函数x会被保存下来。

【图二】

(6)函数y执行,即:浏览器开辟一个全新的栈内存/执行上下文/私有作用域EC(y)#1(假设:标记为1号),把BF0堆内存的函数代码字符串拿出来执行

函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包

console.log(i);

打印输出变量i的值,该函数没有返回值,没有东西被外界占用,执行完后,被销毁。

在当前作用域EC(y)#1下找变量i,没有找到。会沿着作用域链向上级作用域查找,如果有则使用。如果没有找到,会继续向上级作用域查找…..一直找到全局作用域EC(G)的变量对象VO(G),如果找到变量i,则使用;如果在全局作用域都没有找到变量i,那么就会报错了。这种查找机制是作用域链查找

【图三】

(7)函数B已经在变量提升阶段:声明+定义。

(8)函数B执行,即:浏览器开辟一个全新的栈内存/执行上下文/私有作用域,把AF1堆内存中的代码字符串拿出来执行

函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包

var i = 20;

在此作用域下声明的变量i,不会受到EC(A)作用域和EC(G)全局作用域中变量i的干扰,这里操作变量i都是该作用域下的私有变量。

y();函数y在当前作用域EC(B)下没有声明,那么找到上级作用域EC(G),这里存储函数y的地址为:BF0。浏览器会开一个全新的栈内存/执行上下文/私有作用域EC(y)#2(假设标记为2号),把BF0堆内存地址的代码字符串拿出来执行:

函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包

console.log(i);

通过作用域链查找机制,找到上级作用域的活动对象中AO(A)中有变量i,输出:10

EC(A)这个私有作用域里面有东西被外界占用,没有被销毁,会把其中的变量保存起来

i++;

在当前作用域下能找到变量i,那么,它操作的就是自己的。变量i的值变为21。

在VO(G)全局的变量对象和没有被销毁的AO(A)活动对象中保存的变量i是不会受到它的干扰。

综上

函数在哪里创建,哪里就是它的上级作用域。跟函数在哪执行没有半毛钱关系。

函数创建——形成上级作用域

函数执行——判断this

函数执行时,浏览器会开辟一个全新的栈内存/执行上下文/私有作用域,保护它里面的变量不受外界干扰,这种保护机制就是闭包。

遇到变量,如果当前作用域下没有,那么顺着作用域链向上级作用域查找,如果有则使用,如果没有找到则继续查找上级作用域….一直找到全局作用域为止,这种查找机制就是作用域链查找机制。

函数执行结束后,如果有内容被外界占用,则不会被销毁。

1 /

作用:保护

下载一个jQuery

$ npm init -y
$ npm install jquery

找到文件:zhaoxiajingjingnode_modulesjquerydistjquery.js

把代码都收起来查看

(function(global, factory){
    // ... something code
})(typeof window !== "undefined" ? window : this, function(window, noGLobal){
    // ... something code
});

再简化:

// 自执行匿名函数
(function([arg1 [, arg2]]){})([params1], [params2]); 

(function(){})();

函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包。

项目开发时,我们会定义很多方法来实现需求。当引入jQuery类库时,里面有大量的方法,为防止它与我们写的方法冲突,需要使用闭包保护起来,jQuery类库的方法不受外界的干扰。

使用闭包作用:保护

自执行匿名函数:

(function(){})()

~function(){}()

+function(){}()

-function(){}()

!function(){}()

2 /

作用:保存

需求:tab选项卡,切换按钮与卡片的内容对应。

【PS:此处省略…..样式代码】

<div id="tabBox">
    <ul id="navBox">
        <li class="active">公号</li>
        <li>读书</li>
        <li>运动</li>
    </ul>
    <div class="active">
        <p>公号:朝霞的光影笔记</p>
        <p>ID:zhaoxiajingjing</p>
    </div>
    <div>《JavaScript 高级程序设计(第三版)》</div>
    <div>举个铁~~</div>
</div>
var tabBox = document.querySelector('#tabBox'), // tab盒子
    navList = document.querySelectorAll('#navBox li'), // 导航按钮集合
    divList = tabBox.querySelectorAll('div'); // 卡片集合
// 实现选项卡切换
var changeTab = function (index) {
    // index:当前被点击的LI的索引
    for (var i = 0; i < navList.length; i++) {
        navList[i].className = '';
        divList[i].className = '';
    }
    navList[index].className = 'active';
    divList[index].className = 'active';
};
// 循环给每一个LI绑定点击事件
for (var i = 0; i < navList.length; i++) {
    navList[i].onclick = function () {
        // 此时的i是循环结束后的结果,i=>3
        changeTab(i);
    }
}

在浏览器控制台打印dir(navList)可以查看节点集合

△上述代码的简单图解

我们想要点击时候,i是分别是0、1、2,利用闭包解决(解决方案很多种,以后再汇总):

函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包

for (var i = 0; i < navList.length; i++) {
    ~ function (n) {
        navList[n].onclick = function () {
            changeTab(n);
        }
    }(i);
}

△第一种闭包写法

△第一个闭包写法的图解

使用闭包作用:保存

把选项卡的索引通过闭包保存起来,在用户触发点击事件时再去查找对应的变量。

for (var i = 0; i < navList.length; i++) {
    navList[i].onclick = (function (n) {
        return function () {
            changeTab(n);
        }
    })(i);
}

△第二种闭包写法,图解自己去画吧~皮卡丘

3 /

综上

很多人认为,只有形成不销毁的栈内存才是闭包,或者简单的说是一个函数返回一个函数就是闭包。

但,函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包。从函数保护自己的私有变量不受外界干扰那一刻,闭包就形成了。
【PS:男友力爆棚,有木有~闭包就是函数的男友力技能,有木有~】

那么,使用闭包的两个好处/作用:保护和保存。

在阅读jQuery的源码时发现,它使用了自执行匿名函数把自己代码使用闭包包裹起来,不受外界方法的干扰,这样在自己开发过程中命名的方法不会和jQuery里面定义的方法冲突,利用的就是闭包的保护作用。

在tab选项卡这个需求里面,同样可以使用闭包形成的不销毁作用域/栈内存把变量存储起来,用户再点击时触发的方法通过作用域链查找到对应的索引变量,从而实现选项卡的正确切换,这个利用的是闭包的保存作用。

4 /

预告

堆栈内存的销毁方式?

▽大图来啦~今天的内容很长,但不难,仔细消化消化吧~画起来~冲鸭!


作者:Pink