写在前面的话:最近复习过程中积累的笔记,本文内容主要涉及 JS 数据类型。内容来源五花八门,如有不妥或错误欢迎指出。
JS 有哪些数据类型?
基本类型: Number,String,Boolean,Null,Undefined,Symbol(ES6引入),BigInt(ES2020引入)
复杂/对象类型: Object、Function,Array、Date、RegExp、Map 和 Set(ES6引入)
基本类型和复杂类型的区别在于:1. 基本类型的值储存在栈中,如果将a赋值给b(a = b),会开辟一块新的空间储存b,今后再修改a的值,b也不会发生改变。复杂类型储存的是对象的地址,储存在堆中,如果将obj1赋值给obj2,只要修改一个变量所引用的对象,其他引用该对象的变量也会受到影响。2. 可以为复杂类型添加属性和方法,但基本类型不可以。
堆与栈:栈帧随函数返回自动弹栈销毁,栈储存基本类型和对象类型的指针等。堆内存由程序员手动分配与释放(C)或者由 GC 管理( JS / Java )。堆储存对象、数组、闭包等复杂数据。
判断数据类型的方法有哪些?
typeof:能正确检测基本类型,但是会将大多数对象类型都识别为 object。typeof 的底层实现是根据机器码(最后三位)判断。特殊情况:null、undefined、function。
console.log(typeof function(){}); // "function" 函数是唯一返回"function"的引用类型
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" 因为null全为0,object为000,比较机器码是相等的
instanceof :能正确检测对象类型。原理是遍历原型链,判断对象是否由某个构造函数创建,返回true或false。不能判断基本类型,因为基本类型没有构造函数(例如 "str" instanceof String 返回 false)。
Object.prototype.toString.call():能精确区分基本类型和引用类型,返回值形式是 [object [Symbol.toStringTag]]。原理是检查值的内部类型标签 [Symbol.toStringTag] 属性。这在 ES5- 中是固定内置值 [[class]] 属性。
null 和 undefined 的区别?
null 是人为设定的空值,转换为数字是 0。常见于:(1)手动赋值或清除引用;(2)DOM 查询不到时返回 null;(3)作为对象原型链的终点。
undefined 表示此处应该有值但尚未定义,转换为数字是 NaN。常见于:(1)变量被声明但未赋值,就等于 undefined。(2) 调用函数时,应该提供的参数没有提供,该参数等于 undefined。(3)访问不存在的对象属性,该属性的值为 undefined。(4)函数没有返回值时,默认返回 undefined。
如何安全地获取 undefined?应该通过 void 0 获取 undefined。因为 undefined 是一个全局变量,并非关键字,可以被作为变量名,如果它修改过,就不再返回 undefined。
let undefined = "hello";
console.log(undefined); // "hello"
let safeUndefined = void 0;
console.log(safeUndefined); // undefined
在==和===中:
console.log(null == undefined); // true
console.log(null === undefined); // false
字符串 String
const str1 = String(1); // 被强制转换为字符串原始值“1”
const str2 = String(true); // 被强制转换为字符串原始值“true”
console.log('hello'); // hello
console.log(String('hello')); // hello,String()作为普通函数调用输出字符串原始值
console.log(new String('hello')); // String ('hello'),作为构造函数调用
Math、Number、NaN
Math 对象常用静态方法
Math.pow(7, 3); // 343
Math.sqrt(16); // 4,如果参数为负数,返回 NaN
Math.random(); // 返回一个大于等于 0 且小于 1 的伪随机浮点数
Math.abs(-Infinity); // Infinity
Math.floor(); // 向下取整
Math.ceil(); // 向上取整
Math.round(); // 返回最接近的整数
Number 对象
Number.parselnt(string, radix) 会将字符串转换为数字,直到第一个非数字字符,如果第一个非空白字符就不能转换为数字,则返回 NaN。与 Math 对象的取整方法的不同之处在于,Number.parselnt(string, radix) 对于负数,会向上取整到最接近的整数;对于正数,会向下取整到最接近的整数。第二个参数可选,用于处理不同的进制数据。
第二个参数 radix 可选,表示进制,需要在 2 到 36 之间,否则返回 NaN。默认为十进制,如果是 0x 开头则为十六进制,如果以 0 开头则视环境判定为八进制或十进制。
console.log(['1', '2,' '3'].map(parseInt));
// 1 NaN NaN 因为index会作为第二个参数传给radix,index为0时会取默认值十进制
console.log(parseInt(1/0, 19));
// 18 因为1/0的结果是Infinity,十九进制包括0123456789abcdefghi,i对应十进制为18,n不存在,所以返回18
console.log(parseInt(false, 16));
// 250 因为十六进制包括0123456789abcdef,fa对应十进制我250,l不存在,所以返回250
console.log(Math.floor(-4.85)); // -5
console.log(parseInt("-4.05")); // -4
console.log(parseInt("4.05abc")); // 4
parseInt("11",2); // 3
Number.parseFloat(string) 用于将字符串转换为浮点数。如果无法解析出数字,则返回 NaN。
实例方法 toFixed() 用于转换为指定小数点位数的值,toExponential() 用于转换为科学计数法,toPrecision() 用于转换为指定精度的值,返回的均是字符串。
(5.1255).toFixed(2); // 5.13
(5.1255).toPrecision(2); // 5.1
(77).toExponential(); // 7.7e+1
NaN(Not-a-Number)
NaN 是用于检测数值异常、验证数字转换和计算的有效性的特殊值,表示不是一个数,属于 Number 类型。NaN 与任何值比较包括与自身比较都为 false,但如果将两个 NaN 传入 Set 之后会去重只留下一个。
全局方法 isNaN() 会对参数进行类型转换,如果不能被转化为 Number 类型就会返回 true,故不能严格判断 NaN。
Number的方法 Number.isNaN() 不会进行类型转换,只在传入的值严格等于 NaN 时返回 true,这样更加准确。
console.log(Number.isNaN("abc")); // false
console.log(isNaN("abc")); // true
0.1 + 0.2 等于多少?
JavaScript 的 Number 类型使用64位双精度浮点数(1位符号,11位指数,52位尾数)来表示。符号位表示正负,指数位表示数量级,尾数位表示精度。JS 能表示的数字范围是:最大值 Number.MAX_VALUE 为 ±1.7976931348623157e+308,最小值 Number.MIN_VALUE 为 ±5e-324。
const maxNum = (2 - 2⁻⁵²) × 2¹⁰²³ ≈ 1.7976931348623157 × 10³⁰⁸
const minNum = 1.0 × 2⁻¹⁰²² ≈ 2.2250738585072014 × 10⁻³⁰⁸
console.log(0.1 + 0.2); // 0.30000000000000004
64位双精度浮点数表示法无法精确表示某些小数,会出现精度丢失。在这种表示法下,0.1 和 0.2 的二进制是无限循环的,会因为 52 位的尾数被截断。因此 0.1 和 0.2 是近似表示,两个近似值相加之后得到的数略大于 0.3。
++i 和 i++ 的区别?
++i 首先增加i的值,然后返回修改后的值。i++ 首先返回当前的i值,然后增加i的值。
console.log(i++ + ++i); // 1 + 3 = 4
console.log(i++ + ++i * ++i) // 1 + 3 * 4 = 13
Symbol 有什么用?
Symbol 表示独一无二的值,创建的每个 Symbol 值都是唯一的。主要用于防止 id 或属性名冲突、隐藏对象属性。
使用 for...of... 、for...in...、Object.keys()、Object.values()、Object.entries()都无法遍历到 Symbol 属性。那么要如何遍历对象中的 Symbol 属性?
Object.getOwnPropertySymbols()单独遍历对象所有的自有Symbol属性,返回数组。Object.getOwnPropertyDescriptors():可以用来查看对象的所有属性描述符,包括Symbol键。Reflect.ownKeys()遍历对象的所有属性,返回数组,等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols())。
Symbol 常用的属性:description,用于区分不同的 Symbol。
Symbol 常用的方法:Symbol.for() 用来获取在全局注册表中已经定义过的 Symbol。 Symbol.keyFor() 用来获取描述,但只适用于用 Symbol.for() 定义的变量。
let id1 = Symbol("我是1");
let id2 = Symbol("我是2");
let id3 = Symbol.for("我是1");
let id4 = Symbol.for("我是1");
let description1 = Symbol.keyFor(id1);
let description2 = Symbol.keyFor(id3);
console.log(id1 === id2); // false;
console.log(id1 === id3); // false;
console.log(id3 === id4); // true;
console.log(id1) // Symbol(我是1)
console.log(id1.description) // 我是1
console.log(description1) // undefined
console.log(description3) // 我是1
日期对象 Date
// 获取时间戳(毫秒)
const timestamp2 = new Date().getTime();
// 创建特定日期对象
const date1 = new Date(2024, 5, 15, 14, 30, 0); // 2024年6月15日 14:30:00,注意:月份要减1
const date2 = new Date('2024-06-15T14:30:00');
const date3 = new Date(1718433000000);
// 格式化日期为 YYYY-MM-DD HH:mm:ss
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
短路规则
当在 JS 中使用 && 和 || 逻辑运算符时,而且它们的操作数不是布尔值时,我们就需要用短路规则来判断返回结果。
&&:从左往右返回第一个遇到的假值。如果全部是真值,则返回最后一个操作数的值。实际一般用于条件执行:只有前一个操作数为真时,才执行后续操作。
user && user.getName(); // 仅在 user 存在时调用 getName
||:从左到右返回第一个遇到的真值。如果全部是假值,则返回最后一个操作数的值。
实际一般用于默认值设置:当变量为假值时,使用备用值。
const name = user.name || "Guest"; // 如果 user.name 为空,返回 "Guest"
var、let、const 的区别?
JS 引擎在执行代码之前,会将所有变量和函数声明都提升到各自作用域的顶部,这叫变量提升。JavaScript 代码执行分为两个阶段:编译阶段解析代码、创建执行上下文并处理声明,执行阶段逐行执行代码。Brendan Eich 只用了十天设计 JavaScript,早期主要只用于简单的表单验证和页面交互,出于容错性考虑,变量提升能让一些变量即使后声明也不会报错而导致脚本完全停止。而且变量提升简化了编译器的实现(C语言需要两遍扫描,先收集所有声明,再检查所有引用是否已声明,但是 JS 只需一遍扫描)。
var、let 和 const 都会变量提升,但是 var 的提升会伴随初始化,let 和 const 的提升不会初始化,这就导致了 let 和 const 会产生暂时性死区,也就是说在变量声明之前,访问它们将会引发 ReferenceError 错误(暂时性死区是现代 JS 对变量提升问题的修复)。
另外,var 不支持块作用域(所以在外部依然可以访问到变量,容易造成变量污染),可重复声明( var val = 1; var val = 2 不会报错 )。
let 和 const 在 ES6 提出,二者都具有块级作用域(由 {} 创建的代码块内部声明的变量只在代码块内有效,外部无法访问),其中 let 在声明时可以不赋值,const 在声明时必须赋值且不能被重新赋值(如果是对象类型,不能修改整个对象,但可以修改对象属性)。
在 ES6 之前,JavaScript 只有函数作用域和全局作用域,没有块级作用域。在 ES6 之前,开发者使用 IIFE(立即调用函数表达式) 来模拟块级作用域。
// IIFE
(function() {
var privateVar = "我是块级变量";
console.log(privateVar); // 可以访问
})();
console.log(privateVar); // ReferenceError: privateVar is not defined
现代开发中,建议使用 let 和 const 而避免使用 var,变量应当先声明后使用,默认使用 const,如果需要重新赋值才使用 let。
显式 / 隐式类型转换
显式类型转换是程序员手动调用函数进行类型转换。隐式类型转换是 JS 自动进行类型转换,常见的有比较操作(==)、四则运算、条件语句 if 之后(false、0、''、NaN、null、undefined 会被转换为 false,其他为 true)。
加法运算规则
当一个操作数是数字,另一个操作数是字符串时,数字会先被转换为字符串,然后执行字符串连接。
当一个操作数是对象,对象会被解释为基本类型,再按照前面的规则执行加法运算。
当一个操作数是布尔值,布尔值会被转换为数字,再按照前面的规则执行加法运算。
当一个操作数是null 或 undefined ,分别会被转换为 0 和 NaN,再按照前面的规则执行加法运算。
console.log(3 + "4") // "34"
console.log(true + 3) // 4
console.log(false + "4") // "false4"
console.log(null + 2) // 2
console.log(undefined + 2) // NaN
console.log("Hello" + null)// "Hellonull"
减法、乘法、除法运算规则基本一致,非数字类型会被转换为数字类型,再执行数学运算。
console.log('7' - '0');
数组索引背后隐藏了哪些类型转换?
如果数组索引是对象、函数、数字,它们首先会被转化为字符串,symbol作为索引时不会发生类型转换。(数组本质上是对象,它的键是字符串或symbol)
==和===的区别?
== 在比较时,如果操作数类型相同,执行严格比较。若不相同,则会进行隐式类型转换。
隐式类型转换的主要规则有:
如果一个操作数是 number,另一个是 string,则 string 转换为 number 再比较;
如果一个操作数是 boolean,那么 boolean 转为 number 再比较;
如果一个操作数是对象,那么对象会转换为原始值。对象转换为原始值的调用顺序是:如果定义了 Symbol.toPrimitive 方法,则优先调用该方法。如果没有定义 Symbol.toPrimitive 方法,在默认情况下优先调用 valueOf() ,若不为基本类型,再调用 toString()。若为字符串上下文,则优先调用 toString()。Date 对象在默认情况下优先调用 toString()。
=== 与 Object.is() 是严格相等比较,不会进行隐式类型转换,要求值和类型完全相同。主要区别在于:
console.log(-0 === +0); // true
console.log(Object.is(+0, -0)); // false,因此开发中如需区分正负,应该使用Object.is()
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
另外,_.isEqual() 和 JSON.stringify() 是比较对象的实际内容:
consoole.log({} == {}); // false
console.log(Object.is({}, {})); // false
console.log(JSON.stringify({}) === JSON.stringify({})); // true
console.log(_.isEqual({}, {})); // true
位运算符
按位与(&)逐位比较,两个二进制位都为 1 才得 1,否则为 0。
按位或(|)逐位比较,只要有一个二进制位为 1 就得 1。
按位异或(^)逐位比较,相同为 0,不同为 1。
按位非(~)逐位取反:1 变 0,0 变 1。
for of 和 for in 的区别?
for of 一般用于循环数组,遍历的是数组成员,不能用于循环对象,因为对象没有 iterator 接口。for in 一般用于循环对象,遍历的是对象属性名。如果循环数组,遍历的是索引。
数组常用方法
-
遍历操作
forEach原地操作而不会返回新数组,reduce(),map(),filter(),entries()都基于回调函数,不会修改原数组,而是返回新数组或累加值。都不支持中断。for 循环for...in,for...of可以中断。 -
创建数组
// 1. 直接定义 const arr1 = [1, 2, 3]; // 2. 构造函数 Array(),使用或不使用 new 都可以 const fruits = new Array("Apple", "Banana"); const arrayEmpty = Array(7); // 单个数字参数将创造一个长度为数字的数组,元素全部为空 // 3. Array.from(arrayLike, mapFn, thisArg); // 参数分别表示类数组对象或可迭代对象,对每个元素执行的映射函数 (value, index) => { /* ... */ },this 值。 // 可迭代(iterable)对象,是实现了 Symbol.iterator 方法的对象。 // 类数组(Array-like)对象,是有索引和 length 属性的对象,所以它们看起来很像数组。 const arr3 = Array.from({ length: 5 }, (v, i) => i); // [0, 1, 2, 3, 4] const arr4 = Array.from("foo"); // [ "f", "o", "o" ] // 4. Array.of(element0, element1, /* … ,*/ elementN) const arr5 = Array.of(1, 2, 3); // [1, 2, 3] const arr6 = Array.of("foo"); // [ "foo" ] const arr7 = Array.of(7) // 传入单个数字参数,与Array()的区别在于会直接将其视为一个元素,即 [7] -
合并裁切
const fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"]; const slice = fruits.slice(1, 3); // ['Orange','Lemon'] const concat = fruits.concat(slice); const months = ["Jan", "March", "April", "June"]; months.splice(0, 1, "Feb"); // ['Feb', 'March', 'April', 'June'] splice用于原地修改数组,三个参数表示在index为0处删除1个元素之后插入"Feb" -
查找
let arr = Array.of(1, 2, 3, 4); // at() 接收一个整数值并返回该索引对应的元素 console.log(arr.at(-1)); // 4 等价于 arr[arr.length - 1] // find() 返回数组中满足函数的第一个元素,findLast() 反向迭代。 console.log(arr.find(item => item > 2)); // 3 console.log(arr.findLast(item => item > 2)); // 4 // findIndex() 返回数组中满足函数的第一个元素的索引,findLastIndex() 反向迭代。找不到返回 -1 console.log(arr.findIndex(item => item == 2)); // 1 console.log(arr.findLastIndex(item => item > 2)); // 3 // indexOf() 方法返回数组中第一次出现给定元素的下标,如果不存在则返回 -1。lastIndexOf() 反向迭代。 console.log(arr.indexOf(2)); // 1 console.log(arr.lastIndexOf(2)); // 1 // every() 测试一个数组内的所有元素是否都能通过指定函数的测试。返回布尔值。 console.log(arr.every(item => item > 3)); // false // some() 测试数组中是否至少有一个元素通过了由提供的函数实现的测试。返回布尔值。 console.log(arr.some(item => item > 3)); // true // includes() 方法用来判断一个数组是否包含一个指定的值。返回布尔值。 console.log(arr.includes(2)); // true -
修改排序
// sort() 就地排序,toSorted() 返回一个新数组。对数字数组不能直接 arr.sort() 因为这样是对数字的 unicode 排序 arr.sort((a, b) => a - b); // with() 方法是修改指定索引值,并返回新数组。(方括号原地修改指定索引值) let arr1 = arr.with(2, 6) console.log(arr1) // 1, 2, 6, 4
对象常用方法
-
创建对象
const obj1 = {}; const obj2 = new Object(); const obj3 = Object.create(null); // 指定原型对象,指定 null 用于创建纯净对象 const obj4 = new Person('John', 30); // 构造函数(ES5)或类继承(ES6) -
创建和修改属性
动态属性访问:通过方括号
[]访问的属性是动态属性。方括号内常常是变量或表达式,会在运行时计算得出并作为属性名。静态属性访问:通过点符号
.访问的属性是静态的。属性名是硬编码,且在编写代码时就已知,而且只能用于有效标识符(即必须以字母、下划线或美元符号开头,不能以数字开头,后续字符不能包含连字符-、点号.、空格等特殊字符,且不能是保留字)。// Object.defineProperties() 在一个对象上定义新的属性或修改现有属性,并返回该对象。 Object.defineProperties(obj, props) // Object.defineProperty() 在一个对象上定义一个新属性,或修改其现有属性,并返回该对象。 Object.defineProperty(obj, prop, descriptor) -
属性描述符
属性描述符是用于描述对象属性特性的信息。可以通过
Object.defineProperty()和Object.getOwnPropertyDescriptor()定义或读取这些元信息。属性描述符分为数据描述符和存取描述符两类。数据描述符有
value、writable、enumerable、configurable属性,存取描述符有get、set,也可带有enumerable和configurable属性。value:设置属性值,默认值为 undefned。writable:设置属性值是否能被修改,默认值为 true。enumerable:设置属性是否可枚举。比如,是否允许使用 for/in 语句遍历访问,默认为 true。configurable:控制的是属性的配置,包括是否可以更改属性描述符,以及是否可以从对象中删除属性。get:取值函数,当访问该属性时,该方法会被执行,默认为 undefined。set:存值函数,当属性值修改时,触发执行该方法,默认为 undefined。
-
枚举属性、判断对象是否具有某个属性
// Object.entries() 返回一个数组,包含对象自有的可枚举属性的键值对(不包含原型链中的属性)。 // 如果只需要键,使用 Object.keys();如果只需要值,使用 Object.values()。 // Object.getOwnPropertyNames() 用来遍历对象自身的属性(包含不可枚举属性),一般我们不希望获取不可枚举属性,所以较少使用。 for (const [key, value] of Object.entries(object1)) { console.log(`${key}: ${value}`); } // 静态方法 Object.hasOwn() 用于判断对象是否自有某一个属性,如果属性是该对象的直接属性,即使属性值是 null 或 undefined,返回 true;如果属性不存在或者是继承的,则返回 false。 console.log(Object.hasOwn(object1, "property1")); // true console.log(Object.hasOwn(object1, "toString")); // false // 实例方法 hasOwnProperty() 效果类似,但由于可能被覆盖、由 Object.create(null) 创建的纯净对象没有这个方法、判断 null 和 undefined 会报错。建议使用 Object.hasOwn()。 console.log(object1.hasOwnProperty("property1")); // true
Map 与 Set
// Map/Set与数组/字符串转换
const mapFromArray = new Map([["k1", "v1"], ["k2", "v2"]]);
const setFromArray = new Set(["v1", "v2"]);
const arrayFromMap = [...mapFromArray]; // 或 Array.from(mapFromArray)
const arrayFromSet = [...setFromArray]; // 或 Array.from(setFromArray)
const setFromString = new Set('hello'); // Set(4) {'h', 'e', 'l', 'o'}
// 克隆(浅拷贝)
const clonedMap = new Map(originalMap);
const clonedSet = new Set(originalSet);
// 合并(Map 重复键后者覆盖,Set 自动去重)
const mergedMap = new Map([...map1, ...map2]);
const mergedSet = new Set([...set1, ...set2]);
// 增删改查
map.set(key, value).delete(key).clear();
set.add(value).delete(value).clear();
Map 与 Object 的区别?
Map 和 Object 都是用来存储键值对的数据结构。
-
Map的键可以是任意类型(对象、函数、基本类型);Object的键只能是
string或symbol。 -
Map 保留元素顺序,内置迭代器;Object并不保证顺序,没有迭代器。
-
Map 可以通过
size属性获取键值对个数,Object只能遍历计算。 -
JSON 支持 Object,但不支持 Map,在某些需要用到 JSON 的情况下应该优先使用 Object。
-
构建方式:Map 只能使用构造函数定义;Object 与数组类似,可以直接定义或者使用构造函数(但均应该尽量避免使用构造函数定义)。
var map = new Map([1, 2], [2, 3]); var obj1 = {id : 1, name: 'test_obj1'}; var obj2 = new Object(); var obj3 = Object.create(null); // 这样定义的对象没有继承 Object.prototype 的属性和方法 -
遍历方式:
Map常用的遍历方式:
for...of、forEach()Object常用的遍历方式:
for...in、Object.keys()、Object.values()、Object.entries()
深拷贝和浅拷贝
浅拷贝是复制对象的一种方式,它创建一个新对象,并将原始对象的属性值复制到新对象中,但如果属性值是对象或数组,浅拷贝只会复制他们的引用,而不会递归复制内部的嵌套对象。深拷贝会递归地拷贝所有层级的数据,即使属性是引用类型,也会新建一个完全独立的副本。
浅拷贝对象的方法
const obj1 = { name: "张三", age: 25, info: { city: "北京" } };
// 1. 使用展开运算符进行浅拷贝
const obj2 = { ...obj1 };
·
// 2. Object.assign() 拷贝一个或者多个对象中的可枚举的自有属性
const obj3 = Object.assign({}, obj1);
// 3. for in 遍历
const obj4 = {};
for (const key in obj1) {
if (obj1.hasOwnProperty(key)) { // 只复制对象本身的属性,不复制原型链上的属性
obj4[key] = obj1[key];
}
}
// 4. Object.keys()
const obj5 = {};
Object.keys(obj1).forEach((key) => { // Object.keys只遍历obj1本身的属性
obj5[key] = obj1[key];
})
浅拷贝数组的方法
const arr1 = [1, 2, 3];
const arr2 = arr1.slice();
const arr3 = [].concat(arr1);
深拷贝对象的方法
const obj1 = { name: "张三", age: 25, info: { city: "北京" } };
// 1. 使用 JSON.parse(JSON.stringify(obj)) 缺点:无法拷贝函数、Date、正则表达式;不会拷贝原型链上的属性;会忽略symbol和undefined属性
// JSON.stringify() 用于将对象或数组转换为JSON格式字符串。语法:JSON.stringify(value, replacer, space)
// value 必选,要转换为 JSON 字符串的值(对象、数组等)。
// replacer 可选,可以是一个函数或数组,用于控制哪些属性应该被转换。
// space 可选,用于格式化输出的缩进空格数量(number 或 string)。
const obj2 = JSON.parse(JSON.stringify(obj1));
// 2. 使用 lodash.cloneDeep()
const obj3 = _.cloneDeep(obj1);