网站

JavaScript 对象 - W3School

JavaScript 数据类型和数据结构 | MDN

引入

值类型只能保存单个数据,比如一个名字、一个年龄。但实际开发中,数据往往是一组相关的信息,比如一个用户有名字、年龄、邮箱。

引用类型用来保存这种复杂的、由多个数据组成的结构,包括对象、数组和函数。

正文

定义

定义

引用类型的数据保存在堆内存中,变量保存的不是数据本身,而是数据在内存中的地址(引用)。赋值时复制的是地址,不是数据。

引用类型的存储方式

引用类型的数据保存在堆内存中,变量只保存一个指向堆内存的地址。

let person = { name: "Tom", age: 18 };

变量 person 保存的不是对象本身,而是对象在堆内存中的地址。通过这个地址才能找到真正的数据。

赋值是复制地址

引用类型赋值时,复制的是地址,不是数据。

let obj1 = { name: "Tom" };
let obj2 = obj1;

obj1obj2 保存的是同一个地址,指向堆内存中的同一份数据。

修改会影响所有引用

因为多个变量指向同一份数据,通过任何一个变量修改数据,其他变量看到的也会变。

let obj1 = { name: "Tom" };
let obj2 = obj1;
 
obj2.name = "Jerry";
 
console.log(obj1.name);
console.log(obj2.name);

输出结果:

"Jerry"
"Jerry"

obj1obj2 指向同一个对象,修改 obj2 的属性,obj1 也会看到变化。

和值类型的对比

对比值类型引用类型
存储位置栈内存堆内存
变量保存数据本身数据的内存地址
赋值行为复制数据复制地址
修改影响互不影响指向同一份数据时会互相影响
let a = 10;
let b = a;
a = 20;
 
console.log(a);
console.log(b);

输出结果(值类型):

20
10
let 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"

greetsayHello 指向同一个函数,调用哪个都一样。

console.log(typeof sayHello);

输出结果:

"function"

函数是唯一一个 typeof 返回 "function" 的引用类型。

详细内容见 JS 函数

引用类型的相等判断

两个引用类型的值,即使内容完全一样,只要地址不同就不相等。

let obj1 = { name: "Tom" };
let obj2 = { name: "Tom" };
 
console.log(obj1 === obj2);

输出结果:

false

obj1obj2 是两个不同的对象,各自有独立的内存地址。

只有指向同一个地址时才相等:

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]

arr1arr2 指向同一个数组,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 类型转换,了解引用类型在运算和比较时的转换行为。