# JavaScript教程 - 12 对象和数组补充

下面对数组和对象的一些内容做一下补充。

# 12.1 内存结构与拷贝

# 1 数组内存结构

数组是引用类型的,所以下面的代码:

let numbers = [1, 2, 3, 4, 5];
1

在内存中的结构是这样的:

numbers 变量是存储在栈中的,右边的数组值是存储在堆中的,栈中存储堆中的地址。


再看一段代码:

int [] numbers = new int[]{1, 2, 3, 4, 5};
numbers = [1, 2, 3];
1
2

上面的代码,重新将一个数组赋值给 numbers,内存结构如下:

numbers 被赋值为一个新的数组,那么存储的是新数组的地址,numbers 与新的数组建立关系,与原来的数组断开联系了。

# 2 浅拷贝

上面使用 slice() 拷贝数组,是浅拷贝,浅拷贝就是只复制一层。

举个栗子:

let persons = [{'name':'doubi'}, {'name':'niu'}];
let copyPersons = persons.slice();
1
2

内存结构如下:

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

所以此时两个数组指向的是相同的对象,修改数组会影响另一个数组:

console.log(persons[0] === copyPersons[0]);  // true

persons[0].name = 'shabi';
console.log(copyPersons[0].name);  // shabi
1
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,修改不会影响另一个数组。
1
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 } }
1
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属性值覆盖。
1
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(来自原型链)
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
1
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
1
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
1
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
1
2
3
4
5
6
7
8

赋值的时候,还可以跳过项:

let arr = [10, 20, 30];
let [a, , b] = arr;  // 跳过第二个元素

console.log(a);  // 10
console.log(b);  // 30
1
2
3
4
5

还可以与 rest 运算符结合:

const [a, ...b] = [1, 2, 3, 4];

console.log(a);
console.log(b);
1
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
1
2
3
4
5
6
7
  • [b, a] 是创建了一个数组,然后将数组的值解构赋值给变量 a 和 b。

解构的时候,如果你的语句没有写结尾分号; 会报错,所以需要添加分号,可以添加在后面,也可以添加在前面:

;[a, b] = [b, a]   //  分号添加在前面
1

数组解构还支持嵌套:

const [a, [b, c]] = [1, [2, 3]];

console.log(a);   // 1
console.log(b);   // 2
console.log(c);   // 3
1
2
3
4
5

# 2 对象解构

对象解构赋值是按照属性名来进行解构。

举个栗子:

const user = { name: "Doubi", age: 13};
const { name, age } = user;

console.log(name); // Doubi
console.log(age); // 13
1
2
3
4
5

上面是定义变量的时候,同时进行解构,如果先定义的变量,再解构赋值,需要添加括号,否则会认为解构的括号是代码块而报错:

const user = { name: "Doubi", age: 13};
let name;
let age;
({ name, age }) = user;  // 需要添加括号
1
2
3
4

如果对象的属性名和变量名称不一致,还可以指定名称:

const user = { name: "Doubi", age: 13};
const { name: username, age } = user;

console.log(username); // Doubi
console.log(age); // 13
1
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); // 男
1
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);
1
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
1
2
3
4
5

# 12.3 JSON转换

# 1 JSON 简介

什么是JSON?

JSON就是特定格式的字符串。我们可以将各种数据(例如对象)按照这个格式进行封装,然后可以在不同的语言和系统之间进行传送和交互。因为字符串是好传递的,但是对象不好直接传递。


JSON的2种格式:

格式一:

是一个对象格式的结构。{} 括起来,其中是属性。

举个栗子,以下是一个人的信息, JSON 格式:

{
	"name": "zhangsan",
	"age": 18,
  "gender": "男"
}
1
2
3
4
5
  • 注意,最后一个属性后,没有逗号,

格式二:

是列表结构的,[] 括起来,其中是数据的列表。数据可以是不同的数据类型,例如对象、字符串等。

举个栗子,以下是一个人的列表:

[
  {
    "name": "zhangsan",
    "age": 18,
    "gender": "男"
  },
  {
    "name": "lisi",
    "age": 17,
    "gender": "女"
  },
  {
    "name": "wangwu",
    "age": 16,
    "gender": "男"
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

也可以是字符串或其他数据类型的列表:

[
  "zhangsan",
  "lisi",
  "wangwu"
]
1
2
3
4
5

上面两种格式可以相互嵌套,例如对象的属性可以是数组格式:

{
  "class": "高一一班",
  "students": [
    {
      "name": "zhangsan",
      "age": 18,
      "gender": "男"
    },
    {
      "name": "lisi",
      "age": 17,
      "gender": "女"
    }
  ]
}
1
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}
1
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}
1
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}
1
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
1
2
3
4
5
6
7
8
9
  • 通过包装类对象 valueOf() 方法可以得到包装类的原始值。

那么包装类有什么用呢?

举个栗子:

let str = "hello";
console.log(str.toUpperCase()); // 输出:HELLO
1
2
  • toUpperCase() 方法是将字符串转换为大写。
  • "hello" 是字符串,是原始数据类型,按理说不能有方法。那么为什么可以调用方法呢?

当访问原始值的方法或属性时,JavaScript 会自动执行以下步骤:

  1. 将原始值临时转换为包装对象:
new String("hello")
1
  1. 调用方法或属性:
.toUpperCase()
1
  1. 销毁包装对象,返回结果。

再看一下下面的栗子:

let str = "hello";
str.name = "Doubi";
console.log(str.name);  // undefined
1
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
1
2
3
4
5
  • 看到了吧,两个相同值的包装类,比较都不同,你用它干嘛,没事找事吗。
  • 比较是比较两个对象的地址,两个包装类是两个不同的对象,所以地址肯定不一样。

所以你知道有包装类这么个东西就可以了,最好别用!除非你闲得蛋疼。