前端面试技巧:如何在30分钟内展现最佳实力

发表时间: 2024-05-29 11:56

自我介绍

一上来,依旧是亘古不变的定律,就像校园时期甜甜的恋爱,彼此都从相互了解开始,然后再决定是否选择对方。 这里可以参考我的上一篇文章,按照这个模板来介绍自己,因人而异。

一、聊聊vue2和vue3中响应式处理数据的不同

面试官听到我自我介绍说到熟悉vue全家桶,于是上来先探探底,看似问vue2和vue3的区别,其实是想看看你对这两者的底层了解多深 ,依旧是面经,结合我学习源码时的总结,一套高输出爆发给到面试官:

  • vue2中采用的是Object.defineProperty和原生的getter和setter方法,当我们初始化一个数据data的时候,会实例化一个Observe类,首先它会迭代遍历data中的每一个属性,并通过Object.defineProperty给遍历到的每一个属性添加上getter和setter方法,当属性被读取的时候就会触发getter方法进行依赖(Wather)收集,当属性被修改的时候就会触发setter方法进行依赖(Wather)触发。(但是,光靠这些方法并不能建立完全的响应式数据与依赖之间的关系,这种办法效率低、功能弱。当我们使用Object.defineProperty()来进行数据劫持,只有当数据被修改或者读取操作的时候才会触发组件的渲染,当涉及到组件中数据的增加和删除操作时就不能触发组件更新渲染,还需要手动在数组的增删方法内通过重写的方式,在拦截里面进行手动收集依赖和触发依赖进行视图更新,vue2默认做了这个操作)
  • vue3中不同的是采用了ES6新增的Proxy进行代理操作,他提供了一个创建代理对象的构造函数,与vue2不同点就在于它会对整个对象进行监听和拦截,省去了每次迭代遍历data属性的性能开销,并且里面有内置的getter和setter方法,在被读取、赋值、删除属性等操作的时候触发getter方法进行依赖收集,在修改属性值的时候触发setter方法,对收集到的依赖进行触发。vue3里面我们一般通过两种方式来响应式处理一个数据,分别是ref和reactive,简单来讲,ref可以响应式处理原始数据类型和引用数据类型,但是reactive只可以处理对象,原因就在于Proxy只能接受一个对象作为参数。底层原理可以看我之前手写的文章。
  • 【手搓reactive】
  • 【手搓ref】

二、你用过postman吗?说说你了解的请求方法。项目开发过程中分别在什么场景下会用到这些方法?

我在自己写的项目中会通过这个工具来测试我自己写的后端接口是否有效,这里面经常用到的方法就有:get、set、post、delete等等。,这其实就是一种restful风格,具体展示如下。

在项目开发过程中,前后端首先要进行协调沟通,确定好每个接口的请求方式,比如当我们需要对数据库进行查询操作时,一般用的就是get方式,由于get中没有body,参数只能接在url中,而url的长度是有限制的,所以我们在进行查询数据的操作时不会携带太多参数,如果涉及到增加和修改操作时,一般会用到post请求,比如后端通过post方法返回一个响应时是有响应体body的,我们可以携带大量的参数在body中。然后涉及到删除操作显然就是delete方式,涉及到图像之类的就是put/patch方式。

三、vuex和pinia的区别

这两者都是状态管理的一种工具,为什么需要设计出一种状态管理工具呢?原因就是当我们有多个组件进行通讯的时候,涉及到兄弟组件通讯或嵌套的更加深,我们知道父子子父组件之间通信通过的是$emit以及pros方式进行组件通讯的,但是我们依靠这种链式的组件通信当组件过多时效果就很差,于是我们需要借助一个状态管理仓库来管理这些响应式数据,这样就可以更加方便且高效的实现组间通讯。

vuex给我的印象首先就是代码风格类似于vue2中的选项式API风格,而且里面有state数据源、mutation方法、以及action方法等等,而且限制要求比较繁琐,比如规定在mutation中只能存放同步的代码,在action中只能存放异步的代码,这么做的目的就是为了防止当调用了两个包含异步回调的 mutation 来改变状态,我们无法知道什么时候回调和哪个先回调?这里主要的缺点就是代码不简洁、过于繁琐。于是我们程序员更倾向于使用pinia来进行状态管理。

【vuex使用以及详解】

pinia不同于vuex,首先它使用的类似于vue3中的组合式API风格,当然也可以同vuex一样使用组合式API风格,不过省去了action存放不同类型代码的区别,不管是异步还是同步代码都可以放进mutation,pinia会进行托管代理。更重要的一点是它使用的是函数式编程,我们在pinia中定义的每一个仓库其实都是一个函数,这样做的优点就是所用需要用到这个仓库中数据的组件不会共享一个数据源,而是分为一个个函数来管理各自的数据源,不仅提高了代码的可读性,而且也避免了数据污染的潜在危害可能。

【pinia使用及详解】

四、CSS中display:grid,网格默认会分为多少份

当我们设置容器属性display:grid,不做其他设置的时候,会初始化容器为网格布局并且默认分为1等分。如果没有明确地定义网格的列和行大小,网格会根据可用空间平均分配列和行。

当我们设置容器属性为display: table,也会初始化容器为网格布局并且默认分为1等分。它会将元素显示为表格布局。表格的列宽和行高会根据内容和容器大小进行调整,以适应元素的布局。

五、token一般的业务作用

这里我在项目中用到了JWT鉴权,于是就顺着跟面试官聊了下JWT相关的知识点,这个功能主要使用在登入注册后,用户想要浏览其他页面时进行的一个校验手段。浏览器的存储方式简单来说有cookie,sessionStorage、localStorage,但是将用户的信息以及合法性判断时直接放在这些存储中并不安全,所有我们还需要用到JWT(Json Web Token)来生成一个以16进制存储的令牌数据。主要流程如下:

前端登录后,后端校验账号密码的成功后,靠jwt来生成一个token,并将该token返回给前端,或者也可以让后端直接将token保存在cookie中,因为cookie是浏览器的内存空间,但是受后端的掌控,通常是后端将JWT(登录令牌)保存在cookies中,所有被保存在cookie中的数据,都会在每一次的http请求时自动被携带在请求头中返回给后端。;

我还封装了axios,在请求拦截当中为每一次的请求头中添加一个authorization字段,之后的接口请求,后端获取到请求头中的token,并进行校验,如果token合法,则返回数据,否则返回401状态码告诉前端token失效。

六、cookie和sessionStorge和localStorge

于是聊完Token,面试官很感兴趣的继续问道:请简单说说你对 cookie和sessionStorge和localStorge 的理解。

首先,这三种都是浏览器本次存储的方式,但是它们有一些自己的特点和彼此间的差异:

cookie

  • 大小只有4kb
  • 涉及到跨域时里面的资源不可共享
  • 安全性很差:
    • 客户端可修改: 客户端可以自由修改 Cookie 中的数据,不具备数据完整性保障,容易受到篡改攻击,影响数据的安全性。
    • 跨站脚本攻击(XSS): 如果网站存在 XSS 漏洞,攻击者可以通过注入恶意脚本来窃取用户的 Cookie 数据,进而获取用户的敏感信息。
    • 跨站请求伪造(CSRF): 攻击者可以利用 CSRF 攻击来伪造用户的请求,实现恶意操作,例如利用用户的 Cookie 发起恶意请求,执行不当操作。
  • 只存在请求头中
  • 每一次发送请求响应时,都会将cookie中的数据全部返回给后端

sessionStorage

  • 存在在浏览器内存中,相对于cookie体积更大
  • 页面关闭时,数据会消失

localStorage

  • 相对于前两者,体积更大,可以存储更多内容
  • 关闭页面,数据不会消失,除非用户手动删除
  • 相对于cookie,数据存在硬盘中,不会携带给后端

七、数组的头插尾插和头删尾删

当面试官问到这种类型问题的时候,我们可不能一股脑将数组中的那些方式零碎的讲出来,要有条理性,这样面试官也会认为我们对这类知识的理解和记忆方式更加优秀。

数组的头插

  • splice 设置第三个参数,可以实现将想要的内容插入数组中的指定位置(包括头部)
  • unshift 在数组的开头插入新的元素
  • concat 通过数组拼接,将想要插入的元素拼接在数组之前

数组的尾插

  • push 直接在数组尾部插入元素
  • splice 设置第三个参数,可以实现将想要的内容插入数组中的指定位置(包括尾部)
  • concat 通过数组拼接,将想要插入的元素拼接在数组之后

数组的头删

  • shift 删除数组的第一个元素,并返回被删除的元素。
  • splice 不设置第三个参数,可以实现指定数组中的位置删除元素(包括头部)
  • slice 指定数组中的某个位置删除元素

数组的尾删

  • pop 直接修改原数组,减少数组的长度,并返回被删除的元素
  • 设置length属性 通过缩短数组的长度也可以达到尾删的效果
  • splice 不设置第三个参数,可以实现指定数组中的位置删除元素(包括尾部)
  • slice 指定数组中的某个位置删除元素

八、 filter和find的区别

这两者都是可以用来操作数组的方法,作用就是根据提供的回调函数测试数组中的每个元素,其中它们有以下区别:

Array.prototype.filter() 方法:

  • 功能:对数组中的每个元素执行给定的函数,并创建一个新的数组,包含所有使得函数返回值为 true 的元素。
  • 返回值:一个新数组,其中包含了原数组中满足条件的所有元素。
  • 使用场景:当你需要筛选出数组中满足特定条件的所有元素时,通常使用 filter。

示例:

const numbers = [1, 2, 3, 4, 5, 6];const evenNumbers = numbers.filter(num => num % 2 === 0);console.log(evenNumbers); // 输出: [2, 4, 6]

Array.prototype.find() 方法:

  • 功能:对数组中的每个元素执行给定的测试函数,直到找到第一个使测试函数返回值为 true 的元素,然后返回该元素。如果没有找到这样的元素,则返回 undefined。
  • 返回值:找到的第一个满足条件的元素或 undefined(如果未找到符合条件的元素)。
  • 使用场景:当只需要找到数组中满足特定条件的第一个元素时,通常使用 find。

示例:

const users = [    { id: 1, name: 'Alice' },    { id: 2, name: 'Bob' },    { id: 3, name: 'Charlie' }];const userById = users.find(user => user.id === 2);console.log(userById); // 输出: { id: 2, name: 'Bob' }

总结起来,filter 是为了过滤出所有匹配项并生成一个新的数组,而 find 则是为了寻找并返回第一个匹配项(或者是 undefined)。

九、 聊聊你知道哪些git指令

聊到git指令,由于我接触到的业务中用到git是从初始化仓库开始到提交代码到仓库里面,还有更多的是团队管理管理一个项目时,会涉及到的git指令,于是我就聊了初始化仓库开始到提交代码到远程仓库分支这一系列过程涉及到的git指令。

初始化和克隆仓库

  1. git init :用于在一个目录中创建一个新的Git仓库。
  2. git clone [url] :从远程仓库复制一份完整的项目到本地,同时自动创建一个指向远程仓库的追踪分支。

文件操作

  1. git add [file/folder] :将指定文件或目录下的所有修改添加到暂存区。
  2. git add . :将当前目录下所有文件的改动(包括新建、修改、删除)添加到暂存区。
  3. git status :显示工作目录中文件的状态,包括未追踪的文件、待暂存的改动以及与远程分支差异等。

提交与历史管理

  1. git commit -m "commit message" :将暂存区的改动以指定的提交消息提交到本地仓库。
  2. git commit --amend :强制覆盖提交(不建议常规使用),主要用于修改上一次提交的内容或提交信息。
  3. git log :显示详细的提交历史记录。

分支操作

  1. git branch [branch-name] :创建新的本地分支。
  2. git checkout [branch-name] :切换到指定的分支。
  3. git merge [branch-name] :将指定分支的更改合并到当前所在分支。

远程交互

  1. git fetch [remote-name] :获取远程仓库的所有分支及提交,但不改变当前工作区或HEAD指针。
  2. git pull [remote-name] [branch-name] :将远程分支的最新提交下载到本地并尝试自动合并。
  3. git push [remote-name] [branch-name] :将本地分支的提交推送到远程仓库对应的分支。

从初始化或者克隆一个仓库到提交代码到本地仓库再到远程仓库的对应分支这一系列操作涉及到的git指令就讲了大概这些,其实这些只是Git命令中的一部分,实际上Git的功能远不止这些,还包括标签管理、解决冲突、子模块操作、重写历史等多个方面。

总结

这一次面试官很有耐心,而且能够将业务中复杂的逻辑通俗易懂地解释给我听,收获了很多知识点。