美文网首页饥人谷技术博客
JS-执行环境-作用域(链)-垃圾收集

JS-执行环境-作用域(链)-垃圾收集

作者: 学的会的前端 | 来源:发表于2019-01-18 11:56 被阅读16次

    执行环境

    • 执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个变量对象。解析器在处理数据的时候,会在后台使用它。在所有的代码执行完毕后,执行环境就会被销毁。
    • 全局执行环境是最大的一个执行环境,在web浏览器中,window被认为是最大的执行环境,所有的变量和函数都是作为window对象的属性和方法存在的。当应用程序退出(关闭网页或者浏览器),window就会被销毁。
    • 每个函数也都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中,而在函数执行完之后,栈会将其环境弹出,恢复到上一个执行环境中。

    作用域链(scope chain)

    • 当代码在一个环境中执行时,会创建变量对象的一个作用域链。用途:保证对执行环境有权访问的所有变量和函数的有序访问。
    • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象,如果这个环境是函数,则将其活动对象(activation object)作为变量对象,活动对象最开始的时候只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。全局执行环境中的变量始终都是作用域链中的最后一个对象。
    • 标识符的解析是沿着作用域链一级一级的搜索标识符的过程。
    • 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
    var color = 'red';
    function changeColr(){
        var another = 'blue';
        function swapColpr() {
            var temp = 'yellow';
            console.log(color); //red 可以访问祖先执行环境
            console.log(another); //blue  可以访问父执行环境
            console.log(temp); //yellow 可以访问自身执行环境
        }
        swapColpr();
        console.log('------------------');
        console.log(color); //red 可以访问父执行环境
        console.log(another); //blue 可以访问自身执行环境
        console.log(temp); // error not defined 不能访问子执行环境
    }
    console.log('------------------');
    console.log(color); //red 可以访问自身执行环境
    console.log(another); //error not defined 不能访问子执行环境
    console.log(temp); //error not defined 不能访问子执行环境
    changeColr();
    

    延长作用域链

    • 原因:有些语句可以在作用域链的前端临时增加一个变量,该变量对象会在代码执行后被移除。所以通过此方法可以延长作用域链。
    • 方法:当执行流进入到下列任何一个语句时,作用域链就会被延长
    1. try-catch语句的catch块。会创建一个新的变量,其中包含抛出错误对象的声明。
    2. with语句。会将指定对象添加到作用域链中。
    function bulidUrl(){
        var qs = "debug = true";
        with(location){
            var url = href + qs;
        }
        return url
    }
    var result = bulidUrl();
    console.log(result);
    
    TIM图片20190118103029.png

    以上代码假设with不能延长作用域链的话,那么url就是with函数内的局部变量,他的父执行环境bulidUrl是无法访问的,那么return url就会报错,但是with可以延长作用域链,他接收到的是location对象,因此其变量对象中就包含了location对象所有的属性和方法,并且把这个变量对象添加到作用域链的前端,当在with语句中引用变量href时(实际上引用的是location.href),可以在当前执行环境的变量对象中找到,当变量引用qs时,引用的是在bulidUrl()中定义的那个变量,至于with语句内部,则定义了一个名为url的变量,因此url就成了函数执行环境的一部分,可以作为函数值被返回。

    没有块级作用域

    if(true){
        var color = "blue";
    }
    console.log(color)
    
    TIM图片20190118103912.png
    在其他强类型语言,例如C语言中,color是定义在if语句内的变量,if是具有块级作用域的,当if执行完,color就会被销毁,所以最后输出undefined,但是在JavaScript语言中,if语句中的变量声明会将变量添加到当前的执行环境(此时当前的执行环境就是全局执行环境)中。for语句也是这样的道理

    垃圾收集

    JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存,所需内存的分配以及无用内存的回收完全实现了自动化管理。
    原理:垃圾回收器按照固定的时间间隔找出那些不在继续使用的变量,然后释放其占用的内存。

    • 实现方式:
    1. 标记清除
      先给即将使用的变量标记---清除标记---标记使用后的变量,便于删除
    2. 引用计数:跟踪标记记录每个值被引用的次数。
      当声明了一个变量a并且将一个引用类型的值Q赋值给a,Q的引用就+1,如果Q又被b引用,Q的引用在+1,此时为2。当a不引用Q而引用T的时候,Q的引用就-1,此时Q的引用为1,当Q的引用为0时,就将Q占用的内存空间收回来。
    3. 引用计数的缺点:循环引用
    function fn(){
        var obj1 = new Object();
        var obj2 = new Object();
        obj1.someOtherObject = obj2;
        obj2.anotherObject = obj1;
    }
    

    例如以上代码,obj1通过someOtherObject引用obj2,obj2通过anotherObject引用obj1,那么obj1和obj2的引用都是2,永远不会变成0,所以使用引用计数永远无法回收,假设这个函数被重复多调用,那么就会导致大量的内存得不到回收。
    在IE浏览器中,为了避免DOM对象和JavaScript原生对象之间循环引用问题,最好在不使用它们的时候,手动断开它们之间的联系。

    myObject.element = null;
    element.somObject = null;
    

    设置为null就断开了它们之间的联系,垃圾回收机制就会对他们进行内存回收。
    原代码:

    /*原生对象与DOM对象是循环使用的*/
    var element = document.getElementById('some_element'); //DOM对象
    var myObject = new Objrct(); //JavaScript原生对象
    myObject.element = element;  // 原声对象与DOM对象的联系
    element.someObject = myObject; // DOM对象与原声对象的联系
    

    为了解决上诉问题,IE9把DOM和BOM对象都转成了真正的JavaScript对象。

    • 性能问题
      垃圾收集器是周期性运行的,确定垃圾收集的时间间隔是一个非常重要的问题,JavaScript垃圾回收机制:触发垃圾收集的变量分配,字面量和数组元素的临界值是动态修正的。
      在IE中调用window.ClooectGrabage()方法会立即执行垃圾收集。
      在Opera7 + 版本中,调用window.opera.collect()会启动垃圾回收机制。

    管理内存

    分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少,目的:防止运行JavaScript的网页耗尽全部系统内存造成系统崩溃。内存限制的问题不仅会影响给变量分配内存,同时还会影响调用栈以及一个在线程中能够同时执行的语句数量。

    • 优化内存的方法:解除引用
      为执行中的代码保存必要的数据,一旦数据不使用,通过将其值设置为null来释放其引用。
      解除引用不意味着自动回收该值所占用的内存,解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行的时候将其收回。
    function fnname){
        var person = new Object();
        person.name = name;
        return person; //person为局部变量,当函数执行完毕后,自动离开执行环境,不需要设置null。
    }
    var resut = fn('liqi');
    resut = null //手动解决resut的引用
    

    以上内容小结:

    JavaScript变量可以用来保存两种类型的值:基本类型和引用类型。基本类型的值包含5种:undefined,null。number,Boolean,string。基本类型与引用类型具有以下特点:

    • 基本类型的值在内存中占据固定大小的空间,因此被保存在内存中;
    • 从一个变量向另外一个变量copy基本类型的值,会创建这个值的一个副本;
    • 引用类型的值是一个对象,保存在内存中;
    • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
    • 从一个变量向另一个变量copy引用类型的值,实际上copy的是指针,因此两个变量最终指向同一个对象;
    • 确定一个值是哪种基本类型可以使用typeof操作符,而确定一个值是哪种引用类型可以使用instanceof操作符。
      所有变量都存在与一个执行环境中(也成为作用域),这个执行环境决定了变量的声明周期,以及那一部分代码可以访问其中的变量,以下是关于执行环境的总结:
    • 执行环境有全局执行环境(也成为全局环境)和函数执行环境之分;
    • 每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链;
    • 函数的局部环境不仅有权访问函数作用域中的变量,还可以访问其父环境以及全局环境;
    • 全局环境只能访问在全局下定义的变量和函数,不能访问局部环境中的;
    • 变量的执行环境有助于确定如何释放内存。
      JavaScript是一门具有自动垃圾回收机制的编程语言,开发人员不必关心内存分配和回收的问题,垃圾回收机制总结:
    • 离开作用域的值将自动标记为可以回收,因此在垃圾收集期间被删除;
    • 标记清除是目前主流的垃圾回收法,这种算法的思想是给当前不使用的值加上标记,然后在回收其内存;
    • 另一种垃圾回收算法是引用计数,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不在使用这种算法,但在IE浏览器中访问非原生JavaScript对象时,这种算法仍然可能导致问题;
    • 当代码中存在循环引用时,引用计数法就会存在问题;
    • 解除变量的引用不仅有助于消除循环引用现象,而且对于垃圾收集也有好处,为了确保有效的回收内存,应该及时解除不在使用的全局对象,全局对象属性以及循环引用变量的引用。

    代码解析示例1

    • 代码解析示例
    var x = 10
    bar()
    function foo() {
      console.log(x)
    }
    function bar(){
      var x = 30
      foo() //  输出什么
    }
    

    当声明的时候,会出现下面的情况

    globalContext = {
      AO: {
        x: 10
        foo: function
        bar: function
      },
      Scope: null
    }
    
    //声明 foo 时 得到下面
    foo.[[scope]] = globalContext.AO
    //声明 bar 时 得到下面
    bar.[[scope]] = globalContext.AO
    

    注意: 在当前的执行上下文内声明的函数,这个函数的[[scope]]就执行当前执行上下文的 AO
    当调用 bar() 时, 进入 bar 的执行上下文

    barContext = {
      AO: {
        x: 30
      },
      Scope: bar.[[scope]] //globalContext.AO
    }
    

    当调用 foo() 时,先从 bar 执行上下文中的 AO里找,找不到再从 bar 的 [[scope]]里找,找到后即调用
    当调用 foo() 时,进入 foo 的执行上下文

    fooContext = {
      AO: {},
      Scope: foo.[[scope]] // globalContext.AO
    }
    

    所以 console.log(x)是 10

    代码解析示例2

    • 代码解析示例
    var x = 10;
    bar() //  输出什么
    function bar(){
      var x = 30;
      function foo(){
        console.log(x)
      }
      foo();
    }
    

    进行声明的时候,会出现

    globalContext = {
      AO: {
        x: 10
        bar: function
      },
      Scope: null
    }
    
    //声明 bar 时 得到下面
    bar.[[scope]] = globalContext.AO
    

    注意: 在当前的执行上下文内声明的函数,这个函数的[[scope]]就执行当前执行上下文的 AO
    当调用 bar() 时, 进入 bar 的执行上下文

    barContext = {
      AO: {
        x: 30,
        foo: function
      },
      Scope: bar.[[scope]] //globalContext.AO
    }
    //在 bar 的执行上下文里声明 foo 时 得到下面
    foo.[[scope]] = barContext.AO
    

    当调用 foo() 时,先从 bar 执行上下文中的 AO里找,找到后即调用
    当调用 foo() 时,进入 foo 的执行上下文

    fooContext = {
      AO: {},
      Scope: foo.[[scope]] // barContext.AO
    }
    

    所以 console.log(x)是 30

    相关概念

    • 执行上下文 executionContext
    • 活动对象 AO
    • Scope 属性
    • 执行顺序

    笔试题

    var a = 1;
    
    function fn(){
      console.log(a);   //undefined
      var a = 5;
      console.log(a);    // 5
      a++; //a = 6
      var a; // a= 6
      fn3(); 
      fn2(); 
      console.log(a); // 20
    
      function fn2(){
        console.log(a);  //6
        a = 20;
      }
    }
    
    function fn3(){
      console.log(a) //a = 1
      a = 200;
    }
    
    fn();
    console.log(a);  //200
    

    补充说明

    • JS普通变量的作用域是词法作用域:词法分析,不需要执行就可以知道是什么。
    • 词法作用域只能确定他是哪一个,不能确定他具体的取值。
    • this不是词法作用域,必须调用才能确定this。
    • 就近原则,是作用域进,不是代码行数少。

    相关文章

      网友评论

        本文标题:JS-执行环境-作用域(链)-垃圾收集

        本文链接:https://www.haomeiwen.com/subject/fclwdqtx.html