前端八股文

422 阅读45分钟

Q1:vue 的响应式原理

响应式数据与用到了响应式数据的函数相关联,当数据变化时,对应的函数自动执行。

如何监听数据的变化?

js 提供了两种方法实现数据的劫持(监听):Object.definedPerproty 和 Proxy。其中,前者的兼容性更好,而后者需要支持 ES6 的浏览器。

Object.definedPerproty 监听的是对象的属性,而 Proxy 则是监听整个对象(且 Proxy 对于对象的绝大部分的"动作"都有对应的拦截方法,如下图)。

image.png

代理对象时的注意事项

  • 如果代理的不是一个对象,而是一个基本数据类型,直接返回(前面两种方式都要求传入对象);
  • 如果代理的对象已代理过,则直接返回原代理对象(给每个需要代理的对象打上标记);
    const obj = {}
    const ref1 = reactive(obj)
    const ref2 = reactive(obj)
    console.log(ref1 === ref2) // true
    
  • 如果代理的对象是一个响应式对象,则直接返回该响应式对象;

监听到读取属性时,应该做什么?

常见的“读取行为”有 get、has(如 in) 和 iteration(迭代,如 for in),我们可以根据不同的类型,进行依赖收集(如下图,属性将根据不同类型收集)

对于属性和类型很方便获取,但是用到的 function 不得而知,此时需要将每一个用到了响应式数据的函数,当作参数传递到一个公共函数当中,在该公众函数内部调用这个 function

{
    对象1: {
        属性1: {
            get: [fn1, fn2……],
            has: ……,
        },
        属性2: {
        },
    }
}
const obj = { age: 20 }
function fn() {
    console.log(obj.age)
}
effect(fn)
// effect
const currentFn = null;
function effect(fn) {
    currentFn = fn
    fn() // 在调用该函数时,将触发 get,此时的 currentFn 指向该函数
    currentFn = null
}

派发更新

逐层查找并循环调用对应的函数,实现数据的响应式。

Q2:原型及原型链

每一个构造函数都有一个prototype的属性,它是一个对象,这个对象也被叫做原型对象(简称原型)。该原型是显示原型,有一个默认constructer属性指向其构造函数。

而每一个实例(对象)都有一个__proto__的隐式原型对象,它指向它构造函数的原型。而所有原型对象的__proto__都指向Object的原型,Object的原型的__proto__指向null,原型链到这里终止,返回undefined。

所有原型对象都是Object的实例

所有构造函数都是Function的实例,Function是自己的构造函数(因此Function.proto === Function.prototype)

image-20240521064438226.png

Q3:如何理解JS的异步?

Js是一个单线程的语言,因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

如果采用同步的方式,那么容易造成主线程阻塞,从而影响后面的执行,所以浏览器采用异步的方式来避免。

所以当遇到定时器、网路请求、事件监听时,将其交给其他模块中执行,而主线程继续往下执行。

其他模板执行完成后,将回调函数包装成任务推入到任务队列的队尾,排队等待主线程的调度。

任务排队等待主线程的调度,是否有优先级?还是先进先出?

任务在队列中没有优先级,遵循先进先出原则。

但任务队列不只一个,常见的队列有 微队列、交互队列、延时队列,优先级从高到低。

每一次事件循环,调用哪一个队列中的任务,可由浏览器自己决定(因此将产生浏览器差异)。

浏览器满足以下要求(W3C标准):

  • 同一类型的任务必须在同一个队列,而不同类型的任务可以在同一队列。

  • 浏览器必须存在一个微队列(mircotask queen),且微队列中的任务优先级优于其他所有队列任务。

Q4:JS能够做到精确计时吗?

不行:

  1. 计算机硬件计时靠的是晶振和寄存器,存在一定偏差。
  2. 浏览器计时调用的是操作系统的计时函数,不同的操作系统对应不同的处理函数,导致差异。
  3. W3C规定当 setTimeout 层级超过五层时,delay 参数默认大于等于4ms。
  4. 受事件循环机制影响,只有主线程空闲时,才会开始调用任务队列中计时器的回调。

Q5:跨域解决方案

1.  CORS:在服务器端添加CORS头信息,允许特定的源访问资源(前提是能够控制目标服务器的配置)。

// 设置服务端响应头
Access-Control-Allow-Origin: http://localhost:5050
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
// ng配置文件
add_header 'Access-Control-Allow-Origin' 'http://localhost:5050';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';

2.  代理服务器:本地服务器生成代理,将对目标服务器的请求转发给本地服务器,请求第三方接口时,浏览器请求本地的代理服务器。

// 本地服务器启动
const express = require('express');
const request = require('request');
const app = express();

app.use('/proxy', (req, res) => {
    const url = 'http://www.qq.com' + req.url;
    req.pipe(request(url)).pipe(res);
});

app.listen(5050, () => {
    console.log('Proxy server is running on http://localhost:5050');
});
// 前端改调用本地
axios.get('http://localhost:5050/proxy/news/recommended?page=1&per_page=5')
        .then(function (response) {
            console.log(response.data);
        })
        .catch(function (error) {
            console.error('Error:', error);
        });

3.  jsonp: script 标签不受同源策略的限制,可以从任何来源加载和执行脚本。

function handleResponse(data) {
    console.log(data);
}

const script = document.createElement('script');
script.src = 'http://www.qq.com/news/recommended?page=1&per_page=5&callback=handleResponse';
document.body.appendChild(script);

Q6:HTTPS为什么更安全?

HTTPS之所以安全,是因为HTTPS协议对传输的数据进行加密,而加密过程是由非对称加密实现的。

然而,HTTPS在内容传输的加密过程中使用对称加密,而非对称加密只在证书验证阶段发挥作用(因为非对称加密非常耗时)。

对称加密

加解密速度快,性能高。

通信双方协商好密钥,即密钥是相同的,任何人拿到该密钥都能够解密内容,因此,如何安全的传递密钥是一个关键的问题,此时,非对称加密进场。


非对称加密

非对称加密即加解密双方使用不同的密钥,一把作为公钥,可以公开的,一把作为私钥,不能公开,公钥加密的密文只有私钥可以解密。

server 端保存私钥而不公开,并将公钥发布给其他的 client,通过公钥加密上述 【对称加密的密钥】,由于只有 server 端的私钥能解密,因此保证了 client -> server 的安全性。

但是!这里有一个前提,是公钥能够安全的发布给其他的 client,如果在发布过程中,被中间人掉包(中间人充当起了server),那么中间人再通过真公钥,与 server 进行通信……

image.png 如何保证公钥是 server 给我的,而不是中间人?

答案是通过 CA 证书,server 可以向 CA 申请证书,在证书中附上公钥,然后将证书传给 client。

证书除了公钥,还需要提供签名,来保证证书的真实性:首先使用一些摘要算法(如 MD5)将证书明文(如证书序列号,DNS 主机名等)生成摘要,再对生成的摘要进行加密(签名)(内容不同,摘要不同,签名相同的概率可以认为接近于 0)。

那么问题来了:

正常站点和中间人都可以向 CA 申请证书,获得认证的证书由于都是 CA 颁发的,所以都是合法的。那么此时中间人是否可以在传输过程中将正常站点发给 client 的证书替换成自己的证书呢?

No!

client 除了通过验签的方式验证证书是否合法之外,还需要验证证书上的域名与自己的请求域名是否一致,中间人中途虽然可以替换自己向 CA 申请的合法证书,但此证书中的域名与 client 请求的域名不一致,client 会认定为不通过!


总结

HTTPs 将内容进行对称加密,而将对称加密的密钥通过 CA 证书上的公钥进行非对称加密,保证了机密性、完整性,身份认证和不可否认的四大安全通信原则。

Q7:如何取消一次请求?

为什么要取消请求? 连续发送请求后,由于不同请求的数据不同,导致响应的时间也不同,导致最后一次请求拿到的响应,可能是其他请求的响应。

防抖? 不可行,当防抖的时间间隔小于响应的时间间隔时,依然会出现上方的问题。(图片待补充)

节流? 不可取,原因略(图片待补充)

处理方法:

new 一个AbortController

在fetch等请求中的第二个参数对象中,定义signal属性为步骤1的实例的signal属性

取消请求:调用实例的abort()方法

//实例
let controller;
async function fn () {
    // 取消请求:调用实例的abort()方法
    controller && controller.abort();
    // new 一个AbortController
    controller = new AbortController();
    const res = await fetch(
    	'http://xxx.com',
        // 在fetch等请求中的第二个参数对象中,定义signal属性为步骤1的实例的signal属性
        {
            signal: controller.signal,
        }
    ).then()
}

Q8:输入URL到看到页面的全过程

  • 首先浏览器主进程接管,开了一个下载线程。
  • 然后进行 HTTP 请求( DNS 查询、IP 寻址等等),中间会有三次握手,等待响应,开始下载响应报文。
  • 将下载完的内容转交给 Renderer 进程管理。
  • Renderer进程开始解析css rule tree和dom tree,这两个过程是并行的,所以一般我们把 link 标签放在页面顶部。
  • 解析绘制过程中,当浏览器遇到 link 标签或者 script、img 等标签,浏览器会去下载这些内容,遇到时候缓存的使用缓存,不适用缓存的重新下载资源。
  • css rule tree 和 dom tree 生成完了之后,开始合成 render tree,这个时候浏览器会进行 layout,开始计算每一个节点的位置,然后进行绘制。
  • 绘制结束后,关闭 TCP 连接,过程有四次挥手。

Q9:浏览器的缓存机制

浏览器缓存其实是HTTP缓存机制,分为强制缓存、协商缓存两种。其中强制缓存优先级大于协商缓存。

强制缓存

当发起 HTTP 某请求时,首先在浏览器缓存中查看,如果没有强制缓存的标识,直接向服务器发请求(如第一次发请求);

如果有强制缓存标识,但已过期,则进行协商缓存(下文再提);

如果有强制缓存且未过期,则直接返回缓存结果,状态200(未发请求至服务端)。


协商缓存

当需要判断协商缓存时,首先需要把请求带到服务端,根据服务端的响应来判断(因此进行到协商缓存时,是至少需要发起一次请求的)。

如果协商缓存未过期,则返回缓存的结果,状态304。

如果已过期,则无法缓存,再次发送请求获取响应并设置对应缓存,状态200。

Q10:HTTP 是无状态的,那么如何做到用户登录后识别用户身份?

解决方案就是 a 和 b 拥有不一样的标识,也叫 sessionId。也就意味着每个请求去建立连接都会获得一个 sessionId(sessionId 服务端返回)。

如果对应的 sessionId 是存储于服务端的话,那么服务端就会越来越大从影响对应的效率。

所以我们诞生了一个内容来保存这个值到对应浏览器端,这个东西就是cookie。所以cookie的诞生就是为了保存sessionID,从而解决http无状态的问题。

Q11:还有哪一些存储方案,并说出区别?

cookie、localStorage、sessionStorage、indexDB、webSQL(已废弃)

cookie

cookie 大小只有 4k,可设置过期时间。

Lax 模式下,只支持部分 get 请求携带。


localStorage

localStorage 存储大小 5m 左右,永久存储,可手动清除,同源环境下不同页面之间可以数据的共享。


sessionStorage

会话存储,存储大小 5m 左右,浏览器关闭后就会消失,同源环境下不同页面之间数据不可共享。


indexedDB

浏览器提供的非关系型数据库,indexedDB 提供大量的接口提供查询功能,还能建立查询。

  • indexedDB 是异步的,存入数据不会导致页面卡顿。
  • indexedDB 支持事务,事务是一系列操作过程中发生了错误,数据库会回退到操作事务之前的状态。
  • 同源限制,不同源的数据库不能访问。
  • 存储空间没有限制。

Q12:flex:1;表示什么?

flex:1; = = = > 展开为 flex: 1 1 0%;

三个参数分别表示 flex-grow flex-shrink flex-basis。

  • flex-grow:指定项目在剩余空间中所占据的比例,默认值为0,表示项目不会伸展。当设置为正数时,表示该项可以扩展的比例,相对于其他Flex项的比例。

  • flex-shrink:指定项目在空间不足时的收缩比例,默认值为1,表示项目会按比例收缩。当设置为0时,该项不会收缩。

  • flex-basis:定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。

Q13:var、let、const 区别?

函数作用域是指在函数体包裹区域;块级作用域是指 {} 包裹的区域。

区别:

  • var 为全局/函数作用域,而 let、const 为块级作用域;
  • 在同一个作用域中,var 可以重复声明,而 let、const 不可以;
  • var 存在变量提升,在声明前可使用(undefined,不建议),而 let、const 不存在变量提升,在变量之前使用会报错(存在暂存性死区);
  • const 声明常量,必须在声明的时候赋值且不允许修改(声明对象时,是指向对象的指针,对象内部可以进行修改)。

Q14:浏览器是如何渲染页面的?

当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。

在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。


整个渲染流程分为多个阶段,分别是: HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画

每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

这样,整个渲染流程就形成了一套组织严密的生产流水线。


渲染的第一步是解析 HTML

解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。

如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。

如果主线程解析到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。

第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。


渲染的下一步是样式计算

主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。

在这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px

这一步完成后,会得到一棵带有样式的 DOM 树。


接下来是布局,布局完成后会得到布局树。

布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。

大部分时候,DOM 树和布局树并非一一对应。

比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。


下一步是分层

主线程会使用一套复杂的策略对整个布局树中进行分层。

分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。


再下一步是绘制

主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。


完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

合成线程首先对每个图层进行分块,将其划分为更多的小区域。

它会从线程池中拿取多个线程来完成分块工作。


分块完成后,进入光栅化阶段。

合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。

GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

光栅化的结果,就是一块一块的位图


最后一个阶段就是

合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。

变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

什么是 reflow?

reflow 的本质就是重新计算 layout 树。

当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。

也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。

浏览器在反复权衡下,最终决定获取属性立即 reflow。

什么是 repaint?

repaint 的本质就是重新根据分层信息计算了绘制指令。

当改动了可见样式后,就需要重新计算,会引发 repaint。

由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。

为什么 transform 的效率高?

因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段

由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。

Q15:垃圾回收机制

JavaScript具有自动垃圾回收机制,垃圾回收器会按照固定的时间间隔,找出哪些不再继续使用的变量,释放其内存。

回收器为了更好的追踪不再使用的变量,通常对不再有用的变量打上标记,常用的实现为 标记清除引用计数

标记清除

垃圾收集器在运行时给内存中所有的变量都加上标记(方式各式各样)。

然后,它会去掉环境中的变量以及被环境中变量所引用的变量的标记。

最后将销毁那些带标记的值以及所占用的空间,从而完成内存清除

引用计数

引用计数即跟踪每个值被引用的次数。

当声明一个变量并将一个引用类型赋值给该变量时,这个引用类型的引用次数+1,如果同一个值又赋值给另一个对象,则继续+1。

相反,如果对该值引用的变量又取得了其他的值,那么该值的引用次数-1.

当引用次数为0时,说明没有再访问该值的变量,因而将其空间回收。

Q16:v-ifv-for 一起使用的问题

不论是vue2还vue3,都不建议同时使用二者,原因是二者的优先级不明显,且vue2/vue3存在差异。

在vue2,v-for的优先级更高,因此会产生较多的v-if判断,进而影响性能。

推荐将v-for循环的内容设置为计算属性,通过条件(相当于v-if)返回对应的内容,同时计算属性能够缓存,提高性能开销。

另一种做法是,将可以将 v-if 置于外层元素 (或 <template>) 上。

而在vue3中,v-if 的优先级更高。

Q17:关于你知道的组件传参方式

父子

  • props/$emit
  • parent/parent / children
  • refs
  • provide/inject

兄弟

  • eventBus

其他

  • vuex/pinia

Q18:生命周期

beforeCreate是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。

created在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。

beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

Q19:讲讲diff算法

当页面更新时,会生成一个新的虚假dom,在真实dom更新前,先将旧虚拟dom与新虚拟dom进行比较,遵循深度优先、逐层比较原则。

同一个节点(vNode)比较时,优先判断其key值,若没有key则比较tag类型(也就是标签类型)。

如果存在key时,新旧dom的比较过程是:头头比较 -> 头尾比较 -> 尾尾比较 -> 尾头比较……依此类推,期间key值相同时,则继续判断其子节点是否有变化(逐层判断-updateChild方法),无变化直接服用。若key值不相同,则新增标签。

Q20:object的隐式转换规则

JavaScript 对象的隐式转换涉及将对象转换为原始值(即字符串、数字或布尔值)的过程。这个过程可以通过调用对象的内部方法来实现,通常是在对象被用于上下文需要原始值的地方时发生的,例如在算术运算、字符串拼接或条件判断中。

以下是 JavaScript 对象隐式转换的主要步骤和逻辑:

1. 原始值转换的基本概念

JavaScript 提供了两种类型的转换:

  • ToPrimitive: 将对象转换为原始值。
  • ToString: 将值转换为字符串。
  • ToNumber: 将值转换为数字。

2. ToPrimitive 抽象操作

ToPrimitive 操作尝试将对象转换为原始值。它可以接受一个提示参数,该参数可以是 "string" 或 "number"。如果没有提供提示参数,默认情况下为 "default"(等同于 "number")。转换步骤如下:

  1. 检查对象是否有 Symbol.toPrimitive 方法

    • 如果对象具有 Symbol.toPrimitive 方法,则调用此方法并返回结果。

    • 例如:

      let obj = {
        [Symbol.toPrimitive](hint) {
          if (hint === 'string') {
            return 'stringValue';
          }
          return 42;
        }
      };
      console.log(String(obj)); // 'stringValue'
      console.log(Number(obj)); // 42
      
  2. 如果没有 Symbol.toPrimitive 方法

    • 根据提示依次调用对象的 toStringvalueOf 方法,直到得到原始值。
    • 如果提示为 "string",则优先调用 toString 方法,然后是 valueOf 方法。
    • 如果提示为 "number" 或 "default",则优先调用 valueOf 方法,然后是 toString 方法。

3. 示例

默认提示
let obj1 = {
  toString() {
    return 'object as string';
  },
  valueOf() {
    return 100;
  }
};

console.log(String(obj1)); // 'object as string'
console.log(Number(obj1)); // 100
console.log(obj1 + ""); // 'object as string'
提示为 "string"
let obj2 = {
  toString() {
    return 'string representation';
  },
  valueOf() {
    return 200;
  }
};

console.log(String(obj2)); // 'string representation'
console.log(obj2 + ''); // 'string representation'
提示为 "number"
let obj3 = {
  toString() {
    return 'another string';
  },
  valueOf() {
    return 300;
  }
};

console.log(Number(obj3)); // 300
console.log(+obj3); // 300

4. 强制类型转换

  • 字符串上下文:对象在字符串上下文中,例如字符串拼接时,默认会转换为字符串。

    let obj4 = {
      toString() {
        return 'converted to string';
      }
    };
    console.log('Result: ' + obj4); // 'Result: converted to string'
    
  • 数字上下文:对象在数字上下文中,例如算术运算时,默认会转换为数字。

    let obj5 = {
      valueOf() {
        return 10;
      }
    };
    console.log(obj5 * 2); // 20
    

5. 通过 Symbol.toPrimitive 自定义转换

通过实现 Symbol.toPrimitive,可以完全控制对象在所有上下文中的转换行为。

let obj6 = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      return 42;
    }
    if (hint === 'string') {
      return 'custom string';
    }
    return null;
  }
};

console.log(String(obj6)); // 'custom string'
console.log(Number(obj6)); // 42
console.log(obj6 + ''); // 'null'

Q21: new操作符具体干了什么呢?

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
  • 属性和方法被加入到 this 引用的对象中
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this

Q22:请列举几种隐藏元素的方法

  • visibility: hidden; 这个属性只是简单的隐藏某个元素,但是元素占用的空间任然存在
  • opacity: 0; CSS3属性,设置0可以使一个元素完全透明
  • position: absolute; 设置一个很大的 left 负值定位,使元素定位在可见区域之外
  • display: none; 元素会变得不可见,并且不会再占用文档的空间。
  • transform: scale(0); 将一个元素设置为缩放无限小,元素将不可见,元素原来所在的位置将被保留
  • <div hidden="hidden"> HTML5属性,效果和display:none;相同,但这个属性用于记录一个元素的状态
  • height: 0; 将元素高度设为 0 ,并消除边框
  • filter: blur(0); CSS3属性,将一个元素的模糊度设置为0,从而使这个元素“消失”在页面中

Q23:隐式转换规则

1. 转换为布尔值

JavaScript 在需要布尔值的上下文中会将值转换为 truefalse。以下值被认为是 false

  • false
  • 0
  • -0
  • 0n(BigInt 零)
  • ""(空字符串)
  • null
  • undefined
  • NaN

所有其他值都被认为是 true

if (0) {
  console.log("This won't be logged");
}

if ("non-empty string") {
  console.log("This will be logged");
}

2. 转换为数字

以下情况会触发转换为数字的行为:

  • 算术运算符(如 +-*/
  • 一元运算符 +-
  • 比较运算符(如 ><<=>=

字符串转换为数字时,遵循以下规则:

  • 一个纯数字的字符串被转换为相应的数字
  • 包含非数字字符的字符串被转换为 NaN
console.log("5" - 3); // 2
console.log("5" * "2"); // 10
console.log("5" - "foo"); // NaN
console.log(+"5"); // 5
console.log(+true); // 1
console.log(+false); // 0

3. 转换为字符串

以下情况会触发转换为字符串的行为:

  • 字符串连接运算符 +
  • 模板字符串
console.log("Hello " + 5); // "Hello 5"
console.log(5 + "5"); // "55"
console.log(`The number is ${5}`); // "The number is 5"

4. 对象的转换规则

对象转换为原始类型时,JavaScript 会尝试调用对象的 toPrimitive 方法,内部顺序为:

  1. valueOf
  2. toString

对象转换为数字或字符串时,顺序和上下文会有所不同:

  • 数字上下文:调用 valueOf,若结果为原始值,则使用该值;否则调用 toString
  • 字符串上下文:调用 toString,若结果为原始值,则使用该值;否则调用 valueOf
const obj = {
  valueOf: () => 42,
  toString: () => "foo"
};

console.log(obj + 1); // 43 (数字上下文,使用 valueOf)
console.log(String(obj)); // "foo" (字符串上下文,使用 toString)

5. 特殊情况

有一些特殊的隐式转换规则需要注意:

  • nullundefined 转换为数字时分别为 0NaN
  • 使用宽松相等 == 比较时,nullundefined 只等于彼此,不等于其他值
  • 使用宽松相等 == 比较时,空字符串、false0 会被认为相等
console.log(null == undefined); // true
console.log(null == 0); // false
console.log("" == 0); // true
console.log(false == 0); // true
console.log(false == ""); // true

提问:[1,2] + [3,4] 输出什么?

JavaScript 在使用加号运算符时,如果其中一个操作数是字符串,另一个操作数会被隐式转换为字符串。在 [1, 2] + [3, 4] 这个表达式中,两个数组都需要被转换为字符串。

数组转换为字符串的规则是调用数组的 toString 方法。对于数组 toString 方法的行为是将数组中的每个元素转换为字符串,并用逗号分隔这些字符串。

console.log([1, 2].toString()); // "1,2"
console.log([3, 4].toString()); // "3,4"

Q24:为什么0.1+0.2不等于0.3?

因为浮点数运算的精度问题。在计算机运行过程中,需要将数据转化成二进制,然后再进行计算。 因为浮点数自身小数位数的限制而截断的二进制在转化为十进制,就变成0.30000000000000004,所以在计算时会产生误差。

解决方案

将其先转换成整数,再相加之后转回小数。具体做法为先乘10相加后除以10let x=(0.110+0.210)/10; console.log(x===0.3)

使用number对象的toFixed方法,只保留一位小数点。(n1 + n2).toFixed(2)

Q25:ajax、axios、fetch的区别

ajax

  • 基于原生XHR开发,XHR本身架构不清晰。

  • 针对MVC编程,不符合现在前端MVVM的浪潮。

  • 多个请求之间如果有先后关系的话,就会出现回调地狱

  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

axios

  • 支持PromiseAPI
  • 从浏览器中创建XMLHttpRequest
  • 从 node.js 创建 http 请求
  • 支持请求拦截和响应拦截
  • 自动转换JSON数据
  • 客户端支持防止CSRF/XSRF

fetch

  • 浏览器原生实现的请求方式,ajax的替代品

  • 基于标准 Promise 实现,支持async/await

  • fetchtch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理

  • 默认不会带cookie,需要添加配置项

  • fetch没有办法原生监测请求的进度,而XHR可以。

Q26:vnode 到真实 DOM 是如何转变的?

1. 创建 VNode

VNode 是对真实 DOM 元素的抽象表示。一个 VNode 对象通常包含以下信息:

  • 标签名(如 divspan 等)
  • 属性(如 classid 等)
  • 子节点(可以是文本节点或其他 VNode
  • 事件监听器

在 Vue.js 中,一个简单的 VNode 可能看起来像这样:

const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    class: 'container'
  },
  children: [
    {
      tag: 'h1',
      props: {},
      children: ['Hello, World!']
    },
    {
      tag: 'p',
      props: {},
      children: ['This is a paragraph.']
    }
  ]
};

2. 渲染 VNode 到真实 DOM

VNode 转换为真实 DOM 的过程称为渲染。这个过程涉及创建实际的 DOM 元素并将其插入到文档中。伪代码如下:

function createElement(vnode) {
  // 创建元素
  const el = document.createElement(vnode.tag);

  // 设置属性
  for (const [key, value] of Object.entries(vnode.props)) {
    el.setAttribute(key, value);
  }

  // 处理子节点
  vnode.children.forEach(child => {
    const childEl = (typeof child === 'string')
      ? document.createTextNode(child) // 文本节点
      : createElement(child); // 递归创建子元素
    el.appendChild(childEl);
  });

  return el;
}

const root = createElement(vnode);
document.body.appendChild(root);

27:闭包

闭包是外层的函数能够访问内层函数的变量。

如何产生闭包?

  1. 函数嵌套函数并返回
    function fn() {
        var a = 0
        return fnn() {
            console.log(a)
        }
    }
    let res = fn()
    res() // 1
    
  2. IIFE
    var b = 3
    (function(){
        console.log(b) // 3
    })()
    
  3. Ajax、事件监听等回调函数都存在闭包
  4. 函数
    var str = 'haha'
    function demo() {
        var str = 'haha'
        fn1 = function() {
            console.log(str)
        }
        temp(fn1)
    }
    function temp(tempFn) {
        tempFn()
    }
    

Q28:路由

路由的作用就是分发。

路由一共有History、Hash、Memory(vue-router 4新增)三种模式。

History

  • 特点:利用 HTML5 History API 来管理路由,URL 中不再需要 # 符号。

  • 工作原理:通过 pushStatereplaceState 方法,允许改变浏览器的历史记录,而不会引起页面的重新加载。

  • 优点:URL 看起来干净,更符合传统网站的 URL 结构,没有 # 符号。

  • 缺点:在一些较旧的浏览器或不支持 HTML5 History API 的环境中可能会有兼容性问题。

Hash

  • 特点:在 URL 中使用 # 符号来模拟路由。

  • 工作原理:当 URL 中的 # 后面的部分发生变化时,不会向服务器发送请求,而是通过 JavaScript 监听 URL 的变化(HashChange)来更新页面内容。

  • 优点:兼容性较好,支持在不支持 HTML5 History API 的浏览器中使用路由。

  • 缺点:URL 看起来不够干净,因为有一个 # 符号在路由路径中。

Memory

  • 特点:不使用浏览器的 URL 来管理路由状态,而是将路由状态保存在内存中。

  • 工作原理:通过 JavaScript 的状态管理库(如 Vuex、Redux 等)来管理路由状态,而不依赖于浏览器的 URL。

  • 优点:完全控制路由状态的管理,可以更灵活地实现路由状态的存储和更新。

  • 缺点:不利于 SEO(搜索引擎优化),因为搜索引擎通常无法获取和索引基于内存的页面内容。

29:讲讲nextTick的实现原理

nextTick 中的回调函数会确保在dom更新后再调用。

其原理是将其回调函数推入到任务队列的队尾等待主线程的调度。

在vue中,优先采用Promise.then方法,如果环境不支持微任务,则回退使用宏任务(如 setTimeout)。这种优先使用微任务的方式确保了回调函数尽早执行,从而使开发者能够在 DOM 更新之后尽快执行相关逻辑。

function _nextTick(cb, ctx) {
    let _resolve = null
    callbacks.push(() => {
        if(cb){
            cb.call(ctx)
        }  else if (_resolve) { 
            _resolve(ctx)
        }
    })
    if (useMicroTask) { 
        microTimerFunc();  // 微任务,采用Promise.then
    } else {
        macroTimerFunc(); // 延时器
    }
    // 如果未提供回调函数,返回一个 Promise 
    if (!cb) { 
        return new Promise(resolve => { 
            _resolve = resolve
        })
    }
}

Q30:有感刷新与无感刷新

当用户登录时,我们通常会将返回的Token存在前端,并封装在请求头中,每次随请求发送。

同时,在响应拦截器中判断Token是否有效,如果Token过期,则清除Token,跳转至登录页,重新登录,整个过程用户都能感知,称为有感刷新Token。

相反的,在响应拦截中发现Token已经过期时,调用刷新Token的接口,则实现了无感刷新。

通常,前端与后端协商好两个Token,一个用于普通请求时标识用户身份,另一个用于Token过期时调用刷新Token接口的token,为了系统的安全性,当刷新Token接口的token也过期时,应当跳转至登录页,提示用户重新登录验证身份。

Q31:JS继承

原型链继承:

让一个构造函数的原型是另一个类型的实例, 那么这个构造函数new出来的 实例就具有该实例的属性, 原型链继承的。

优点: 写法方便简洁, 容易理解。

缺点: 在父 类型构造函数中定义的引用类型值的实例属性, 会在子类型原型上变成原型属性被所有子 类型实例所共享。同时在创建子类型的实例时, 不能向超类型的构造函数中传递参数。

借用构造函数继承:

在子类型构造函数的內部调用父类型构造函数; 使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。

优点: 解决了原型链实现继承的不能传参的问 题和父类的原型共享的问题。

缺点: 借用构造函数的缺点是方法都在构造函数中定义, 因此无法实现函数复用。在父类型的原型中定义的方法, 对子类型而言也是不可见的, 结果 所有类型都只能使用构造函数模式。

组合继承:

将原型链和借用构造函数的组合到一块。使用原型链实现对原型属性和方法的继承, 而通过借用构造函数来实现对实例属性的继承。 这样, 既通过在原型上定义方法实 现了函数复用, 又能够保证每个实例都有自己的属性。

优点就是解决了原型链继承和借用 构造函数继承造成的影响。

缺点是无论在什么情况下, 都会调用两次超类型构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部

原型式继承:

在一个函数A内部创建一个临时性的构造函数, 然后将传入的对象作为这个 构造函数的原型,最后返回这个临时类型的一个新实例。本质上, 函数A是对传入的对象 执行了一次浅复制。ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数: 作为新对象原型的对象, 以及给新对象定义额外属性的对象(第二个可选) 。 在只有一个参数时, Object.create()与这里的函数A方法效果相同。

优点是: 不需要单独创建构造函数。

缺点是: 属性中包含的引用值始终会在相关对象间共享。

寄生式继承:

寄生式继承背后的思路类似于寄生构造函数和工厂模式: 创建一个实现继承的函数, 以某种方式增强对象, 然后返回这个对象。 优点: 写法简单, 不需要单独创建构造函数。 缺点: 通过寄生式继承给对象添加函数会导致函数难以重用。

寄生组合式继承:

通过借用构造函数来继承属性, 通过原型链的混成形式来继承方法。本质上, 就是使用寄生式继承来继承超类型的原型, 然后再将结果指定给子类型的原型。

优点是: 高效率只调用一次父构造函数, 并且因此避免了在子原型上面创建不必要熟悉,与此同时, 原型链还能保持不变。

缺点是: 代码复杂

Q32:mapweakMap区别?

MapWeakMap 是两种用于存储键值对的数据结构,它们在 JavaScript 中各有其特点和适用场景。以下是它们的详细区别:

1. 键类型

  • Map:可以使用任何值(包括对象和原始类型)作为键。
  • WeakMap:只能使用对象作为键,不能使用原始类型(如字符串、数字、布尔值等)。

2. 垃圾回收

  • Map:对键值对的引用是强引用,只要 Map 对象存在,键值对中的键和值就不会被垃圾回收机制回收。
  • WeakMap:对键值对中的键是弱引用,如果没有其他地方引用该键,对应的键值对会被垃圾回收机制回收。这意味着 WeakMap 不会阻止其键被垃圾回收。

3. 键的枚举

  • Map:可以枚举其中的键、值或键值对。提供了 keys(), values(), entries()forEach() 方法。
  • WeakMap:不可枚举,没有提供方法来遍历其键值对。其键值对是不可遍历和不可枚举的,设计目的是保护隐私和安全。

4. 用途

  • Map:适用于需要存储任意键值对,并且需要频繁读取、写入、删除和遍历键值对的场景。
  • WeakMap:适用于需要将对象作为键,并且希望键能够被垃圾回收的场景。例如,在实现缓存或存储与 DOM 元素相关的数据时,使用 WeakMap 可以避免内存泄漏。

5. API 方法

  • Map:提供了多种操作方法:

    • set(key, value): 设置键值对。
    • get(key): 获取键对应的值。
    • has(key): 检查是否存在键。
    • delete(key): 删除键值对。
    • clear(): 清空所有键值对。
    • size: 获取键值对的数量。
  • WeakMap:提供的方法较少:

    • set(key, value): 设置键值对。
    • get(key): 获取键对应的值。
    • has(key): 检查是否存在键。
    • delete(key): 删除键值对。

代码示例

Map 示例:

let map = new Map();
map.set('a', 1);
map.set(1, 'one');
map.set({}, 'object');

console.log(map.get('a')); // 1
console.log(map.get(1)); // 'one'
console.log(map.size); // 3

map.forEach((value, key) => {
  console.log(key, value);
});

map.delete('a');
console.log(map.has('a')); // false

map.clear();
console.log(map.size); // 0

WeakMap 示例:

let weakMap = new WeakMap();
let obj1 = {};
let obj2 = {};

weakMap.set(obj1, 'object1');
weakMap.set(obj2, 'object2');

console.log(weakMap.get(obj1)); // 'object1'
console.log(weakMap.has(obj2)); // true

weakMap.delete(obj1);
console.log(weakMap.has(obj1)); // false

Q33:js实现保留一位小数

1. 使用 toFixed 方法

toFixed 方法可以将数字转换为字符串,并保留指定的小数位数。

let num = 5.678;
let result = num.toFixed(1);
console.log(result);  // 输出 "5.7"

2. 使用 Math.round 方法

可以通过数学方法 Math.round 来实现,先将数字乘以 10,然后四舍五入,再除以 10。

let num = 5.678;
let result = Math.round(num * 10) / 10;
console.log(result);  // 输出 5.7

3. 使用自定义函数

通过自定义函数实现保留一位小数的功能。

function roundToOneDecimalPlace(num) {
  return Math.round(num * 10) / 10;
}

let num = 5.678;
let result = roundToOneDecimalPlace(num);
console.log(result);  // 输出 5.7

Q34:响应式布局的方案及实践(后面补坑🕳)

什么是响应式布局?

应该是一套代码适配多种机型,比如手机、平板等

常见方案如:媒体查询rem百分比视口单位(vh/vw)

1. 百分比(Percentage %)

百分比用于相对于其包含元素的宽度或高度来定义元素的尺寸。适用于流体布局。

.container {
  width: 100%;
  padding: 10%;
}

.item {
  width: 50%;
  float: left;
}

2. rem 和 em

rem 和 em 都是相对单位,主要用于字体大小和间距。em 是相对于当前元素的字体大小,而 rem 是相对于根元素(通常是 <html>)的字体大小。

/* 根元素设置字体大小 */
html {
  font-size: 16px; /* 1rem = 16px */
}

/* 使用 rem */
.container {
  padding: 1rem; /* 16px */
  margin-bottom: 2rem; /* 32px */
}

/* 使用 em */
.item {
  font-size: 1.5em; /* 1.5倍父元素的字体大小 */
  margin: 2em 0; /* 2倍自身字体大小 */
}

3. 视口单位(Viewport Units: vw, vh, vmin, vmax)

视口单位是相对于视口(浏览器窗口)尺寸的单位,非常适合用于响应式设计。

  • vw:视口宽度的百分之一。
  • vh:视口高度的百分之一。
  • vmin:视口宽度和高度中较小的那个的百分之一。
  • vmax:视口宽度和高度中较大的那个的百分之一。
.container {
  width: 80vw; /* 80% 视口宽度 */
  height: 50vh; /* 50% 视口高度 */
  padding: 2vmin; /* 2% 视口较小的尺寸 */
}

.text {
  font-size: 5vmax; /* 视口较大尺寸的5% */
}

4. 媒体查询中的组合使用

在媒体查询中,可以结合使用不同的单位来实现更加灵活的响应式设计。

body {
  font-size: 1rem; /* 默认字体大小 */
}

@media (max-width: 600px) {
  body {
    font-size: 0.8rem; /* 屏幕较小时,字体变小 */
  }

  .container {
    padding: 5%; /* 使用百分比来调整填充 */
  }
}

@media (min-width: 601px) and (max-width: 1200px) {
  body {
    font-size: 1rem; /* 中等屏幕,保持默认字体大小 */
  }

  .container {
    padding: 2rem; /* 使用 rem 单位来定义填充 */
  }
}

@media (min-width: 1201px) {
  body {
    font-size: 1.2rem; /* 屏幕较大时,字体变大 */
  }

  .container {
    padding: 5vw; /* 使用视口单位来调整填充 */
  }
}

Q35:Vue3中watch、watchEffect、computed的区别

  1. watch是一个侦听器函数,它可以监视一个或多个数据的变化,还可以通过配置选项来进行更复杂的操作。watch需要明确地指定要监视的数据,以及数据变化所执行的回调函数。
  2. watchEffect也是用于监视数据变化的函数,但它更加灵活,可以自动追踪到被用到的响应式数据,并在数据变化时自动重新运行。watchEffect不需要明确地指定要监视的数据,它会自动追踪被用到的响应式数据,并在这些数据变化时重新执行。watchEffect通过用于副作用的场景,比如当数据变化时触发一些异步操作。
  3. computed是一个计算属性,它会根据依赖的响应式数据自动计算出一个新值,并且当依赖的数据发生变化时才会重新计算,它会缓存计算的结果,直到依赖的数据发生变化时才会重新计算。

Q36:css中的盒模型

1. CSS 盒模型

CSS 盒模型描述了一个 HTML 元素在网页上的布局。每个元素都被认为是一个矩形盒子,这个盒子由四个部分组成:

  • Content(内容) :元素的实际内容。
  • Padding(内边距) :内容周围的空白区域。
  • Border(边框) :包围内容和内边距的边框。
  • Margin(外边距) :元素与其他元素之间的空白区域。

CSS 盒模型

2. box-sizing 属性

box-sizing 属性用于定义元素的宽度和高度计算方式。它有两个主要值:

  • content-box(默认值):宽度和高度只包含内容部分,padding 和 border 不计算在内。
  • border-box:宽度和高度包含内容、padding 和 border。

示例

/* content-box 示例 */
.content-box {
  box-sizing: content-box;
  width: 200px;
  padding: 20px;
  border: 10px solid #000;
  /* 实际元素总宽度 = 200px(内容) + 20px(内边距)* 2 + 10px(边框)* 2 = 260px */
}

/* border-box 示例 */
.border-box {
  box-sizing: border-box;
  width: 200px;
  padding: 20px;
  border: 10px solid #000;
  /* 实际元素总宽度 = 200px */
}
3. 实际例子

假设我们需要创建一个固定宽度的按钮,而不希望因为 padding 和 border 影响到按钮的实际宽度,可以使用 box-sizing: border-box 来实现。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Box Model Example</title>
<style>
  .button {
    box-sizing: border-box;
    width: 150px;
    padding: 10px 20px;
    border: 2px solid #007BFF;
    background-color: #007BFF;
    color: #FFF;
    text-align: center;
    cursor: pointer;
  }
</style>
</head>
<body>

<button class="button">Click Me</button>

</body>
</html>

在这个例子中,按钮的总宽度固定为 150px,不会因为 padding 和 border 的增加而改变。这使得布局更加可控和一致。

Q37:动态组件

1. 什么是动态组件?

在 Vue.js 中,动态组件指的是在同一个挂载点根据条件或状态来动态切换不同的组件。Vue 提供了 <component> 元素来实现动态组件的功能。<component> 元素的 is 属性可以绑定到一个动态的组件名,从而实现组件的动态切换。

2. 动态组件的实际使用场景

  • 单页面应用(SPA)中的页面切换:可以根据路由动态切换显示不同的页面组件。
  • 步骤表单:在多步骤表单中,根据当前步骤动态切换不同的表单组件。
  • 标签页:在实现标签页功能时,可以根据选中的标签动态切换不同的内容组件。
  • 弹窗或模态框:在弹窗中,可以根据当前操作动态显示不同的内容组件。

Q38:插槽

1. 什么是 Vue.js 中的插槽(Slots)机制?

插槽(Slots)是 Vue.js 提供的一种在组件中分发内容的方式。它允许你在父组件中向子组件传递嵌套内容。Vue 中的插槽机制类似于其他框架中的内容投影(Content Projection)。

Vue.js 提供了三种类型的插槽:

  • 默认插槽:用于分发不带名字的内容。
  • 具名插槽:用于分发带名字的内容。
  • 作用域插槽:用于分发带有作用域的内容,使得子组件可以向父组件传递数据。

2. 插槽的实际使用场景

  • 布局组件:创建通用布局组件,允许其他组件在特定区域插入内容。
  • 通用组件:创建按钮、卡片等通用组件,可以根据需要插入不同的内容。
  • 动态内容:允许父组件根据特定逻辑向子组件插入动态内容。
  • 作用域插槽:在需要子组件向父组件传递数据的场景下使用。

Q39:forEach 与 for 循环的区别?

forEachfor 循环都是 JavaScript 中常用的迭代数组的方法,但它们有一些显著的区别。以下是它们的主要区别及各自的特点:

forEach

forEach 是数组方法,用于对数组的每个元素执行一次提供的函数。

特点

  1. 易读性forEach 的语法更简洁,更具可读性,尤其适合处理数组的简单操作。
  2. 不能中断forEach 无法在中途退出或中断循环(没有 breakcontinue)。
  3. 不返回值forEach 不返回任何值,总是返回 undefined
  4. 回调函数forEach 接受一个回调函数作为参数,回调函数可以接收三个参数:当前元素的值、当前元素的索引和数组本身。

示例

const array = [1, 2, 3, 4, 5];

array.forEach((element, index, array) => {
  console.log('Element:', element, 'Index:', index, 'Array:', array);
});

for 循环

for 循环是最基础的循环控制结构之一,用于执行一定次数的循环。

特点

  1. 灵活性for 循环非常灵活,可以用于数组和非数组的迭代,也可以控制循环的开始、结束和步长。
  2. 能中断for 循环可以通过 breakcontinue 关键字来中断或跳过循环。
  3. 需要手动索引for 循环需要手动管理索引,因此在处理复杂循环逻辑时,代码可能变得不易读。
  4. 适用范围广:可以用于任何需要循环的场景,而不仅仅是数组。

示例

const array = [1, 2, 3, 4, 5];

for (let i = 0; i < array.length; i++) {
  console.log('Element:', array[i], 'Index:', i, 'Array:', array);
}

区别总结

  1. 使用场景

    • forEach:更适合对数组的每个元素执行相同操作,简洁且易读。
    • for 循环:适用于需要更多控制和灵活性的场景,适合复杂的迭代逻辑。
  2. 语法和可读性

    • forEach:语法简洁,可读性强。
    • for 循环:语法较冗长,需要管理索引,但更灵活。
  3. 中断和跳过

    • forEach:不能中断或跳过循环。
    • for 循环:可以使用 breakcontinue 中断或跳过循环。
  4. 返回值

    • forEach:没有返回值。
    • for 循环:可以有返回值(通过设置函数返回值),但通常不返回任何值。

Q40:escape、encodeURI、encodeURIComponent 的区别?

encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合 法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。

encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊 字符也会得到转义。

escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式, 再在每个字节前加上 %

Q41:JWT

什么是 JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
  • 是一种认证授权机制
  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。
  • 可参考:阮一峰老师的 JSON Web Token 入门教程

JWT 认证流程:

  • 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
  • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
  • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样
Authorization: Bearer <token>
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为

Q42: BOM

一、window对象

(一)常见方法
a. 打印方法
1) log() 以日志形式
window.console.log( ' 日志 ' )	// 以 日志 形式打印
2) error 以 错误 的形式
window.console.error( ' 错误 ' )	// 以 错误 形式打印
3) warn 以 警告 的形式
window.console.warn( ' 警告 ' )	// 以 警告 形式打印
4) info 以 消息 的形式
window.console.info( ' 消息 ' )	// 以 消息 形式打印
5) debug 以 测试 的形式
window.console.debug( ' 测试 ' )	// 以 测试 形式打印
b. 弹窗方法
1) alert() 提示窗
alert( ' 提示窗 ' )	// 弹出提示窗
2) confirm() 交互窗
var isTrue = confirm( ' 交互窗 ' )		// 弹出交互窗( Boolean类型 ) ,用户确认是 true,取消是 false;
console.log( isTrue )	// true/false
3) prompt() 输入窗
var str = prompt( ' 输入窗 ' )		// 输入窗口,返回的字符串
console.log( typeof str )	// string类型
c. 打开/关闭窗口
window.open ( url , name , [ options ] ); 	// 地址,title, 设置的参数(窗口的高度 宽度...)target(打开方式 _blank _self _parent)	
window.close( ); 	// 关闭自己
eg:
open( 'http://www.baidu.com' , '百度' , 'width=100,hright=200' )	// 打开一个百度官网( 宽100,高200 )
close();	// 关闭了自己这个窗口
// open 还要很多其他options选项
// height=100 	窗⼝⾼度;
// width=400 	窗⼝宽度;
// top=0 	窗⼝距离屏幕上⽅的象素值;
// left=0 	窗⼝距离屏幕左侧的象素值;
// toolbar=no 	是否显⽰⼯具栏,yes为显⽰;
// menubar,scrollbars 	表⽰菜单栏和滚动栏。
// resizable=no 	是否允许改变窗⼝⼤⼩,yes为允许;
// location=no 	是否显⽰地址栏,yes为允许;
// status=no 	是否显⽰状态栏内的信息(通常是⽂件已经打开),yes为允许;
d. 移动窗口
1) moveBy() / moveTo()
// movaBy(x: number, y: number)
moveBy( 100, 100 )		// x,y 都移动自身的100;增加量!!
movaTo( 100, 100 )		// x,y 都移动至100;坐标!!
e. 设置窗口大小
1) resizeBy() / resizeTo()
//改变对应的窗口大小
window.resizeBy(200,200) 	//width+200 height+200
//resizeTo
window.resizeTo(200,200) 	//width=200 height=200
f. 打印方法
1) print()
//print打印方法
window.print()
g. 聚焦与失焦方法
1) focus() / blur()
//focus 聚焦 blur 失去焦点
window.focus()
window.blur()
h. 查找方法
1) find() 相当于ctrl+f
//find查找 
window.find()
i. 滚动栏位置改变
1) scrollBy() / scrollTo()
//滚动栏位置改变 初始位置 x:0,y:0 
// window.scrollBy( options ? : ScrollToOptions )
window.scrollBy(100,100) 	//原本的位置 x+100,y+100
window.scrollTo(500,500) //到达位置 x=500 y=500 	//回到顶部
  1. window在调用方法时,通常可以省略window,如:alter(),open()……
  2. 弹窗方法中,注意各个方法的返回值类型。
  3. 注意 By 和 To 的区别。

二、history对象

(一)属性
a. length 历史页面个数
// 历史页面个数 (本页操作)
console.log(history.length)	// 1,2,3……
b. state 状态存储的对象
// state 状态值  null(默认)
console.log(history.state)	// null
c. scrollRestoration 滚动栏恢复
// 滚动条恢复属性   auto(默认)  manual(手动)
console.log(history.scrollRestoration);	// auto
(二)方法
a. forward() 前进
<button onclick=fnForward()>前进</button>
<script>
    function fnForward() {
        history.forward()
    }
</script>>
b. back() 后退
<button onclick=fnBack()>后退</button>
<script>
    function fnBack() {
        history.back()
    }
</script>>
c. go() 去任意页面 0(自己)、小于零后退,大于零前进
<button onclick=fnGo()>GO</button>
<script>
    function fnGo() {
        history.go(-1)
    }
</script>>
d. pushState()
<button onclick=pushState()>Push</button>
<script>
    function pushState() {
        // pushState( data: any, unused: string, url?: string | URL )	state数据,第二个填'',第三个地址( 跨域问题 )
        history.pushState('111','')
    }
</script>>
e. replaceState()
<button onclick=replaceState()>replace</button>
<script>
    function replaceState() {
        // replaceState( data: any, unused: string, url?: string | URL )	state数据,第二个填'',第三个地址( 跨域问题 )
        history.replaceState('222','')
    }
</script>>

三、location对象

(一)属性
console.log(location);

// 相关属性
console.log(location.hash);     // hash #号后面的内容(#)
console.log(location.search);   // search ?号后面的内容(?)  不与hash同时使用

console.log(location.protocol); // 协议: http: 80      https: 443
console.log(location.host);     // ip号 + 端口号    127.0.0.1:5500
console.log(location.hostname); // ip号     127.0.0.1
console.log(location.port);     // 端口号   5500
console.log(location.pathname); // 路径名(后面的内容)
console.log(location.href);     // 跳转地址 (全称)  可以设置

console.log(location.origin);   // 跨域
(二)方法
a. assign() 跳转 可以返回
function fn() {
    location.assign('http://www.baidu.com')
}
b. replace() 替换 直接替换,无法返回
function fn() {
    location.replace('http://www.4399.com')
}
c. reload() 重新加载(相当于刷新)
function fn() {
    location.reload(true)   // true 为服务器加载  false为本地缓存加载
}

四、frames、screen、navigator

  • frames与第三方框架有关
  • screen为屏幕对象
  • navigator为浏览器以及系统对象
(一)screen 对象
a. 属性 (记得即可)
avaliHeight 	// 可占用的最大高度
avaliWidth 		// 可占用的最大宽度
avaliLeft 		// 离屏幕左侧的距离
avaliTop 		// 离屏幕上方的距离
(二)navigator对象
a. 属性
userAgent 		// 用户浏览器设置信息

五、路由(拓展)

路由分为:前端路由和后端路由

前端路由:根据不同的访问路径(path),渲染不同的内容(组件)
  • 页面路由
  • hash路由
  • H5

Q43:了解过shallowRefshallowReactive吗?

shallowRefshallowReactive 都是 Vue 3 中提供的响应性 API,用于创建响应式数据。但它们的工作方式略有不同,适用于不同的场景。

shallowRef

  • shallowRef 创建一个浅层的响应式引用。这意味着对引用本身的更改是响应式的,但引用的值(如果是对象)内部的属性更改不会触发响应。
import { shallowRef } from 'vue';

const shallow = shallowRef({ name: 'John' });

// 更改引用本身是响应式的
shallow.value = { name: 'Doe' }; // 会触发响应

// 更改对象内部的属性不会触发响应
shallow.value.name = 'Jane'; // 不会触发响应

shallowReactive

  • shallowReactive 创建一个浅层的响应式对象。这意味着对象的顶层属性是响应式的,但嵌套属性的更改不会触发响应。
import { shallowReactive } from 'vue';

const state = shallowReactive({
  user: {
    name: 'John'
  }
});

// 更改顶层属性是响应式的
state.user = { name: 'Doe' }; // 会触发响应

// 更改嵌套属性不会触发响应
state.user.name = 'Jane'; // 不会触发响应

使用场景

  • shallowRef:适合需要控制复杂对象引用的情况,比如避免深层次属性变化触发不必要的重新渲染。
  • shallowReactive:适合需要创建浅层响应式对象的情况,尤其是在需要确保嵌套对象不触发响应的场景下。

总结来说,如果你只关心顶层属性或引用的变化,而不希望深层次的变化触发响应时,可以使用 shallowRefshallowReactive