《JavaScript高级程序设计》 - 读书笔记 - 第4章 变量、作用域和内存问题

时间:2022-04-08 06:44:37

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

JavaScript变量是松散类型的,它只是保存特定值的一个名字而已。

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

在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值;而引用类型的值是保存在内存中的对象,JavaScript不允许直接访问内存中的位置,所以在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值是按引用访问的。

定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。但是对值可以进行的操作却不同,只能对引用类型值动态添加、改变和删除属性和方法。

复制不同类型变量值的时候,也是不同的。
对于基本类型值,创建了新值然后复制到新变量。对于引用类型值,也创建了新值然后复制到新变量。不同的是,这个值实际上是一个“指针”,而这个指针指向存储在堆中的一个对象。两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响到另一个变量的使用。

ECMAScript中所有函数的参数都是按值传递的。也就是说,把函数外的值复制给函数内部的参数。
基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。

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

示例:函数传递引用类型值

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

分析:创建了一个对象,并将其保存在了变量person中。然后,这个变量被传递到setName()函数中之后就被复制给了obj。obj和person引用的是同一个对象。

有的开发人员错误地认为:在局部作用域内修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。下面的例子是为了证明对象是按值传递的。

示例2:证明对象是按值传递的

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

分析:如果person是按引用传递的,那么person就会自动被修改为指向新的对象,其name属性为"Greg"。但是,当接下来再访问person.name时,显示的值仍然是"Nicholas"。这说明,即使在函数内部修改了参数的值,但原始的引用仍然保持未变。

检测变量是不是一个基本数据类型,typeof操作符是最佳的工具。

 var s = "Nicholas";
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object;
alert(typeof s); // string
alert(typeof b); // boolean
alert(typeof i); // number
alert(typeof u); // undefined
alert(typeof n); // object
alert(typeof o); // object

但是在检测引用类型的值时,这个操作符的用处不大。通常,我们并不想知道某个值是对象,而是想知道它是什么类型的对象。为此,ECMAScript提供的instanceof操作符。其语法为:

 result = variable instanceof constructor

4.2 执行环境及作用域

执行环境(execution context)是 JavaScript 中最为重要的一个概念。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。

执行环境有两种:全局环境和局部环境(函数)。
全局环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象。每个函数都有自己的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境中所有变量和函数的有序访问。
作用域链的前端,始终是当前代码所在环境的变量对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境。作用域链的最后始终是全局执行环境的变量对象。

标识符解析是沿着作用域链一级一级搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

示例3:

 var color = "blue";

 function changeColor(){
if(color == "blue"){
color = "red";
} else {
color = "blue";
}
} changeColor();
alert("Color is now " + color);

分析:函数changeColor()包含两个变量对象:它自己的变量对象和全局环境的变量对象。

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

虽然只有两种执行环境——全局和局部(函数),但有两个语句可以延长作用域链:try-catch语句的catch块和with语句。这两个语句都会在作用域链前端临时添加一个变量对象。
对with语句来说,会将指定的对象添加到作用域链中。对于catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

示例4:

 function builderUrl(){
var qs = "Hello world!"; with(location){
var url = href + qs;
} return url;
}

分析:with语句在作用域链的前端添加了一个变量对象,此变量对象中包含了location对象的所有属性和方法。当在with语句中引用变量href时(实际引用的是location.href),可以在当前执行环境的变量对象中找到。当引用变量qs时,该变量在函数环境的变量对象中。而在with语句内部定义的变量url,则是函数执行环境的一部分。

JavaScript 没有块作用域。使用for语句时要牢记这一点,for语句创建的变量i即使在for循环执行结束后,也依旧会存在于循环外部的执行环境中。

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

在JavaScript中,不声明而直接初始化变量是一个常见的错误做法,这很可能会导致意外。建议在初始化变量之前,一定要先声明。

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

4.3 垃圾收集

JavaScript具有自动垃圾收集机制。执行环境会负责管理代码执行过程中使用的内存。
垃圾收集机制的原理很简单:找出那些不再使用继续使用的变量,然后释放其占用的内存。垃圾收集器需要定期执行该操作。

函数中的局部变量,在函数执行完后,就没有存在的必要了,所以垃圾收集器很容易判断是否回收该变量。但对于其他情况就不那么容易判断了。

JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep)。目前,IE、Firefox、Opera、Chrome、和Safari和JavaScript实现使用的都是标记清除式的垃圾收集策略(或类似的策略)。

标记清除策略:当变量进入环境时,就将这个变量标记为“进入环境”。而当变量离开环境时,则将其标记为“离开环境”。

可以使用任何方式来标记变量。比如通过记录某个位的状态,或者使用一个“进入环境”的变量列表及一个“离开环境”的变量列表来跟踪哪些变量发生了变化。

垃圾收集器运行时会给内存中所有变量加上标记;然后,为环境中的变量和被环境引用的变量去除标记。之后,剩余的变量被认为是要删除的变量。最后,垃圾收集器完成内存清除工作。

另一种不太常见的垃圾收集策略叫做引用记数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。但这种方式存在一个“循环引用”的严重问题。

Netscape Navigator 3.0 是最早使用引用计数策略的浏览器,因为“循环引用”的问题,Netscape Navigator 4.0中放弃了引用记数方式,转而采用标记清除方式。

IE的BOM和DOM中的对象就是使用C++以COM对象的形式实现的。而COM对象的垃圾收集机制采用的就是引用记数策略。因此,只要在IE中涉及COM对象,就会存在循环引用的问题。
IE9把BOM和DOM对象都转换成了真正的JavaScript对象。这样,就消除了常见的内存泄漏现象。

确定垃圾收集的时间间隔是一个非常重要的问题。会影响JavaScript的性能。

JavaScript在进行内存管理及垃圾收集时面临的问题与一般程序有点不同,主要就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的是为了安全考虑,防止运行JavaScript的网页耗尽系统内存而导致系统崩溃。

因此,要确保占用最少的内存可以让页面获得更好的性能。而优化内存的最好的方式,就是只保存必要数据。一旦数据不再有用,最好通过将其值设置为null的方式来释放其引用,这个方法叫解除引用。解除引用的作用在于让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
解除引用适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。

示例5:

 function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
} var globalPerson = createPerson("Nicholas"); globalPerson = null

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