2024前端面试题总结

362 阅读21分钟

简单描述从输入url到页面渲染期间发生了什么

网络请求

  • DNS 解析,要把域名解析成IP地址,浏览器会先向本地缓存查找是否有缓存好的IP地址,没有缓存就会进行递归解析。详细描述
  • 建立TCP连接(三次握手),通过 TCP 协议与服务器建立连接,如果是 HTTPS 请求,还会进行 SSL/TLS 握手,建立加密的连接。详细描述
  • 发送HTTP请求,建立连接后,浏览器构建 HTTP 请求,如果是https请求还需要多加一个TLS进行加密,HTTPS=HTTP+TLS,包括请求行(请求方法、URL、HTTP 版本)、请求头部(请求头字段)和请求体(如 POST 请求的数据)
  • 服务器处理请求,服务器接收到请求后解析请求头和请求体,执行对应得业务逻辑,生成相应内容等
  • 服务器响应,服务器处理完成后,向浏览器发送 HTTP 响应,包括状态码、响应头部和响应体(HTML 文档)。

渲染页面

  • 浏览器解析html文件构建,解析CSS 文件,构建 CSSOM(CSS 对象模型)
  • DOM树和CSSOM合并成渲染树
  • 计算布局layout(重排或者回流)
  • 绘制页面(重绘),览器使用渲染树来绘制页面上的内容,这个过程称为“栅格化”
  • 页面渲染完成

跨域

跨域问题是前端开发中常见的问题,主要是由于浏览器的同源策略导致的。同源策略是为了保证用户信息的安全,防止恶意网站窃取数据。如果它们的协议(protocol)、域名(host)和端口号(port)都相同则被认为是同源的。当一个网页尝试请求另一个域的资源时,就会遇到跨域问题

常见的跨域解决方案

  • JSONP(JSON with Padding):通过动态创建 <script> 标签实现跨域请求,它利用 <script> 标签可以加载不同域下的资源的特性。但 JSONP 只支持 GET 请求,并且存在安全隐患,是一种早期的解决跨域的方法
//myCallback 是客户端定义的一个函数,服务器返回的数据作为参数传递给它。
//客户端请求:
<script src="http://zhumimi.com/api/data?callback=myCallback"></script>

//服务器响应:
myCallback({ "name": "John", "age": 30 });
  • CORS(Cross-Origin Resource Sharing):通过服务器设置响应头 Access-Control-Allow-Origin 来允许或限制跨域请求。这是一种官方推荐的跨域解决方案,支持所有类型的 HTTP 请求
  • 代理服务器:通过在服务器端设置代理,将前端请求转发到目标服务器上,从而避免跨域问题。这种方法会增加服务器的负担,但可以很好地解决跨域问题
  • Nginx 反向代理:在服务器端使用 Nginx 等反向代理服务器,将前端的请求转发到后端服务器上,并将后端服务器的响应结果返回给客户端,这样,客户端并不直接和后端服务器通信,实现跨域访问
  • WebSocket:WebSocket 是一种网络通信协议,可以实现浏览器与服务器之间的全双工通信,不受同源策略限制,因此可以用于跨域通信,但他本身并没有直接解决跨域问题的机制,但在一定程度上绕过传统的 HTTP 跨域限制。这主要是因为 WebSocket 的握手过程使用了 HTTP,但在连接建立后,通信不再使用 HTTP 请求,因此不受浏览器的同源策略限制。
  • postMessage:这是 HTML5 提供的 API,允许来自不同源的页面间相互通信。通过 window.postMessage 方法,页面可以发送消息给其他页面,实现跨域通信
  • document.domain + iframe:当两个页面的主域相同,子域不同时,可以通过设置 document.domain 属性为相同的主域来实现跨域通信
  • location.hash + iframe:利用不同域的 iframe 的 hash 值进行通信,通过同域的页面来中转数据,实现跨域通信
  • window.name + iframe:利用 window.name 属性在不同页面间传递数据,可以实现不同域之间的数据传递

简单介绍下Vue

Vue是一款基于MVVM架构的渐进式框架,它主要用于构建单页面应用(spa),它的特点有声明式渲染、响应式两大点。

MVVM

MVVM(Model-View-ViewModel)是一种软件架构设计模式,主要用于构建用户界面。它将应用程序分为三个核心组件:模型(Model)、视图(View)和视图模型(ViewModel)。这种模式特别适用于开发具有复杂用户界面的应用程序,如桌面应用程序和移动应用程序。

  • 模型(Model):代表应用程序的数据逻辑和业务逻辑。它直接与数据库或后端服务进行交互,负责数据的存储、检索和更新。
  • 视图(View):是用户界面的表示层,负责显示数据(即模型)并收集用户的输入。在MVVM中,视图不直接与模型交互,而是通过视图模型进行通信。
  • 视图模型(ViewModel):充当视图和模型之间的中介。它从模型中获取数据,并将这些数据转换为视图可以显示的格式。同时,它也处理用户的输入,将这些输入转换为模型可以理解的命令。

优势

  1. 分离关注点:将业务逻辑、数据和用户界面分离,使得代码更易于管理和维护。
  2. 提高可测试性:由于业务逻辑被封装在模型中,可以更容易地进行单元测试。
  3. 提高代码重用性:视图模型可以独立于特定的视图技术,这意味着你可以在不同的视图(如Web、桌面或移动)之间重用相同的业务逻辑。
  4. 提高用户界面的响应性:通过数据绑定,视图可以自动更新以反映模型的变化,从而提高应用程序的响应性。

渐进式框架

渐进式框架指的就是一种框架概念,一般来说,使用渐进式框架时,无需引入其所有功能,而是需要什么就用什么,就拿Vue来说,我们可以引入一个vue.js的文件,然后在其它框架中去使用Vue,也可以使用它的脚手架,来进行构建一个Vue项目,这完全取决于用户想怎么使用,而框架为我们提供了多种使用方式以及各个模块的功能。

优势

  • 灵活:开发者可以按需引入框架的各个功能;
  • 可维护性:开发者可以先少量引入框架部分功能,然后在需要的时候引入其它功能,防止项目从一开始就变得结构复杂、难以维护。

SPA单页面

单页应用程序(SPA)是一种现代Web应用模型,它通过动态重写当前页面来与用户交互,避免了页面之间切换打断用户体验。它的意思就是一个网站中,只有一个HTML文件,用户在进行页面交互时,或者刷新页面时,只是利用JavaScript动态变换HTML的内容,而并非真正意义上的变换页面。

优势

  • 良好的交互体验:因为用户在交互时,只是动态刷新局部内容,并不用请求新的HTML文件,因此也就不会造成长时间的页面白屏;
  • 良好的工作模式:更好的实现前后端分离,让不同岗位的工程师专注于自己的领域,提升代码的性能以及复用性;
  • 路由:使用前端路由,通过浏览器的API来模拟前进后退操作,让用户在使用感知上并无变化。

缺点

  • 初始加载速度较慢:由于SPA需要在加载页面时加载所有的HTML,JavaScript和CSS,因此初始加载速度可能会较慢。
  • 前进和后退按钮可能无法正常工作:由于SPA不刷新页面,因此前进和后退按钮可能无法正常工作。
  • 难以进行代码拆分和模块化:由于SPA将所有的代码都放在一个页面上,因此难以进行代码拆分和模块化。
  • 为了实现SPA,通常需要使用前端框架,如Angular、React或Vue.js。这些框架提供了路由管理、组件生命周期管理等功能,使得开发SPA更为便捷。例如,使用React创建SPA时,可以通过ReactDOM.render方法将应用渲染到DOM中,并通过BrowserRouter来管理路由。

改进

  • 代码压缩与混淆:使用工具(如UglifyJS)对代码进行压缩与混淆,减小文件体积。
  • 减少HTTP请求:合并和压缩文件,减少页面所需的HTTP请求数。
  • 延迟加载:将不必要立即加载的资源(如图片、脚本)进行延迟加载。
  • 使用CDN:使用内容分发网络(CDN)加速静态资源的加载。
  • 服务端渲染(SSR):在服务器端进行页面渲染,减轻客户端负担。

声明式渲染

它是一种构建用户界面的方法,声明式渲染中,你只需要描述数据和视图之间的映射关系,而不需要编写额外的命令式代码来操作 DOM。

响应式

响应式就是在我们修改数据之后,无需手动触发视图更新,视图会自动更新。

<div id="app">
  {{ message }}
  <input v-model="inputValue" />
  <ul>
    <li v-for="(item, index) in list" :key="index">{{ item }}</li>
  </ul>
  <button v-on:click="reverseMessage">反转消息</button>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!',
    inputValue: '',
    list: ['Apple', 'Banana', 'Cherry']
  },
  methods: {
    reverseMessage: function() {
      this.message = this.message.split('').reverse().join('');
    }
  }
});
</script>

Vue2和vue3的响应式

Vue2

vue2中响应式系统是通过依次遍历data返回的对象,将里面每一个属性通过Object.defineProperty进行定义,然后在属性描述符中添加get/set,实现getter/setter方法,在访问属性时,在getter函数中收集依赖(记录哪些方法或变量在使用这个属性),在修改属性时,在setter函数中派发依赖(将收集到的依赖依次更新),从而达到响应式。

缺点

  • Object.defineProperty只能对对象的属性进行监听,也就是说当我们想对某个对象进行监听时,必须将这个对象遍历,然后对其中的每一个属性进行监听。如果说对象中的某个属性又是一个对象,那就需要递归遍历,将每一层都进行监听,这样的性能肯定是比较低的。
  • Object.defineProperty只能对已有属性进行监听,也就是说,在Vue2中,created()阶段Vue内部已经帮我们把data中的属性遍历完毕并且对每个属性进行监听了,如果在之后的阶段我们给某个对象使用obj.xx的方式给对象添加了一个新属性,这个属性就不再是响应式了,这也是为什么我们在添加新属性时,需要使用this.$set的方式。
  • Object.defineProperty不能监听数组长度的改变,这也就造成了我们在使用一些影响原数组的数组方法比如pop、shift、push时,它监听不到,如果需要监听数组内容变化,需要像对象一样,把数组进行遍历,然后对每一个索引值进行监听。如果对数组进行遍历监听每一项,代价无疑是巨大的。

Vue3

Vue3的响应式系统是通过 ES6 的 Proxy 实现的。Proxy 可以拦截对象的几乎所有操作,包括属性访问、赋值、枚举、删除等。

  • 监听变化:Proxy 可以用来监听对象和数组的变化。Vue 3 利用这一点,当数据变化时,可以自动更新视图。
  • 拦截方法数量:Proxy 有 14 种拦截方法(handler traps)。这些方法包括 get、set、deleteProperty、has、ownKeys 等。
  • handler.get:在 handler.get 陷阱中,Vue 3 可以实现依赖收集,即当访问某个属性时,将当前的渲染效果(如组件的渲染函数)作为依赖记录下来。
  • handler.set:在 handler.set 陷阱中,Vue 3 可以实现派发更新,即当属性被设置一个新的值时,通知所有依赖于该属性的渲染效果进行更新。
  • 响应式转换:Vue 3 还提供了 reactive 和 ref 函数来将普通对象和数组转换为响应式对象。这些函数内部使用 Proxy 来实现响应式行为。

Vue2是如何检测数组得变化

针对无法监听到数组索引的直接修改或数组长度的变化,Vue2对数组的原型进行了劫持,重写了数组的一些方法,如 push、pop、shift、unshift、splice、sort、reverse 等,使得当这些方法被调用时,Vue 可以检测到数组的变化并更新视图。

当Vue初始化一个数组时,它会检查这个数组,并且使用一个特殊的观察者(Observer)来处理它。

Vue 不能检测到以下数组的变化:

  • 直接通过索引设置数组项,例如:vm.items[indexOfItem] = newValue
  • 修改数组的长度,例如:vm.items.length = newLength

为了解决这些问题,Vue 提供了Vue.set(array, index, value)this.$set(this.array, index, value)方法来触发更新。如果需要修改数组的长度,可以使用 splice 方法来实现。

此外,如果数组中包含的是对象或数组,Vue 会对这些嵌套的引用类型进行递归遍历,并对它们进行监控,以确保它们的任何变化也能够被检测到。

vue的生命周期

Vue2中

  • beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。此时组件尚未创建,data 还未被初始化,访问不到this。
  • created:在实例创建完成后被立即调用,此时实例已完成数据观测、属性和方法的运算,$el 属性还未显示出来,data 已经被初始化,访问不到this,可以访问到data、methods中的内容。
  • beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用,模板编译完成但是还未将模板渲染成dom。
  • mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子,可以获取到一些dom信息。
  • beforeUpdate:在数据变化之后,DOM被重新渲染之前调用,此时可以在这个钩子中进一步地更改状态。
  • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy:在实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed:在实例销毁之后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
  • activated:当一个组件被 keep-alive 缓存时,此钩子在组件激活时被调用。
  • deactivated:当一个组件被 keep-alive 缓存时,此钩子在组件停用时被调用。

vue3中

  • setup():这是 Composition API 的入口点,在组件实例创建之前执行。它在 beforeCreate 和 created 钩子之前执行,因此不需要显式定义这两个钩子 -onBeforeMount:在挂载开始之前被调用,相当于 Vue 2 中的 beforeMount 钩子。
  • onMounted:在组件挂载到 DOM 之后调用,相当于 Vue 2 中的 mounted 钩子。这时可以访问到 DOM 元素。
  • onBeforeUpdate:在组件更新之前调用,相当于 Vue 2 中的 beforeUpdate 钩子。
  • onUpdated:在组件更新之后调用,相当于 Vue 2 中的 updated 钩子。这时可以执行依赖于最新 DOM 的操作。
  • onBeforeUnmount:在组件卸载之前调用,相当于 Vue 2 中的 beforeDestroy 钩子。
  • onUnmounted:在组件卸载之后调用,相当于 Vue 2 中的 destroyed 钩子。这时可以进行清理工作,如移除自定义的事件监听器或定时器。
  • onActivatedonDeactivated:这两个钩子与 keep-alive 缓存的组件相关,分别在组件被激活和停用时调用。
  • onErrorCaptured:当捕获一个来自子孙组件的错误时被调用。
  • onRenderTrackedonRenderTriggered:这两个是调试钩子,分别在依赖项被追踪和触发重新渲染时调用。

Vue中常见的指令

  • v-bind:动态地绑定一个或多个属性,或一个组件 prop 到表达式。
  • v-model:在表单控件元素上创建双向数据绑定。常用于输入框、文本域等,如 <input v-model="message">
  • v-for:常用于循环渲染列表,如 <li v-for="item in items">{{ item.text }}</li>
  • v-if:条件性地渲染一块内容。
  • v-on:监听 DOM 事件并在触发时执行一些 JavaScript 代码。常用于事件绑定,如 <button v-on:click="doSomething">Click me</button>
  • v-once:只渲染元素和组件一次,随后的重新渲染将不再更新。常用于静态内容,如 <span v-once>This will never change: {{ message }}</span>
  • v-show:根据表达式的真假值切换 CSS 的 display 属性。常用于条件显示,如 <div v-show="isVisible">Toggle me</div>
  • v-cloak:用于解决 Vue 在解析模板时出现的闪烁问题。通常与 CSS 规则一起使用,如 [v-cloak] { display: none; }
  • v-text:更新元素的文本内容。常用于更新少量文本
  • v-html:更新元素的 HTML 内容。常用于更新大量 HTML
  • v-pre:跳过元素的编译过程。常用于显示原始 Mustache 标签
  • v-memo(非官方,但社区常用):用于缓存模板或组件的渲染结果。需要借助第三方库实现,如 <div v-memo="memoData"></div>
  • v-slot:用于定义插槽分发内容。常用于组件中,如<template v-slot:default="slotProps">{{ slotContent }}</template>
  • v-el(非官方,已废弃):用于将 DOM 元素注册为 Vue 实例的引用。可以使用 ref 属性替代。
  • v-ref:用于注册引用信息,引用可以是 DOM 元素也可以是子组件实例。常用于访问子组件或 DOM 元素,如<div v-ref:myElement></div>
  • v-bind:class / v-bind:style:用于动态绑定 class 和 style。常用于根据条件切换样式或类,如<div v-bind:class="{ active: isActive }"></div>

Vue修饰符

事件修饰符

  • .prevent:阻止默认事件。
  • .capture:使用事件捕获模式。
  • .self:只在当前元素本身触发。
  • .once:只触发一次。
  • .passive:默认行为将会立即触发。

按键修饰符

  • .left:左键
  • .right:右键
  • .middle:滚轮
  • .enter:回车
  • .tab:制表键
  • .delete:捕获 “删除” 和 “退格” 键
  • .esc:返回
  • .space:空格
  • .up:上
  • .down:下
  • .left:左
  • .right:右
  • .ctrl:ctrl 键
  • .alt:alt 键
  • .shift:shift 键
  • .meta:meta 键

表单修饰符

  • .lazy:在文本框失去焦点时才会渲染
  • .number:将文本框中所输入的内容转换为number类型
  • .trim:可以自动过滤输入首尾的空格

v-if和v-for优先级哪个高

vue2中v-for的优先级要高于v-if,vue3中v-for的优先级要低于v-if。

  • vue2中Vue.js 会遍历 items 数组,为每个元素创建一个 <li> 元素,然后对每一项进行if判断
<ul>
  <li v-for="item in items" v-if="item.isVisible">
    {{ item.text }}
  </li>
</ul>
  • Vue3中v-if 和 v-for 不能同时用在同一个元素上,否则会报错,相当于会在v-for外面包裹一层v-if ,v-if 会先判断是否需要渲染该元素,然后再由 v-for 进行遍历。如果 v-if 的条件不满足,那么即使 v-for 中有数据,元素也不会被渲染。
<div v-if="item === 1" v-for="item in 6" :key="index">
    <span>{{ item }}</span>
</div>

<!-- 等价于 -->
<template v-if="item === 1" > <!-- 此时肯定会报错 -->
    <div v-for="item in 6" :key="index">
        <span>{{ item }}</span>
    </div>
</template>
  • 官方推荐的做法是避免将 v-if 和 v-for 同时用在同一个元素上,而是通过计算属性或 <template> 标签来实现条件渲染和列表渲染的逻辑分离。

v-for中得key

key的作用就是标识当前VNode节点,从而可以高效地进行元素的重用和重新排序。 当数据发生变化时,Vue.js可以通过key来识别哪些元素可以复用,哪些需要重新渲染,没有key的情况下,Vue.js可能会错误地重新渲染整个列表,即使某些项目实际上并没有变化。

注意

  • 不要使用随机数作为key:key应该是稳定的,不会变化的。
  • 尽量避免使用索引作为key:除非列表不会发生排序、添加或删除操作,因为这些操作会导致Vue.js难以正确地跟踪每个元素。
  • 确保key的唯一性:在同一列表中,每个元素的key应该是唯一的。
<template>
  <div>
    <span v-for="(item, index) in arr" :key="index">{{ item }}</span>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const arr = ref([1, 2, 3, 4, 5])

onMounted(() => {
  arr.value.unshift(0)
})


</script>

随着数组前方放入了一个新的值,每个值对应得key值相应发生了变化,原本数组里1得key值是0,变化后数组里key得值变成了1,以此类推,每个元素都需要变化更新,重新渲染了整个列表,,即使只是新增了一个节点

如何优化vue项目

  • 合理使用v-ifv-show
  • key 保证唯一;防抖、节流得运用,第三方模块按需导入;
  • 路由图片的懒加载使用;
  • keep-alive 来缓存组件状态,减少 DOM 操作;
  • 使用现代图片格式如 WebP;
  • 避免在组件中使用大量数据,减少响应式依赖。
  • 利用浏览器缓存、IndexedDB 或 Service Workers 缓存数据。
  • 优化 API 请求,可以使用请求拦截和响应拦截来统一处理请求和响应,使用 axios 的取消令牌来取消未完成的请求。
  • 利用 Webpack 的 Tree Shaking 移除未使用的代码,使用 Webpack 的 DllPlugin 来提高构建速度。
  • 使用服务端渲染 (SSR),对于首屏加载要求高的应用,可以使用 Vue SSR 来提升首屏加载速度。
  • 优化 CSS:使用 CSS 预处理器如 SCSS 或 Less。使用 CSS Modules 或 Scoped CSS 避免样式污染。

foreach如何跳出循环?

forEach 是无法直接通过 breakreturn 来跳出循环的,因为 forEach 不支持这种操作。如果你需要跳出循环,推荐以下几种替代方式:

  • 使用for循环
const arr = [1, 2, mimi, 3, 4, 5,];
for (let i = 0; i < arr.length; i++) {
  if (arr[i] === mimi) {
    break;  // 跳出循环
  }
  console.log(arr[i]);  // 输出 1, 2
}
  • 使用for...of...循环
const arr = [1, 2, mimi, 3, 4, 5,];

for (let num of arr) {
  if (num === mimi) {
    break;  // 跳出循环
  }
  console.log(num);  // 输出 1, 2
}
  • 使用 someevery 方法
//如果你需要在遍历时退出,并且仍然希望使用数组的高阶函数,可以使用 `some` 或 `every`,因为它们允许返回 `false` 或 `true` 来终止循环。


//some,只要回调返回 `true`,就会停止循环。
const arr = [1, 2, 3, 4, 5];

arr.some((num) => {
  if (num === 3) {
    return true;  // 停止循环
  }
  console.log(num);  // 输出 1, 2
  return false;
});

//every,只要回调返回 `false`,就会停止循环。
const arr = [1, 2, 3, 4, 5];

arr.every((num) => {
  if (num === 3) {
    return false;  // 停止循环
  }
  console.log(num);  // 输出 1, 2
  return true;
});

懒加载的实现方式?

懒加载是一种优化网页性能的技术,它允许网页或应用延迟加载非关键资源,可以减少初始页面的加载时间提高用户体验,常见的懒加载方式有:图片视频懒加载、路由懒加载、第三方库的懒加载

state如何实现持久化(浏览器存储)

在前端开发中,state(状态)的持久化是指将应用的状态保存起来,以便在页面刷新或重新加载后能够恢复之前的状态。以下是一些常见的实现状态持久化的方法:

  • LocalStorage提供了一种简单的机制来存储和检索同源窗口的数据。它在用户的浏览器中以键值对的形式存储数据,并且即使在浏览器关闭后数据也会保留,但只有5mb左右。
  • SessionStorage,与 LocalStorage 类似,但 SessionStorage 的数据只在当前会话中有效,关闭浏览器标签或窗口后数据会被清除,也只有5mb左右。
  • Cookies也是一种存储状态的方式,在浏览器中存储少量文本数据的方式,通常用于在浏览器和服务器之间交换数据,但通常不推荐用于存储大量数据,因为它们有大小限制,并且每次请求都会发送到服务器。
  • IndexedDB是一种在用户浏览器中存储大量结构化数据的方式,包括文件/blobs。它比 LocalStorage 提供了更复杂的查询功能和更大的存储空间。

js深浅拷贝

在JavaScript中,对象和数组是通过引用传递的,这意味着当你将一个对象或数组赋值给另一个变量时,两个变量实际上指向内存中的同一个位置。因此,对其中一个变量的修改会影响另一个变量。为了解决这个问题,我们通常需要创建原始数据的一个副本,这个过程称为拷贝,

浅拷贝

浅拷贝会创建一个新对象,它的字段值与原始对象相同。如果字段是基本类型,它会复制值,如果是引用类型,它只会复制引用,而不是引用的对象。

Object.assign()

const original = { a: 1 };
const copy = Object.assign({}, original);

展开运算符(Spread Operator)

const original = { a: 1 };
const copy = { ...original };

Array.prototype.slice()方法(对于数组)

const original = [1, 2, 3];
const copy = original.slice();
深拷贝

深拷贝会创建一个新对象,并且递归地复制所有子对象。这意味着原始对象和新对象将没有任何共享的引用。

使用JSON.parse()和JSON.stringify()方法

const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));

使用递归函数手动实现深拷贝:

function deepCopy(obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    let copy = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepCopy(obj[key]);
        }
    }
    return copy;
}

使用第三方库,如lodash的_.cloneDeep()方法:

// 需要先安装lodash库
const _ = require('lodash');
const original = { a: 1, b: { c: 2 } };
const copy = _.cloneDeep(original);

([][[]] + [])[+!![]] + ([] + {})[+!![]+ +!![]]

这个表达式由两部分组成:

  1. ([][[]] + [])[+!![]]
[][[]]  
=> [][''//数组转为字符串:将数组里面的元素罗列出来,所以 [] 为 ''  
=> undefined //[] 里面没有 '' 这个属性,即undefined

//将 `undefined` 和一个空数组 `[]` 相加:原始类型与对象类型相加,会将对象转原始,再进行运算。
undefined + []  
=> undefined + '' //[] 转原始类型为 ''  
=> 'undefined'


//`[]` 是一个空数组,在 JavaScript 中,任何对象(包括数组)都是真值(truthy)
//`!![]`:双重取反,空数组本身为真值,进行两次取反操作,结果仍为 `true`
//`+!![]`:通过加号 `+`,我们将 `true` 转换为数值 `1`
+!![]  
=> +true  
=> 1

//最终表达式变成了
'undefined'[1]  
=> 'n'
  1. ([] + {})[+!![] + +!![]]
//JavaScript 会将空数组转换为一个空字符串 `""`,将对象转换为字符串 `"[object Object]"`。所以,最终的结果是,即 `"[object Object]"`。
[] + {}  
=> '' + '[object Object]'  
=> '[object Object]'

//`+!![]` 的结果是 `1`。这里有两个 `+!![]` 相加,所以结果是 `1 + 1 = 2`
+!![] + +!![]  
=> 1 + 1  
=> 2

//最终表达式变成了
"[object Object]"[2]  
=> 'b'
  1. 所以,这个表达式最终返回的结果是字符串 "nb"
"n" + "b" // 结果是 "nb"

涉及到的知识点:强制类型转换规则 image.png

//含字符串  
'a'+1 // 'a1'  
  
'a'+true // 'atrue'  
  
'a'+null // 'anull'  
  
//不含字符串  
1+true // 2  
  
1+null // 1   
  
1+undefined // NaN //undefined转数字为NaN   
  
NaN+1 // NaN

对象转原始类型

1、如果对象拥有  [Symbol.toPrimitive] 方法,调用该方法:

该方法能得到原始值(返回值),使用该原始值,得不到原始值,抛出异常

2、调用对象的valueOf方法:

该方法能得到原始值,使用该原始值,得不到原始值,进入下一步

3、调用对象的toString方法:

该方法能得到原始值,使用该原始值,得不到原始值,抛出异常

const obj = { a'前端'b'学研社' }
console.log(obj.toString()); //对象转原始是 [object Object] (valueOf方法得到对象本身)

const arr = [123545]
console.log(arr.toString()); //1,2,3,5,45  数组转原始是 罗列数组每一项 (valueOf方法得到数组本身)