真正的认识闭包

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

一:预编译

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

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

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

先介绍一波知识点:java

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

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

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

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

看码:bash

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步的实参和形参统一;
ui

如今来回答上面的问题:

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,其余的前段技术也多多少少了解点。正在找一份前端开发的工做,坐标成都。但愿有找人的大佬,给点内推,面试机会。谢谢大佬们。

相关文章
相关标签/搜索