目录

第四章 - 变量、作用域和内存问题

本章内容

  • 理解基本类型和引用类型的值
  • 理解执行环境
  • 理解垃圾收集

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

ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。 基本数据类型:Undefined、Null、Boolean、Number 和 String。这 5 种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。 引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。引用类型的值是按引用访问的(但在为对象添加属性时,操作的是实际的对象)。

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

4.1.1 动态的属性

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

4.1.2 复制变量值

对于基本数据类型,如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值。 对于引用类型,复制引用类型的值时,这个值的副本实际上是一个指针

4.1.3 传递参数

ECMAScript 中所有函数的参数都是按值传递的。对于基本数据类型和引用类型,分别按照4.1.2的规则来(这在很多编程语言上有所体现)。 在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数arguments对象里的一个元素)。 在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量(因此不是按引用传递,而是按值传递)。 看这个例子来说明是按值传递的:

function setName(obj) { 
    obj.name = "Nicholas"; 
    obj = new Object(); //函数执行完之后这个obj还会被立即销毁
    obj.name = "Greg"; 
} 
 
var person = new Object(); 
setName(person); 
alert(person.name);    //"Nicholas"

4.1.4 检测类型

typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。 检测是什么类型的对象使用instanceof 操作符。其语法如下所示:

result = variable instanceof constructor 

补充一下:用variable.__proto__也是能查看的。

4.2 执行环境及作用域

执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。 全局执行环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象(第 7 章将详细讨论),因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。 当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的变量对象由内逐渐延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象。 举例说明(很多编程语言都有这个概念,即从局部变量、全局变量来理解):

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

4.2.1 延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。当执行流进入下列任何一个语句时,作用域链就会得到加长:

  • try-catch 语句的 catch 块;
  • with 语句。 对 with 语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。 看个例子:
function buildUrl() { 
    var qs = "?debug=true"; 
 
    with(location){ 
        var url = href + qs;         
    } 
 
    return url; 
}

其实就相当于(with的写法仅仅为了方便而已):

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

4.2.2 没有块级作用域

看个例子:

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

在其它语言中,color 会在 if 语句执行完毕后被销毁。但在 JS 中,if 语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中。在使用 for 语句时尤其要牢记这一差异(函数内不会,因为函数有自己的局部环境)(这将在ES6使用let从而改善)。

  • 声明变量 使用 var 声明的变量会自动被添加到最接近的环境中。如果初始化变量时没有使用 var 声明,该变量会自动被添加到全局环境:
function add(num1, num2) { 
    sum = num1 + num2; 
    return sum; 
} 
 
var result = add(10, 20);  //30 
alert(sum); //30

一定记得要声明。不声明而直接初始化变量是一个常见的错误做法,因为这样可能会导致意外。

  • 查询标识符 当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。

/images/%E9%AB%98%E7%BA%A7%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/search_path.png

4.3 垃圾收集

垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。 用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。

4.3.1 标记清除

JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。而当变量离开环境时,则将其标记为“离开环境”。 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 可以使用任何方式来标记变量,如何标记变量其实并不重要,关键在于采取什么策略。

4.3.2 引用计数

另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。一个严重的问题:循环引用。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。 看例子:

function problem(){ 
    var objectA = new Object(); 
    var objectB = new Object(); 
 
    objectA.someOtherObject = objectB; 
    objectB.anotherObject = objectA; 
} 

函数执行完毕后,objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0。 所以现在应该都是用“标记清除”的策略了吧。

4.3.3 性能问题

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。此前IE的垃圾收集器根据内存分配量来运行的,达到某个临界值垃圾收集器就会运行,若是某个脚本在其生命周期中一直保有那么多的变量,垃圾收集器就会频繁运行造成严重性能问题。后来临界值被调整为动态修正,改善了性能。

4.3.4 管理内存

一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。 不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。