首页 > 代码库 > JS基础知识回顾:变量、作用域和内存问题

JS基础知识回顾:变量、作用域和内存问题

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。

基本类型值指的是简单的数据段,而引用类型值指的是那些可能由多个值构成的对象。

 

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

在操作对象时,实际上是在操作对象的引用而不是实际的对象。

在很多语言中,字符串以对象的形式来表示,因此被认为是引用类型的,ECMAScript放弃了这一传统。

 

定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。

但是,当这个值保存到变量当中之后,对于不同类型的值可以执行的操作却大相径庭。

 

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

例如:var person=new Object();person.name="Name";alert(person.name);//"Name"(如果对象不被销毁或者这个属性不被删除,则这个属性将一直存在)

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

例如:var name="Name";name.age=27;alert(name.age);//undefined

 

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

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到新变量分配的位置上。

例如:var num1=5;var num2=num1;//尽管此时num1和num2中保存的值都是5,但是两个变量是完全独立的,接下来可以参与任何操作而不互相影响

如果从一个变量向另一个变量复制引用类型的值,也会将存储在变量中的值复制一份放到为新变量分配的空间中,只不过这个值的副本实际上是一个指针,复制结束后,两个变量实际上将引用同一个对象,改变其中一个,就会影响到另外一个。

例如:var obj1=new Object();var obj2=obj1;obj1.name="Name";alert(obj2.name);//"Name"(因为二者指向同一个对象,所以改变一个会影响另外一个)

 

尽管ECMAScript中访问对象有按值访问和按引用访问两种,但是所有函数的参数都是按值传递的。

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象的一个元素)。

在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反应在函数的外部。

可以把ECMAScript函数的参数想象成局部变量,很多人错误的一位在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的,下面这个例子可以很好的证明这个想法是错误的:

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

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

即使在函数内部修改了参数的值,但原始的引用仍然保持不变。

实际上,在函数内部重写obj时,这个变量引用的就是一个局部对象了,而这个局部对象会在函数执行完毕后立即被销毁。

 

在检测基本数据类型时typeof是非常得力的助手,但由于它只能检测出对象而不能检测出对象的类型,因此它在检测引用类型的值是作用不大。

为此ECMAScript提供了instanceof操作符:result=variable instanceof constructor

如果变量是给定引用类型的实例,那么instanceof操作符会返回true。

所有引用类型的值都是Object的实例,因此,在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true。

当然,如果使用instanceof操作符检测基本类型的值,该操作符始终会返回false,因为基本类型不是对象。

 

执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

每个执行环境都有一个与之相关的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象当中。

执行环境分为全局(最外围的执行环境)和局部(每个函数各自的执行环境)两种。

当执行流进入一个函数时,函数的环境就会被推入一个环境栈当中,而在函数执行完成后,栈将其环境弹出,把控制权返回给之前的执行环境。

某个执行环境中的所有代码被执行完成后,该环境被销毁,保存在其中的变量和函数定义也随之销毁。

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

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

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

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

 

在执行流进入下列两种语句中时,作用域链会得到加长:try-catch语句的catch块,with语句。

因为这个种语句都会在作用域链的前端添加一个变量对象。

对于with语句来说,会将指定的对象添加到作用域链中。

对于catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

在IE8及之前版本的JavaScript实现中,存在一个与标准不一致的地方,即在catch语句中捕获的错误对象会被添加到执行环境的变量对象,而不是catch语句的变量对象中,所以在catch块的外部也可以访问到错误对象。IE9修复了这个问题。

 

JavaScript中没有块级作用域的概念,所以在循环语句中定义的变量在循环语句的块级作用域结束之后并不会被销毁,其中的变量可以在其所属的函数的执行环境中被访问。

使用var声明的变量会自动被添加到最接近的环境中,在函数内部,最近接的环境就是函数的局部环境;

如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境,但由于这种做法可能导致意想不到的错误且为调试造成麻烦,所以并不推荐使用。

当在某个环境中读取或写入一个标识符时,必须通过搜索来确定该标识符实际代表什么。

搜索过程由作用域链的前端开始,向上逐级查询与给定名字匹配的标识符,如果在局部环境中找到了标识符,搜索停止变量就绪,如果未找到则会一直向上追溯到全局环境的变量对象,如果在全局环境中仍未找到,则意味着该变量尚未声明。

在这个搜索过程中,如果存在一个局部的变量的定义,则搜索会自动停止,不再进入另一个变量对象,所以,如果局部环境中存在着同名标识符,就不会使用位于父环境中的标识符。

变量查询也是有代价的,访问局部变量明显要比访问全局变量速度更快,因为不同向上搜索作用域链,不过由于JavaScript引擎在查询标识符方面一直在不断的优化,这个差别在未来应该就可以忽略不计了。

 

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

具体到浏览器中的实现,则通常有标记清除(mark-and-sweep)和引用计数(reference counting)两种策略。

标记清除是JavaScript中最常用的垃圾收集方式:当变量进入环境时,就将这个变量标记为”进入环境“,而当变量离开环境时,则将其标记为”离开环境“,最后垃圾收集器完成内存清除工作,销毁那些带有”离开环境“标记的值并收回它们所占用的内存空间。

到2008年为止,IE、Firefox、Opera、Chrome、Safari的JavaScript实现使用的都是标记清除式的垃圾收集策略,只是垃圾收集的事件间隔互不相同。

另外一种不太常见的垃圾收集策略叫做引用计数:当声明一个变量并讲一个引用类型值赋给该变量时,这个值的引用次数被记做1,如果同一个值又被赋给其他变量,则该值的引用次数加1,相反,如果包含对这个值引用的变量又取得了另外一个值,那么这个值的引用次数减1,当这个值的引用次数变为0时,则说明无法再访问这个值了,此时就可以将其占用的内存空间收回。

可是在出现循环引用的时候,这种垃圾收集机制显然就会出现问题,Netscape3是最早引用计数策略的浏览器,不过由于这样的问题它在4.0中放弃了该方式。

另外,IE中有一部分对象并不是原生JavaScript对象,而是用COM对象的形式实现的,而COM对象的垃圾收集机制采用的就是引入计数策略,所以只要IE涉及COM对象就会存在循环引用的问题,为了避免这样的问题,最好是在不使用它们的时候手动断开原生JavaScript对象与DOM元素之间的连接(将变量设置为null就意味着切断变量与它们此前引用的值之间的连接)。

为了解决上述问题,IE9把BOM和DOM对象都转换成了真正的JavaScript对象,这样就避免了两种垃圾收集策略并行导致的问题,也消除了常见的内存泄露现象。

 

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。

IE的垃圾收集器是根据内存分配量运行的,这种实现方式的问题在于,如果一个脚本包含一定个数的变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量,这样一来就迫使垃圾收集器不得不频繁的运行,由此引发的严重性能问题促使IE7重写了其垃圾收集历程。

事实上,在有的浏览器中可以触发垃圾收集过程:IE中调用window.CollectGarbage()方法,Opera7及更高版本中调用window.opera.collect()方法,但是并不建议这样做。

 

使用具备垃圾收集机制的语言编写程序,一般不必担心内存管理的问题,但是由于分配给WEB浏览器的可用内存数量通常要比分配给桌面应用程序的少,所以对程序进行内存管理也是必不可少的。

而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据,一旦数据不再有用,最好通过将其值设置为null来释放其引用(解除引用 dereferencing),这一做法适用于大多数全局变量和全局对象的属性。

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

其中分配给WEB浏览器的可用内存数量较少的原因是要处于安全考虑,防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃。