Appearance
JavaScript教程 - 12 对象和数组补充
下面对数组和对象的一些内容做一下补充。
12.1 内存结构与拷贝
1 数组内存结构
数组是引用类型的,所以下面的代码:
js
let numbers = [1, 2, 3, 4, 5];在内存中的结构是这样的:

numbers 变量是存储在栈中的,右边的数组值是存储在堆中的,栈中存储堆中的地址。
再看一段代码:
js
int [] numbers = new int[]{1, 2, 3, 4, 5};
numbers = [1, 2, 3];上面的代码,重新将一个数组赋值给 numbers,内存结构如下:

numbers 被赋值为一个新的数组,那么存储的是新数组的地址,numbers 与新的数组建立关系,与原来的数组断开联系了。
2 浅拷贝
上面使用 slice() 拷贝数组,是浅拷贝,浅拷贝就是只复制一层。
举个栗子:
js
let persons = [{'name':'doubi'}, {'name':'niu'}];
let copyPersons = persons.slice();内存结构如下:

当复制数组后,只是复制第一层,也就是数组中的内容。

所以此时两个数组指向的是相同的对象,修改数组会影响另一个数组:
js
console.log(persons[0] === copyPersons[0]); // true
persons[0].name = 'shabi';
console.log(copyPersons[0].name); // shabi3 深拷贝
深拷贝就是所有的层都会复制,所以深拷贝内存结构如下:

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