真正的认识闭包

文体两开花 ,说道闭包就必须要说下作用域;说道作用域我就想到了作用域链,说道作用域链,我就想到了执行期上下文,说道执行期上下文,我就想到了预编译。所以为了说下闭包,就得从预编译说起。(第二次写文章,文笔不怎么好,加上内容本身难度挺大,请多多包涵,仔细阅读,看懂每一个字,理解图片中的每一个字)

一:预编译

众所周知js两大特点 1.单线程;2.解释执行(解释一行,执行一行)。那么问题来了 如下代码的输入结果是什么?

console.log(a,b)
function a(){}
var b = 0;复制代码

解释一行执行一行, 第一句直接console.log(a,b), 还没有执行到他们的 声明语句,那结果会不会是  xxx is not defient 。 明显不是,稍微有点基础的人都知道 var 和 函数声明会有提升。 这个提升为 函数声明整体提升 , 变量   声明提升。这个提升的过程就是发生在预编译。但是预编译不止这些。

先介绍一波知识点:

js运行三部曲:
  1. 语法分析
  2. 预编译
  3. 解释执行

对函数:预编译发生在函数执行前一刻;

对整js :预编译发生在全局执行的前一刻;

那么问题来了:函数声明和变量声明都提升谁的优先级高呢?

看码:

 function fn(a) { 
   console.log(a)    
   console.log(b)    
   var a = 123;    
   console.log(a)    
   function a(){}    
   var b = function(){}    
   console.log(b)    
   function d (){} 
} 
fn(1)复制代码

上面的代码分别输出什么,可以自己先看看有了答案,再往下看。

在回答上面的问题前先列出   函数预编译4步曲:

  1. 创建AO对象
  2. 找到形参和变量声明,将变量和形参作为AO属性名,属性值为undefined
  3. 找到实参的值,赋值给形参
  4. 函数体里面找到函数声明, 值为函数体

全局的预编译,省略 2.的找形参和 3步的实参和形参统一;

现在来回答上面的问题:

fn(1) 执行,传递形参为1;
在fn执行的前一刻发生了预编译,生成了AO对象 (Actived Object )可以理解为执行期上下文
第一步:创建AO对象
AO={
}
第二步:找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
注意: var b = function(){} 这个叫做函数表达式,不是函数声明
AO={
a:undefined;
b:fundefined;
}
第三步:将实参的值 赋值给 形参
AO={
a:1;
b:fundefined;
}
第四步.函数体里面找到函数声明, 值为函数体
AO={
a:1;
b:undefined;
d:functioin d(){}
}
然后就按照代码里面的解释执行即可,结果分别为
1 , undefined ,123 , function(){} .


对于全局的就分析第一个列出的代码了。
第一步:创建GO对象 (global object)
GO = {
}
第二步:找到变量声明
GO = {
b:undefined
}
第三步:找到全局的函数声明
GO = {
b:undefined,
a :funciton a () {}
}

下面给出2个题,然后结束预编译。(大家可以先自己坐下,再去自己打下代码,如果跟自己的答案个控制台输出有出入,就看再看看4部曲再做做。熟悉了再往下看哟)

console.log(test);
function test(test) {    
   console.log(test)    
   var test = 234;    
   console.log(test)    
   function test(){} 
} 
test(1) 
var test = 123;复制代码

function test (a,b){   
    console.log(a);   
    c = 0;   
    a = 3;   
    b = 2;   
    console.log(b);   
    function b(){}   
    function d(){}   
    console.log(b)
}
test(1);复制代码

二:作用域,作用域链

讲作用域前先给出一些定义:
作用域定义: 变量(变量作用域又称为 上下文) 就是变量生效(能被访问)的区域


作用域目前被分为: 全局作用域 ,函数作用域, 块级作用域(es6引入,先不做讨论)


[[scopes]] : 每个JavaScript 函数都是一个对象, 对象中有些属性我们可以访问到(如:test.name),但是有些是不可以的,这些属性仅供JavaScript引擎存取,[[scopes]]就是其中一个。如同,这个是 Object() 的内部属性



作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链


运行期上下文: 当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文, 当函数执行完毕,执行上下文被销毁

来看码:

function a() {    
    function b(){       
        var b = 234;    
    }    
    var a = 123;    
    b();
}
var glob = 100;
a();复制代码

当代码执行到 a() 执行前是
第一步: a 定义,全局生成了一个 GO ,a也是其中的一个属性,a 的[[scope]] 0位指向GO


第二步:a执行,生成了自己的AO,插入到了[[scopes]] 0 位GO 往后延


第三步:a执行引起的b的定义,这个时候b继承了a的劳动成果,直接把a的生成的作用域链的引用拿来挂到自己的身上。这个时候 b.scope[0] 指向的AO和 a.scope[0] 指向的AO是同一个。(结合一下自己写代码就能知道,在子函数里面改变了外部函数的值,外部函数的值也改变了)


第四步:b函数执行,生成了自己的AO,把自己的AO插入到刚刚获取的作用域链


看完这个再去看看刚刚的文字的定义 。

弄懂之后就明白了,为什么子函数能拿到父级函数的变量,而父级函数拿不到子函数内的变量。因为子函数的作用域链里面存储了父级函数的变量,而父级函数的作用域链并没有子函数的变量。 当在函数内部使用 a + b 的时候,系统会沿着作用域链去查找,自己有就用自己的,自己没有就 去找父级的,直到找到最底端。如果还是没有,  变量赋值就暗示全局变量, 变量取值就报错 , xx is not defined。
再举一个例子,加深理解

function a (){   
    function b(){      
        function c(){                          
        }        
        c();   
    }   
    b(); 
} 
a();复制代码

上的码
a defined a.[[scope]] -- > 0:GO
a doing    a.[[scope]] -- > 0:aAO
                                      1:GO
b defined b.[[scope]] --> 0:aAO
                                         1:GO
b doing b.[[scope]] --> 0:bAO
                                      1:aAO
                                      2:GO
c defined c.[[scope]] --> 0:bAO
                                        1:aAO
                                        2:GO
c doing c.[[scope]] --> 0:cAO
                                     1:bAO
                                     2:aAO
                                     3:GO

看了这个例子,大家应该能明白点了,(如何还没明白,怪我,第二次写文章,表达不好) 上面所有的AO和BO 都是一个AO和BO,他们拿到地址指向后就可以直接用,一个修改全部都被修改。

三: 文体两开花 ,终于到了 闭包

 定义:  当 内部函数被保存到外部,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄漏。

讨论之前,先说一句话,一句开始我加粗了的话 :当函数执行完毕,执行上下文被销毁

看码;

function a () {   
    function b() {      
        var bbb = 234;      
    console.log(aaa)   
    }   
    var aaa = 123;  
 return b;
}
var glob = 100;
var demo = a();
demo();复制代码

来按照上面的作用域分析再来一波:
a defined a.[[scope]] -- > 0:GO
a doing    a.[[scope]] -- > 0:aAO
                                         1:GO
b defined b.[[scope]] --> 0:aAO
                                          1:GO
a执行引起b的定义,b定义的时候直接继承a的劳动成果,把 a的作用域链全部加持到自己身上。当执行return b ; 的时候, b函数带着他身上的作用域链一起被返回了出去,然后a执行结束, 这个时候 a.[[scope]] [0] 指向的 aAO,就会被销毁,就相当于是把a.[[scope]] [0] 连接 的 aAO的那根线给剪短了。但是整个b函数全部返回出去赋值给了demo, demo就变成了函数,demo的作用域链就是b函数的作用域链,而b的作用域链又是继承的 a函数的作用域链, 里面包含了所有在a函数里面定义的变量 。所以demo 函数就可以对自己的作用域链里面的值为所欲为。而且当a再次执行的时候,他创建的是一个新的AO对象,他也管不了上一次创建的变量。所以 demo函数的作用域里面的变量就成为了demo的私有变量,除了他自己,谁用调用不了。

这就是一个闭包形成的真正过程。



到这里文体两开花学闭包就结束了。至于闭包的用处和规避措施。大家就可以网上搜索了,很多。(如何没有看懂,是我的问题,文笔太差, 大家多多包涵,有问题欢迎评论区 指出,我看到就会马上回复,谁叫我没工作呢)。

最后祝各位前端攻城师们都能对js了如指掌,虽然很多js的一些特性(或者说缺陷)确实恶心,但是说不定哪一天就被触发了,如果对这些不了解,那bug是真的找不到了。


ps:最后的最后打个广告,本人普通二本软件工程大四学生,js基础还行,了解一点node,其他的前段技术也多多少少了解点。正在找一份前端开发的工作,坐标成都。希望有找人的大佬,给点内推,面试机会。谢谢大佬们。


转载于:https://juejin.im/post/5c25a432518825085060766a

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值