相比于其他语言,JavaScript 中的变量可谓独树一帜。正如 ECMA-262 所规定的,JavaScript 变量是松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。由于没有规则定义变量必须包含什么数据类型,变量的值和数据类型在脚本生命期内可以改变。这样的变量很有意思,很强大,当然也有不少问题。
JavaScript 变量可以保存两种类型的值:原始值和引用值。
原始值可能是以下 6 种原始数据类型之 一:Undefined、Null、Boolean、Number、String 和 Symbol。
原始值和引用值有以下特点。
- 原始值
- 大小固定,因此保存在栈内存上。
- 从一个变量到另一个变量复制原始值会创建该值的第二个副本。
- typeof 操作符可以确定值的原始类型。
- 引用值
- 是对象,存储在堆内存上。
- 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。
- 从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
- 而 instanceof 操作符用于确保值的引用类型。
原始值与引用值
ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。
在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值。
有6 种 原始值:Undefined、Null、Boolean、Number、String 和 Symbol。保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。
动态属性
原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大有不同。
对于引用值而言,可以随时添加、修改和删除其属性和方法。比如,看下面的例子:
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
这里,首先创建了一个对象,并把它保存在变量 person 中。然后,给这个对象添加了一个名为name 的属性,并给这个属性赋值了一个字符串"Nicholas"。在此之后,就可以访问这个新属性,直到对象被销毁或属性被显式地删除。
原始值不能有属性,尽管尝试给原始值添加属性不会报错。比如:
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
在此,代码想给字符串 name 定义一个 age 属性并给该属性赋值 27。紧接着在下一行,属性不见了。记住,只有引用值可以动态添加后面可以使用的属性。
注意:原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值。下面来看看这两种初始化方式的差异:
let name1 = "Nicholas";
let name2 = new String("Matt"); name1.age = 27;
name2.age = 26; console.log(name1.age); // undefined
console.log(name2.age); // 26 console.log(typeof name1); // string
console.log(typeof name2); // object
复制值
除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。请看下面的例子:
let num1 = 5;
let num2 = num1;
这里,num1 包含数值 5。当把 num2 初始化为 num1 时,num2 也会得到数值 5。这个值跟存储在 num1 中的 5 是完全独立的,因为它是那个值的副本。 这两个变量可以独立使用,互不干扰。这个过程如下图所示。
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "hello";
console.log(obj2.name); // "hello"
在这个例子中,变量 obj1 保存了一个新对象的实例。然后,这个值被复制到 obj2,此时两个变量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。下图展示了变量与堆内存中对象之间的关系。
时两个变量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。下图展示了变量与堆内存中对象之间的关系。