在本教程中,你将学习 JavaScript 中原始值和引用值之间的区别。
JavaScript 中的两种值类型
JavaScript 中的值分为两类:
- 原始值(Primitive Value)
- 引用值(Reference Value)
原始值
原始值是不可更改(不可变)的简单值。
JavaScript 支持以下原始值类型:
undefined
null
boolean
number
bigint
string
symbol
当你将一个原始值赋给变量时,变量中存储的是实际的值。
例如:
1 2 3 |
let x = 10; let y = x; |
在上面的代码中,变量 x
的值是 10
。将 x
的值赋给 y
时,y
也获得了 10
。此时,x
和 y
是两个彼此独立的变量,各自持有值 10
。
更改其中一个变量的值不会影响另一个:
1 2 3 |
x = 20; console.log(y); // 输出: 10 |
引用值
引用值是复杂的数据类型,如对象、数组和函数。
当你将一个引用值赋给变量时,变量中存储的不是值本身,而是对该值的引用(即内存地址)。
例如:
1 2 3 4 5 6 |
let person = { name: 'John' }; let someone = person; |
现在,person
和 someone
指向同一个对象。
因此,如果你修改 someone
对象的属性,也会影响到 person
:
1 2 3 |
someone.name = 'Jane'; console.log(person.name); // 输出: Jane |
因为 person
和 someone
引用的是同一个对象。
栈内存与堆内存(Stack and Heap Memory)
当你在 JavaScript 中声明变量时,JavaScript 引擎会在两种内存位置中为它们分配空间:栈内存(stack) 和 堆内存(heap)。
静态数据(Static Data)
静态数据是指在编译时其大小固定的数据,例如原始值(Primitive Value)(如字符串、数字、布尔值等)。这类数据会被存储在栈内存中。
栈内存具有以下特点:
- 按顺序存储;
- 分配速度快;
- 生命周期短(随着函数调用和返回自动创建/销毁);
- 只用于存储固定大小的数据;
- 是线程安全的。
例如:
1 2 3 4 |
let a = 10; let b = true; let c = 'hello'; |
在上述代码中,变量 a
、b
和 c
的值(数字、布尔、字符串)都属于原始类型,因此被存储在栈内存中。
以下示例定义了变量 name
、age
和 person
:
1 2 3 4 5 6 7 |
let name = 'John'; let age = 25; let person = { name: 'John', age: 25, }; |
在内部,JavaScript 引擎为这些变量分配内存,如下图所示:
在这张图中,JavaScript 在栈内存中为三个变量 name
、age
和 person
分配内存。
JavaScript 引擎在堆内存中创建了一个新对象,并将栈内存中的 person
变量链接到堆内存中的该对象。
因此,我们说 person
变量是对一个对象的引用。
动态数据(Dynamic Data)
与栈不同,JavaScript 将对象(和函数)存储在堆中。JavaScript 引擎不会为这些对象分配固定大小的内存,而是根据需要分配更多的空间。
如果数据的大小不确定,或者在运行时才被确定,就会存储在堆内存中。这通常指的是引用类型,如对象、数组和函数。
引用值允许你随时添加、修改或删除属性。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let person = { name: 'John', age: 25, }; // add the ssn property person.ssn = '123-45-6789'; // change the name person.name = 'John Doe'; // delete the age property delete person.age; console.log(person); |
输出:
1 |
{ name: 'John Doe', ssn: '123-45-6789' } |
与引用值不同,原始值不能拥有属性。
如果你尝试向一个原始值添加属性,是不会生效的。例如:
1 2 3 4 |
let name = 'John'; name.alias = 'Knight'; console.log(name.alias); // undefined |
在这个示例中,我们向原始值 name
添加了 alias
属性。但当我们通过原始值 name
访问 alias
属性时,返回的是 undefined
堆内存的特点:
- 结构复杂;
- 分配和回收成本较高(由垃圾回收器控制);
- 可存储大小可变的数据;
- 生命周期由作用域和引用决定。
例如:
1 2 3 4 5 |
let person = { name: 'Alice', age: 30 }; |
这里变量 person
本身是一个引用,存在于栈中,而其指向的实际对象 {name: 'Alice', age: 30}
则存储在堆内存中。
如果你再这样写:
1 2 3 |
let anotherPerson = person; anotherPerson.age = 40; |
anotherPerson
只是复制了对堆中同一个对象的引用,所以对其做出的修改也会影响原始对象 person
。
图示说明(简化版)
复制值
当你将一个原始值从一个变量赋值给另一个变量时,JavaScript 引擎会创建该值的一个副本,并将其赋给新变量。例如:
1 2 |
let age = 25; let newAge = age; |
在这个示例中:
-
首先,声明一个新变量
age
,并将其初始化为 25; -
然后,声明另一个变量
newAge
,并将age
的值赋给newAge
。
在幕后,JavaScript 引擎会创建原始值 25 的一个副本,并将其赋给变量 newAge
。
下图展示了赋值后的栈内存情况:
在栈内存中,newAge
和 age
是彼此独立的变量。如果你更改其中一个变量的值,不会影响另一个变量。
例如:
1 2 3 4 5 |
let age = 25; let newAge = age; newAge = newAge + 1; console.log(age, newAge); |
当你将一个引用值从一个变量赋值给另一个变量时,JavaScript 引擎会创建一个引用,使得两个变量都指向堆内存中的同一个对象。这意味着,如果你更改其中一个变量,另一个变量也会受到影响。
例如:
1 2 3 4 5 6 7 8 9 10 11 |
let person = { name: 'John', age: 25, }; let member = person; member.age = 26; console.log(person); console.log(member); |
运行机制如下:
首先,声明一个变量 person
,并用一个包含两个属性 name
和 age
的对象来初始化它的值:
1 2 3 4 |
let person = { name: 'John', age: 25, }; |
其次,将变量 person
赋值给变量 member
。在内存中,这两个变量都引用同一个对象,如下图所示:
第三,通过变量 member
修改该对象的 age
属性:
1 |
<span><code class="hljs">member.age = 26;</code></span> |
由于
person
和 member
两个变量都引用同一个对象,因此通过 member
变量修改该对象时,person
变量中也会反映出相应的变化。
wer
在函数中传递原始值与引用值
当你将变量作为参数传递给函数时,JavaScript 采用**按值传递(pass by value)**的方式。对原始值和引用值的处理方式有所不同。
传递原始值
当你将原始值传递给函数时,函数接收的是该值的拷贝,对该值的更改不会影响原始变量。
例如:
1 2 3 4 5 6 7 8 9 10 11 |
function square(x) { x = x * x; return x; } let num = 10; let result = square(num); console.log(result); // 输出: 100 console.log(num); // 输出: 10 |
传递引用值
当你将引用值传递给函数时,函数接收的是引用的拷贝(即指向同一对象)。因此,对该对象的修改会影响原始对象。
例如:
1 2 3 4 5 6 7 8 9 10 11 |
function changeName(obj) { obj.name = 'Jane'; } let person = { name: 'John' }; changeName(person); console.log(person.name); // 输出: Jane |
然而,如果你在函数内部更改引用变量本身,而不是对象的属性,原始引用不会受到影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function changePerson(obj) { obj = { name: 'Bob' }; } let person = { name: 'John' }; changePerson(person); console.log(person.name); // 输出: John |
在上面的代码中,函数内部的 obj
被重新赋值为一个新对象。但这个变化仅在函数作用域内生效,不会影响原始的 person
变量
总结
- 原始值是不可变的,赋值时创建独立的拷贝。
- 引用值指向对象的内存地址,赋值会复制引用。
- 原始值作为参数传递时,函数不会影响原始变量。
- 引用值作为参数传递时,函数可以修改对象内容,但不能更改原始引用本身。
- JavaScript 有两种类型的值:原始值和引用值。
- 对于引用值,你可以添加、修改或删除属性;而对于原始值,这是不允许的。
- 将一个原始值从一个变量复制到另一个变量时,会创建一个独立的值副本,这意味着修改其中一个变量的值不会影响另一个变量。
- 将一个引用值从一个变量复制到另一个变量时,会创建一个引用,使两个变量都指向同一个对象。这意味着通过其中一个变量修改对象,另一个变量也会反映出相应的变化。