网站
引入
值类型只能保存单个数据,比如一个名字、一个年龄。但实际开发中,数据往往是一组相关的信息,比如一个用户有名字、年龄、邮箱。
引用类型用来保存这种复杂的、由多个数据组成的结构,包括对象、数组和函数。
正文
定义
定义
引用类型的数据保存在堆内存中,变量保存的不是数据本身,而是数据在内存中的地址(引用)。赋值时复制的是地址,不是数据。
引用类型的存储方式
引用类型的数据保存在堆内存中,变量只保存一个指向堆内存的地址。
let person = { name: "Tom", age: 18 };变量 person 保存的不是对象本身,而是对象在堆内存中的地址。通过这个地址才能找到真正的数据。
赋值是复制地址
引用类型赋值时,复制的是地址,不是数据。
let obj1 = { name: "Tom" };
let obj2 = obj1;obj1 和 obj2 保存的是同一个地址,指向堆内存中的同一份数据。
修改会影响所有引用
因为多个变量指向同一份数据,通过任何一个变量修改数据,其他变量看到的也会变。
let obj1 = { name: "Tom" };
let obj2 = obj1;
obj2.name = "Jerry";
console.log(obj1.name);
console.log(obj2.name);输出结果:
"Jerry"
"Jerry"obj1 和 obj2 指向同一个对象,修改 obj2 的属性,obj1 也会看到变化。
和值类型的对比
| 对比 | 值类型 | 引用类型 |
|---|---|---|
| 存储位置 | 栈内存 | 堆内存 |
| 变量保存 | 数据本身 | 数据的内存地址 |
| 赋值行为 | 复制数据 | 复制地址 |
| 修改影响 | 互不影响 | 指向同一份数据时会互相影响 |
let a = 10;
let b = a;
a = 20;
console.log(a);
console.log(b);输出结果(值类型):
20
10let obj1 = { value: 10 };
let obj2 = obj1;
obj1.value = 20;
console.log(obj1.value);
console.log(obj2.value);输出结果(引用类型):
20
20对象
对象是最常见的引用类型,用键值对保存数据。
let user = {
name: "Tom",
age: 18,
isLogin: true
};
console.log(user.name);
console.log(user.age);输出结果:
"Tom"
18对象用 {} 创建,数据以 键: 值 的形式保存。
详细内容见 JS 对象。
数组
数组是有序的值的集合。
let list = [1, 2, 3, 4, 5];
console.log(list[0]);
console.log(list.length);输出结果:
1
5数组用 [] 创建,下标从 0 开始。
let fruits = ["苹果", "香蕉", "橘子"];
console.log(fruits[1]);输出结果:
"香蕉"数组本质上是特殊的对象,typeof 返回 "object":
console.log(typeof [1, 2, 3]);
console.log(Array.isArray([1, 2, 3]));输出结果:
"object"
true详细内容见 JS Array 对象。
函数
函数也是引用类型。
let sayHello = function () {
console.log("hello");
};
let greet = sayHello;
greet();
sayHello();输出结果:
"hello"
"hello"greet 和 sayHello 指向同一个函数,调用哪个都一样。
console.log(typeof sayHello);输出结果:
"function"函数是唯一一个 typeof 返回 "function" 的引用类型。
详细内容见 JS 函数。
引用类型的相等判断
两个引用类型的值,即使内容完全一样,只要地址不同就不相等。
let obj1 = { name: "Tom" };
let obj2 = { name: "Tom" };
console.log(obj1 === obj2);输出结果:
falseobj1 和 obj2 是两个不同的对象,各自有独立的内存地址。
只有指向同一个地址时才相等:
let obj1 = { name: "Tom" };
let obj2 = obj1;
console.log(obj1 === obj2);输出结果:
true重新赋值和修改属性的区别
修改属性:
let obj1 = { name: "Tom" };
let obj2 = obj1;
obj1.name = "Jerry";
console.log(obj2.name);输出结果:
"Jerry"修改属性是修改同一份数据里的内容,obj2 会看到变化。
重新赋值:
let obj1 = { name: "Tom" };
let obj2 = obj1;
obj1 = { name: "Jerry" };
console.log(obj1.name);
console.log(obj2.name);输出结果:
"Jerry"
"Tom"重新赋值是让 obj1 指向一个全新的对象,obj2 还是指向原来的对象。
数组的引用行为
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1);
console.log(arr2);输出结果:
[1, 2, 3, 4]
[1, 2, 3, 4]arr1 和 arr2 指向同一个数组,arr2 添加元素后,arr1 也能看到。
函数传参的引用行为
函数参数传入引用类型时,传的是地址。
let user = { name: "Tom", age: 18 };
function changeName(obj) {
obj.name = "Jerry";
}
changeName(user);
console.log(user.name);输出结果:
"Jerry"函数内部修改了对象的属性,外部也能看到变化。
但如果函数内部重新赋值参数:
let user = { name: "Tom", age: 18 };
function replaceUser(obj) {
obj = { name: "Jerry", age: 20 };
}
replaceUser(user);
console.log(user.name);输出结果:
"Tom"重新赋值参数只是让局部变量指向新对象,不影响外部。
浅拷贝
如果不想让赋值后的修改互相影响,可以浅拷贝。
对象浅拷贝
let obj1 = { name: "Tom", age: 18 };
let obj2 = { ...obj1 };
obj2.name = "Jerry";
console.log(obj1.name);
console.log(obj2.name);输出结果:
"Tom"
"Jerry"使用展开语法 … 可以创建一份新的对象副本。
数组浅拷贝
let arr1 = [1, 2, 3];
let arr2 = [...arr1];
arr2.push(4);
console.log(arr1);
console.log(arr2);输出结果:
[1, 2, 3]
[1, 2, 3, 4]浅拷贝只复制第一层,如果属性值本身是对象或数组,里面的引用不会断开。
let obj1 = { name: "Tom", hobbies: ["读书", "跑步"] };
let obj2 = { ...obj1 };
obj2.hobbies.push("游泳");
console.log(obj1.hobbies);
console.log(obj2.hobbies);输出结果:
["读书", "跑步", "游泳"]
["读书", "跑步", "游泳"]hobbies 是一个数组,浅拷贝后两个对象的 hobbies 仍然指向同一个数组。
常见写法
| 写法 | 作用 |
|---|---|
let obj = { name: "Tom" }; | 创建对象 |
let arr = [1, 2, 3]; | 创建数组 |
let fn = function () {}; | 创建函数 |
obj1 === obj2 | 判断是否指向同一地址 |
Array.isArray(arr) | 判断是否为数组 |
let copy = { …obj }; | 对象浅拷贝 |
let copy = […arr]; | 数组浅拷贝 |
例子
修改对象互相影响
let config1 = { theme: "dark", lang: "zh" };
let config2 = config1;
config2.theme = "light";
console.log(config1.theme);
console.log(config2.theme);输出结果:
"light"
"light"拷贝后互不影响
let config1 = { theme: "dark", lang: "zh" };
let config2 = { ...config1 };
config2.theme = "light";
console.log(config1.theme);
console.log(config2.theme);输出结果:
"dark"
"light"数组去重后判断
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(arr1 === arr2);输出结果:
false内容一样,但不是同一个数组。
特点
- 引用类型保存在堆内存中,变量保存的是地址
- 赋值时复制的是地址,不是数据
- 多个变量指向同一份数据时,修改会互相影响
- 对象、数组、函数都是引用类型
===判断的是地址,不是内容- 重新赋值和修改属性的行为不同
- 浅拷贝可以创建第一层独立的副本
- 函数传参时传入的是地址
理解
可以把引用类型理解成一把钥匙。变量保存的不是房间里的东西,而是房间的钥匙(地址)。
赋值就是把钥匙复制了一把给别人,两个人拿的是同一把钥匙,开的是同一个房间。一个人进去搬了家具,另一个人进去看到的也是搬过之后的样子。
如果想各自独立,就要复制一个全新的房间(浅拷贝),拿到新房间的钥匙,两边就互不影响了。
=== 比较的是两把钥匙是不是开同一个房间,不是比较房间里的东西是否一样。
引出
理解了引用类型后,可以继续学习 JS 对象、JS Array 对象 和 JS 函数,分别了解三种主要引用类型的详细用法。
也可以学习 JS 类型转换,了解引用类型在运算和比较时的转换行为。