【深入JavaScript日记六】闭包及相关知识应用

前言

学习了上下文的相关内容之后,我们继续跟着前辈们的思路学习。这一次,我们来探究一个新的东西,闭包。


正文内容

先来看看定义

w3school

  • 闭包有函数和与其相关的引用环境的组合而成
  • 闭包允许函数访问其引用环境中的变量(又称自由变量)
  • 广义上来说,所有 JavaScript 的函数都可以成为闭包,因为 JavaScript 函数在创建时保存了当前的词法环境

简单的理解就是 闭包是指那些能够访问自由变量的函数。
好的,那自由变量是啥嘞。简单理解,就是你在自己家(当前作用域)吃别人家里做好(定义)拿过来的饭(变量)

    var x = 10;

    function F() {
        console.log(x);		//10
    }

在 F 作用域中使用的变量 x ,却没有在 F 作用域中声明(即在其他作用域中声明的),对于 F 作用域来说,x就是一个自由变量

所以,闭包的概念也就通俗易懂了。

闭包 = 一个函数 + 这个函数可以访问的自由变量

所以稍加思索之后…
理论上来说,那所有的函数都是闭包咯,因为我们刚刚学过,一个函数在创建的时候就已经保存了上一级的上下文数据,即使简单的全局变量也是这样,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

但是毕竟还是要以实践为主,要真是按照理论的解释干嘛还要研究闭包…

实践上来说,通过翻文档,发现有两个要求
1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
2. 在代码中使用了自由变量

举个例子(就拿我们上一篇文章留下来没有解释的那个例子吧,稍加改动)

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

根据我们学习的知识:

  1. 进入全局代码,创建全局执行上下文,压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行 f 函数,创建 f 函数执行上下文,压入执行上下文栈
  7. f 执行上下文初始化,创建变量对象、作用域链、this等
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

这个流程很好理解,但是问题是执行 f 函数的时候,checkscope 的执行上下文已经被弹出栈了,怎么还会读取到 checkscope 作用域下的 scope 值呢???这要是换成其他语言可能就报错了。
这时候作用域链的功能就展现了出来。也说明了闭包是自带了执行环境的函数

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

所以在这个时候,不管checkscope 函数活了死了,反正咱这里自己有一份,不会被影响到。

也就是说,当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

来两个例子巩固一下

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

这个例子会打印出三个3,现在我们来分析下
当执行到 data[0] (); 之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3			//注意这里不是2
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3,data[1]Context 和 data[2]Context 也一样。所以最后打印的值是三个 3.

第二个例子

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

和第一个很相似,但是有点不一样,我们来分析一下…

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:(和原来一样)

globalContext = {
    VO: {
        data: [...],
        i: 3			
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

为什么会这样??因为在这个例子里多了个
return function(){ },这就代表着在执行过程中会多产生一个上下文,而这个函数又是匿名函数。所以有了和上面那段代码不同的作用域链。因此在每一次执行 for 循环,都会把当前的 i 值保存下来。
这个匿名函数的上下文为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

所以在执行时,data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以data [0] () 打印的结果就是 0。data [1] () 和
data [2] () 也是这个原理,所以最后打印的结果就是 0,1,2.


最后回到我们上一篇学习笔记留下的问题
图片
通过分析我们看到他有两处不同,我们看他的第二个不同之处,根据查资料我们知道了

f()执行f函数,返回子函数
f()()执行子函数,返回孙函数
f()()()执行孙函数,返回重孙函数

    function f() {
        console.log("当前函数")
        return k;

        function k() {
            console.log("子函数")
            return l;

            function l() {
                console.log("孙函数")
            }
        }
    }

    console.log(f())
    console.log("-------------------------------------");
    console.log(f()())

结果的确是 f() 执行当前函数,返回了子函数的内容,f()()一直执行到子函数,返回孙函数内容。
结果
明白了这些之后,那就很好解释第二段代码了,其实就是两种写法,多出的一个()正好对应函数中 return 后面少掉的 ()。


总结

在最后我们也终于明白了闭包究竟是什么东西,最后再总结一点,只要函数没有执行完,上下文就不会被弹栈,例如

    function f() {
        console.log(f.arguments)
        k();
        return "当前函数执行完成";

        function k() {
            console.log(f.arguments)
            return "子函数执行完成";

        }
    }

    f()

比如这样调用,在执行子函数 k 时, f 函数的上下文依然存在。
结果
但是如果这样

    function f() {
        console.log(f.arguments)
        return k;

        function k() {
            console.log(f.arguments)
            return "子函数执行完成";

        }
    }

    f()();

结果就是在执行k函数时 f 的上下文已经被弹出栈了,所以执行顺序我们一目了然。
图片

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AntyRia

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值