Corbusier/Professional-JavaScript

变量、作用域和内存问题.md

Opened this issue · 0 comments

变量、作用域和内存问题

1.基本类型和引用类型的值

变量包含两种不同类型的值:基本类型值和引用类型值。

基本类型值指的是简单的数据段,而引用类型值是可能由多个值构成的对象。基本数据类型值:Boolean、String、undefined、Number、Null是按值传递的,因为可以操作保存在变量中的实际的值。
引用类型的值是保存在内存中的对象,在复制对象时,操作的是对象的引用,添加属性时,操作的是实际的对象。

1.动态的属性

对于引用类型值,可以添加属性和方法,也可以改变和删除属性和方法。

    var person = new Object();
    person.name = "Nicholas";
    alert(person.name);

以上的代码中给对象添加了name属性,只要对象不被销毁,或者属性不删除,那么这个属性将一直存在。
而给一个基本类型值添加属性,尽管没有错误发生,但是访问属性时是不存在的。

    var name = "Nicholas";
    name.age = 25;
    alert(name.age);//undefined
说明只能给引用类型值动态的添加属性,以便将来使用。

2.复制变量值

除了保存的方式不同,在复制基本类型值和引用类型值时,也存在不同。

    var num1 = 5;
    var num2 = num1;

num1和num2的5是相对独立的,该值只是num1中5的副本而已,此后两个变量可以参与任何操作而不会相互影响。比如直接更改num2的值,并不会引起num1的变化。

当复制引用类型值时,复制值的同时,还会将两个变量引用同一个对象,更改其中一个,另一个就会发生变化。

    var obj1 = new Object();
    obj1.name = "Nicholas";
    var obj2 = obj1;
    obj2.name = "Greg";
    alert(obj1.name);//"Greg"

3.传递参数

在函数的内部,参数是按值传递的。原因如下:
1.向函数传递基本类型值时,函数内部的变化并不会反映到外部的全局变量上。

    function add(num){
        num += 10;
        return num;
    }
    var count = 10;
    var result = add(count);
    alert(result);//20
    alert(count);//10

按以上的代码执行,如果参数是按引用传递,那么传递进add函数内的count变为了20,那么外面的count也会变为20。显然naive。

2.向函数传递引用类型值时:

    function setName(obj){
        obj.name = "Nicholas";
    }
    var person = new Object();
    setName(person);
    alert(person.name);//"Nicholas"

在这个例子中,对象传入函数中,obj与这个person对象引用的是同一个对象,所以即使参数是按值传递的,到最后依然会按引用访问同一个对象。因此person会有name属性。而如果这样修改函数:

    function setName(obj){
        obj.name = "Nicholas";
        obj = new Object();
        obj.name = "Greg";
    }
    var person = new Object();
    setName(person);
    alert(person.name);//"Nicholas"

如果参数是按引用传递,那么在函数内部obj被修改,得到了新属性,person也应该发生变化,但是其依然为原属性。说明其原始引用未改变。在函数内部重写obj时,这个变量引用已经是局部对象了,在函数执行完毕后立即被销毁。

2.执行环境及作用域

执行环境定义了所有的变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境中都有一个与之关联的变量对象,环境中定义的所有变量和函数都保留在这个对象中。

全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境被认为是window对象,所有的全局对象和全局函数都保存在window对象下,随着代码执行完毕,执行环境里的函数和变量都会被销毁,而全局执行环境知道关闭网页或浏览器才会被销毁。

执行流机制:

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境会被推入环境栈中,当函数执行之后,环境栈将其推出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会给变量对象创建一个作用域链,作用域链的用途是保证环境内的函数和变量的有序访问。标识符解析是沿着作用域链一步一步搜索的过程。从作用域链的前端一直到全局执行环境的变量对象。

1.延长作用域链

虽然执行环境只有全局和局部(函数),但还是有办法延长作用域链,有些语句可以在作用域的前端临时添加一个变量对象,该变量在代码执行后被移除。在以下两种语句中会出现情况:

  • try-catch语句的catch块;
  • with语句;

对于try-catch来说,会创建一个新的变量对象,包含的是被抛出的错误对象的声明。而在with语句中,会添加指定的对象到作用域链中。

    function BuildUrl(){
        var qs = "?debug=true";
        with(location){
            var url = href + qs;
        }
        return url;
    }

对于这个函数,在with语句中,可以将location对象延长到作用域链中,因此href是在location中找到的,然后再函数内部找到了qs,最后url作为函数执行环境中的一部分被返回。

Bug:
    在IE8之前的版本中,catch块里的错误声明会被添加到执行环境的变量对象中,所以在catch块的外部也能访问错误对象。

2.没有块级作用域

对于ES5来说,for循环中的变量即使在循环结束后,依然存在于循环外的执行环境中。

    for(var i = 0;i<10;i++){
        console.log(i);
    }
    alert(i);//10

ES6中将var改为let或const就可以使{}内变为块级作用域。

1.声明变量

使用var声明会自动被添加到最接近的环境中,在函数内部,最接近的环境就是函数的局部环境;在with语句中,最接近的环境就是函数环境。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境。

2.查询标识符

当某个环境中为了读取或写入而引用一个标志符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链前端开始m向上逐渐查询,如果在局部环境找到了,则停止,否则一直追到全局对象。

3.垃圾收集

JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。

1.标记清除

最常用的垃圾收集方式。当变量进入环境,就将这个变量标记为"进入环境",当变量离开环境时,则将其标记为"离开环境"。

垃圾收集器会在运行时给储存在内存中的所有变量都打上标记。然后去掉环境中的变量,以及被环境中的变量引用的变量的标记。在此之后再被标记的变量则是为准备删除的变量,最后完成内存清除工作,销毁带标记的值,并释放占用的内存空间。

2.引用计数

引用计数的含义是追踪每个值被引用的次数,当为0时则被清除。但是如果有循环引用的情况时,则永远无法清除。将变量设置为null,意味着切断变量与它此前引用的值的连接。下次运行垃圾收集器是,就会删除这些值并回收它们占用的内存。

3.性能问题

IE的垃圾收集器是根据内存分配量运行的,具体就是256个变量、4096个对象字面量或者64KB字符串,达到这个临界值就会运行。而有这么可观数量的脚本,可能在生命周期内一直都会保存这么多变量。垃圾收集器就会频繁的运行,导致性能降低。

IE7之后的版本改变了工作方式,临界值被调整为动态修正。当回收过程的内存分配量低于15%时,临界值就会加倍。如果回收历程回收了85%的内存分配量,临界值回到默认值。

4.管理内存

一旦数据不再有用,最好通过其值设置为null来释放其引用,即解除引用。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。