函数的底层处理机制

130 阅读6分钟

JS中上下文分类

  • 全局上下文EC(G):会创建全局变量对象VO(G)
  • 私有上下文:函数执行,会形成全新的私有上下文
  • 块级私有上下文

创建函数

需要开辟一个堆内存,对象的堆内存中,存储的是它的键值对。

函数的堆内存中存储的:

  • 代码串: 是它的代码,而且是以 字符串 形式存储的。

  • 作用域[[scope]]:创建函数的时候,就声明了它的作用域scope。scope的值就是当前创建函数所处的上下文。在哪个上下文中创建的,作用域就是谁。

  • prototype: 原型对象

  • __proto__: 基于所有的函数都是Function的实例。包括Function内置类也是自身的实例。实例对象都有__proto__属性。所以每个函数也都有。而且Function.__proto__===Function.prototype

  • name: 函数名

  • length: 形参个数

执行函数

  1. 会形成一个全新的私有上下文即当前上下文,也会有私有变量对象。即AO。Active Object活动对象(变量对象的一种,函数形成上下文中的变量对象叫做AO)。全局的变量对象成为VO(G)。

    额外知识点——闭包的形成:里面的私有变量,受到了私有上下文的保护,不受外界干扰。有可能形成不被释放的上下文,里面的私有变量和一些值,就会保存起来。这些值可以供其下级上下文中调取使用。我们把函数的这种 保存保护 机制,称之为闭包。!! 闭包只是一种机制。 闭包具有保存保护机制。有的说任何函数都是闭包,因为存在自己的私有作用域,但是这样的话闭包存在很短暂,所以更多的理解是产生了,不释放的空间,这种情况称为闭包。看自己的理解。

    创建好后进栈执行

  2. 初始化

  • 初始化作用域链 Scope-chain <当前上下文,函数作用域(创建函数的时候的上下文)(相当于当前上下文的上级上下文)>

  • 初始化this,函数执行的时候,函数的执行主体,和执行上下文不是一个概念。函数执行主体:谁执行的这个函数,全局的this是window,我们研究的都是函数中的this。函数执行上下文:在哪里执行。

  • 初始化arguments

    • (实参集合[类数组集合]):{ 0:第一个实参, 1:第二个实参....以此类推, length:实参的个数, callee:function (){}:获取函数本身,方便递归调用}

    • 在JS的非严格模式下,当初始化arguments和形参赋值完成之后,会给两者建立一个映射机制。arguments的每一项和对应的形参变量绑定在一起。一个修改都修改。而且只会发生在代码执行之前建立这个机制。(如果实参和形参不对应,那么arguments和形参只有对应的会映射上,那么没有赋值的哪项,再修改,不用影响arguments)

    • 在JS的严格模式下,没有映射机制,也没有arguments.callee这个属性;

    • 箭头函数中没有arguments

  • 形参声明并赋值:在当前上下文中声明一个形参变量,并且赋值。

  • 变量提升

  1. 代码执行:把之前创建函数在堆内存中存储的代码字符串拿出来,变为代码一行行执行

    作用域链查找机制:私有上下文中代码执行,如果遇到一个变量,首先看是否为自己的私有变量,如果是私有的,则操作自己的。如果不是私有的,则基于作用域链向其上下文中查找, 看是否为上级上下文中私有的......一直找到EC(G)全局上下文为止,这种查找的过程就是作用域链查找机制。

  2. 出栈释放

重点:

函数每执行一次,都会形成一个全新的私有上下文,和之前执行形成的上下文没有必然的联系。即便之前的被保存下来了(闭包的情况)。

函数执行,它的上级作用域(上下文)是谁和函数在哪里执行是没有关系的,只和在哪创建有关系:在哪里创建的,它的作用域[[scope]]就是谁,也就是他的上级上下文就是谁!!

GC:浏览器的垃圾回收机制(内存释放机制)

  • 堆内存释放 -不同浏览器处理机制不太一样。

    方案一 谷歌为主的: 查找引用。

    浏览器在空闲或者指定时间内,查看所有的堆内存,把没有被任何东西占用的堆内存释放掉。但是占用的是不会释放的。

    方案二 IE为主的: 引用计数方式。

    创建了堆内存,被占用一次,则浏览器计数+1,取消占用则计数-1。当记录的数字为0,则内存释放掉。某些情况会导致计数数据混乱,出现内存泄漏现象,即该释放没释放。

    手动释放,将变量赋值为null。

    特殊: 当前上下文中开辟的某个堆内存(函数和对象),被当前上下文以外的变量或者其他事物所占用,此时当前上下文是不能被出栈释放的。

  • 栈内存释放

    加载页面,形成一个全局的上下文,只有页面关闭的后,全局上下文才会被释放。

    函数执行会形成私有的上下文,进栈执行;当函数中的代码执行完成,大部分情况下,形成的上下文都会被出栈释放掉,以此优化栈内存大小。特殊情况不是这样的,比如闭包

几种常见的内存泄漏

  • 全局变量

    全局变量什么时候需要自动释放内存空间很难判断,所以在开发中尽量避免使用全局变量,以提高内存有效使用率。

  • 未移除的事件绑定

    dom元素虽然被移除了,但元素绑定的事件还在,如果不及时移除事件绑定,在IE9以下版本容易导致内存泄漏。现代浏览器不存在这个问题了,了解一下即可。

    let div = document.querySelector(".div");
    let name = 'lmy'
    let handler = function () {
        console.log(name);
    }
    div.addEventListener('click', handler, false)
    
    div.parentNode.removeChild(div) // 在IE9以下的老版本事件还在
    
  • 无效的dom引用

    有时候将dom作为对象的key存储起来很有用,但是在不需要该dom时,要记得及时解除对它的引用。

    var ele = {
      node: document.getElementById('node')
    };
    
    document.body.removeChild(document.getElementById('node')); // 此时ele中还存在对node的引用
    
  • 定时器setInterval/setTimeout

    看下面的一段定时器代码,一旦我们在其它地方移除了node节点,定时器的回调便失去了意义,然而它一直在执行导致callback无法回收,进而造成callback内部掉数据resData也无法被回收。所以我们应该及时clear定时器。

    let resData = 100
    let callback = function () {
        let node = document.querySelecter('.p')
        node && (node.innerHTML = resData)
    }
    
    setInterval (callback, 1000)
    

另外单独说一下闭包,闭包和内存泄漏没有半毛钱关系,只是由于IE9之前的版本垃圾收集机制的原因,导致内存无法进行回收,这是IE的问题,现代浏览器基本都不存在这个问题。当然闭包要是使用不当肯定是会造成内存泄漏。

如有理解不正确的,欢迎指出~