# JavaScript原始类型和引用类型

## 原始值和引用值

JavaScript的变量可以包含两种类型的值：原始类型和引用类型。

原始类型有

* String
* Boolean
* Number
* Null
* Number
* BigInt
* Symbol

引用类型有

* Array
* Function
* Date
* RegExp
* Set
* Map
* WeakSet
* WeakMap
* .......

引用类型的种类很多，可以这样认为，如果不是以上7种原始类型之一，那么它就必定是引用类型。

包含原始类型的值称为原始值，包含引用类型的值称为引用值。

## 按值和按引用

对于原始值变量是按值保存，对于引用值变量是按引用保存。

按值保存就是变量保存的就是值本身，而按引用保存就是变量保存的并不是值，而是一个指向存放真实值的地址。

对于保存原始值的变量来说，访问变量就是访问值，而对于引用类型的变量来说，访问变量会先获取到存放真实数据的地址，然后通过这个地址访问到真实的数据。

## 复制变量

除了存储方式不同，原始值和引用值在通过变量复制时也有所不同。

在复制原始值时，新变量的值是旧变量的值的副本，两者互相独立，改变其中一个不会影响另一个。

```js
let num1 = 5
let num2 = a;
console.log(num1,num2)
// 5 5
num1 = 1;
console.log(num1,num2)
1,5
```

![image-20211031155542082](https://raw.githubusercontent.com/const-love-365-10000/cloudImg/master/img/image-20211031155542082.png)

但是引用值则不同，在复制引用值时，因为引用类型的变量的值是指向真实数据的地址，因此复制的也是这段地址，也就是说新变量和旧变量保存的都是指向同一个对象的地址。

```js
let a = [1,2,3]
// b = a 其实就是复制了a保存的地址，使a和b都指向同一块地址的数据
let b = a ;
console.log(a,b)
// [1,2,3]  [1,2,3]
```

`b = a` 其实就是复制了a保存的地址，使a和b都指向同一块地址的数据。而因为此时a和b都指向同一块地址的数据，也就是说两者共用一块地址的数据，那么无论通过哪个变量来改变数据，都必然会影响到另一个变量。

```js
a[0] = 10;
console.log(a,b)
// [10,2,3]  [10,2,3]
```

## 传递参数

既然变量分为按值保存和按引用保存，那么传递参数时又是如何呢？对于引用值，是传递变量保存的地址(指针)，还是传递指向的真实数据。

首先我们要知道，函数里的参数本质上是局部变量，可以简单理解

```js
function fn(a,b){
    // 这等于
    let a = a;
    let b = b
}
```

如果传递的是原始值，那么很好理解，参数和函数外的变量相互独立,互不影响。

但是如果是引用值呢？

```js
function fn(obj){
	obj.a = 2
    console.log(obj.a,value.a) // 2  2
}
let value = {
    a: 1
}
fn(value)
console.log(value.a) // 2
```

如果这样看，你可能以为函数参数是按引用传递的，就是说将`value`保存的真实对象传递到函数参数`obj`，但是我们稍微改一下代码。

```js
function fn(obj){
	obj.a = 2
    console.log(obj.a,value.a) // 2  2
    obj = {a: 100}
}
let value = {
    a: 1
}
fn(value)
console.log(value) // {a: 2}
```

这里只增加了一条代码，就是将obj重新赋值为一个对象。如果参数传递是按值传递，那么`obj={a: 100}`改变了真实的数据，而`value`又指向了这个数据，那么最后打印`value`的时候就应该输出`{a: 100}`,而不是`{a: 2}`，由此可见，参数传递是按值传递的。

**函数只有按值传递**，简单来说就是变量保存了什么值，就传递什么值。原始值变量保存的就是真实的数据，所以就传递真实的数据的副本，而对于引用值来说，变量保存的是地址，那么传递也就是那个地址的副本。

## 从内存角度理解

### 栈内存和堆内存

JavaScript内存空间分为栈内存和堆内存。

栈是一种特殊的列表，栈内的元素只能通过列表的一端访问，这一端称为栈顶。 栈被称为是一种后入先出（LIFO，last-in-first-out）的数据结构。 由于栈具有后入先出的特点。

堆是一种经过排序的树形数据结构，每个结点都有一个值。 通常我们所说的堆的数据结构，是指二叉堆。 堆的特点是根结点的值最小（或最大），且根结点的两个子树也是一个堆。

**原始类型的变量保存在栈内存里，引用类型的真实数据保存在堆内存里，但是同时会保存一个变量在栈内存，这个变量的值就是指向真实数据的地址。**

这样区分是因为原始值大小固定，占用内存空间小，而引用类型大小不固定、占用内存空间大。

### 从内存角度看变量复制

#### 基本数据类型的复制

```
let a = 20;
let b = a;
b = 30;
console.log(a); // 此时a的值是多少，是30？还是20？
复制代码
```

答案是：20

在这个例子中，a、b 都是基本类型，它们的值是存储在栈内存中的，a、b 分别有各自独立的栈空间， 所以修改了 b 的值以后，a 的值并不会发生变化。

从下图可以清晰的看到变量是如何复制并修改的。

![image-20211031163839389](https://raw.githubusercontent.com/const-love-365-10000/cloudImg/master/img/image-20211031163839389.png)

#### 引用数据类型的复制

```
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此时m.a的值是多少，是10？还是15？
复制代码
```

答案是：15

在这个例子中，m、n都是引用类型，栈内存中存放地址指向堆内存中的对象， 引用类型的复制会为新的变量自动分配一个新的值保存在变量中， 但只是引用类型的一个地址指针而已，实际指向的是同一个对象， 所以修改 n.a 的值后，相应的 m.a 也就发生了改变。

![image-20211031163904250](https://raw.githubusercontent.com/const-love-365-10000/cloudImg/master/img/image-20211031163904250.png)

## 参考

《JavaScript高级程序设计第四版》

《JavaScript权威指南》

[《「前端进阶」JS中的栈内存堆内存》](https://juejin.cn/post/6844903873992196110#heading-2)

部分图片出自了[《「前端进阶」JS中的栈内存堆内存》](https://juejin.cn/post/6844903873992196110#heading-2)
