Skip to content

金三银四面试题汇总之三月面试遇见的前端题

作者:isolcat
发表于:2023-03-25
更新于:7 个月前
字数统计:21.1k 字
阅读时长:65 分钟

前言

从三月刚开始就疯狂的海投简历,到最后顺利拿下某中厂的日常实习 OC,中间还是遇见了很多的坎坷,这里来按照不同的公司来记录一下我的前端面试经历吧

知乎

这是我最后悔的一场面试,不是因为别的,就是因为我没有准备充分,错过了我这个很喜欢的公司 由于是第一次面试,所以紧张的不行,不过面试官人真的很好,一直笑眯眯的鼓励我继续说下去,作为所有面试的开头,这场面试让我对后面的面试更加有信心和好奇了

CSS 部分

  • 常用的选择器有哪些?(我是傻逼,太紧张了乱说一通)

常用的选择器包括标签选择器、类选择器、ID 选择器、后代选择器、子元素选择器、相邻兄弟选择器、通用选择器和属性选择器等

  • 权重

CSS 的权重是指在样式冲突时,某一条样式被应用的优先级。权重值的计算方式是根据选择器的类型和数量来确定的。选择器的权重值从高到低为:!important > 行内样式 > ID选择器 > 类选择器、属性选择器和伪类选择器 > 标签选择器和伪元素选择器。具体来说,ID 选择器的权重值为 100,类选择器、属性选择器和伪类选择器的权重值为 10,标签选择器和伪元素选择器的权重值为 1。在样式冲突时,权重值高的样式会覆盖权重值低的样式。

  • 比如我想隐藏一个元素应该怎么办呢?这里我回答了 visibility 和 display,还自作聪明的说了 z-index,自己给自己挖了坑
  1. 使用display属性:设置元素的 display 属性为 none,这样元素在页面上不会占用任何空间,同时也不会对其他元素产生影响。
  2. 使用visibility属性:设置元素的 visibility 属性为 hidden,这样元素在页面上不可见,但仍然占用空间。
  3. 使用opacity属性:设置元素的 opacity 属性为 0,这样元素在页面上不可见,但仍然占用空间。
  4. 使用position属性:将元素的 position 属性设置为 absolute 或 fixed,然后将元素移动到屏幕外或者使其超出容器的范围之外,这样元素在页面上不可见,但仍然占用空间。
  5. 使用z-index属性:将元素的 z-index 属性设置为负值,这样元素在页面上不可见,但仍然占用空间。
  6. 使用clip属性:将元素的 clip 属性设置为一个矩形区域,这样元素只会显示矩形区域内的部分,其余部分会被裁剪掉。
  • 接着上面的继续问,比如我想要一个元素,既不会被移除,但又要被隐藏(这里我回答的是 visibility,但面试官不满意,让我想想其他的方法) 最后说的方法是将其移除视窗外,或者直接缩小到不可见(这个方法确实出乎我的意料)

使用position属性:将元素的 position 属性设置为 absolute 或 fixed,然后将元素移动到屏幕外或者使其超出容器的范围之外,这样元素在页面上不可见,但仍然占用空间。

  • css 当中如果出现了两个一样的类定义,你如何避免冲突(有点太紧张了,就说的是 scoped 进行样式隔离,然后又按照自己的记忆随便回答了个外部引入的方式,结果把面试官弄笑了,唉)
  1. 优先级:CSS 中每个选择器都有一个优先级,可以通过优先级来确定哪个样式定义将被应用。如果两个类定义中的样式相冲突,可以通过调整优先级来解决冲突。
  2. 父元素选择器:可以使用父元素选择器来限定样式的范围,从而避免冲突。比如,如果两个类定义中的样式都应用到某个父元素的不同子元素上,可以使用父元素选择器来限定样式的范围,从而避免冲突。
  3. 命名空间:可以使用命名空间来区分不同模块或组件的样式定义,从而避免冲突。比如,可以为不同模块或组件的类定义添加不同的命名空间前缀,从而将它们区分开来。
  4. scoped 样式:可以使用 scoped 样式来将样式限定在特定的组件或模块中,从而避免与其他组件或模块的样式冲突。scoped 样式是一种 Vue 框架提供的特殊样式,可以通过在 style 标签中添加 scoped 属性来实现。

后面的有点记不清楚了

JavaScript 部分

  • 问我基本数据类型和引用数据类型(这里大意了,说的很快,可能没说全面。。。)
  • 基本数据类型:Undefined null Boolean number string
  • 引用数据类型:object array function
  • 问我 es6 里的新引入的东西,这里面试官想让我说 map forEach,我说了箭头函数那些,他就鼓励我继续说下去,结果大脑当机了没想到这些
  • 问我 map 和 forEach 的区别,我又乱回答了一通,js 基础真的太差了我,唉

map()forEach()都是 JavaScript 数组对象的方法,用于遍历数组。它们的区别在于返回值和使用方式。

forEach()方法会对数组的每个元素执行一次回调函数,没有返回值,仅仅是遍历数组。

arr = [1, 2, 3, 4];
arr.forEach((num) => {
console.log(num * 2);
});
// 输出2 4 6 8

map()方法会对数组的每个元素执行一次回调函数,并将回调函数的返回值组成一个新的数组返回,不会修改原数组。

arr = [1, 2, 3, 4];
const newArr = arr.map((num) => {
return num * 2;
});
console.log(newArr); // 输出[2, 4, 6, 8]

因此,如果我们想对数组进行遍历并执行一些操作,可以使用forEach()方法;如果我们需要在遍历数组的同时生成一个新的数组,可以使用map()方法。

记不清楚了后面,反正回答的有点差

Vue 部分

  • 面试官本身是 react 的,不是很清楚 vue,就让我介绍 vue,我就说了 Vue 的一些特点和 react 的区别之类的
  • 然后问我 Vue 双向绑定之类的,我就又讲了一些底层的东西
  • 问我接触过 react 没,我回答没 😢

项目部分

我这次能拿到这个面试完全看的是组件库的项目,面试官似乎对我组件库的项目很感兴趣

  • 就说为什么我的 button 按钮那里不进行一个直接的映射,而是还需要进行调用颜色,我说这部分实现遇见了 bug
  • 项目的组件还是太少了
  • 用过 git 吗?给我介绍一下,我就开始介绍了,又给自己挖了坑,提到了 git merge,然后问我如何切换分支,我说不会,只能硬着头皮说自己项目没分支,面试官看上去很惊讶 😢

手撕 js 部分

没有我背的八股 js,直接哭死

实现了将一个由键值对对象组成的数组转换成一个键为对象中 key 属性值、值为 value 属性值的对象

function change(arr) {
    return arr.reduce((pre, { key, value }) => {
        pre[key] = value;
        return pre
    }, {})
}

let a = change([{ key: 'a', value: '1' }, { key: 'b', value: '2' }])
console.log(a)

我直接不会,然后面试官教我怎么实现

反问

我第一次面试,所以我感觉很烂,您觉得怎么样呢,还有后续吗?

  • 我感觉还行,但你的 js 部分可能还不是很好 🥹😭 感觉很阳光开朗

知乎的技术栈是什么呢?

  • 大部分是 react,有一些是 Vue

请问你们的组件是直接用现成的还是自己弄呢?

  • b 端的话会自己弄,c 端需要很多自己定义样式的地方,大都手搓

好的,没有了,谢谢您

  • 好的

总结

自己太紧张了,很多地方发挥的很失败,面试没准备好,在面试的时候我才在搭建环境,自己的 js 基础太差了,git 命令也很差,需要恶补一下,知乎的寄了,接下来加油吧

扁鹊健康

第二场面试,八股文基本上全答出来了,结果反手删我微信给我挂了 😓

第二次面试很明显比第一次好多了,没有那么紧张,情绪也没有失控,可能是没开视频的原因?感觉面试的时候还是得尽量严肃一点吧,控制好自己的情绪和面部表情才能够更好的回答问题,在面试前让自己冷静下来,像对待考试一样去对待每一场面试

面试官迟到了 6 分钟的样子 电话面试 本以为只会面试 10 分钟,结果面试了半小时

之前看牛客上的面经,以为是很简单的那种,结果大意了,把我简历扒拉干净了的感觉

一上来还是自我介绍

数据结构

一上来就进行数据结构的拷打呜呜,我数据结构很烂诶

  • 有哪些排序的方法?
  1. Array.prototype.sort():该方法可以对数组进行原地排序,即直接修改原数组,不会返回新的数组。默认情况下,它会将数组元素转换为字符串,然后按照 Unicode 码点排序。如果需要按照其他方式排序,可以传入一个比较函数作为参数。
  2. Array.prototype.reverse():该方法可以将数组中的元素按照相反的顺序重新排列,并返回新的数组。
  3. 冒泡排序(Bubble Sort):这是一种简单的排序算法,它重复地遍历要排序的数组,比较相邻的元素并交换位置,直到整个数组都已经排序。
  4. 快速排序(Quick Sort):这是一种快速的排序算法,它的基本思想是选择一个基准元素,然后将数组中的元素分为小于基准元素和大于基准元素的两部分,再对这两部分分别进行排序。
  5. 插入排序(Insertion Sort):这是一种简单的排序算法,它将数组分为已排序和未排序两部分,然后将未排序部分的第一个元素插入到已排序部分的正确位置上。
  6. 选择排序(Selection Sort):这是一种简单的排序算法,它将数组分为已排序和未排序两部分,然后从未排序部分选择最小的元素并放到已排序部分的末尾。
  7. 归并排序(Merge Sort):这是一种分治的排序算法,它将数组分成两个子数组,分别对这两个子数组进行排序,然后将排序后的子数组合并成一个有序的数组
  • 我说了快排和冒泡排序后问我快排的时间复杂度

快速排序的平均时间复杂度为 O(nlogn)。具体来说,当待排序数组的划分比较平均时,快速排序的时间复杂度是最优的。而当待排序数组已经有序或接近有序时,快速排序的时间复杂度会退化为 O(n^2)。

快速排序的时间复杂度分析如下:

  • 每次划分操作需要对整个数组进行一次遍历,时间复杂度为 O(n);
  • 快速排序的递归树的深度为 logn,因为每次划分都会将数组一分为二,所以深度为 logn;
  • 每次划分的时间复杂度为 O(n),因此快速排序的总时间复杂度为 O(nlogn)。

需要注意的是,快速排序的最坏时间复杂度为 O(n^2),但这种情况很少出现,通常情况下快速排序的时间复杂度为 O(nlogn),是一种高效的排序算法。

  • 我说了有两种情况,然后追问我如何对快排进行优化(这里没回答好)

JavaScript 中,可以使用以下技巧来优化快速排序算法:

  1. 三数取中:在选择基准元素时,使用数组中间、头部和尾部的三个元素的中位数作为基准元素。这可以降低最坏情况的出现概率。
  2. 插入排序:在数组的长度小于某个值(如 10)时,使用插入排序算法而不是快速排序。插入排序在处理小数组时比快速排序更快。
  3. 随机化数组:在每次执行快速排序时,随机打乱数组,以增加算法的随机性。
  4. 尾递归优化:使用尾递归优化快速排序的实现,避免栈溢出。
  • 问我快排最差的情况是什么

最差情况是每次选取的基准元素都是当前子数组中最大或最小的元素。在这种情况下,每次划分都只能排除一个元素,因此需要进行 n 次划分才能完成排序,时间复杂度为 O(n^2)。这种情况发生的概率非常低,但是如果数据本身就是有序的,或者是基本有序的,快排容易陷入最差情况。

后面突然问我计算机组成原理了解吗?把我吓到了,连忙说之前学过,但没怎么了解

CSS

CSS 问题我记得不是很清楚了下次面试一定要录音!

  • CSS 盒模型
  • 出了个场景题,说 content 大小为 100px,border 为 100px,问此时怪异盒模型的宽高(这里我电话没听太清楚,好像是说我出了问题了,但我下来一看感觉是面试官说错了(・∀・(・∀・(・∀・*))

js

  • es6 新特性
  • 箭头函数和普通函数区别
  1. 语法不同:箭头函数使用箭头符号(=>)来定义函数,而普通函数使用关键字 function 来定义。
  2. this 的指向不同:箭头函数没有自己的 this,它会继承父级作用域中的 this 值。而普通函数中的 this 则是在函数被调用时动态确定的,它的值取决于调用函数的方式。
  3. 无法使用 arguments 对象:箭头函数没有自己的 arguments 对象,因此在箭头函数中使用 arguments 会引用外部作用域的 arguments
  4. 不能用作构造函数:箭头函数不能使用 new 关键字来创建实例,因为它们没有自己的 this,也没有原型对象。
  5. 没有原型:箭头函数没有 prototype 属性,因此不能通过它来定义方法。
  6. 没有自己的 arguments, super, new.target 对象:箭头函数没有自己的 arguments, super, new.target 对象,它们都是从外部继承的。
  • Promise
  • Promise 的参数有哪些

Promise 构造函数的参数为一个函数,这个函数接收两个参数:resolve 和 reject,它们分别表示 Promise 对象的两种状态:已解决(fulfilled)和已拒绝(rejected)

  • Promise.all 了解吗
  • Promise.all 的使用场景
  1. 多个异步操作并行执行,且需要等待所有操作完成后进行下一步处理,比如从多个 API 接口获取数据,然后将所有数据合并到一起再进行渲染
  2. 多个异步操作中有一个操作失败就立即停止所有操作,并执行错误处理逻辑
  • 数据类型,基本数据类型和引用数据类型
  • symbol 了解吗 说说他的使用场景

定义对象的私有属性:Symbol 值作为属性名是唯一的,可以防止属性名的冲突,因此可以用来定义对象的私有属性

防止对象属性被意外修改:由于 Symbol 值是唯一的,因此可以用来定义对象属性,防止属性被意外修改

定义常量:由于 Symbol 值是唯一的,因此可以用来定义常量

定义枚举:由于 Symbol 值是唯一的,因此可以用来定义枚举

计算机网络

  • 说说 http 和 https 的区别
  1. 安全性:HTTP 传输的数据是明文的,容易被窃取和篡改,而 HTTPS 使用 SSL/TLS 加密传输数据,可以保证数据的机密性和完整性,防止数据被窃取和篡改
  2. 端口号:HTTP 默认使用端口号 80,而 HTTPS 默认使用端口号 443
  3. 证书:HTTPS 使用 SSL/TLS 协议对传输数据进行加密,需要使用证书对网站进行身份验证,防止中间人攻击。HTTP 不需要证书进行身份验证
  4. 速度:由于 HTTPS 使用 SSL/TLS 加密传输数据,会增加传输数据的时间和带宽消耗,因此速度比 HTTP 慢一些
  5. 缓存:HTTP 可以使用浏览器缓存来提高访问速度,而 HTTPS 在加密传输数据时会禁止浏览器缓存,以保证数据的安全性
  • 说说 http 状态码
  • 了解 https 数据传输的过程吗?(没太回答上来)
  1. 客户端向服务器发起 HTTPS 请求,请求中包含了 SSL/TLS 支持的信息,比如支持的 SSL/TLS 版本号、加密算法等。
  2. 服务器返回证书给客户端,证书中包含了服务器的公钥、服务器的身份信息和证书的有效期等。
  3. 客户端验证服务器的身份,包括验证证书的有效性、证书是否过期、证书中的域名与服务器的域名是否一致等。
  4. 如果证书验证通过,客户端生成一个随机的加密密钥,并使用服务器的公钥进行加密,然后发送给服务器。
  5. 服务器使用私钥解密客户端发来的密钥,然后生成一个随机数作为会话密钥,并将会话密钥加密后发送给客户端。
  6. 客户端和服务器使用会话密钥进行数据传输,客户端和服务器之间的所有数据都使用会话密钥进行加密和解密,保证数据的机密性和完整性

项目

移动端媒体项目:

  • 你这个项目为什么不用 vuex 而是 pinia 呢?说说二者的区别吧
  • 你这个 token 持久化是怎么实现的?(乱编了我就)
  • 除了 pinia 那个方法还有什么呢?我说了 session
  • session 关掉后就没有了哦,还有吗?(答案是 localstorage)

组件库项目:

  • 为什么想到做个组件库的项目呢?
  • 你是如何实现组件库的封装呢?(这里回答 defineComputed 然后讲我是如何写的组件就行)
  • 说一下常用的 git 命令吧
  • 经典问题之 git merge 和 git rebase 的区别

git merge 命令会生成一个新的合并提交,并且会保留原来的分支历史记录,合并后的提交包含两个分支的修改。而 git rebase 命令则是将当前分支的修改应用到目标分支上,重新生成一颗新的分支历史记录,使得分支历史记录更加线性化

  • vite 和 webpack 的区别
  1. 构建方式不同:Vite 利用 ES Modules 的特性进行构建,每个文件都是一个独立的模块,开发过程中只需要编译修改的文件,不需要每次都编译整个项目;而 Webpack 采用静态分析的方式进行构建,需要分析整个项目中的依赖关系,每次修改后需要重新编译整个项目。
  2. 开发体验不同:Vite 支持快速的热更新和即时预览,开发者可以在修改代码的同时,立即在浏览器中查看到最新效果;而 Webpack 需要重新编译后才能查看最新效果。
  3. 对 Vue 的支持:Vite 是 Vue.js 官方推荐的开发工具,内置了对 Vue 单文件组件的支持,可以直接在浏览器中运行 Vue 组件;而 Webpack 需要通过插件等方式进行支持。
  4. 总体来说,Vite 更适合于轻量级的应用,对于 Vue 单文件组件的支持更加完善,而 Webpack 则更适合于复杂的应用,可以通过插件等方式进行更加灵活的配置
  5. vite 支持热重载

热重载(Hot Reload)是指在应用程序运行时对代码进行修改,而无需重新启动应用程序或重新加载整个页面,即可使更改的部分立即生效并反映在应用程序中。热重载可以帮助开发人员更快地调试和开发应用程序,同时减少开发周期

还有一些我就忘记了哈哈

反问

  • 公司技术栈?

react 少部分是 vue2

  • 多久出结果

一周内吧

感觉还算是很不错的面试,基本上都回答出来了,不过这个是阿里的外包公司,一半员工都是阿里过去的,看了一下公司规模很小,估计也没 hc 让我进去,就当一次电话面试的体验了,加油!

即刻 app

很热情的一个面试官!反问环节最有意思的一家,不过难度也算最高的一家

这是我目前面过的公司里难度感觉最高的一家了,不是常规的那种八股文直接硬背,而是真的结合实际的场景进行出题的,对于一个技术栈的考察的深度很深,是一个很不错的面试官

首先第一点给我感觉不一样的就是,没有自我介绍!上来就直接发了个链接,可能是打算结合实际代码来对我进行考察,可惜这里失败了(学校的网真的差。。。)

ref 和 reactive 的区别

ref 是一个函数,它可以将一个普通数据类型(如数字、字符串等)转换为一个响应式对象,从而让这个数据在 Vue 的响应式系统中被追踪。ref 返回一个对象,这个对象有一个.value 属性,用来获取和设置这个响应式对象的值

js
import { ref } from 'vue'

const count = ref(0)

console.log(count.value) // 0

count.value = 1

console.log(count.value) // 1

而 reactive 则是一个函数,它可以将一个普通的 Javascript 对象转换为一个响应式对象。它会递归地将这个对象的所有属性都转换为响应式对象,从而让整个对象在 Vue 的响应式系统中被追踪。reactive 返回一个 Proxy 对象,用来代理原始对象的访问和修改

js
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  message: 'hello',
})

console.log(state.count) // 0
console.log(state.message) // 'hello'

state.count = 1
state.message = 'world'

console.log(state.count) // 1
console.log(state.message) // 'world'

因此,ref 主要用于创建一个单一的响应式数据,而 reactive 则适用于创建一个复杂的、包含多个属性的响应式数据对象

ref 可以大量的替换成 reactive 吗

不能直接把 ref 替换成 reactive。

ref 主要用于将基本数据类型(如字符串、数字等)转换为响应式数据,并提供一个.value 属性用于访问和修改该数据。而 reactive 则用于将一个普通的 JavaScript 对象转换为响应式对象,并使用 Proxy 来拦截对该对象的访问和修改,以实现响应式更新。

因此,如果你需要使用响应式数据来存储基本数据类型,或者你只需要响应式地跟踪一个值的变化,那么 ref 仍然是更合适的选择。而如果你需要管理一个对象的多个属性,并希望这些属性可以响应式地更新,那么 reactive 会更加合适

为什么 vue 和 react 都去选择自己实现一个路由,是出于什么目的呢

  1. 更好地集成到框架中:由于路由是前端应用中必不可少的一部分,因此框架集成路由功能可以提供更好的用户体验和开发效率。通过自己实现路由库,Vue 和 React 可以将路由功能无缝集成到框架中,提供更好的开发体验和更高的开发效率。
  2. 更好地控制代码和依赖:Vue 和 React 自己实现的路由库可以更好地控制代码和依赖。如果使用第三方路由库,可能会增加代码的复杂性和依赖关系,而自己实现路由库可以避免这些问题。
  3. 更好地满足框架的需求:Vue 和 React 的路由库可以更好地满足框架的需求。由于框架本身的特性和设计思想,可能需要特定的路由实现方式来满足这些需求。通过自己实现路由库,可以更好地满足框架的需求,提供更好的开发体验和更高的性能。
  4. 更好地控制性能:Vue 和 React 的路由库可以更好地控制性能。由于路由是前端应用中的关键部分,性能往往是一个重要的考虑因素。通过自己实现路由库,Vue 和 React 可以更好地控制路由的性能,从而提供更好的用户体验和更高的性能

总之,Vue 和 React 都实现了自己的路由库,主要是为了更好地集成到框架中,更好地控制代码和依赖,更好地满足框架的需求,以及更好地控制性能

浏览器自己本身就有路由,为什么不直接用 a 标签进行一个跳转,而是选择用 router 来进行一个跳转呢

浏览器本身的路由是基于 URL 的,即每个页面都有一个唯一的 URL 地址。使用浏览器本身的路由,需要在 URL 中手动编写参数,处理页面刷新和前进/后退等操作的逻辑,这样会使得代码复杂性增加,并且不太方便维护。

而使用 router 库可以将路由相关的逻辑抽象出来,让开发者可以更加方便地处理页面跳转和传递参数等操作。使用 router 可以实现单页应用(SPA),使得用户在应用中的操作更流畅,且无需每次跳转都重新加载整个页面。

此外,使用 router 还可以提供一些额外的功能,如路由守卫、动态路由等,这些功能可以帮助开发者更好地控制路由的行为,提高应用的性能和安全性

虽然浏览器本身也有路由,但使用 router 库可以提供更好的开发体验和更丰富的功能,使得应用的开发更加方便、高效和可维护

浏览器为什么支持单页面路由呢?

参考链接:https://developer.mozilla.org/zh-CN/docs/Web/API/History

浏览器支持单页面路由的一个重要原因是History API。

在传统的多页面应用中,页面之间的跳转通过超链接或表单提交等方式实现,每个页面都有一个唯一的 URL 地址。而在单页面应用中,页面的跳转是通过 JavaScript 代码控制,使用 history API 可以更加方便地实现这种页面切换逻辑。

history API 是 HTML5 规范中新增的一组 API,可以让开发者更加方便地操作浏览器的历史记录。通过 history API,开发者可以在不重新加载整个页面的情况下,改变浏览器的 URL 地址,添加或修改历史记录,以及监听历史记录的变化等操作。

在单页面应用中,开发者可以使用 history API 来实现前端路由,即在不重新加载整个页面的情况下,通过改变 URL 地址,实现不同页面之间的切换。这样可以提高应用程序的性能,并且使得应用程序更具交互性和动态性

当我们在使用 history 进行导航的时候,我们的页面真的进行了一个切换吗?是怎么做到的呢

当我们使用 history 进行导航时,实际上并没有进行页面的刷新或重新加载。相反,浏览器仅仅是通过修改 URL 地址和浏览器历史记录,模拟了一个页面的切换效果。

具体来说,使用 history 进行导航时,我们通常会调用 history.pushState 或 history.replaceState 方法,这些方法可以向浏览器历史记录中添加或修改一个记录,并且同时修改当前 URL 地址。然后,浏览器会根据新的 URL 地址重新渲染页面,并且在浏览器的历史记录中添加或修改一个记录。

当我们使用 history 进行导航时,虽然页面并没有进行刷新或重新加载,但是浏览器会触发一些相关的事件,如 popstate 事件,用来处理导航过程中的一些逻辑。开发者可以在这些事件中添加相关的处理逻辑,从而实现前端路由的功能

vue 如何监听路由的变化呢?

在 Vue 中,可以使用 Vue Router 提供的导航守卫(Navigation Guards)来监听路由的变化。

导航守卫是 Vue Router 提供的一组钩子函数,可以在路由发生变化时被触发。通过使用导航守卫,开发者可以实现一些常见的路由控制逻辑,如路由权限控制、路由拦截、路由跳转前的确认等等。

Vue Router 提供了三种类型的导航守卫:

  1. 全局守卫:在整个应用程序中,所有的路由变化都会触发这些守卫。可以通过 Vue Router 实例的 beforeEach、beforeResolve 和 afterEach 方法来注册全局守卫。
  2. 路由独享守卫:在某个路由上,只有该路由变化时才会触发这些守卫。可以在路由配置对象中通过 beforeEnter 属性来注册路由独享守卫。
  3. 组件内守卫:在某个路由对应的组件中,可以通过 Vue 组件生命周期钩子函数来监听路由的变化,实现一些组件内部的路由控制逻辑。

以下是一个使用全局守卫来监听路由变化的示例:

js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
  ],
})

router.beforeEach((to, from, next) => {
  console.log('路由变化:', from.path, ' => ', to.path)
  next()
})

在上面的示例中,我们通过 Vue Router 实例的 beforeEach 方法注册了一个全局守卫,在每次路由变化时都会触发这个守卫,并且打印出路由变化的信息。可以根据实际需求,编写自己的路由守卫逻辑

原生 js 如何进行监听路由的变化

在原生 JavaScript 中,可以使用 window.onpopstate 事件来监听路由的变化。

当用户在浏览器中进行前进或后退操作时,或者通过 JavaScript 调用 history.pushState()history.replaceState() 方法时,都会触发 window.onpopstate 事件。

可以通过如下代码来监听 window.onpopstate 事件:

window.onpopstate = function(event) {
  console.log("当前路由:", window.location.pathname);
};

在上面的代码中,当用户在浏览器中进行前进或后退操作时,或者通过 JavaScript 调用 history.pushState()history.replaceState() 方法时,会触发 window.onpopstate 事件,并输出当前路由路径到控制台。

需要注意的是,这种方式只能监听浏览器历史记录中的路由变化,对于通过 AJAX 或其他方式进行的路由变化是无法监听的。如果需要监听所有的路由变化,可以考虑使用一些现有的路由库,如 React Router、Vue Router 等

没有 hash 的路由如何进行监听

如果你使用的是 HTML5 History API 来管理路由,而不是 hash 路由,那么可以通过监听 popstate 事件来实现路由变化的监听。

HTML5 History API 可以让我们使用 pushState()replaceState() 方法来操作浏览器的历史记录,并可以修改当前页面的 URL,而不会导致页面的刷新。

当通过 pushState()replaceState() 方法修改浏览器历史记录时,会触发 popstate 事件。我们可以通过监听这个事件来获取路由的变化。

例如,我们可以使用如下代码来监听路由的变化:

window.addEventListener('popstate', function(event) {
  console.log("当前路由:", window.location.pathname);
});

在上面的代码中,当用户通过浏览器前进或后退按钮,或者通过 pushState()replaceState() 方法修改浏览器历史记录时,会触发 popstate 事件,并输出当前路由路径到控制台

onpopstate 可以监听到一个 pushstate 的事件吗

这里回答错了,我说的是可以进行监听的,面试官让我回去再好好看看

onpopstate 事件只能监听到由浏览器触发的历史记录变化,例如点击浏览器的前进或后退按钮,或者调用 history.back()history.forward() 方法。

如果你在 JavaScript 中调用 history.pushState()history.replaceState() 方法来修改浏览器的历史记录,那么不会触发 onpopstate 事件。但是,调用这两个方法会添加新的历史记录,并且可以在历史记录中回退和前进,这些历史记录变化会触发 onpopstate 事件。

因此,如果你想要在调用 pushState()replaceState() 方法后立即获取路由变化,可以在调用这两个方法后手动触发 popstate 事件,例如

history.pushState({}, '', '/new-path');
window.dispatchEvent(new PopStateEvent('popstate'));

在上面的代码中,我们先调用 pushState() 方法来修改浏览器的历史记录,并修改当前页面的 URL 为 /new-path。然后,手动触发 popstate 事件,这会立即触发绑定在 window.onpopstate 上的事件处理函数,并获取到新的路由信息

ts 泛型的作用,在开发当中最常用在哪里

TypeScript 中的泛型(generics)是一种用于在编译时期处理类型的工具。泛型可以让我们编写更通用、更可重用的代码。

泛型最常用的场景之一是在函数和类中使用。通过使用泛型,我们可以编写可重用的函数或类,可以支持多种不同类型的参数或属性。例如,下面是一个使用泛型的函数示例

typescript
function reverse<T>(list: T[]): T[] {
  return list.reverse();
}

let numbers = [1, 2, 3, 4];
let reversedNumbers = reverse(numbers);

let letters = ["a", "b", "c"];
let reversedLetters = reverse(letters);

在上面的代码中,我们定义了一个名为 reverse 的函数,它使用了一个类型参数 T。我们可以将 reverse 函数应用于任何具有 reverse 方法的数组类型。在调用 reverse 函数时,我们将一个类型为 T[] 的数组作为参数传递,并返回一个类型为 T[] 的数组。

除了函数和类,泛型还可以应用于 TypeScript 中的其它特性,例如接口、类型别名等

在开发中,我们最常使用泛型的场景是编写通用的数据结构、算法和函数,例如列表、树、排序算法、搜索算法等等。泛型可以让我们编写更通用、更可重用的代码,并且可以提高代码的灵活性和可扩展性。同时,使用泛型还可以让我们在编译时期发现类型错误,避免一些潜在的运行时错误

axios 二次封装的好处

通过对 axios 进行二次封装,我们可以实现以下功能:

  1. 统一处理请求参数和响应数据格式:我们可以对请求参数和响应数据进行预处理或格式化,以便在多个请求中使用相同的格式。
  2. 统一处理错误信息:我们可以对错误信息进行统一处理或格式化,以便在多个请求中使用相同的错误信息处理逻辑。
  3. 添加请求头、设置超时时间等功能:我们可以在二次封装中添加一些公共的请求头、超时时间等参数,以便在多个请求中使用相同的参数。
  4. 支持自定义拦截器:我们可以通过自定义拦截器来对请求或响应进行处理,例如添加 token、在请求头中添加认证信息等

如何标识用户已经登录

在 Web 应用程序中,标识用户是否已经登录通常使用 Session 或 Token 的方式。

Session 是一种服务器端的技术,用于跟踪用户的状态。当用户登录成功后,服务器会创建一个 Session,并为该用户分配一个唯一的 Session ID,将该 Session ID 存储在 Cookie 中或者通过 URL 传递给客户端。在用户访问其他页面时,客户端会将 Session ID 发送回服务器,并使用该 ID 来查找服务器端的 Session 数据。如果 Session 数据存在,说明用户已经登录,否则用户未登录。使用 Session 的优点是可以在服务器端存储敏感的用户信息,不会在客户端暴露。

Token 是一种基于客户端的技术,通常使用 JSON Web Token (JWT) 或类似的技术。当用户登录成功后,服务器会生成一个 Token,并将该 Token 发送给客户端。客户端在后续的请求中携带该 Token,服务器可以通过解析 Token 来验证用户的身份。使用 Token 的优点是可以让客户端缓存用户的登录状态,减轻服务器的负担,同时可以在不同的服务之间共享用户的登录状态。

在实现登录功能时,通常需要将用户的登录信息存储在服务器端,例如数据库、缓存等等。当用户登录成功后,服务器会创建 Session 或生成 Token,并将其返回给客户端。客户端可以将 Session ID 存储在 Cookie 中,或将 Token 存储在本地存储或会话存储中。在后续的请求中,客户端会将 Session ID 或 Token 发送回服务器,服务器可以根据 Session ID 或解析 Token 来验证用户的身份

token 已经过期的话,我想要刷新 token 如何实现

在实现 Token 刷新功能时,通常需要注意以下几个步骤:

  1. 在服务器端,需要定义一个用于刷新 Token 的 API 接口,该接口需要验证当前 Token 的有效性,并根据需要生成一个新的 Token,并返回给客户端。
  2. 在客户端,当发现当前 Token 已经过期时,需要向服务器端发送一个刷新 Token 的请求,并将当前 Token 和刷新 Token 的回调函数传递给服务器端。
  3. 在服务器端,当收到客户端发送的刷新 Token 的请求时,需要验证当前 Token 的有效性。如果当前 Token 有效,生成一个新的 Token,并将其返回给客户端。如果当前 Token 无效,需要向客户端返回一个错误码或提示信息。
  4. 在客户端,当收到服务器端返回的新 Token 时,需要将新 Token 存储到本地存储或会话存储中,并调用刷新 Token 的回调函数

无感刷新 token

在前后端分离的应用中,为了保证安全性和用户体验,通常会使用 token 来实现用户身份认证。当 token 过期时,需要重新获取新的 token,以保持用户的登录状态。在这种情况下,无感刷新 token 可以提高用户体验,使用户无需手动重新登录即可继续访问应用程序。

以下是一种基本的无感刷新 token 的实现思路:

  1. 定义 token 的过期时间,例如 30 分钟。
  2. 在用户每次发送请求时,检查 token 是否快要过期,例如在 token 过期时间前 5 分钟进行检查。
  3. 如果 token 即将过期,发送一个请求给后端,请求新的 token。
  4. 如果后端返回新的 token,将新的 token 保存在本地,同时更新所有请求中的 token 值。
  5. 如果后端返回错误或者新的 token 无效,清除本地 token,跳转到登录页面。

通过这种方式,即使 token 过期,用户也不需要手动重新登录即可继续使用应用程序,从而提高用户体验和应用程序的安全性。当然,具体实现细节可能因具体应用场景而异,需要根据实际情况进行调整。

将上面的操作写在哪里呢?

在实现无感刷新 token 的过程中,主要涉及到两个方面的实现:前端和后端。

前端方面,可以在请求拦截器中实现 token 的检查和更新。可以通过在请求头中设置 Authorization 字段,将 token 发送给后端。当 token 即将过期时,可以在请求拦截器中发送一个刷新 token 的请求,并将新的 token 保存在本地存储中,同时更新所有请求的 Authorization 字段。这样,即使 token 过期,用户也无需手动刷新 token 即可继续使用应用程序。

后端方面,需要实现一个 token 的刷新接口,接收旧的 token 并返回新的 token。在处理刷新请求时,需要对旧的 token 进行验证,以确保该请求是合法的。如果验证通过,则生成新的 token 并返回给前端,否则返回错误信息。

总之,在实现无感刷新 token 的过程中,前端和后端都需要进行一定的实现。前端需要在请求拦截器中进行 token 的检查和更新,后端需要实现一个 token 的刷新接口,并对旧的 token 进行验证。同时,还需要定义 token 的过期时间,并根据实际情况进行调整

响应拦截器的功能

响应拦截器是前端网络请求中一个非常重要的概念,它的主要功能是在从服务器接收到响应数据之后,对响应数据进行处理,然后再将其返回到调用处。

响应拦截器的主要功能包括以下几个方面:

  1. 错误处理:响应拦截器可以检查响应数据中是否存在错误信息,例如请求失败、权限不足等。如果存在错误信息,则可以根据实际情况进行处理,例如跳转到错误页面、显示错误信息等。
  2. 数据处理:响应拦截器可以对响应数据进行处理,例如对数据进行格式化、过滤、排序等操作。这样可以提高应用程序的可读性和可维护性。
  3. 统一处理:响应拦截器可以对所有的响应数据进行统一处理,例如添加一些公共的响应头、对返回的数据进行加密等操作。这样可以提高应用程序的安全性和可扩展性。
  4. token 刷新:响应拦截器可以在响应数据中检查 token 的过期时间,如果即将过期,则可以自动进行 token 的刷新,从而实现无感刷新 token 的功能。
  5. 缓存处理:响应拦截器可以对响应数据进行缓存处理,例如将响应数据存储在本地存储中,以提高应用程序的性能和用户体验

反问

  • 您对我的面试表现和之后学习前端的建议

看我自己的兴趣,看我对之后的哪些技术比较感兴趣,然后说了好几分钟的一些我都没听过的前端技术/(ㄒ o ㄒ)/~~

一下子就感觉自己的前端之路才刚刚开始起步,后面的前端技术好多呀

  • 之后还会有二面吗?

楠哥会给我消息,最迟是明天

  • 公司技术栈和技术氛围

你知道你投递是小宇宙吧,我们公司小宇宙全是 react,然后很详细的给我介绍了 react 里的很多东西

丁香园

面试体验最差的一家!面试官一进来就挎着个脸,怨气十足的样子,随便问了一个八股就开始手撕,因为当时我心里想着另一家公司的二面,所以没怎么走心了(互相 KPI?)

  • 讲一下防抖和节流
  • 手撕一下
  • 做个算法(内容我忘记了)

反问环节,不好意思 😅 完全不想反问你任何问题,然后面试官就嘲讽了我一顿就挂电话了 😅

合合信息

面试官是一个听声音感觉年纪蛮大的一个人,人很和蔼!很愉快的一次面试

上来面试官就自我介绍,然后介绍公司,然后就问我有什么需要反问的?第一次遇见这种情况哈哈哈

vue 响应式原理

http1 和 http1.1 的区别

HTTP(Hypertext Transfer Protocol)是用于在 Web 上传输数据的协议。HTTP / 1.0 和 HTTP / 1.1 是两个版本的 HTTP 协议,下面是它们之间的一些区别:

  1. 持久连接:HTTP / 1.1 引入了持久连接,这意味着在单个 TCP 连接上可以发送多个请求/响应对,从而减少了每个请求的延迟。HTTP / 1.0 在每个请求/响应之后关闭 TCP 连接。
  2. 块传输编码:HTTP / 1.1 支持块传输编码,这意味着可以在接收响应时逐步解压缩数据,而不必等待整个响应。这对于处理大型响应或流式数据非常有用。
  3. 身份验证:HTTP / 1.1 提供了更安全的身份验证方法,例如基于令牌的身份验证方案,可以替代 HTTP / 1.0 中的基本身份验证。
  4. 缓存处理:HTTP / 1.1 对缓存处理进行了改进,包括新的 Cache-Control 指令,可以更好地控制缓存行为。
  5. 响应码:HTTP / 1.1 引入了更多的响应码,例如“100 Continue”,这使得客户端可以更好地控制它们的请求行为。
  6. 主机头字段:HTTP / 1.1 要求在每个请求中都包含主机头字段,这使得服务器可以更好地处理多个虚拟主机。
  7. 管道化:HTTP / 1.1 支持管道化,允许客户端同时发送多个请求,从而提高性能。HTTP / 1.0 不支持管道化。

除了前端你其他的学的怎么样?比如计算机组成原理和网络之类的

其他的我不太行诶(这里希望是别问太难),结果面试官就笑了,说那咱们就不问了

然后就开始闲聊了 😂

这里面试官建议我好好学学计算机基础那些,那些相比于前后端的技术,可以让我在 AI 的冲击下走的更远,心里也蛮有感触的,毕竟大家都看见了 chatGPT 带来的威力,算是一次很轻松愉快的面试吧

同程旅行

项目

看得出来面试官是提前对我的简历看了很多的,一上来就先对我的项目进行了分析,然后问我最近的一次实习(就这个最近的一次实习直接把我整不会了,一下就紧张了起来 😭)和项目是什么时候

我最近还没有实习,零经验,我直接介绍我自己的项目可以吗?

这里感觉自己介绍的项目太拉胯了,没有很好的体现出自己的项目,接下来应该将自己的项目进行概括才行啊

你为什么要用 jsx 进行开发组件库呢?有什么好处呢?

JSX 是 React 中一种用于编写组件的语法,它可以将 HTML 和 JavaScript 结合起来,让开发者更加方便地编写动态组件。使用 JSX 进行组件库开发的好处如下:

  1. 增加可读性和可维护性:JSX 让代码看起来更像是 HTML 模板,这使得代码更容易阅读和理解,也更容易进行修改和维护。
  2. 提高开发效率:使用 JSX 可以减少开发者在编写组件时需要编写的模板代码,这可以减少代码量,提高开发效率。
  3. 更好的性能:JSX 可以通过使用虚拟 DOM 来优化组件渲染性能。React 在每次组件更新时会生成新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行比较,然后只更新需要更新的部分,从而提高渲染效率。
  4. 易于与 React 集成:React 是一种流行的前端框架,使用 JSX 可以使组件库更容易与 React 集成,从而提高组件库的适用性。

综上所述,使用 JSX 进行组件库开发可以提高开发效率、可读性和可维护性,并且可以提高组件渲染性能,从而使组件库更加适用于 React 等前端框架

pinia 在这个项目里解决了什么问题

在一个 Vue 3 项目中使用 Pinia 可以解决以下问题:

  1. 简化状态管理:Pinia 提供了一个简洁的 API,使得我们可以更容易地定义和管理状态,并在整个应用程序中共享它们。
  2. 更好的类型支持:Pinia 提供了一个类型安全的 API,可以让我们更容易地编写类型安全的代码,并减少错误。
  3. 更好的可测试性:Pinia 的状态管理使得我们可以更容易地对 Vue 3 组件进行单元测试,从而提高代码的可测试性。
  4. 更好的性能:Pinia 的状态管理实现了基于 Proxy 的响应式系统,从而提高了性能并减少了不必要的重渲染

综上所述,Pinia 能够帮助我们更好地管理 Vue 3 应用程序中的状态,并且提供了更好的类型支持、可测试性和性能,从而使得我们可以更容易地编写高质量的 Vue 3 应用程序。在这个项目中使用 Pinia,可以提高项目的开发效率和代码质量

pinia 的 store 你是如何进行设计的

在设计 Pinia 的 store 时,我们通常需要考虑以下几个方面:

  1. 状态的划分:我们需要考虑应用程序中需要管理的状态,并根据不同的功能和需求进行划分。通常情况下,我们会将状态划分为多个 store,每个 store 管理一部分相关的状态。
  2. Store 的命名:我们需要为每个 store 提供一个唯一的名称,以便在整个应用程序中引用它们。
  3. Store 的定义:我们需要定义每个 store 的结构,包括 store 的状态、getter、mutation 和 action 等。
  4. Store 的注册:我们需要将定义好的 store 注册到应用程序中,以便在应用程序的其他地方使用。
  5. Store 的使用:我们需要在组件中使用 store,通过 getter 获取 store 的状态,并在需要时通过 mutation 和 action 来修改 store 的状态。

在实际的应用程序中,我们通常会根据具体的业务需求来设计 store 的结构和划分。一般来说,我们会将相关的状态放在一个 store 中,并通过模块化的方式来组织多个 store,从而实现更好的可维护性和可扩展性。

在使用 Pinia 的过程中,可以参考官方文档提供的示例和最佳实践,以便更好地设计和管理 store,从而提高应用程序的性能和可维护性

你的 store 有进行模块的拆分吗?还是说放在一起

在实际应用程序中,将 store 模块化并组织成多个文件通常是比较好的实践,这有助于提高应用程序的可维护性和可扩展性。

通常情况下,我们会将 store 模块化为多个文件,每个文件对应一个 store 模块,然后通过 Vuex 的模块化功能来组织它们。每个 store 模块负责管理自己的状态、getter、mutation 和 action,并且可以通过模块间的调用来实现跨 store 的状态共享。

模块化的方式可以使得 store 更加易于维护和扩展,因为每个模块只负责自己的一部分功能,而不会将所有的状态都放在同一个 store 中,从而导致代码的臃肿和难以维护。

在使用 Pinia 时,同样可以将 store 模块化为多个文件,并使用 Pinia 提供的模块化功能来组织它们。每个 store 模块也负责管理自己的状态、getter、mutation 和 action,并且可以通过其他 store 模块的调用来实现跨 store 的状态共享。

综上所述,将 store 模块化并组织成多个文件是一个比较好的实践,可以提高应用程序的可维护性和可扩展性

你做的组件库当中有遇见什么困难吗?可以举例说明

html5

h5 当中新增了哪些

以下是 HTML5 中一些新增的功能和特性:

  1. 新的语义化标签:HTML5 新增了一些语义化标签,如 <header><footer><nav><article><section><aside> 等,可以更好地描述页面的结构和内容,有助于提高页面的可读性和可访问性。
  2. 多媒体支持:HTML5 提供了更好的多媒体支持,包括 <audio><video> 标签,可以直接在网页中嵌入音频和视频。
  3. 本地存储:HTML5 提供了本地存储功能,包括 localStorage 和 sessionStorage,可以在客户端浏览器中存储数据,从而提高应用程序的性能和用户体验。
  4. Web Workers:HTML5 中新增了 Web Workers,可以在后台线程中执行 JavaScript 代码,从而提高应用程序的性能和响应速度。
  5. Canvas:HTML5 中新增了 <canvas> 标签,可以在网页中绘制各种图形和动画,有助于实现更加复杂的交互效果。
  6. 地理位置 API:HTML5 提供了地理位置 API,可以获取用户的地理位置信息,有助于实现基于地理位置的应用。
  7. Web Socket:HTML5 中新增了 Web Socket,可以实现双向通信,从而实现更加实时和高效的应用程序。
  8. Web Storage:HTML5 中新增了 Web Storage,可以在客户端浏览器中存储数据,从而提高应用程序的性能和用户体验。

综上所述,HTML5 提供了许多新的功能和特性,可以帮助开发人员更加方便地实现一些复杂的应用场景,提高应用程序的性能和用户体验

html 的行内元素和块级元素的区别,都有哪些

HTML 元素可以分为两类:行内元素和块级元素。它们的主要区别在于:

  1. 显示方式:块级元素在页面上以块的形式展现,它会占据一整行的空间,可以设置宽度、高度、内边距和外边距等属性。而行内元素则不会独占一行,它们在一行内按照从左到右的顺序排列,并且不能设置宽度、高度和内边距等属性。
  2. 内容模型:块级元素通常用于包含其他块级元素或行内元素,可以包含任何其他元素。而行内元素通常用于包含文本或其他行内元素,不能包含块级元素。
  3. 默认样式:块级元素通常具有明显的外观特征,例如:段落 <p> 元素会在前后添加空白,标题 <h1>~<h6> 元素会加粗并换行等等。而行内元素通常没有这些明显的外观特征,例如:超链接 <a> 元素只是有下划线,并且字体颜色有所变化等等。

以下是一些常见的 HTML 块级元素和行内元素:

块级元素:

  • <div>
  • <p>
  • <h1>~<h6>
  • <ul><ol><li>
  • <table>
  • <form>
  • <hr>
  • <header><footer><nav><section> 等 HTML5 新增的语义化标签

行内元素:

  • <a>
  • <span>
  • <strong><em>
  • <img>
  • <input>
  • <label>
  • <br>
  • <button>
  • <select>
  • <textarea>

img 说行内还是块呢?span 说行内还是块

元素默认是行内元素,但可以通过设置 display 属性为 block 或 inline-block 等值来改变其显示方式。

<span> 元素是一个纯粹的行内元素,它不能包含块级元素,但可以包含其他行内元素。

css

盒子模型

盒子模型是指 HTML 元素在渲染时呈现为一个矩形盒子的模型。这个矩形盒子包含了元素的内容、内边距(padding)、边框(border)和外边距(margin)等部分。

具体来说,盒子模型包含以下几个部分:

  1. 内容区域(content area):元素内部的实际内容,包括文本、图像、嵌套的元素等。
  2. 内边距(padding):内容区域和边框之间的空白区域,可以通过 CSS 属性 paddingpadding-* 来设置。
  3. 边框(border):包围内容和内边距的线条,可以通过 CSS 属性 borderborder-* 来设置。
  4. 外边距(margin):边框和周围元素之间的空白区域,可以通过 CSS 属性 marginmargin-* 来设置。

盒子模型在 Web 页面布局中扮演着重要的角色,可以用来控制元素的尺寸、位置、边距和内边距等方面的表现。默认情况下,元素的尺寸是指内容区域的大小,但是通过设置 box-sizing 属性可以改变元素尺寸的计算方式,使其包括内边距和边框

will-change

CSS
https://developer.mozilla.org/zh-CN/docs/Web/CSS
属性 will-change 为 web 开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏

链接:https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change

垂直居中布局

translate 是基于自身还是父元素

translate() 是一种 CSS transform 函数,用于在元素上进行平移变换。它是相对于元素自身的位置进行平移,而不是相对于父元素或其他元素。

具体来说,translate() 函数接受两个参数:translate(x, y),其中 xy 分别代表在水平和垂直方向上的平移距离。如果只给出一个参数,则默认为水平方向上的平移。

translate() 函数的平移距离是相对于元素自身的初始位置进行计算的,而不受到父元素的影响。因此,如果我们想要实现相对于父元素进行平移,可以考虑使用 position 属性和 topbottomleftright 属性来控制元素的位置

移动端兼容问题你是如何解决的

移动端兼容问题是前端开发中非常重要的一环,以下是我通常采用的一些解决方法:

  1. 使用移动端适配方案:移动端设备的屏幕尺寸和像素密度差异较大,因此需要使用适配方案来保证页面在不同屏幕上显示效果一致。常见的适配方案有:rem、vw/vh、flexible.js 等。
  2. 使用 CSS3 和 ES6 功能时需要进行前缀处理:移动端的浏览器兼容性不如 PC 端,因此在使用 CSS3 和 ES6 功能时需要进行前缀处理,例如 -webkit--moz--ms--o- 等。
  3. 避免使用过多的图片和动画效果:移动设备的网络环境和硬件性能相对较弱,因此需要尽量减少页面中的图片数量和动画效果,以提高页面的加载速度和流畅性。
  4. 使用移动端专用的 UI 组件和交互方式:移动设备的操作方式和 PC 端有较大差异,因此需要使用移动端专用的 UI 组件和交互方式,例如滑动、轻扫、长按等。
  5. 针对不同设备的浏览器进行测试:移动设备的浏览器种类繁多,不同浏览器在兼容性上也有所不同,因此需要在开发完成后对不同设备的浏览器进行测试,以确保页面在各种浏览器上的兼容性。

除此之外,还可以使用一些工具来帮助解决移动端兼容问题,例如 Autoprefixer 可以自动添加 CSS3 前缀,FastClick 可以解决移动端点击事件的延迟等

css 的相对单位有哪些

在 CSS 中,相对单位有以下几种:

  1. em:相对于父元素的字体大小。例如,如果父元素的字体大小为 16px,子元素的 font-size 设为 1.5em,则子元素的字体大小为 24px。
  2. rem:相对于根元素的字体大小。例如,如果根元素的字体大小为 16px,元素的 font-size 设为 1.5rem,则元素的字体大小为 24px。与 em 不同的是,rem 取决于根元素的字体大小,而不是父元素的字体大小。
  3. vwvh:相对于视口宽度和高度的百分比。例如,如果视口宽度为 1000px,元素的 width 设为 50vw,则元素的宽度为 500px。
  4. vminvmax:相对于视口宽度和高度中较小或较大的那个值的百分比。例如,如果视口宽度为 1000px,视口高度为 800px,元素的 width 设为 50vmin,则元素的宽度为 400px(因为视口宽度为较大的值,所以按照视口宽度计算)。
  5. %:相对于父元素的宽度或高度的百分比。例如,如果父元素的宽度为 1000px,元素的 width 设为 50%,则元素的宽度为 500px。

相对单位与绝对单位(如像素、英寸等)相比,具有更好的响应式特性,可以根据不同的屏幕尺寸和设备类型自适应地调整大小,因此在响应式设计中得到广泛应用

计算机网络

输入 URL

渲染进程

js

普通数据类型存储在哪里?堆还是栈

在 JavaScript 中,普通数据类型的值通常存储在栈内存中。栈是一种后进先出的数据结构,可以高效地管理函数调用和局部变量。

当我们声明一个变量并赋值时,JavaScript 引擎会为该变量分配一段栈内存,并将变量的值存储在其中。当该变量不再使用时,这段栈内存也会被释放,变成可用的空间。

常见的普通数据类型包括数字、字符串、布尔值、null 和 undefined 等。这些类型的值都比较简单,不需要过多的内存空间来存储,因此通常存储在栈内存中。

与普通数据类型不同,引用数据类型(如对象、数组、函数等)的值存储在堆内存中。堆内存是一种动态分配的内存空间,可以存储复杂的数据结构和对象。

当我们声明一个引用类型的变量时,JavaScript 引擎会为该变量分配一段栈内存,并将其指向堆内存中的实际值。因为引用类型的值通常比较复杂,包含大量的属性和方法,因此需要较大的内存空间来存储

深拷贝和浅拷贝的区别。 让你实现一个深拷贝的思路

深拷贝和浅拷贝都是对于复杂数据类型进行复制的操作,区别在于复制的方式不同。

浅拷贝是指创建一个新对象,这个新对象有着原始对象属性值的一份精确拷贝,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝是指创建一个新对象,这个新对象的值和原始对象的值完全没有关联,即便原始对象中有引用类型的属性,新对象也会开辟新的内存地址,完全拷贝一份新的对象,修改一个对象不会影响到另一个对象。

一个实现深拷贝的思路是:

  1. 首先判断需要拷贝的对象是否是引用类型,如果是基本类型则直接返回该值。
  2. 如果是引用类型,则创建一个新的空对象或数组(取决于原始对象的类型)。
  3. 遍历原始对象的所有属性或元素,将它们的值递归地拷贝到新对象中,这个递归过程需要注意以下几点:
    • 如果属性或元素的值是基本类型,则直接复制该值;
    • 如果属性或元素的值是引用类型,则递归调用深拷贝函数,并将结果赋值给新对象的相应属性或元素。
  4. 返回新对象或数组
js
function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null)
    return obj

  const newObj = Array.isArray(obj) ? [] : {}
  for (const key in obj)
    newObj[key] = deepCopy(obj[key])

  return newObj
}

除了这个方法你还有其他的思路吗?json 如果来做深拷贝存在哪些问题

除了递归拷贝之外,还有其他实现深拷贝的思路:

  1. 使用 Object.assign()方法实现浅拷贝,然后对于每个属性值是引用类型的属性,再递归调用深拷贝函数。
  2. 使用 ES6 的展开运算符(…)实现浅拷贝,然后对于每个属性值是引用类型的属性,再递归调用深拷贝函数。
  3. 使用第三方库,如 Lodash 的_.cloneDeep()方法,该方法能够递归地深拷贝一个对象。

使用 JSON.stringify()和 JSON.parse()方法进行深拷贝是一种常见的错误做法。虽然这种方法能够将一个对象序列化为 JSON 字符串,再将 JSON 字符串解析为一个新对象,但是存在以下几个问题:

  • 该方法只能序列化对象中的可枚举属性,不能序列化对象的原型链和方法。
  • 如果对象中有循环引用(即一个对象引用了自身),则该方法会抛出错误。
  • 该方法不能序列化 RegExp、Date、Map、Set 等特殊类型的对象,会将其序列化为字符串或空对象。

因此,使用 JSON.stringify()和 JSON.parse()方法进行深拷贝并不可靠,建议使用其他方法实现深拷贝

箭头函数的作用,箭头函数和普通函数的区别

箭头函数是 ES6 中新增的一种函数定义方式,主要有以下几个作用:

  1. 简化函数定义:箭头函数可以使用更短的语法定义函数,省略了 function 关键字和 return 语句。
  2. 更简洁的 this 指向:箭头函数没有自己的 this,它的 this 指向最近的一层非箭头函数作用域的 this,可以避免 this 指向混乱的问题。
  3. 更简洁的代码结构:箭头函数通常可以使代码更加简洁易懂,特别是当需要传递回调函数或者进行函数式编程时,箭头函数可以使代码更加简洁易读。

与普通函数相比,箭头函数的主要区别在于 this 的指向和函数定义的语法结构:

  1. this 指向:箭头函数的 this 指向在定义时就已经确定了,指向最近的一层非箭头函数作用域的 this。而普通函数的 this 指向在运行时才能确定,可能会受到调用方式、绑定方式等多种因素的影响。
  2. 语法结构:箭头函数省略了 function 关键字和 return 语句,更加简洁明了。同时,箭头函数的参数只有一个时可以省略括号,而普通函数的参数需要用括号括起来。

需要注意的是,由于箭头函数的 this 指向与普通函数不同,因此在某些场景下可能会出现错误的结果。此外,箭头函数也不能作为构造函数使用,因为它没有自己的 this。因此,需要根据实际情况选择合适的函数定义方式

箭头函数的 this 指向哪里?它的 this 可以被改变吗

箭头函数的 this 指向在函数定义时就已经确定了,它的 this 指向的是定义时所在的作用域中的 this,而不是在调用时所在的作用域。

具体来说,箭头函数的 this 指向最近的一层非箭头函数作用域的 this。如果箭头函数本身没有定义作用域,则指向全局对象。这与普通函数不同,普通函数的 this 指向在调用时才能确定,可能会受到调用方式、绑定方式等多种因素的影响。

由于箭头函数的 this 指向在定义时就已经确定,因此它的 this 不能被改变。即使使用 apply、call 等方法来改变 this 指向,也无法改变箭头函数的 this 指向。

需要注意的是,箭头函数的 this 指向是静态的,不能动态改变,因此在某些场景下可能会出现错误的结果。在这种情况下,可以使用普通函数来替代箭头函数,或者使用 bind 方法来绑定 this 指向

typeof 检测 null

使用 typeof 检测 null 的结果是”object“。

这是因为在 JavaScript 中,null 被认为是一个空对象引用。虽然它不是对象,但 typeof 检测 null 的结果是”object”,这是一个历史遗留问题。在 ES6 中,通过 Symbol.hasInstance 方法可以正确地检测 null,但它并不常用。如果需要判断一个值是否为 null,可以直接使用严格等于(===)运算符,因为 null 只等于它本身,不等于任何其他值

了解微前端吗?微前端目前业内的解决方案 阿里的乾坤了解吗

微前端是一种将前端应用程序拆分为更小、更容易管理的部分的架构风格,每个部分可以独立地开发、测试、部署和扩展。它的主要目的是解决单体应用程序的复杂性和可维护性问题,以及不同应用程序之间的耦合问题。

在业内,目前有许多微前端的解决方案,包括 Single-SPA、qiankun、Mosaic、Piral、Luigi 等等。这些解决方案都有各自的优缺点和适用场景,可以根据实际需求进行选择。

阿里的 qiankun 是一种在 React、Vue、Angular 等前端框架上实现微前端的解决方案。它使用了主应用和子应用的概念,主应用负责整体框架的搭建和管理子应用,子应用则可以使用不同的前端框架进行开发。qiankun 提供了统一的路由、状态管理、样式隔离等功能,可以有效地实现微前端架构

微前端的好处

  1. 技术栈无关性:不同的团队可以使用不同的技术栈来开发不同的微前端应用,而这些应用可以无缝地集成到一个统一的应用中,不会出现技术栈不一致的问题。
  2. 模块化开发:每个微前端应用都是独立开发的,可以根据需求进行拆分成多个小模块,每个模块可以独立开发、测试、部署和升级,从而提高了开发效率和代码质量。
  3. 独立部署:每个微前端应用都是独立部署的,可以快速部署新的功能和修复 bug,而不需要整个应用重新部署,从而提高了部署效率和灵活性。
  4. 高可维护性:由于每个微前端应用都是独立开发、测试、部署和升级的,因此可以更容易地维护和更新每个应用,从而提高了整个应用的可维护性。
  5. 更好的扩展性:微前端应用可以在需要时独立开发和扩展,可以更好地满足业务需求,同时也可以更容易地扩展到新的平台和设备

数组如何进行扁平化的处理?给你几个多维数组,将其平展开来

数组扁平化可以将多维数组转化为一维数组,常用的方法有递归方法和非递归方法。以下是一些实现方法:

递归方法

js
function flatten(arr) {
  let result = []
  arr.forEach((item) => {
    if (Array.isArray(item))
      result = result.concat(flatten(item))
    else
      result.push(item)

  })
  return result
}

// 示例
const arr1 = [1, 2, [3, 4], 5, [6, 7, [8, 9]]]
console.log(flatten(arr1)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

非递归方法

js
function flatten(arr) {
  const result = []
  const stack = [...arr]
  while (stack.length) {
    const item = stack.pop()
    if (Array.isArray(item))
      stack.push(...item)
    else
      result.unshift(item)

  }
  return result
}

// 示例
const arr2 = [1, 2, [3, 4], 5, [6, 7, [8, 9]]]
console.log(flatten(arr2)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

ES6 方法

js
function flatten(arr) {
  return arr.flat(Number.POSITIVE_INFINITY)
}

// 示例
const arr3 = [1, 2, [3, 4], 5, [6, 7, [8, 9]]]
console.log(flatten(arr3)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

需要注意的是,如果数组元素中包含了对象、函数等复杂类型,则需要根据具体情况进行处理

flat 参考链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flat

场景题

实现一个功能,我在页面 A 点击了一个按钮,进入页面 B,这个时候我在页面 B 进行了一个操作,这时候如何让 A 进行一个刷新,也就是如何实现两个进程的通信

实现两个进程的通信可以通过以下几种方式:

  1. 使用浏览器的 localStorage 或者 sessionStorage 来存储需要传递的数据,然后在另一个页面中读取存储的数据并进行处理。需要注意的是,存储的数据类型必须是字符串,因此需要使用 JSON.stringifyJSON.parse 进行数据的转换。

示例代码:

在页面 A 中:

js
localStorage.setItem('data', JSON.stringify({ name: 'John', age: 25 }))
// 跳转到页面B
window.location.href = 'pageB.html'

在页面 B 中:

js
// 从localStorage中读取数据
const data = JSON.parse(localStorage.getItem('data'))
// 处理数据
console.log(data.name, data.age) // John 25
// 删除localStorage中的数据
localStorage.removeItem('data')
// 触发页面A的刷新
window.location.reload()
  1. 使用浏览器的 window.postMessage 方法进行跨窗口通信。该方法可以在不同窗口之间传递数据,并且不同窗口可以处于不同的域名和协议下。

示例代码:

在页面 A 中:

js
// 在A页面中注册message事件的监听器
window.addEventListener('message', (event) => {
  if (event.origin !== 'http://localhost:3000') {
    // 如果不是指定的来源,不予处理
    return
  }
  // 处理接收到的数据
  console.log(event.data)
  // 触发页面A的刷新
  window.location.reload()
})

// 在A页面中向B页面发送数据
window.open('pageB.html')

在页面 B 中:

// 在B页面中向A页面发送数据
window.opener.postMessage({ name: 'John', age: 25 }, 'http://localhost:3000');

需要注意的是,该方法存在跨站点脚本攻击(XSS)的风险,因此需要在处理消息时进行数据的合法性检验,确保消息的来源和内容都是可信的。

  1. 使用第三方的库来实现进程间通信,例如 SignalR、Socket.IO 等。这些库提供了更高级的通信功能,并且支持实时通信、广播消息等特性,但也需要相应的服务器支持。

需要根据具体的场景和需求来选择合适的通信方式

vue 的响应式系统。我分了 vue2 和 vue3 来讲

在 Vue 2 中,Vue 通过 Object.defineProperty() 来实现响应式系统。当一个对象被传入 Vue 实例进行响应式处理时,Vue 会遍历这个对象的每一个属性,并使用 Object.defineProperty() 把这个属性转换成 getter 和 setter。当这个属性被读取时,getter 会被触发,这个属性就会被添加到依赖中;当这个属性被修改时,setter 会被触发,这个属性的依赖就会被通知,并执行相应的更新操作。这样,当数据被修改时,所有依赖这个数据的地方都会自动更新。

但是,Vue 2 的响应式系统存在一些问题。首先,它只能监听对象的属性,而不能监听新增的属性和删除的属性;其次,它无法监听数组的变化,只能监听数组的索引变化,即当使用数组的 push、pop、shift、unshift、splice 等方法时才能触发更新。

在 Vue 3 中,Vue 引入了 Proxy 对象来实现响应式系统。当一个对象被传入 Vue 实例进行响应式处理时,Vue 会使用 Proxy 对象对这个对象进行代理,这样就可以监听新增的属性和删除的属性,同时也可以监听数组的变化。当一个属性被读取或修改时,Proxy 对象的 get 和 set 方法会被触发,这样就可以实现响应式更新。

Vue 3 的响应式系统还有一个优点,就是它支持了多个根节点,也就是 Fragment。这样可以在不需要添加额外的 DOM 节点的情况下,返回多个元素。

总体来说,Vue 3 的响应式系统更加灵活和高效,能够更好地应对复杂的应用场景

vue2 是如何解决数组检测的问题

在 Vue 2 中,对于数组的检测是通过对数组的原型进行改写来实现的。Vue 2 中通过 Object.defineProperty() 方法对数组原型上的 7 个变异方法进行重写,分别是 push()pop()shift()unshift()splice()sort()reverse()。在这些方法被调用时,除了执行它们本身的操作外,还会通知依赖更新。

当数据是数组类型时,Vue 会先判断该数组是否具有 __ob__ 属性(Observer 对象),如果有则说明已经被观测过,直接返回该 __ob__ 对象;如果没有,则会创建一个 Observer 对象来观测该数组,然后返回该 __ob__ 对象。

虽然这种方式可以监听数组的变化,但是存在以下问题:

  1. 监听不到索引值的变化,比如 arr[1] = newValue
  2. 对象的新增和删除也需要进行额外处理。
  3. 遍历数组时会将数组中的每一项都进行依赖收集,造成性能问题。

Vue 提供了 $set 方法,可以用来给数组添加新元素或者修改已有元素的值,使得这些修改也能够被 Vue 监听到并更新视图。例如:

js
this.$set(this.array, 1, 'new value')

这行代码会将 array 数组中索引为 1 的元素的值改为 'new value',并通知 Vue 去更新视图。

除了 $set 方法,Vue 还提供了 $delete 方法来删除数组中的元素

为了解决这些问题,Vue 3 采用了更加高效的响应式系统

vue2 的缺陷,性能问题。如果 data 里的层次很深的话,进行多层次的监听开销是很大的

Vue 2 在进行响应式处理时,会递归遍历数据对象中的每一个属性,并将这些属性转化为 getter 和 setter。当数据层次比较深时,这种递归遍历的开销就会非常大,会导致页面渲染性能下降。

除此之外,Vue 2 还存在以下性能问题:

  1. 每个组件都会创建自己的观察者 Watcher 实例,当组件数量较多时,会导致大量的内存开销和性能问题;
  2. 每次数据变化都会导致重新渲染整个组件,即使数据变化的影响仅限于某个子组件;
  3. 在大型列表渲染时,使用 v-for 进行循环渲染会产生大量的 DOM 操作,影响渲染性能

vue 的模版解析过程

在 Vue 中,模板是由 HTML 代码和 Vue 特定的模板语法组成的。Vue 的模板编译器会将模板编译成渲染函数,然后再生成 Virtual DOM,最终进行渲染。

下面是 Vue 的模板解析过程:

  1. 解析模板,生成 AST 抽象语法树:Vue 的编译器会将模板转换为 AST 抽象语法树,这是一个树形结构,代表了模板的结构,包括元素节点、文本节点、指令等。
  2. 优化 AST:在生成 AST 之后,Vue 的编译器会对其进行优化,例如静态节点提取、静态根节点提取等优化操作,以提高渲染性能。
  3. 生成代码:最后,Vue 的编译器会将 AST 转换为渲染函数的代码。这个渲染函数是一个 JavaScript 函数,接收一个参数 h,返回一个 Virtual DOM 节点。
  4. 生成 Virtual DOM:渲染函数生成后,Vue 会使用它来生成 Virtual DOM,然后对比新旧 Virtual DOM,计算出需要更新的部分,最终只更新需要更新的部分。
  5. 渲染:最后,Vue 将更新后的 Virtual DOM 渲染到真实的 DOM 中。

这个过程是 Vue 的模板编译过程的核心,也是 Vue 能够高效渲染页面的重要原因

反问

  • 你对我的面试表现怎么样

挺好的 基础问题基本上都回答的出来,前端的广度还不够 挺好的

  • 还会有二面吗?

这个我们得和后面的小组商量一下

  • 同程旅行的技术栈是什么?

苏州这边是 vue2 为主,做一个低代码平台的组件化

同程旅行二面

二面是业务部门负责人+HRBP 面,很轻松愉快的一次面试

基本上就是闲聊,问我对前端的一些看法,还有实习、到时候答辩会怎么解决之类的,顺便问了我为什么掘金 ID 叫八岁小孩学编程,是八岁就开始了吗 😂 我连忙回答说是乱取的一个名字,然后就是很愉快的交流了

后续

成功 OC 了,打算去苏州实习咯,这边也和辅导员商量好了,等正式 offer 邮件下来就去打印出来,和学校申请去实习了,我的三月份面试也在这里画上了一个还算完美的句号了,谢谢同程的收留哈哈哈,也谢谢除丁香园以外的每个面试官 😽

Contributors

isolcat