4.1 基本类型和引用类型的值
ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值则是指那些保存在堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。
在将一个值赋给变量时,解析器必须确定这个值是基本类型值,还是引用类型值。第3章讨论了5种基本数据类型:Undefined、Null、Boolean、Number 和 String。这5种基本数据类型的值在内存中分别占有固定大小的空间,因此可以把它们的值保存在栈内存中。而且,这样也可以提高查询变量的速度。对于保存基本类型的变量,我们说它们是按值访问的,因为我们操作的是它们实际保存的值。
在某些语言中,字符串以对象的形式来表示,因此被认为是引用类型的,ECMAScript 放弃了这一传统。
如果赋给变量的是一个引用类型的值,则必须在堆内存中为这个值分配空间。由于这种值的大小不固定,因此不能把它们保存到栈内存中。但内存地址的大小是固定的,因此可以将内存地址保存在栈内存中。这样,当查询引用类型的变量时,就可以首先从栈中读取内存地址,然后再 "顺藤摸瓜" 地找到保存在堆中的值。对于这种查询变量值的方式,我们把它们叫做按引用访问,因为我们操作的不是实际的值,而是被那个值所引用的对象。下图形象地说明了如何在内存中保存这两种不同数据类型的值。
图中展示了一些保存在栈内存中的基本类型值。保存在栈内存中的每个值,分别占据着固定大小的空间,可以按照顺序来访问它们。如果栈内存中保存的是一块内存的地址,则这个值就像是一个指向对象在对内存中位置的指针。保存在堆内存中的数据不是按照顺序访问的,因为每个对象所需要的内存空间并不相等。
4.1.1 动态属性
定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。但是,当这个值保存到变量中以后,对不同类型值可以执行的操作则大相径庭。对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。请看下面的例子:
var person = new Object();
person.name = "Nicholas";
alert(person.name); // “Nicholas”
以上代码创建了一个对象并将其保存在了变量 person 中。然后,我们为该对象添加了一个名为 name 的属性,并将字符串值 "Nicholas" 赋给了这个属性。如果对象不被销毁或者这个属性不被删除,则这个属性将一直存在。
但是,我们不能给基本类型的值添加属性,尽管这样做不会导致任何错误。比如:
var name = "Nicholas";;
name.age = 27;
alert(name.age); // undefined
在这个例子中,我们为字符串 name 定义了一个名为 age 的属性,并为该属性赋值 27 。但在下一行访问这个属性时,发现该属性不见了。这说明只能给引用类型值动态地添加属性,以便将来使用。
4.1.2 复制变量值
除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。如果从一个变量向另一个变量复制基本类型的值,会在栈中创建一个新值,然后把该值复制到为新变量分配的位置上。来看一个例子:
var num1 = 5;
var num2 = num1;
在此,num1 中保存的值是 5 。当使用 num1 的值来初始化 num2 时,num2 中也保存了值 5 。但 num2 中的 5 与 num1 中的 5 是完全独立的,该值只是 num1 中 5 的一个副本。此后,这两个变量可以参与任何操作而不会相互影响。下图形象地展示了复制基本类型值的过程。
当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在栈中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量,如下面的例子所示:
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); // "Nicholas"
首先,变量 obj1 保存了一个对象的新实例。然后,这个值被复制到了 obj2 中;换句话说,obj1 和 obj2 都指向同一个对象。这样,当为 obj1 添加 name 属性后,可以通过 obj2 来访问这个属性 -- 因为这两个变量引用的都是同一个对象。下图展示了保存在栈中的变量和保存在堆中的对象之间的这种关系。
4.1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。有不少开发人员在这一点上可能会感到困惑,因为访问变量有按值和按引用两种方式,而参数只能按值传递。
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量 (即命名参数,或者用 ECMAScript 的概念来说,就是 arguments 对象中的一个元素) 。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。请看下面这个例子:
function addTen (num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
alert(count); // 20, 没有变化
alert(result); // 30
这里的函数 addTen() 有一个参数 num ,而参数实际上是函数的局部变量。在调用这个函数时,变量 count 作为参数被传递给函数,这个变量的值是 20 。于是,数值 20 被复制给参数 num 以便在 addTen () 中使用。在函数内部,参数 num 的值被加上了 10 ,但这一变化不会影响函数外部的 count 变量。参数 num 与变量 count 互不相识,它们仅仅是具有相同的值。假如 num 是按引用传递的话,那么变量 count 的值也将变成 30,从而反映函数内部的修改。当然,使用数值等基本类型值来说明按值传递参数比较简单,但如果使用对象,那问题就不怎么好理解了。再举一个例子:
function setName (obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); // "Nicholas"
以上代码中创建一个对象,并将其保存在了变量 person 中。然后,这个对象被传递到 setName() 函数中之后就被复制给了 obj 。在这个函数内部, obj 和 person 引用的是同一个对象。换句话说,即使这个对象是按值传递的,obj 也会按引用来访问同一个对象。于是,当在函数内部为 obj 添加 name 属性后,函数外部的 person 也将有所反映;因为 person 指向的对象在堆内存中只有一个,而且是全局对象。有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。为了证明对象是按值传递的,我们再看一看下面这个经过修改的例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); // "Nicholas"
这个例子与前一个例子的唯一区别,就是在 setName()函数中添加了两行代码:一行代码为 obj 重新定义了一个对象,另一行代码为该对象定义了一个带有不同值的 name 属性。在把 person 传递给 setName() 后,其 name 属性被设置为 "Nicholas" 。然后,又将一个新对象赋给变量 obj ,同时将其 name 属性设置为 "Greg" 。如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值为 "Greg" 的新对象。但是,当接下再访问 person.name 时,显示的值仍然是 "Nicholas" 。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。
(person 按值传递,就说明,person引用的对象不会改变,开始 obj 与 person 引用同一个对象,obj修改对象,所以 person 会跟着变,但 obj 改变引用以后,person引用的还是原来的对象,这时,obj 与 person 引用的已经不是同一个对象了;如果 person 是按引用传递的,那么 obj 引用的对象变了,person引用的对象就得跟着变 ) 。
可以把 ECMAScript 函数的参数想象成局部变量。
4.1.4 检测类型
要检测一个变量是不是基本数据类型?第3章介绍的 typeof 操作符是最佳工具。说得更具体一点,typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。如果变量的值是一个对象或null ,则 typeof 操作符会像下面例子中所示的那样返回 "object" :
var s = "Nicholas" ;
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object();
alert(typeof s); // string
alert(typeof i); // number
alert(typeof b); // boolean
alert(typeof u); // undefined
alert(typeof n); // object
alert(typeof o); // object
虽然在检测基本数据类型时 typeof 是个非常得力的助手,但在检测引用类型的值时,这个操作符的用处不大。通常,我们并不是想知道某个值是对象,而是想知道它是什么类型的对象。为此,ECMAScript 提供了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 的实例。因此,在检测一个引用类型值和 Object 构造函数时, instanceof 操作符始终会返回 true 。当然,如果使用 instanceof 操作符检测基本类型的值,则该操作符始终会返回 false ,因为基本类型不是对象。
使用 typeof 操作符检测函数时,该操作符会返回 "function" 。在 Safari 和 Chrome 中使用 typeof 检测正则表达式时,这个操作符会错误地也返回 "function" 。