JavaScript高级程序设计:第四章

时间:2023-12-19 12:49:14

变量、作用域和内存问题

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

2.动态的属性:定义一个基本类型值和引用类型值的方法是类似的:创建一个变量并为该变量赋值。但是,当这个值保存到变量中以后,对不同类型值可以执行的操作大相庭径。对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。如下例子:

var  person = new Object();

person.name = “Nicholas”;

alert(person.name);        //“Nicholas”

以上代码创建了一个对象并将其保存在了变量person中。然后,我们又为该对象添加了一个名为name的属性。如果对象不被销毁或者这个属性不被删除,则这个属性一直都在。

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

var  name = “Nicholas”;

name.age = 27;

alert(name.age);      //undefined

3.复制变量值:

当从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。来看一个例子:

var  num1 = 5;

var  num2 = num1;

num1和num2的值都为5,但是它们的操作互相独立。

当从一个变量向另一个变量复制引用类型值的时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量,如下面的例子所示:

var  obj1 = new  Object();

var  obj2 = obj1;

obj1.name = “Nicholas”;

alert(obj2.name);           //“Nicholas”

4.传递参数:

ECMAScript中所有函数的参数都是按值传递的。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的赋值一样。虽然访问变量有按值和按引用两种方式,但是参数只能按值传递。

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

function  addTen(num){

num+=10 ;

return  num ;

}

var  count = 20 ;

var  result = addTen(count);

alert(count);     //20,没有变化

alert(result);     //30

5.检测类型:使用instanceof操作符可以检测类型。

语法如下:

result = variable  instanceof  constructor

如果变量是给定引用类型的实例,instanceof操作符就会返回true。请看下面的例子:

alert(person  instanceof  Object);     //变量person是Object吗?

alert(colors  instanceof  Array);       //变量colors是Array吗?

alert(pattern  instanceof  RegExp);    //变量pattern是RegExp吗?

根据规定,所有引用类型的值都是Object的实例。

6.执行环境及作用域:

(1)执行环境: 执行环境是JavaScript中最为重要的一个概念。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的一个执行环境。全局执行环境直到应用程序退出时才会被销毁。每个函数都有自己的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前段开始,逐级向后回溯直到找到标识符为止。

(2)作用域链:

①延长作用域链:

以下语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。

(a)try-catch语句的catch块;

(b)with语句。

对with语句来说,会将指定的对象添加到作用域链中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。请看下面的例子:

function  buildUrl(){

var  qs = “?debug = true”;

with(location){

var  url = href + qs ;

}

return  url ;

}

在此,with语句接收的是location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域链的前端。buildUrl()函数中定义了一个变量qs。当在with语句中引用变量href时,可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在buildUrl()中定义的那个变量,而该变量位于函数环境的变量对象中。至于with语句内部,则定义了一个名为url的变量,因而url就成了函数执行环境的一部分,所以可以作为函数的值被返回。

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

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

(b)查询标识符:当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿用作用域链向上搜索。搜索过程将一直追溯到全局环境的变量对象。如果在全局环境中也没有找到这个标识符,则意味着该变量未声明。

7.垃圾收集

(1)标记清除:JavaScript中最常用的垃圾收集方式是标记清除。当变量进入环境时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

(2)引用计数:另一种不太常见的垃圾收集策略叫做引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1.相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1.当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

(3)性能问题:垃圾收集是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的,在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。

(4)管理内存:优化占用内存的最佳方式就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用,如下面这个例子:

function  createPerson(name){

var  localPerson = new  Object();

localPerson . name = name;

return  localPerson;

}

var  globalPerson = creatPerson(“Nicholas”);

//手工解除globalPerson的引用

globalPerson = null;

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