前端系统化学习【JS篇】:(十一)创建并执行一个函数到底会发生什么?+循环事件绑定问题

306 阅读12分钟

前言

  • 细阅此文章大概需要 10分钟\color{red}{10分钟}左右
  • 本篇中讲述了:
      1. 函数创建和执行的底层处理机制
          1. 创建一个函数发生了什么?
          1. 执行函数时发生了什么?
          1. 函数执行完成后发生了什么?
      1. 作用域
      1. 作用域链查找机制
      1. 关于函数创建执行、作用域、作用域链的总结
      1. 闭包的扩展:利用闭包解决循环事件绑定的问题
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!
  • 欢迎转载,注明出处即可。

函数创建和执行的底层处理机制


说在前面:

  • 函数的两种创建方式:

    1. var fn = function(){};
    2. function fn(){};
      • 意义是相同的:都是创建了一个名为fn的函数
      • 区别只体现在变量提升
  • 另一种情况:

        var fn = 'xxx';
        function fn(){};
        //1.先声明一个变量fn
        //2.变量提升时把函数的堆内存地址赋值给fn
        //3.代码执行时,fn被赋值为字符串'xxx',不再指向函数地址
    
        fn(8)(9); //代表的含义为先执行fn(8),把返回的结果再执行(9)   
    
    • 两个fn都是全局变量,而且是同一个变量,只是赋值不同

函数的创建和执行:

  • 创建一个函数:

    1. 先开辟一个堆内存,有一个16进制的内存地址
    2. 声明当前函数的 作用域[[scope]], 【在哪个上下文中声明的,该函数的作用域就是谁】 【此作用域并不是【当前】执行上下文】
      • 这里要把 【该函数的作用域】【该函数执行时产生的私有执行上下文】 区分开
        • 函数执行时产生的私有执行上下文 是指:在函数执行时开辟一小块栈内存,存储变量和值等等...
        • 该函数的作用域[[scope]] 是指:这个函数是在哪个上下文中声明的,哪个上下文中就是该函数的作用域;若在函数中在再声明一个函数,那么里面的函数的 作用域[[scope]] 指的就是外面函数执行时创建的私有执行上下文
    • 顺便标记一下形参,【方便我们自己后期执行的时候看看有没有形参】【不是浏览器机制,只是为了自己方便】
    1. 将函数体中的代码当作"字符串"存储在堆内存中 (创建一个函数,堆内存中存储的是一堆字符串,所以只要函数不执行,就没有什么意义。)
    2. 把这个函数所创建的堆内存的地址,存储到栈内存中(值存储区),供变量(函数名)来调用

  • 执行函数:

    1. 会形成一个全新的私有上下文EC(xx)(目的是供堆内存中的函数体中的代码执行)
    2. 在私有上下文中有一个存放私有变量的私有变量对象Ao(xx)
    3. 在代码执行之前要做的事情:
      1. 初始化该私有上下文EC(xx)的 【作用域链scope-chain】===><自己的上下文,函数的作用域>
      2. 初始化THIS(箭头函数没有THIS)
      3. 初始化ARGUMENTS实参集合(箭头函数没有ATGUMENTS)
      4. 形参赋值(形参变量是函数的私有变量,需要存储在AO中)
      5. 变量提升(在私有上下文中声明的变量都是私有变量)
      6. ......
    4. 代码执行 (将之前在函数存储在堆中的字符串形式的代码,拿到栈中在当前上下文中依次执行)
      • 在代码执行的过程中,遇到的变量都会遵循 【作用域链查找机制】
    • 注意: 当同一个函数再次被执行,无论第一次执行产生的上下文是否释放,都会形成一个全新的私有上下文,把之前做过的事情会原封不动的再执行一次,此时形成的上下文和上一次形成的上下文之间没有必然的联系


  • 执行完成后:

    1. 根据实际情况确定当前上下文是否要释放出栈
      • 为了保证栈内存的大小【内存优化】,一般情况下,如果当前函数执行产生的上下文,在进栈且代码执行完毕之后,会把此上下文移除出栈。【上下文释放掉了:则之前在此上下文中存储的私有变量等信息也就跟着释放掉了】
      • 全局上下文是在打开页面时生成的,也需要再关闭页面的时候释放掉,(只有在关闭页面的时候,才会释放掉)【而在刷新页面时,会把之前的上下文全都释放调,然后创建全新的上下文。】
      • 特殊情况: 只要当前上下文中的某些内容,被当前上下文以外的东西占用,那么当前上下文是不能被释放的(上下文中存储的变量等信息也一样会得到保留)【闭包的保存机制】

作用域

  • 作用域 [[SCOPE]] , 该函数是在哪个上下文中声明的,该函数的作用域就是谁 【此作用域并不是函数执行时产生的私有执行上下文】
    • 这里要把 【该函数的作用域】【该函数执行时产生的私有执行上下文】 区分开
      • 函数执行时产生的私有执行上下文 是指:
        • 在函数执行时开辟一小块栈内存,存储变量和值等等...
      • 该函数的作用域[[scope]] 是指:
        • 这个函数是在哪个上下文中声明的,哪个上下文就是该函数的作用域; 若在函数中在再声明一个函数,那么里面的函数的 作用域[[scope]] 指的就是外面函数执行时创建的私有执行上下文

作用域链查找机制

  • 在代码执行的过程当中,遇到一个变量,我们需要:
    1. 查看是否为当前上下文的私有变量: 如果是私有变量,则接下来针对该变量的所有操作都是私有的(和外界没有直接联系)
    2. 如果不是当前上下文私有的变量: 则遵循【作用域链查找机制】,按照当前上下文的 【作用域链scope-chain<自己的上下文,函数的作用域>】 ,向上级上下文中查找(如果是上级上下文的私有变量,则接下来针对该变量的所有操作都是操作上级上下文中的这个变量的)....,如果在 上级上下文 也没有找到,则顺着 上级上下文的作用域链 接着向 上级上下文 查找,直到找到 EC(G) 为止。

关于函数创建执行、作用域、作用域链的总结

  • 如果要从原理角度理解:
    • 变量的作用域机制依赖于执行上下文
      • 全局代码执行对应全局执行上下文,函数代码执行对应函数私有执行上下文
    • 每调用一次函数,会创建一次函数执行上下文,这过程中 ,浏览器会解析函数代码,在这过程中会:创建活动对象 AO,将函数内声明的变量、形参、arguments、this、函数自身引用都添加到AO中
    • 函数内对各变量的操作实际上是对上个步骤添加到 AO 对象内的这些属性的操作
    • 函数执行,创建私有执行上下文的阶段中,会创建一个属性:作用域链。 对于函数私有的执行上下文,其值为 <当前私有执行上下文的 EC(xx) ,当前函数的作用域 [[Scope]]> 对于全局执行上下文,其值为上 EC(G)
    • 函数内部属性 [[Scope]] 存储着它外层函数的作用域链,是在外层函数创建函数对象时,从外层函数的执行上下文的作用域链复制过来的值。
    • 总之,JavaScript 中的 变量之所以可以在定义后被使用,是因为定义的这些变量都被添加到当前执行上下文 EC 的变量对象 VO 中了 ,而之所以有全局和函数私有两种作用域,是因为当前执行上下文 EC 的作用域链属性的支持。 也可以说一切都依赖于执行上下文机制。
  • 那么,如果想通俗的理解:
    • 函数内操作的变量,如果在其内部没定义,那么在其外层函数内寻找,如果还没有找到,继续往外层的外层函数内寻找,直到外层是EC(G)为止。
    • 这里的外层函数【是相对概念】 ,指的是针对于函数声明位置的外层函数,而不是函数调用位置的外层函数。作用域链只与函数声明的位置有关系。

闭包

  • 闭包是一种机制
  • 闭包的作用:保护/保存
    1. 闭包是函数运行时产生的机制,函数执行会在执行环境栈【ECStack】中形成一个全新的私有上下文【EC(私有)】并且在私有上下文中声明新的作用域【scope】和作用域链【scope-chain】,可以保护里面的私有变量【Ao】和外界【Vo】互不干扰 【保护机制】
    2. 且若私有上下文中的某些内容,被当前上下文以外的东西占用,那么当前上下文是不能被出栈释放的(这样私有变量及它的值等也不会被释放掉) 【保存机制】
  • 大量应用闭包一定会导致内存消耗,但是闭包的保护和保存作用,在真实开发中我们还是需要使用,所以要合理使用闭包
  • 闭包的应用:
    1. 真实项目用途
    2. 高阶编程:柯里化/惰性函数/compose函数
    3. 源码分析: JQ/LOADASH/REACT(REDUX/高阶组件/HOOKS)...
    4. 自己封装插件组件时
  • 总结时涉及到【ECSTACK/EC/AO/VO/SCOPE/SCOPE-CHAIN/释放不释放/垃圾回收机制】

利用闭包解决循环事件绑定的问题

  • 先来看一个代码,我们想要通过五个按钮,来控制背景变成不同的颜色,想要的结果是,按不同按按钮会改变为对应的颜色。
  • 但是当循环绑定完成后,点击按钮触发事件时,不管按哪个按钮,发现i一直都是5,可是数组中并不存在第六种元素,就会导致错误。
  • 【分析】: 由于给事件绑定的函数是在触发时调用执行的,在给onclick进行事件绑定的时候,事件处理函数里的代码并不会运行【只会在点击触发时执行】,所以i并【没有被传入onclick的执行函数里】,因此在触发click事件之前,【onclick的事件处理函数】并不知道i等于几,【这里的onclick要执行的函数中的变量i】指向的内存地址,【在循环结束后】变成了【5】,所以所有的按钮在点击后都输出【5】。
    var arr = ['red','green','blue','black','pink'];
        var buttonList = document.getElementsByTagName('button');
        for(var i = 0;i<buttonList.length;i++){
                buttonList[i].onclick = function () {
                    var color = arr[i];
                    console.log(i);
                    document.body.style.backgroundColor = color;
                };
        }


  • 我们可以使用闭包的原理来解决这个问题
    //第一种闭包写法
        var arr = ['red','green','blue','black','pink'];
        var buttonList = document.getElementsByTagName('button');
        for(var i = 0;i<buttonList.length;i++){
            // 创建一个自执行函数,将循环事件绑定的函数包住
            // 在循环中,每次循环,自执行函数都会执行,而每次执行都会有形参i传入
            // 并且创建一个新的函数的堆内存并且将地址赋值给外部的每个buttonList[i].onclick。
            //【此时自执行函数内部的函数地址被外部占用,每次循环创建的堆内存都不会被释放
            // 从而每次循环执行自执行函数的私有上下文也不会被释放,于是不同的私有上下文中对应也保存了形参变量i和堆内存地址】
            (function(i){
                buttonList[i].onclick = function () {
                    var color = arr[i];
                    document.body.style.backgroundColor = color;
                    // 而当每次点击事件触发小函数时,小函数顺着自己的作用域链找上一级上下文中的形参变量i
                    // 就不会再去全局中找早已变成5的i了。
                };
            })(i);
    //第二种闭包写法【与上原理相同】
        var arr = ['red','green','blue','black','pink'];
        var buttonList = document.getElementsByTagName('button');
        for(var i = 0;i<buttonList.length;i++){
        
            buttonList[i].onclick = (function(i){
                return function () {
                    var color = arr[i];
                    document.body.style.backgroundColor = color;
                  
                };
            })(i);
        }
  • 也可以利用let会形成块级作用域,从而保存变量的机制(也是利用了闭包的机制)
//基于ES6处理使用LET,也是利用了闭包的原理【性能更好】【因为浏览器底层实现的,比我们自己实现性能好一点】
    let arr = ['red','green','blue','black','pink'];
    let buttonList = document.getElementsByTagName('button');
    for(let i = 0;i<buttonList.length;i++){
    //每一轮循环都会形成一个私有的上下文/块级作用域,并且有一个私有变量i,分别存储每一轮的循环索引
        buttonList[i].onclick =function () {
                var color = arr[i];
                document.body.style.backgroundColor = color;
            }; 
    }
  • 真实项目中遇到循环事件绑定,最好还是告别闭包,(包括LET),使用自定义属性......【但是性能依然不是最好】

    • setAttribute给元素添加自定义属性,从而在每次循环时,将变量i保存到自定义属性中
  • 最好的方式还是事件委托【性能最好】

    var arr = ['red','green','blue','black','pink'];
    document.body.onclick = function (ev) {
        let target = ev.target,
            targetTag = target.tagName;
        //当前点击的是五个按钮中的一个,target事件源就是点击的这个按钮
        if(targetTag==='BUTTON'){
            var index = target.getAttribute('index');
            document.body.style.backgroundColor = arr[index];
        }  
}