web前端面试笔记
1.v-if和v-show的区别
特性 | v-if | v-show |
DOM 元素是否存在 | 条件为 false 时不存在 | 始终存在 |
初始渲染开销 | 条件为 false 时开销低 | 始终有渲染开销 |
状态保留 | 销毁后重新创建,状态丢失 | 状态保留(display:none) |
频繁切换性能 | 开销大 | 开销小 |
适用场景 | 条件不频繁变化 | 频繁切换显示状态 |
2.computed和watch的区别
对比项 | computed | watch |
功能侧重点 | 计算衍生数据,适用于一个数据依赖多个数据计算出新值 | 关注数据变化,用于数据变化时执行副作用操作,比如异步请求、操作DOM、打印日志等,watch是更好的选择 |
缓存机制 | 有缓存,依赖数据不变时,多次访问直接返回缓存结果,不会重复计算,只有它依赖的数据发生变化时,它才会重新计算。 | 无缓存,监听数据变化就执行回调,无论新值和旧值是否相同 |
数据来源 | 依赖其他响应式数据进行计算。不能直接修改依赖的数据,只能返回计算结果。 | 可监听任何响应式数据的变化,如 data、props、computed 等。并且在回调函数中可以对监听的数据进行进一步处理,甚至修改其他数据。 |
返回值 | 必须有返回值,为计算结果,会缓存起来 | 回调函数本身无返回值,主要用来执行无副作用操作,如异步请求,修改其他数据状态等。 |
使用语法 | 在 computed 选项中以对象形式定义,属性值为计算函数,属性名为计算属性名,函数返回计算结果。 | 在 watch 选项中以对象形式定义,属性值可为函数或含 handler、immediate\deep 等选项的对象,属性名为要监听的数据 |
应用场景 | 计算购物车总价、根据多个状态计算某个综合状态等 | 监听用户登录状态变化做相应操作、搜索框输入变化触发搜索请求等 |
3.Vue3和Vue2的区别
对比项 | Vue 2 | Vue 3 |
性能 | 使用 Object.defineProperty 实现响应式,深度监听需递归遍历,对大型数据结构性能有影响;细粒度更新不完善,可能导致不必要重渲染 | 基于 Proxy 实现响应式,直接监听属性增删,数组监听更原生;虚拟 DOM 重写,diff 算法优化,渲染性能更高 |
API 设计 | 主要使用 Options API,按 data 、 methods 等选项组织逻辑,组件复杂时代码冗长,逻辑分散 | 引入 Composition API,按功能组织逻辑,提高代码复用性和可维护性;生命周期钩子在 setup 中使用 on + 钩子函数名 形式,新增 onRenderTracked 和 onRenderTriggered 钩子 |
响应式原理 | 利用 Object.defineProperty 为属性定义 getter 和 setter 实现数据劫持,无法检测属性增删,数组需重写变异方法实现响应式 | 使用 Proxy 创建代理对象实现深度监听,结合 Reflect 转发操作,能直接监听属性增删,数组监听更自然 |
模板语法 | 具名插槽用 定义,作用域插槽通过 slot - scope 获取数据 | 具名插槽用 ( v - slot 可简写为 # ),作用域插槽通过解构更简洁获取数据;进行静态提升和预字符串化优化 |
组件 | 组件模板必须有单个根元素包裹 | 支持多根节点;新增 Teleport 组件可将模板渲染到其他 DOM 位置 |
打包体积 | 打包体积相对较大 | 采用 tree - shaking 技术,未使用代码不打包,体积更小 |
4.箭头函数与普通函数的对比
对比项 | 普通函数 | 箭头函数 |
语法形式 | 使用 function 关键字定义,有完整的参数列表和函数体 function add(a, b) { return a + b; } | 语法简洁,省略 function 关键字,用 => 定义 一个参数时参数括号可省,函数体一条语句时花括号和 return 可省 const square = num => num * num; |
this 指向 | 调用时确定,取决于调用方式。全局调用指向全局对象,作为对象方法调用指向该对象 | 定义时确定,继承自外层作用域的 this |
arguments 对象 | 函数内部有 arguments 类数组对象,包含所有传入参数 | 无自身的 arguments 对象,访问外层作用域的 arguments ,可通过剩余参数获取自身参数 |
new 关键字 | 可使用 new 关键字创建实例对象,作为构造函数 | 不能使用 new 关键字调用,无 prototype 属性 |
函数名 | 有自己的名字,可通过 name 属性获取 | name 属性通常为 "" ,部分环境调试工具可能给出类似 名称 |
5.v-for中的:key作用是什么,如果设置一样的key会怎么样
- key 的作用
- 高效更新虚拟 DOM:key 主要用于帮助 Vue 区分列表中的各个节点,以便在数据变化时,通过 key 更高效地对比新旧虚拟 DOM 树,从而精准地找到需要更新的节点,最小化 DOM 操作,提升渲染性能。例如,有一个列表展示用户信息,每个用户对象有唯一的 id,将 id 作为 key,当用户数据变化时,Vue 能快速定位到具体发生变化的用户节点,只更新该节点对应的 DOM,而不是重新渲染整个列表。
- 维护状态:在列表渲染时,key 可以确保每个节点的状态在更新过程中得以正确维护。比如,一个包含输入框的列表,每个输入框有其输入状态,如果给每个列表项设置唯一的 key,当列表数据变化时,输入框的输入状态能正确保留;若没有 key 或 key 不唯一,可能会出现输入状态错乱的情况。
- 设置一样的 key 值会怎么样
- 性能问题:Vue 无法准确判断哪些节点发生了变化,可能会导致不必要的 DOM 重新渲染。例如,一个商品列表,本应该只更新某个商品的价格,但由于 key 值相同,Vue 可能会认为整个列表都发生了变化,从而重新渲染所有商品的 DOM,浪费性能。
- 状态维护错乱:如前面提到的带输入框的列表例子,相同的 key 会使 Vue 混淆各个节点的状态,导致输入框的输入状态在更新后出现错乱,用户输入的数据可能对应到错误的节点上。
在使用
v - for 时,必须为列表项设置唯一的
key 值,这是保证 Vue 高效渲染和正确维护列表状态的关键
6.display:none
在前端CSS中, display: none 是一个用于控制元素显示与隐藏的属性值。
作用:
- 当元素应用 display: none 后,该元素及其所有子元素会从文档流中完全移除。这意味着元素在页面上不会占据任何空间,就好像这个元素在HTML文档中根本不存在一样。例如一个按钮元素设置 display: none ,不仅按钮本身看不见,原本按钮所占的位置也会被其他元素填充。
- 对应用了 display: none 的元素,无法触发其相关的用户交互事件,如点击、悬停等,因为从布局角度它已不存在。
应用场景:
- 初始隐藏元素:在页面加载时,某些元素可能不需要立即显示,例如一些提示框、模态框等,初始设置 display: none ,在特定条件下(如用户点击按钮)通过JavaScript动态将其显示。
- 根据设备或屏幕尺寸隐藏元素:在响应式设计中,通过媒体查询结合 display: none ,可以针对不同屏幕尺寸隐藏特定元素。比如在手机屏幕上隐藏某些只适合在桌面端展示的导航栏部分。
与其他隐藏方式对比:
- 与 visibility: hidden 不同, visibility: hidden 虽然让元素不可见,但元素依然占据文档流中的空间,并且可以触发其相关的用户交互事件(尽管不可见)。而 display: none 使元素完全脱离文档流。
- 与 opacity: 0 也有区别, opacity: 0 只是将元素透明度设为0使其看不见,但元素仍在文档流中占据空间,且可以响应交互事件,类似于元素处于“透明”状态。
7.vue的响应式表现在哪里
1. 数据变化自动更新视图
- 当 Vue 实例的数据发生变化时,与之绑定的 DOM 元素会自动更新。例如在模板中有
{{ message }}
,在 data 中定义 message: '初始值'。若后续通过 this.message = '新值' 改变 message 的值,页面上显示的内容会立即从 “初始值” 变为 “新值”。这得益于 Vue 的数据劫持和发布 - 订阅模式,数据变化时通知相关依赖(即使用该数据的 DOM 元素对应的渲染函数)进行更新。 - 在复杂的组件结构中同样如此,比如组件有多层嵌套,子组件依赖父组件传递的 props 数据,当父组件中该 props 数据变化,子组件视图也会自动更新。
2. 深度监听对象和数组变化
- 对象:
- Vue 2:通过 Object.defineProperty 对对象属性进行遍历,为每个属性定义 getter 和 setter 实现数据劫持。但它无法检测对象属性的新增和删除,需要使用 Vue.set 和 Vue.delete 方法来触发响应式更新。例如:
// Vue 2 示例 const vm = new Vue({ data: { user: { name: 'John' } } }); // 直接添加属性不会触发视图更新 // vm.user.age = 30; // 使用 Vue.set 才会触发 Vue.set(vm.user, 'age', 30);
- Vue 3:使用 Proxy 实现深度监听,可直接检测对象属性的新增、删除以及属性值变化。例如:
// Vue 3 示例 import { reactive } from 'vue'; const state = reactive({ user: { name: 'John' } }); // 直接添加属性会触发视图更新 state.user.age = 30;
- 数组:
- Vue 2:重写数组的变异方法(如 push、pop、shift、unshift、splice、sort、reverse),当调用这些方法时,会触发视图更新。例如:
// Vue 2 示例 const vm = new Vue({ data: { list: [1, 2, 3] } }); vm.list.push(4); // 数组变化,视图更新
- Vue 3:基于 Proxy 能更自然地监听数组变化,除了变异方法,对数组元素的直接赋值等操作也能被检测到并触发更新。例如:
// Vue 3 示例 import { reactive } from 'vue'; const state = reactive({ list: [1, 2, 3] }); state.list[0] = 10; // 数组元素变化,视图更新
3. 依赖收集与更新
- Vue 会在组件渲染过程中进行依赖收集。当数据被读取(如在模板中使用数据或在计算属性中依赖数据)时,会将使用该数据的地方(通常是组件的渲染函数或计算属性函数)收集为依赖。
- 当数据发生变化时,会通知这些依赖进行更新。比如一个计算属性 computedValue 依赖于 data 中的 value1 和 value2,在计算 computedValue 时,会收集 computedValue 的计算函数作为 value1 和 value2 的依赖。当 value1 或 value2 变化,就会通知 computedValue 重新计算,并更新相关视图。
4. 计算属性和监听器的响应式表现
- 计算属性:具有缓存机制,只有依赖的数据发生变化时才会重新计算。例如计算购物车商品总价的计算属性,若商品价格和数量不变,多次访问该计算属性会直接返回缓存结果,不会重复计算。但只要商品价格或数量变化,计算属性就会重新求值,并更新相关视图。
- 监听器:用于监听数据变化并执行副作用操作。当监听的数据变化时,无论新值与旧值是否相同(可通过配置选项调整),都会执行相应的回调函数,比如监听用户输入的搜索关键词变化,在回调中发起搜索请求。
8.常见状态码
1xx(信息性状态码)
表示服务器已接收请求,正在处理:
- 100 Continue:服务器已接收请求头部,客户端可继续发送请求体。
2xx(成功状态码)
表示请求已成功处理:
- 200 OK:请求成功,服务器返回对应资源(最常见)。
- 201 Created:请求成功且服务器创建了新资源(如 POST 提交表单创建数据)。
- 204 No Content:请求成功,但服务器无返回内容(如 DELETE 请求)。
3xx(重定向状态码)
表示需要客户端进一步操作以完成请求:
- 301 Moved Permanently:资源永久移动到新 URL,后续请求应使用新地址。
- 302 Found:资源临时移动,暂时使用新 URL(历史上可能被滥用为临时重定向)。
- 304 Not Modified:资源未修改,客户端可使用本地缓存(常用于缓存优化)。
4xx(客户端错误状态码)
表示客户端请求存在错误:
- 400 Bad Request:请求语法错误或参数无效,服务器无法理解。
- 401 Unauthorized:请求需要身份验证(如未登录时访问需权限的接口)。
- 403 Forbidden:服务器拒绝请求(已验证身份,但无权限)。
- 404 Not Found:服务器未找到请求的资源(URL 错误或资源已删除)。
- 405 Method Not Allowed:请求方法不被允许(如用 POST 访问仅支持 GET 的接口)。
5xx(服务器错误状态码)
表示服务器处理请求时发生错误:
- 500 Internal Server Error:服务器内部错误(最常见的服务器端错误)。
- 502 Bad Gateway:服务器作为网关 / 代理时,收到上游服务器的无效响应。
- 503 Service Unavailable:服务器暂时无法处理请求(如维护中)。
9.什么是原型链?请简要描述 JavaScript 是如何通过原型链实现继承的?
- 原型链:每个 JavaScript 对象都有一个 __proto__ 属性,指向其原型对象。原型对象本身也是一个对象,也有自己的 __proto__ 属性,这样就形成了一条从对象到其原型,再到原型的原型,直至 null 的链条,这就是原型链。
- 继承实现:通过将子构造函数的原型指向父构造函数的实例,子构造函数的实例就可以访问父构造函数原型上的属性和方法,从而实现继承。例如:
function Parent() { this.name = 'parent'; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child() { Parent.call(this); this.age = 10; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; const child = new Child(); child.sayName();
10.说说你对 async/await 的理解,它与 Promise 有什么关系?
- async/await 理解:async 函数是异步函数,它返回一个 Promise 对象。await 只能在 async 函数内部使用,用于暂停 async 函数的执行,等待一个 Promise 对象解决(resolved)或拒绝(rejected),然后恢复 async 函数的执行,并返回 Promise 对象的解决值。
- 与 Promise 关系:async/await 是基于 Promise 实现的更高级的异步处理语法糖。它让异步代码看起来像同步代码,提高了代码的可读性和可维护性。例如:
async function getData() { try { const response = await fetch('https://example.com/api'); const data = await response.json(); return data; } catch (error) { console.error(error); } }
11.Vuex 的状态持久化如何实现?
- Vuex 的状态持久化如何实现?
- 这考查对 Vuex 和数据持久化的掌握。常见方法是使用插件,如 vuex - persistedstate。它可以将 Vuex 中的状态保存到本地存储(localStorage 或 sessionStorage)中,在页面刷新或重新加载时恢复状态。安装插件后,在 Vuex 的配置文件中引入并配置,指定要持久化的状态模块等。另外,也可以手动在 beforeunload 事件中保存 Vuex 状态到本地存储,在页面加载时读取并设置回 Vuex 状态。
- 在 Vue 组件中,data 为什么必须是一个函数,而不能是一个对象?
- 因为 Vue 组件可能会被多次复用,如果 data 是一个对象,那么所有组件实例将共享同一个 data 对象,一个组件对 data 的修改会影响到其他组件。而 data 是函数时,每次创建组件实例都会调用这个函数,返回一个全新的 data 对象,保证每个组件实例都有自己独立的 data 状态,互不干扰。
12.简述 CSS 的层叠性、继承性以及优先级是如何计算的?
- 层叠性:指当多个 CSS 规则应用到同一元素时,浏览器如何处理这些规则的冲突。
- 继承性:一些 CSS 属性(如 color、font - family 等)会自动从父元素继承到子元素。
- 优先级:通过选择器的特殊性(内联样式 > ID 选择器 > 类选择器、属性选择器、伪类选择器 > 标签选择器、伪元素选择器)、重要性(!important 声明)以及出现顺序(后定义的样式覆盖先定义的样式)来计算。
13.可继承的CSS样式和不可继承的CSS样式
- 可继承的 CSS 样式
- 文本相关样式:
- 字体相关:
- font - family(字体系列)、font - size(字体大小)、font - weight(字体粗细)、font - style(字体样式,如斜体)等。例如,若在 body 标签设置 font - family: Arial, sans - serif;,那么 body 内所有子元素,如 p、h1 等标签的文本都会使用该字体系列,除非子元素自己重新定义了 font - family。
- 文本颜色:
- color 属性
- 比如在父元素设置 color: blue;,子元素文本颜色会继承为蓝色,除非子元素另行设置。
- 文本装饰:text - decoration(如 underline、overline、line - through 等),若父元素设置 text - decoration: underline;,子元素文本也会有下划线效果,除非子元素重新设置。
- 列表样式:
- list - style - type(列表项标记类型,如 disc、circle、square、decimal 等)、list - style - position(列表项标记的位置,如 inside、outside)等。如果在 ul 或 ol 父元素设置 list - style - type: square;,其内部的 li 元素会继承该样式,显示为方形列表项标记。
- 表格相关样式(部分):
- border - collapse(设置表格边框是否合并)在一定程度上具有继承性。例如,在 table 元素设置 border - collapse: collapse;,表格内部的 tr、td 等元素会受此样式影响。
- 文本相关样式:
- 不可继承的 CSS 样式
- 盒模型相关样式:
- 尺寸相关:
- width(宽度)、height(高度)、padding(内边距)、margin(外边距)、border(边框)等。例如,父元素设置 width: 200px;,子元素不会继承这个宽度,子元素需要单独设置自己的宽度。
- 盒模型类型:display 属性(如 block、inline、flex 等)不继承。父元素设置为 display: flex;,子元素不会自动变为 flex 布局,需要单独为子元素设置合适的 display 值。
- 定位相关样式:
- position(如 static、relative、absolute、fixed)、top、bottom、left、right、z - index 等。例如,父元素设置 position: relative; top: 10px;,子元素不会继承这些定位属性,子元素如需定位需单独设置。
- 背景相关样式:
- background - color(背景颜色)、background - image(背景图像)、background - repeat(背景重复方式)等。父元素设置 background - color: red;,子元素不会继承该背景颜色,子元素需要单独设置自己的背景样式。
- 布局相关样式:
- float(浮动)、clear(清除浮动)、overflow(溢出处理)等。例如,父元素设置 float: left;,子元素不会自动浮动,需单独设置 float 属性。
- 盒模型相关样式:
14.Promise 和 Async / Await
Promise
Promise 是 JavaScript 中用于处理异步操作的对象,它能避免多层嵌套的回调函数(“回调地狱”),让异步代码更清晰。
- 三种状态
- pending(进行中):初始状态,操作未完成。
- fulfilled(已成功):操作完成,会触发 then 方法。
- rejected(已失败):操作出错,会触发 catch 方法。
- 基本用法
const promise = new Promise((resolve, reject) => { // 异步操作(如请求数据) if (操作成功) { resolve(结果); // 状态变为 fulfilled } else { reject(错误信息); // 状态变为 rejected } }); promise.then(result => { console.log("成功:", result); }).catch(error => { console.log("失败:", error); });
Async / Await
Async / Await 是基于 Promise 的语法糖,让异步代码写法更接近同步代码,可读性更高。
- 使用规则
- async 用于修饰函数,使其返回一个 Promise 对象。
- await 只能在 async 函数内部使用,用于等待一个 Promise 完成(得到结果或错误)。
- 基本用法
async function fetchData() { try { const result = await promise; // 等待 Promise 完成 console.log("成功:", result); } catch (error) { console.log("失败:", error); } } fetchData();
(相当于用 try/catch 替代了 Promise 的 then/catch)
15.深拷贝
对象和数组的深拷贝是指完全复制其所有层级的内容,确保原数据和拷贝数据相互独立(修改一方不会影响另一方)。常用的深拷贝方法可分为简易方法和复杂场景方法,具体如下:
一、简易方法(适合基础数据类型场景)
1. JSON 序列化与反序列化通过
JSON.stringify() 将对象 / 数组转为 JSON 字符串,再用
JSON.parse() 转回对象 / 数组,实现深拷贝。
示例:
const originalObj = { name: "张三", info: { age: 20 }, hobbies: ["篮球", "游戏"] }; const deepCopyObj = JSON.parse(JSON.stringify(originalObj)); // 修改拷贝对象,原对象不受影响 deepCopyObj.info.age = 21; deepCopyObj.hobbies.push("跑步"); console.log(originalObj.info.age); // 20(原对象未变) console.log(originalObj.hobbies); // ["篮球", "游戏"](原数组未变)
优点:简单快捷,适合纯 JSON 数据(数字、字符串、布尔、数组、普通对象)。
缺点:
- 无法拷贝特殊类型:函数、正则表达式、Date 对象(会被转为字符串)、Symbol、Map、Set 等;
- 会忽略 undefined 和 Symbol 类型的属性;
- 循环引用的对象会报错(如 obj.self = obj)。
2. 数组专用:Array.prototype.concat() 或扩展运算符(仅表层深拷贝,嵌套数组需注意)
这两种方法对一维数组是深拷贝,但对嵌套数组是浅拷贝(仅复制引用),需结合场景使用。
// 一维数组:深拷贝 const arr1 = [1, 2, 3]; const copy1 = [...arr1]; // 或 arr1.concat() copy1[0] = 100; console.log(arr1[0]); // 1(原数组未变) // 嵌套数组:仅表层深拷贝,内层是浅拷贝 const arr2 = [1, [2, 3]]; const copy2 = [...arr2]; copy2[1][0] = 200; console.log(arr2[1][0]); // 200(原数组被修改,因为内层数组是引用)
适用场景:仅一维数组,且元素为基本类型。
二、复杂场景方法(处理特殊类型、循环引用等)
1. 递归实现深拷贝(基础版)
通过递归遍历对象 / 数组的每一层,手动复制所有属性,支持大多数普通类型。
function deepClone(target) { // 非对象类型(基本类型、null)直接返回 if (target === null || typeof target !== 'object') { return target; } // 初始化拷贝对象(区分数组和普通对象) let cloneTarget = Array.isArray(target) ? [] : {}; // 遍历目标对象的所有自有属性 for (let key in target) { if (target.hasOwnProperty(key)) { // 递归拷贝子属性(处理嵌套层级) cloneTarget[key] = deepClone(target[key]); } } return cloneTarget; }
测试示例:
const obj = { a: 1, b: { c: 2 }, d: [3, 4] }; const cloneObj = deepClone(obj); cloneObj.b.c = 200; cloneObj.d[0] = 300; console.log(obj.b.c); // 2(原对象未变) console.log(obj.d[0]); // 3(原数组未变)
优点:支持普通对象、数组的嵌套拷贝。
缺点:
- 不支持 Date、RegExp、Map、Set 等特殊对象;
- 无法处理循环引用(如 obj.self = obj 会导致递归死循环)。
2. 递归实现深拷贝(增强版,处理特殊类型和循环引用)在基础版递归的基础上,增加对特殊类型的判断和循环引用的处理(用
WeakMap 缓存已拷贝的对象)。
实现代码:
function deepClone(target, map = new WeakMap()) { // 处理基本类型和null if (target === null || typeof target !== 'object') { return target; } // 处理循环引用(如果已拷贝过,直接返回缓存的结果) if (map.has(target)) { return map.get(target); } let cloneTarget; // 处理 Date 类型 if (target instanceof Date) { cloneTarget = new Date(target); map.set(target, cloneTarget); return cloneTarget; } // 处理 RegExp 类型(复制正则的源文本、修饰符) if (target instanceof RegExp) { cloneTarget = new RegExp(target.source, target.flags); map.set(target, cloneTarget); return cloneTarget; } // 处理数组和普通对象 cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); // 缓存已拷贝的对象,避免循环引用 // 遍历并递归拷贝属性(包括 Symbol 类型的键) Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key], map); }); return cloneTarget; }
测试特殊类型:
const obj = { date: new Date(), reg: /abc/g, arr: [1, { x: 2 }] }; obj.self = obj; // 循环引用 const cloneObj = deepClone(obj); console.log(cloneObj.date instanceof Date); // true(正确拷贝Date) console.log(cloneObj.reg.source); // 'abc'(正确拷贝正则) console.log(cloneObj.self === cloneObj); // true(正确处理循环引用)
优点:支持 Date、RegExp、循环引用等场景,适用性更广。
缺点:仍需根据实际需求扩展(如 Map、Set 等类型需额外处理)。
3. 使用第三方库
成熟的库已封装完善的深拷贝逻辑,适合生产环境。
- Lodash 的 _.cloneDeep():支持几乎所有 JavaScript 类型,包括 Map、Set、Symbol、循环引用等。
import _ from 'lodash'; const obj = { a: 1, b: { c: 2 } }; const cloneObj = _.cloneDeep(obj);
- Immer:通过 “修改草稿” 的方式生成新对象,间接实现深拷贝(适合状态管理)。
总结
方法 | 适用场景 | 缺点 |
JSON 序列化 | 纯 JSON 数据(无特殊类型) | 不支持函数、正则、循环引用等 |
基础递归 | 普通对象 / 数组(无特殊类型) | 不支持特殊类型和循环引用 |
增强递归 | 含特殊类型、循环引用的场景 | 需手动扩展更多类型(如 Map、Set) |
Lodash _.cloneDeep | 生产环境、复杂类型 | 需引入第三方库 |