掌握了值类型和引用类型的区别,你才能真正理解变量、赋值、函数传参等行为。
简单来说:
-
值类型 (Value Types):复制的是值本身。像给朋友一张纸,上面写着 "100",他修改自己的纸,你的纸还是 "100"。
-
引用类型 (Reference Types):复制的是指向数据的地址(引用)。像给朋友一把你家的钥匙,他用钥匙进你家把电视换了,你回家看到的也是新电视。
下面我们来详细分解。
一、两大类型
JavaScript 的所有数据类型都属于这两类之一。
1. 值类型 (基本数据类型 / Primitive Types)
-
包含:
String
,Number
,Boolean
,Undefined
,Null
,Symbol
,BigInt
(共7种) -
特点:
-
存储位置: 它们的值直接存储在栈 (Stack)内存中。栈内存空间小,但访问速度快。
-
不可变性 (Immutability): 你不能改变一个基本类型的值。例如,你不能改变数字
5
本身,或者字符串"hello"
的某个字符。所有看起来像修改的操作,实际上都是创建了一个新的值。 -
赋值/复制: 当你把一个值类型的变量赋给另一个变量时,计算机会为新变量分配一个全新的内存空间,然后把原始值完整地复制一份过来。这两个变量从此完全独立,互不影响。
-
代码示例:
let a = 100;
let b = a; // 复制 a 的值 (100) 给 b
console.log(`a = ${a}, b = ${b}`); // a = 100, b = 100
// 修改 b 的值
b = 200;
console.log(`a = ${a}, b = ${b}`); // a = 100 (a 没有任何变化), b = 200
内存示意图:
a
和 b
在栈内存中是两个完全独立的存储空间。
2. 引用类型 (对象类型 / Object Types)
-
包含:
Object
,以及它的所有子类型,如Array
,Function
,Date
,RegExp
等。 -
特点:
-
存储位置: 它的数据本身(比如对象的属性和值)存储在堆 (Heap)内存中。堆内存空间大,可以存储复杂和大小不定的数据。而变量名和指向堆中数据的内存地址(引用)则存储在栈 (Stack)中。
-
可变性 (Mutability): 引用类型的值是可以被修改的。
-
赋值/复制: 当你把一个引用类型的变量赋给另一个变量时,复制的是栈中的内存地址(引用),而不是堆中的实际数据。因此,这两个变量指向的是同一个堆内存中的对象。对其中任何一个变量进行操作,都会影响到另一个变量。
-
代码示例:
let obj1 = { name: 'Alice', age: 25 };
let obj2 = obj1; // 复制 obj1 的引用(内存地址)给 obj2
console.log(obj1.name); // 'Alice'
console.log(obj2.name); // 'Alice'
// 修改 obj2 的属性
obj2.name = 'Bob';
// 因为 obj1 和 obj2 指向同一个对象,所以 obj1 也被影响了
console.log(obj1.name); // 'Bob'
console.log(obj2.name); // 'Bob'
内存示意图:
obj1
和 obj2
存储在栈中,但它们存的都是同一个地址,这个地址指向堆内存中唯一的一个对象。
二、函数参数传递 (最能体现区别的地方)
面试官经常会通过函数传参来考察这个知识点。
结论先行:JavaScript 中函数参数的传递,永远是“按值传递” (Pass by Value)。
这句话听起来有点绕,我们来解释:
-
当传递值类型时,传递的是值的副本。
-
当传递引用类型时,传递的是引用的副本(地址的副本)。
1. 传递值类型
function changeValue(num) {
num = 1000; // 在函数内部,num 是 a 的一个副本
console.log(`Inside function: num = ${num}`); // 1000
}
let a = 10;
changeValue(a);
console.log(`Outside function: a = ${a}`); // 10 (原始的 a 没有被改变)
解释:a
的值 10
被复制给了参数 num
。函数内部对 num
的修改,只是修改了这个副本,完全不影响外部的 a
。
2. 传递引用类型
function changeName(person) {
// person 是 myPerson 引用的一个副本,它们指向同一个对象
person.name = 'Charlie';
console.log(`Inside function: person.name = ${person.name}`); // 'Charlie'
}
let myPerson = { name: 'David' };
changeName(myPerson);
console.log(`Outside function: myPerson.name = ${myPerson.name}`); // 'Charlie' (原始对象被改变了!)
解释:myPerson
变量中存储的内存地址被复制给了参数 person
。现在 myPerson
和 person
都指向堆中的同一个对象。通过 person.name
修改对象属性,自然会影响到 myPerson
。
3. 引用类型传递的一个陷阱
如果在函数内部重新赋值整个对象,而不是修改其属性,会发生什么?
function reassignObject(person) {
// 这里,person 这个局部变量被赋予了一个全新的引用,指向一个新对象
person = { name: 'Eve' };
console.log(`Inside function reassign: person.name = ${person.name}`); // 'Eve'
}
let myPerson2 = { name: 'Frank' };
reassignObject(myPerson2);
console.log(`Outside function reassign: myPerson2.name = ${myPerson2.name}`); // 'Frank' (原始对象没有被改变!)
解释:进入函数时,person
持有和 myPerson2
相同的地址。但 person = { name: 'Eve' }
这行代码,让 person
这个变量指向了一个全新的对象。它切断了和原始对象 myPerson2
的联系。所以,这个操作只改变了函数内部的局部变量 person
的指向,而外部的 myPerson2
仍然指向它最初的对象。
总结表格
特性 | 值类型 (Primitive Types) | 引用类型 (Object Types) |
---|---|---|
包含类型 | String , Number , Boolean , Null , Undefined , Symbol , BigInt | Object , Array , Function , 等 |
存储位置 | 值直接存在 栈 (Stack) 中 | 数据存在 堆 (Heap) 中,引用(地址)存在 栈 (Stack) 中 |
赋值操作 | 复制值的副本,两个变量完全独立 | 复制引用的副本,两个变量指向同一个对象 |
函数传参 | 传递值的副本。函数内修改不影响外部。 | 传递引用的副本。函数内修改对象属性会影响外部,但重新赋值对象则不会。 |
如何判断一个类型? 一个简单的方法:除了7种基本类型之外的所有类型,都是引用类型。