# JavaScript教程 - 12 对象和数组补充
下面对数组和对象的一些内容做一下补充。
# 12.1 内存结构与拷贝
# 1 数组内存结构
数组是引用类型的,所以下面的代码:
let numbers = [1, 2, 3, 4, 5];
在内存中的结构是这样的:
numbers 变量是存储在栈中的,右边的数组值是存储在堆中的,栈中存储堆中的地址。
再看一段代码:
int [] numbers = new int[]{1, 2, 3, 4, 5};
numbers = [1, 2, 3];
2
上面的代码,重新将一个数组赋值给 numbers,内存结构如下:
numbers 被赋值为一个新的数组,那么存储的是新数组的地址,numbers 与新的数组建立关系,与原来的数组断开联系了。
# 2 浅拷贝
上面使用 slice()
拷贝数组,是浅拷贝,浅拷贝就是只复制一层。
举个栗子:
let persons = [{'name':'doubi'}, {'name':'niu'}];
let copyPersons = persons.slice();
2
内存结构如下:
当复制数组后,只是复制第一层,也就是数组中的内容。
所以此时两个数组指向的是相同的对象,修改数组会影响另一个数组:
console.log(persons[0] === copyPersons[0]); // true
persons[0].name = 'shabi';
console.log(copyPersons[0].name); // shabi
2
3
4
# 3 深拷贝
深拷贝就是所有的层都会复制,所以深拷贝内存结构如下:
对象也会复制一份,复制后,因为 name
的值是 string
,值是一样的,所以指向是同一份,如果还是对象类型,还是要复制的。此时修改一个数组,不会影响另一个数组。
深拷贝,在浏览器中,可以使用 structuredClone()
函数。
let persons = [{'name':'doubi'}, {'name':'niubi'}];
let copyPersons = structuredClone(persons);
console.log(persons[0] === copyPersons[0]); // false,两个对象,不相同
persons[0].name = 'shabi';
console.log(copyPersons[0].name); // doubi,修改不会影响另一个数组。
2
3
4
5
6
7
在实际的开发中,根据需要选择浅拷贝或深拷贝,一般浅拷贝用的多。
# 4 对象浅拷贝
对对象进行浅拷贝,可以使用 Object.assign()
方法,Object.assign()
可以将被复制对象中的属性复制到目标对象,并返回目标对象。
举个栗子:
let original = { a: 1, b: { c: 2 } };
let copy = {};
Object.assign(copy, original);
console.log(copy); // { a: 1, b: { c: 2 } }
copy.a = 10; // 不影响原对象
copy.b.c = 20; // 会影响原对象!
console.log(original); // { a: 1, b: { c: 20 } }
2
3
4
5
6
7
8
9
Object.assign(copy, original);
会将 original 的属性复制给 copy 对象。
如果目标对象已经存在属性,属性会保留,如果复制的属性同名,则会覆盖。
let original = { a: 1, b: { c: 2 } };
let copy = { abc: 123, a: 2 };
Object.assign(copy, original);
console.log(copy); // {abc: 123, a: 1, b: { c: 2 }}
// abc 属性保留,a属性值覆盖。
2
3
4
5
6
需要注意,,Object.assign()
只能复制可枚举的自有属性(不复制原型链上的属性)。
如果希望同时复制对象自身属性 + 原型链属性,可以使用以下方法:
const parent = { a: 1 };
const child = Object.create(parent);
child.b = 2;
// ---- 下面的代码开始复制 child 对象
// 1. 复制原型链
const copy = Object.create(Object.getPrototypeOf(child));
// 2. 复制自身属性
Object.assign(copy, child);
console.log(copy); // { b: 2 }
console.log(copy.a); // 1(来自原型链)
2
3
4
5
6
7
8
9
10
11
12
- 上面的代码保留了原型链关系(
copy.__proto__ === child.__proto__
)。
# 5 对象深拷贝
对象深拷贝也可以使用 structuredClone()
函数,就不说了。
# 12.2 解构赋值
解构赋值(Destructuring Assignment)就是可以从数组或对象中提取数据,一次赋值给多个变量,就是为了图个方便。
# 1 数组解构
举个栗子:
let arr = [10, 20, 30];
let [a, b, c] = arr;
console.log(a); // 10
console.log(b); // 20
console.log(c); // 30
2
3
4
5
6
- 会分别将数组中的元素赋值给变量
a、b、c
。
如果变量的个数小于数组的长度,那么多出的数组元素会被忽略:
let arr = [10, 20, 30];
let [a, b, c] = arr;
console.log(a); // 10
console.log(b); // 20
2
3
4
5
如果变量的个数大于数组的长度,那么多出的变量会被赋值给 undefined
:
let a;
let b;
let c = 100;
let arr = [10, 20];
[a, b, c] = arr; // 解构赋值
console.log(a); // 10
console.log(b); // 20
console.log(c); // undefined
2
3
4
5
6
7
8
9
10
如果变量的个数大于数组的长度的时候,不想变量被赋值给 undefined,可以为变量指定默认值:
let arr = [10, 20];
let [a, b, c = 100] = arr; // 为c指定默认值
[a, b, c = c] = arr; // 如果数组长度小于变量个数,c的值保持原来的值
console.log(a); // 10
console.log(b); // 20
console.log(c); // 100
2
3
4
5
6
7
8
赋值的时候,还可以跳过项:
let arr = [10, 20, 30];
let [a, , b] = arr; // 跳过第二个元素
console.log(a); // 10
console.log(b); // 30
2
3
4
5
还可以与 rest
运算符结合:
const [a, ...b] = [1, 2, 3, 4];
console.log(a);
console.log(b);
2
3
4
- 将数组后面的元素赋值给
b
变量,b
变量变为数组。
还可以交换两个变量的值:
let a = 10;
let b = 20;
[a, b] = [b, a]; // 交换a、b的值
console.log(a); // 20
console.log(b); // 10
2
3
4
5
6
7
[b, a]
是创建了一个数组,然后将数组的值解构赋值给变量 a 和 b。
解构的时候,如果你的语句没有写结尾分号;
会报错,所以需要添加分号,可以添加在后面,也可以添加在前面:
;[a, b] = [b, a] // 分号添加在前面
数组解构还支持嵌套:
const [a, [b, c]] = [1, [2, 3]];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
2
3
4
5
# 2 对象解构
对象解构赋值是按照属性名来进行解构。
举个栗子:
const user = { name: "Doubi", age: 13};
const { name, age } = user;
console.log(name); // Doubi
console.log(age); // 13
2
3
4
5
上面是定义变量的时候,同时进行解构,如果先定义的变量,再解构赋值,需要添加括号,否则会认为解构的括号是代码块而报错:
const user = { name: "Doubi", age: 13};
let name;
let age;
({ name, age }) = user; // 需要添加括号
2
3
4
如果对象的属性名和变量名称不一致,还可以指定名称:
const user = { name: "Doubi", age: 13};
const { name: username, age } = user;
console.log(username); // Doubi
console.log(age); // 13
2
3
4
5
- 上面的代码中,
name: username
中,name
是对象中的属性名,username
是变量的名称。
如果对象中没有某个属性,无法解构到对应的变量,还可以变量的默认值:
const user = { name: "Doubi", age: 13};
const { name, gender = '男' } = user;
console.log(name); // Doubi
console.log(gender); // 男
2
3
4
5
- 上面的代码中,对象没有 gender 属性,所以 gender 值为男。
还可以嵌套对象解构,从多层嵌套的对象中取出内部属性,不必一步步写 obj.a.b.c
。
举个栗子:
const person = {
name: "Bob",
address: {
city: "New York",
zip: 10001,
},
};
const { address: { city } } = person;
console.log(city);
2
3
4
5
6
7
8
9
10
- 在上面的代码中,是直接解构出
city
属性,但是注意,并没有获得变量address
。
如果想获取 address 对象,可以先把 address
解构出来,再从中提取:
const { address } = person; // 解构出address
const { city, zip } = address; // 解构出city、zip
// 或者
const {address, address: { city, zip }} = person; // 解构出address和city、zip
2
3
4
5
# 12.3 JSON转换
# 1 JSON 简介
什么是JSON?
JSON就是特定格式的字符串。我们可以将各种数据(例如对象)按照这个格式进行封装,然后可以在不同的语言和系统之间进行传送和交互。因为字符串是好传递的,但是对象不好直接传递。
JSON的2种格式:
格式一:
是一个对象格式的结构。{}
括起来,其中是属性。
举个栗子,以下是一个人的信息, JSON 格式:
{
"name": "zhangsan",
"age": 18,
"gender": "男"
}
2
3
4
5
- 注意,最后一个属性后,没有逗号
,
。
格式二:
是列表结构的,[]
括起来,其中是数据的列表。数据可以是不同的数据类型,例如对象、字符串等。
举个栗子,以下是一个人的列表:
[
{
"name": "zhangsan",
"age": 18,
"gender": "男"
},
{
"name": "lisi",
"age": 17,
"gender": "女"
},
{
"name": "wangwu",
"age": 16,
"gender": "男"
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
也可以是字符串或其他数据类型的列表:
[
"zhangsan",
"lisi",
"wangwu"
]
2
3
4
5
上面两种格式可以相互嵌套,例如对象的属性可以是数组格式:
{
"class": "高一一班",
"students": [
{
"name": "zhangsan",
"age": 18,
"gender": "男"
},
{
"name": "lisi",
"age": 17,
"gender": "女"
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
需要注意,JSON 字符串的 属性名
必须使用双引号括起来,属性值只能是数字、布尔、字符串、null、对象、数组类型!
一般情况下,前端页面请求后端服务器,服务器返回的就是 JSON 格式的字符串,前端对 JSON 字符串进行解析,然后处理。前端在请求服务器的时候,也可以将对象数据转换为 JSON 字符串,然后传递给服务器。
所以这里就涉及对象和 JSON 字符串之间的转换,将对象转换为 JSON 字符串称为序列化,将 JSON 字符串转换为对象称为反序列化。
# 2 对象转JSON字符串
通过 JSON.stringify(obj)
可以将对象转换为字符串。
const person = {
name: "Doubi",
age: 18,
};
const str = JSON.stringify(person); // 转换
console.log(str); // {"name":"Doubi","age":18}
2
3
4
5
6
7
- 通过
JSON.stringify(obj)
将对象转换为了字符串,这样就可以将数据存储到缓存,或者其他操作。 - JSON 字符串中的属性是有引号的。
# 3 JSON字符串转对象
同样可以将 JSON 字符串转换为对象,这样就可以对数据进行操作。
通过 JSON.parse(str)
可以将 JSON 格式的字符串转换为对象。
举个栗子:
const str = `{"name":"Doubi","age":18}`;
const person = JSON.parse(str); // 转换
console.log(person); // {name: 'Doubi', age: 18}
2
3
- 上面将 JSON 字符串转换为对象。
需要注意,将 JSON 字符串转换为对象,是新创建的对象,和其他对象没有任何关系:
const person1 = {
name: "Doubi",
age: 18,
};
const str = JSON.stringify(person1);
const person2 = JSON.parse(str);
console.log(person1 == person2); // {"name":"Doubi","age":18}
2
3
4
5
6
7
8
- 上面的 person1 和 person2 只是属性相同,所以通过 JSON 转换也是可以实现对象的深拷贝。
# 12.4 包装类
什么是包装类?
前面讲了 JavaScript 中有很多的基本数据类型,这些基本数据类型还对应着一些包装类,也就是基本数据类型有与之对应的类。
JavaScript 中有 3 个主要的包装类:
原始类型 | 包装类 |
---|---|
string | String |
number | Number |
boolean | Boolean |
先看一下如何通过原始数据创建包装类对象:
// 创建包装类对象
let num = new Number(123);
let bool = new Boolean(true);
let str = new String('abc');
// 通过包装类得到原始数据
console.log(num.valueOf()); // 123
console.log(bool.valueOf()); // true
console.log(str.valueOf()); // abc
2
3
4
5
6
7
8
9
- 通过包装类对象
valueOf()
方法可以得到包装类的原始值。
那么包装类有什么用呢?
举个栗子:
let str = "hello";
console.log(str.toUpperCase()); // 输出:HELLO
2
toUpperCase()
方法是将字符串转换为大写。"hello"
是字符串,是原始数据类型,按理说不能有方法。那么为什么可以调用方法呢?
当访问原始值的方法或属性时,JavaScript 会自动执行以下步骤:
- 将原始值临时转换为包装对象:
new String("hello")
- 调用方法或属性:
.toUpperCase()
- 销毁包装对象,返回结果。
再看一下下面的栗子:
let str = "hello";
str.name = "Doubi";
console.log(str.name); // undefined
2
3
为什么上面给 str 设置 name 属性和值,为什么打印却为 undefined ?
这是因为 str.name = "Doubi"
会创建一个临时包装对象,然后销毁;然后 str.name
获取属性值的时候,又创建了一个临时对象,两个临时对象是不一样的,所以读取不到。
那我们开发的时候,什么场景下使用包装类呢?
你最好永远都不要用,为什么呢?看一下下面的栗子:
let num1 = new Number(123);
let num2 = new Number(123);
console.log(typeof num1); // object
console.log(num1 == num2); // false
2
3
4
5
- 看到了吧,两个相同值的包装类,比较都不同,你用它干嘛,没事找事吗。
- 比较是比较两个对象的地址,两个包装类是两个不同的对象,所以地址肯定不一样。
所以你知道有包装类这么个东西就可以了,最好别用!除非你闲得蛋疼。
← 11-函数进阶 13-字符串和正则表达式 →