按照ECMA-262的定义,JavaScript的变量与其他语言的变量有很大区别。JavaScript变量松散类型的本质,决定了它只是在特定时间用于保存特定值的一个名字而已。变量的值及其数据类型可以在脚本的生命周期内改变。

4.1 基本类型和引用类型的值

ECMAScript变量可能包含两种不同数据类型的值:基本类型值引用类型值。基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。

在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。

引用类型的值是保存在内存的对象。与其他语言不同,JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。

4.1.1 动态的属性

对于引用类型的值,我们可以为其添加舒心和方法,也可以改变和删除其属性和方法。如下:

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

但是,我们不能给基本类型的值添加属性,尽管这样做不会导致任何错误。比如:

    var name = "Nicholas";
    name.age = 27;
    alert(name.age);//undefined

我们通过结果就能看出只能给引用类型值动态地添加属性,以便将来使用。

4.1.2 复制变量值

除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。

    var num1 = 5;
    var num2 = num1;

num2中的5和num1中的5是完全独立的,该值只是num1中5的一个副本。此后,这两个变量可以参与任何操作而不会互相影响。

当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中额值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。

4.1.3 传递参数

有不少开发人员在这一点上可能会感到困惑,因为访问变量有按值和按引用两种方式,而参数只能按值传递

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。

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

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

显示的值仍然是”Nicholas”。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写obj时,这个变量引用过的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。

可以把ECMAScript函数的参数想象成局部变量

4.1.4 检测类型

要检测一个变量是不是基本数据类型,第三章介绍的typeof操作符时最佳的工具,但在检测引用类型的值时,这个操作符的用处不大。

根据规定,所有引用类型的值都是Object的实例。因此,在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true。当然,如果使用instanceof操作符检测基本类型的值,则该操作符始终会返回false,因为基本类型不是对象。

4.2 执行环境及作用域

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

每个函数都有自己的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

在局部作用域中定义的变量可以在局部环境中与全局变量互换使用。

    var color = "blue";
    function changeColor(){
        var anotherColor = "red";
        function swapColor(){
            var tempColor = "anotherColor";
            anotherColor = color;
            color = tempColor;
            //这里可以访问color,anotherColor,tempColor
        }
        //这里可以访问color,anotherColor,但不能访问tempColor
        swapColor();
    }
    //这里只能访问color
    changeColor();

swapColor()的局部环境中有一个变量tempColor,该变量只能在这个环境中访问到。无论全局环境还是changeColor()的局部环境都无权访问tempColor。然而,在swapColors()内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性,有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。

如果在自己的变量对象中搜索不到变量和函数名则再搜索上一级作用域链。

函数参数也被当做变量来对待,因此其访问规则与执行环境中的其他变量相同。

延长作用域链

虽然执行环境的类型总共只有两种——全局和局部(函数),但还是有其他办法来延长作用域链。这么说是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。在两种情况下会发生这种现象:

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

当在with语句中引用变量href时(实际引用的是location.href),可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在buildUrl()中定义的那个变量,而该变量位于函数环境的变量对象中。至于with语句内部,则定义了一个名为url的变量,而url就成了函数执行环境的一部分,所以可以作为函数的值被返回。

4.2.2 没有块级作用域

JavaScript没有块级作用域经常会导致理解上的困惑。

    if(true){
    var color = "blue";
    }
    alert(color);

这里是在一个if语句中定义了变量color。如果是在C,C++或Java中,color会在if语句执行完毕后被销毁。但在JavaScript中,if语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中。在使用for语句时尤其要牢记这一差异,例如:

for(var i=0; i<10;i++){
    doSomething(i);
}
alert(i);//10

对于有块级作用域的语言来说,for语句初始化变量的表达式所定义的变量,只会存在于循环的环境之中。而对于JavaScript来说,由for语句创建的变量i即使在for循环执行结束后,也依旧会存在于循环外部的执行环境中。

1.声明变量

如果初始化变量时没有使用var声明,该变量会被添加到全局环境。关于JavaScript中定义全局变量时有var和没有var有什么区别呢?

通过var声明的全局变量无法使用delete删除;

还有的区别是:

示例代码如下:

alert(a);var a = 2; //undefined

alert(a);a = 2; //ReferenceError: a is not defined

从上面的两段代码分析:

var声明的全局变量在javascript代码中会”代码提升”(hoisting), var a = 2被分解为var a; a = 2; 代码parse(解析)的时候var a;会被提升到代码的最前面 ,这个时候 a 为 undefined。在执行到 a=2;的时候再对a 赋值。第二段代码变量没有使用var声明,没有提升,在alert(a)的时候a没有被定义。

javascript定义全局变量的时候有var和没有var的区别

我们建议在初始化变量之前,一定要先声明。在严格模式下,初始化未经声明的变量会导致错误。

2.查询标识符

换句话说,如果局部环境中存在着同名标识符,就不会使用位于父环境中的标识符。

变量查询也不是没有代价的。很明显,访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链。

4.3 垃圾收集

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

这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。

4.3.1 标记清除

JavaScript中最常用的垃圾收集方式是标记清除。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存时间。

4.3.2 引用计数

另一种不太常见的垃圾收集策略叫做引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

将变量设置为null以为这切断变量与它之前引用的值之间的连接。

4.3.3 性能问题

说到垃圾收集器多长时间运行一次,不仅让人联想到IE因此声名狼藉的性能问题。

IE的垃圾收集器是根据内存分配量运行的,有时垃圾收集器就不得不频繁地运行。结果,由此引发的严重性能问题促使IE7重写了其垃圾收集例程。改为临界值被调整为动态修正。

4.3.4 管理内存

JavaScript在运行内存管理及垃圾收集时面临的问题还是有点与众不同。其中最主要的一个问题就是分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少。目的是出于安全方面的考虑,放置运行JavaScript的网页耗尽全部系统内存而导致系统崩溃。

优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——这个做法叫做解除引用。局部变量会在它们离开执行环境时自动被解除引用。

function createPerson(name){
    var localPerson = new Object;
    localPerson.name = name;
    return localPerson;
}

var globalPerson = createPerson("Nicholas");
//手工解除globalPerson的引用
globalPerson = null;

由于localPerson在createPerson()函数执行完毕后就离开了其执行环境,因此无需我们显示地去为它解除引用。但是对于全局变量globalPerson而言,则需要我们在不使用它的时候手工为它解除引用。

不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

4.4 小结