4.1 原始值与引用值
ECMAScript 变量包含两种数据:原始值和引用值。
- 原始值(Primitive Value):简单的数据,包括 Undefined、Null、Boolean、Number、String 和 Symbol。操作原始值时,我们直接操作存储在变量中的实际值,因此是按值(by value)访问的。
- 引用值(Reference Value):保存在内存中的对象。JavaScript 操作对象时,实际上操作的是对该对象的引用,而非对象本身。因此,保存引用值的变量是按引用(by reference)访问的。
4.1.1 动态属性
在JavaScript中,原始值和引用值在定义后展现出了不同的行为特性,特别是在添加属性方面。
- 原始值(如字符串、数字等)在创建后是不可变的,且不能添加属性。例如:
let name = "Nicholas";
name.age = 27; // 尝试添加属性,但不会生效
console.log(name.age); // 输出 undefined,因为原始值不能添加属性
- 引用值(如对象)则是可变的,并且可以随时添加、修改或删除其属性和方法。例如:
let person = {}; // 创建一个空对象
person.name = "Nicholas"; // 添加属性
console.log(person.name); // 输出 "Nicholas",属性添加成功
需要注意的是,尽管使用new关键字配合原始类型的构造函数(如String、Number等)可以创建出看似原始值的对象,但这些对象实际上是引用类型,可以添加属性。例如:
let name1 = "Nicholas"; // 使用字面量创建原始值字符串
let name2 = new String("Matt"); // 使用String构造函数创建对象
name1.age = 27; // 尝试给原始值添加属性,不会生效
name2.age = 26; // 给对象添加属性,会生效
console.log(name1.age); // 输出 undefined
console.log(name2.age); // 输出 26
console.log(typeof name1); // 输出 "string",表明name1是原始值
console.log(typeof name2); // 输出 "object",表明name2是对象
在这个例子中,name1是一个原始值字符串,不能添加属性。而name2是一个String对象,可以添加属性并且这些属性是可以访问的。通过typeof操作符可以区分出原始值和对象。
4.1.2 复制值
在JavaScript中,原始值和引用值在复制时的行为是不同的。
- 原始值:当复制一个原始值时,实际上是将该值的一个副本存储在新的变量中。因此,原始值和它的副本是两个独立的存在,修改其中一个不会影响另一个。例如:
let num1 = 5;
let num2 = num1; // num2 得到 num1 中数值 5 的副本
num2 = 10; // 修改 num2 不会影响 num1
console.log(num1); // 输出 5
console.log(num2); // 输出 10
在这个例子中,num1 和 num2 最初都包含数值 5,但随后对 num2 的修改并不会影响 num1,因为 num2 存储的是 5 的一个副本,而不是对 num1 的引用。
这两个变量可以独立使用,互不干扰。这个过程如图所示。
在JavaScript中,引用值复制时并非复制对象本身,而是复制一个指向该对象的指针(或引用),它指向存储在堆内存中的对象。因此,两个变量实际上指向同一个对象,对对象的修改会反映在两个变量上。
let obj1 = new Object(); // obj1 指向一个新对象
let obj2 = obj1; // obj2 复制了 obj1 的指针,现在两者都指向同一个对象
obj1.name = "Nicholas"; // 通过 obj1 修改对象
console.log(obj2.name); // 输出 "Nicholas",因为 obj2 也指向这个被修改的对象
在这个例子中,obj1 和 obj2 都指向同一个新创建的对象。因此,通过 obj1 对对象进行的修改(添加 name 属性)也会通过 obj2 反映出来。这表明 obj1 和 obj2 并不是独立的对象副本,而是指向同一个对象的两个不同引用。
4.1.3 传递参数
在ECMAScript中,函数的参数传递机制是基于“按值传递”(pass by value)的。这一原则对于理解函数如何与变量交互至关重要,尤其是当涉及到原始值和引用值时。
原始值传递:
当传递一个原始值(如数字、字符串、布尔值或null、undefined)给函数时,实际上是将该值的一个副本传递给函数的参数。这意味着,在函数内部对参数的任何修改都不会影响到函数外部的原始变量。因为函数内部操作的是该值的副本,而非原始值本身。
例如:
function addTen(num) {
num += 10;
return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 输出 20,原始值未改变
console.log(result); // 输出 30,函数返回的是修改后的副本
引用值传递(指针副本) :
对于引用值(如对象、数组、函数等),情况就复杂一些。虽然仍然说是“按值传递”,但这里的“值”实际上是指向对象在内存中位置的指针(或引用)。因此,当传递一个对象给函数时,传递的是该指针的一个副本。
在函数内部,通过这个指针副本可以访问并修改原始对象。这些修改会反映到函数外部的原始对象上,因为两者都指向同一个对象。但是,需要注意的是,如果函数内部将参数重新指向一个新的对象(即改变了指针的指向),这个改变不会影响到函数外部的原始对象。函数外部的变量仍然指向原来的对象。
例如:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object(); // 这一行对函数外部的 person 变量没有影响
obj.name = "Greg"; // 这一行同样对函数外部的 person 变量没有影响
}
let person = new Object();
setName(person);
console.log(person.name); // 输出 "Nicholas",原始对象属性已改变
在这个例子中,setName 函数接收一个名为 obj 的参数。当 person 对象作为参数传递给 setName 函数时,传递的是 person 对象在内存中位置的指针(或引用)的一个副本。因此,obj 和 person 在内存中并不是两个独立的对象,而是都指向同一个对象。
在函数内部,通过 obj 可以访问并修改这个对象的属性。因此,当 obj.name 被设置为 "Nicholas" 时,这个改变会反映到函数外部的 person 对象上。
然而,当函数内部执行 obj = new Object(); 时,obj 被重新分配了一个新的对象。这个改变只影响了 obj 这个局部变量,它现在指向了一个新的对象。但原始的 person 变量仍然指向原来的对象,因此这个改变对 person 没有任何影响。
同样地,当函数内部再次修改 obj.name 为 "Greg" 时,这个改变也只影响新的对象,对原始的 person 对象没有任何影响。
4.1.4 确定类型
在JavaScript中,typeof操作符是判断变量是否为原始类型的有效工具,包括字符串、数值、布尔值和undefined。然而,在处理对象或null时,typeof会返回"object",这限制了它在确定具体对象类型方面的作用。以下是一些示例:
let s = "Nicholas";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s); // 输出: string
console.log(typeof i); // 输出: number
console.log(typeof b); // 输出: boolean
console.log(typeof u); // 输出: undefined
console.log(typeof n); // 输出: object (注意: 这是typeof的一个特殊情况)
console.log(typeof o); // 输出: object
为了更精确地确定对象的类型,ECMAScript提供了instanceof操作符。该操作符通过检查对象的原型链来确定对象是否属于特定的构造函数类型。以下是instanceof操作符的使用示例:
// 假设有以下变量定义
let person = new Object();
let colors = new Array();
let pattern = new RegExp();
console.log(person instanceof Object); // 输出: true (因为person是Object的实例)
console.log(colors instanceof Array); // 输出: true (因为colors是Array的实例)
console.log(pattern instanceof RegExp); // 输出: true (因为pattern是RegExp的实例)
// 所有引用值都是Object的实例
console.log(colors instanceof Object); // 输出: true (因为Array是Object的子类型)
console.log(pattern instanceof Object); // 输出: true (因为RegExp也是Object的子类型)
// 原始值不是对象,因此instanceof返回false
console.log(typeof i instanceof Number); // 输出: false (因为typeof i返回"number",而不是一个对象)
console.log(s instanceof String); // 输出: false (因为s是一个字符串原始值,不是一个String对象)
按照定义,所有引用值都是 Object 的实例,因此通过 instanceof 操作符检测任何引用值和Object构造函数都会返回 true。类似地,如果用 instanceof 检测原始值,则始终会返回 false,因为原始值不是对象。