简单描述从输入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):充当视图和模型之间的中介。它从模型中获取数据,并将这些数据转换为视图可以显示的格式。同时,它也处理用户的输入,将这些输入转换为模型可以理解的命令。
优势
- 分离关注点:将业务逻辑、数据和用户界面分离,使得代码更易于管理和维护。
- 提高可测试性:由于业务逻辑被封装在模型中,可以更容易地进行单元测试。
- 提高代码重用性:视图模型可以独立于特定的视图技术,这意味着你可以在不同的视图(如Web、桌面或移动)之间重用相同的业务逻辑。
- 提高用户界面的响应性:通过数据绑定,视图可以自动更新以反映模型的变化,从而提高应用程序的响应性。
渐进式框架
渐进式框架指的就是一种框架概念,一般来说,使用渐进式框架时,无需引入其所有功能,而是需要什么就用什么,就拿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 钩子。这时可以进行清理工作,如移除自定义的事件监听器或定时器。onActivated和onDeactivated:这两个钩子与 keep-alive 缓存的组件相关,分别在组件被激活和停用时调用。onErrorCaptured:当捕获一个来自子孙组件的错误时被调用。onRenderTracked和onRenderTriggered:这两个是调试钩子,分别在依赖项被追踪和触发重新渲染时调用。
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-if和v-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 是无法直接通过 break 或 return 来跳出循环的,因为 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
}
- 使用
some或every方法
//如果你需要在遍历时退出,并且仍然希望使用数组的高阶函数,可以使用 `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);
([][[]] + [])[+!![]] + ([] + {})[+!![]+ +!![]]
这个表达式由两部分组成:
([][[]] + [])[+!![]]
[][[]]
=> [][''] //数组转为字符串:将数组里面的元素罗列出来,所以 [] 为 ''
=> undefined //[] 里面没有 '' 这个属性,即undefined
//将 `undefined` 和一个空数组 `[]` 相加:原始类型与对象类型相加,会将对象转原始,再进行运算。
undefined + []
=> undefined + '' //[] 转原始类型为 ''
=> 'undefined'
//`[]` 是一个空数组,在 JavaScript 中,任何对象(包括数组)都是真值(truthy)
//`!![]`:双重取反,空数组本身为真值,进行两次取反操作,结果仍为 `true`
//`+!![]`:通过加号 `+`,我们将 `true` 转换为数值 `1`
+!![]
=> +true
=> 1
//最终表达式变成了
'undefined'[1]
=> 'n'
([] + {})[+!![] + +!![]]
//JavaScript 会将空数组转换为一个空字符串 `""`,将对象转换为字符串 `"[object Object]"`。所以,最终的结果是,即 `"[object Object]"`。
[] + {}
=> '' + '[object Object]'
=> '[object Object]'
//`+!![]` 的结果是 `1`。这里有两个 `+!![]` 相加,所以结果是 `1 + 1 = 2`
+!![] + +!![]
=> 1 + 1
=> 2
//最终表达式变成了
"[object Object]"[2]
=> 'b'
- 所以,这个表达式最终返回的结果是字符串
"nb"。
"n" + "b" // 结果是 "nb"
涉及到的知识点:强制类型转换规则
//含字符串
'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 = [1, 2, 3, 5, 45]
console.log(arr.toString()); //1,2,3,5,45 数组转原始是 罗列数组每一项 (valueOf方法得到数组本身)