大家好,我是刘布斯。
最近面了不少参加春招的同学,发现应届生或者实习生的专业能力,两级分化很严重,有同学知识的广度和深度都很不错,有的却连 js 的基础数据类型都说不明白。
我们今天来看看一位同学面试字节实习生的经历,答案也整理的很详细,大家来感受下字节的面试强度吧~
年前准备还行,回校三月春招的第一周,小厂面试练手都蛮顺利的。基本都被面试官一顿夸,然后给offer,约实习入职。母胎单身终于体会了把当渣男的感觉,皆拒之。
这周的投递策略调整为300一天
以上实习,可没想, 简历发给BOSS的那一刹那,才发现是字节跳动的商业产品与技术
前端岗。就当一个退无可退
的英雄赵信吧,闪现也难逃被抓的运命。只要不面的太差,不进黑名单,三个月后还是条好汉,再面之。
BOSS直聘
里的这位BOSS, 仿佛蛤蟆卡萨丁
,快速将我活吞入肚,丝毫不给半点反应,马上发来了面试安排....
写下此文,记字节跳动拿了我的一血。感谢信在此,各位烧纸...
function formatNumberWithCommas(number) {
// 类型检测
if (typeof num !== 'number') {
return '';
}
// 先转成字符串
number += '';
// 支持小数,按小数点分成两部分 使用了es6解构
let [integer,decimal] = number.split('.');
// 封装了doSplit方法 第二个参数isInteger来表示是整数部分还是小数部分
const doSplit = (num,isInteger = true) => {
// 如果为空,直接返回
if(num === '') return '';
// 如果是整数部分 先按位切割再返转
// 整数部分数字从右往左数,每3位插入一个逗号
// 小数部分从左往右数
// 两次反转,它的逗号顺序是一样的。
if(isInteger) num = num.split('').reverse();
let str = [];
for(let i = 0; i < num.length; i++){
if(i !== 0 && i % 3 === 0) str.push(',');
str.push(num[i]);
}
if(isInteger) return str.reverse().join('');
return str.join('');
};
integer = doSplit(integer);
decimal = doSplit(decimal,false);
return integer + (decimal === '' ? '' : '.' + decimal);
};
function formatNumberWithRegex(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
语言中会有不同时区、地区的一些本地化功能,符合格式化数字的需求
function formatNumberWithLocale(number) {
let [integer,decimal = ''] = (number + '').split('.');
// 1,234,567 整数部分可通过toLocalString实现
integer = (+integer).toLocaleString();
if(decimal === '') return integer;
// 小数部分另外处理
decimal = decimal.split('').reverse().join('');
decimal = (+decimal).toLocaleString();
decimal = decimal.split('').reverse().join('');
return integer + '.' + decimal;
}
有如下结构的树,使用DFS遍历
遍历一棵树,我们可以使用深度优先和广度优先,都是面试中的常客。
从上图可以看出,深度优先通过递归
深入到树或图的最深层,然后回溯到上一层继续搜索。递归函数可以很好地模拟这种深度搜索过程,使得代码简洁且易于理解。
function dfs(node) {
// 访问当前节点
console.log(node.value);
// 遍历子节点
for (let child of node.children) {
dfs(child);
}
}
// 1 2 3 4 5
我们也把广度优先的代码一起复习下:
const bfs = (root) => {
const queue = [root];
while (queue.length > 0) {
// 先出队
const currentNode = queue.shift();
console.log(currentNode.value);
if (currentNode.children) {
// 进队
currentNode.children.forEach(child => {
queue.push(child);
});
}
}
};
// 1 2 5 3 4
广度优先搜索
需要按层级遍历,队列的先进先出(FIFO)特性适合处理这种需求,确保每一层的节点都能按顺序被访问和处理,从而实现广度优先搜索的效果。
所以,本题是大厂考查同学数据结构基本功的热题。只要我们能将DFS与递归,BFS与队列对应理解,那就是一道送分题。
介绍项目的时候,有聊到jwt生成token登录。这是相对于传统的cookie(客户端+服务器)+session(服务端)来问的,我打算先从cookie+session的登录方式讲起,再说缺点,然后jwt怎么解决了这些问题。
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
在Cookie + Session登录中,用户登录成功后,服务器生成唯一sessionId,存储在用户浏览器的Cookie中。每次请求时,浏览器发送Cookie中的sessionId给服务器,服务器根据sessionId查找对应会话信息,维持用户状态。Session数据通常存储在服务器端,安全性高;sessionId则作为指向对应session数据的标识符,通过Cookie在用户端和服务器端间传递。
JWT(JSON Web Token)登录原理是用户登录成功后,服务器生成一个包含用户身份信息的JWT,并将其发送给客户端。客户端在以后的请求中携带该JWT,在每次请求中服务器验证JWT的有效性,从而实现用户的身份验证和状态保持。JWT包含了加密签名,因此可以确保数据的完整性和安全性。
无需服务器存储状态
传统的基于会话的认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中,使得系统可以更容易地进行水平扩展。
跨域支持
由于 JWT 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权
适应微服务架构
在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。
自包含
JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制
扩展性 JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。
COOKIE不安全
Cookie 在传输过程中可能受到跨站脚本攻击(XSS)和跨站请求伪造(CSRF)等安全威胁,而 JWT 通过签名验证确保数据的完整性,防止篡改。此外,JWT 可以使用加密算法对数据进行加密,增强安全性。相比之下,Cookie 存储在客户端的敏感信息可能被窃取,而 JWT 不存储敏感信息,减少了泄霏风险。
果然,在聊完JWT
的概念和代码流程后,面试官继续追问。但其实追问也是有套路的,我在面试准备的时候,JWT相关题就有思考顺势聊localStorage,cookie等,所以这道题很轻松。
特点 | Cookie | LocalStorage | Token |
---|---|---|---|
存储位置 | 存储在客户端,通过HTTP请求发送给服务器 | 存储在客户端,仅在客户端使用 | 存储在客户端,作为身份验证令牌 |
大小限制 | 每个Cookie大小通常限制为4KB | 存储容量通常为5MB | 长度没有严格限制 |
跨域 | 受同源策略限制 | 受同源策略限制 | 可以跨域发送 |
自动发送 | 每次请求自动发送给服务器 | 不会自动发送给服务器 | 需要手动添加到请求头或参数中 |
安全性 | 可能受到XSS和CSRF攻击 | 相对较安全,不易受到攻击 | 通过签名验证确保安全性 |
使用场景 | 保存会话信息、身份验证等 | 临时数据存储、本地缓存等 | 身份验证、授权令牌传递等 |
存储有效期 | 可设置过期时间,可以是会话性(当浏览器关闭时过期)或持久性 | 永久存储,除非被清除或手动删除 | 可以设置过期时间,通常用于控制访问权限时效性 |
网络安全是大厂考查的点,面试官顺着JWT
追问的第三板斧也砍下来了,还好有准备。
csrf
全称跨站请求伪造(Cross-site request forgery),指的是攻击者携带网站cookie,冒充用户请求的攻击行为
xss攻击主要指网站被攻击者利用漏洞恶意注入脚本并执行,是一种注入类攻击;
如果 xss攻击的执行脚本包含请求服务器的行为,其也会携带用户cookie达到冒充用户请求的行为,这时候也属于 csrf攻击,不过我们一般称这样的情况为 xsrf攻击。
假设被攻击的网站是一个在线银行系统,该系统使用基于 Cookie 的身份验证机制来识别用户。攻击者创建了一个恶意网站,并在该网站上放置了一个图片标签,如下所示:
<img src="https://bank.com/transfer?to=attacker&amount=10000" width="0" height="0">
当被攻击者访问恶意网站时,浏览器会自动加载图片,并向 https://bank.com/transfer
发送请求,携带了被攻击者的身份信息和转账参数。由于被攻击者已经在银行系统中登录,服务器会认为这个请求是合法的,并执行了转账操作,将 10000 单位的资金转到攻击者的账户。
这种攻击方式的危险性在于,攻击者利用了用户已经登录的会话状态,绕过了常规的身份验证机制。为了防范 CSRF 攻击,开发者可以采取以下措施:
同源策略:浏览器的同源策略可以帮助防止其他网站对用户敏感操作的伪造请求。
使用验证码:在执行敏感操作前,要求用户输入验证码进行二次验证,降低攻击成功的可能性。
限制权限:降低被攻击者账户的权限,特别是对于敏感操作。
开发的时候不是VUE就是REACT, 有点懵。给面试官怎啥绝活啊?我这么回答的:
import { ref } from 'vue';
const useRequest = (url) => {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
loading.value = true;
try {
const response = await fetch(url);
const result = await response.json();
data.value = result;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
return { data, loading, error, fetchData };
};
export default useRequest;
import { defineComponent } from 'vue';
import useRequest from './useRequest';
export default defineComponent({
setup() {
const { data, loading, error, fetchData } = useRequest('https://api.example.com/data');
fetchData();
return {
data,
loading,
error
};
}
});
怎么面试官从JWT的三连问,突然问了这么个问题?这是啥脑回路啊?可能是到饭点了,面试官想不按常理出牌,一记重拳想KO我,还好接的可以。
接下来是关于VUE的三板斧。
模板语法和指令
提供了直观的模板语法和丰富的指令,易于学习和使用
响应式和数据绑定
现代MVVM开发方式,比传统DOM编程性能更好,更关注业务
Composition API
相比于选项式API, vue3 提供的Composition API使得代码组织更加清晰
如果组件逻辑比较复杂,代码行数>100, 那么跟一个状态相关的computed、事件、生命周期就得坐电梯查找,而compostion api 可以把它们放一起。
比如 vue-eleme-app/src/components/goods/goods.vue at master · cccyb/vue-eleme-app (github.com) 这样的代码用vue3 composition api 就好管理很好。我们可以把相关的状态、生命周期、事件、计算属性等放一起。
单文件组件
支持单文件组件格式,包含模板、样式和逻辑代码
生态系统和社区支持
生态系统丰富,拥有大量插件和工具,社区支持良好, 文档可读性良好。
TypeScript 支持
对 TypeScript 有较好的支持
Hooks 编程
VUE3也全面支持hooks编程。
函数式编程:使用 Hooks 可以将组件编写为纯函数,使组件的逻辑更加简洁、可预测和易于测试
逻辑复用:Hooks 允许将组件的逻辑进行拆分和复用,通过自定义 Hooks 可以将一组相关的逻辑抽象成可重用的函数。
组件的生命周期,由各种钩子函数按生成、挂载、更新、卸载等几个阶段构成。
生命周期钩子函数 | 作用 |
---|---|
onBeforeCreate | 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。 |
onCreated | 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。 |
onBeforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用。 |
onMounted | 实例已经挂载完成时调用。注意如果根实例挂载到了一个文档内的元素,当 mounted 被调用时 vm.$el 还不可用,因为不是所有子树都被挂载了。 |
onBeforeUpdate | 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。 |
onUpdated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁完成之后调用。 |
onBeforeUnmount | 在卸载之前调用。可以用来清理定时器或监听的事件。 |
onUnmounted | 在实例被销毁之后调用。 |
onErrorCaptured | 当捕获一个来自子孙组件的错误时被调用。 |
onRenderTracked | 当一个依赖项在渲染函数中被追踪时被调用。 |
onRenderTriggered | 当一个依赖项在渲染函数中触发更新时被调用。 |
这个比较简单,就先略过了
防抖是等待一段时间后执行函数,而节流是设定一个时间间隔,在该时间间隔内只执行一次函数。两者的主要区别在于函数执行的时机和频率。
比如页面滚动事件用节流, 搜索建议/窗口调整大小用防抖。
function debounce(fun, delay) {
return function (args) {
let that = this
let _args = args
clearTimeout(fun.id)
fun.id = setTimeout(function () {
fun.call(that, _args)
}, delay)
}
}
function throttle(fun, delay) {
let last, deferTimer
return function (args) {
let that = this
let _args = arguments
let now = +new Date()
if (last && now < last + delay) {
clearTimeout(deferTimer)
deferTimer = setTimeout(function () {
last = now
fun.apply(that, _args)
}, delay)
}else {
last = now
fun.apply(that,_args)
}
}
}
水平垂直居中是常见的布局需求,比如登录弹窗。
方法 | 描述 |
---|---|
使用 Flexbox 布局 | 使用 Flexbox 布局可以轻松实现元素的水平和垂直居中。设置父元素的 display: flex 和 justify-content: center; align-items: center; 。 |
使用 Grid 布局 | 使用 CSS Grid 布局同样可以实现元素的水平和垂直居中。将父元素设为网格容器,然后使用 place-items: center; 。 |
使用绝对定位和 transform 属性 | 将要居中的元素设置为 position: absolute ,然后利用 top: 50%; left: 50%; transform: translate(-50%, -50%); 来进行居中。 |
使用表格布局 | 创建一个包含单元格的表格,然后将内容放置在表格单元格中,并设置单元格样式为 text-align: center; vertical-align: middle; 。 |
使用绝对定位和负 margin 值 | 将要居中的元素设置为 position: absolute ,然后使用 top: 50%; left: 50%; margin: -128px(元素大小) 来进行居中。 |
Flex布局
水平/垂直方向都可以(flex-direction),按1:2:1设置子元素的比例就好。
Grid布局
display: grid;
grid-template-rows: 1fr 2fr 1fr;
grid-template-columns也可以,看哪个方向
使用绝对定位和百分比高/宽
这是小红书经典的事件相关热题目。这是因为每个点击事件处理函数都需要单独注册和管理,当子节点数量较多时,会增加页面的事件监听器数量,影响性能。
使用事件代理(Event Delegation)。通过将点击事件绑定到父元素上,利用事件冒泡机制,在父元素上捕获所有子元素的点击事件,然后通过判断点击的具体子元素(event.target)来执行相应的操作。这样可以减少事件监听器的数量,提高性能。
事件会经历事件捕获和事件冒泡两个阶段。通过捕获浏览器知道事件发生在哪些元素上,再冒泡将这些事件都触发。
也可以通过修改第三个参数为true(useCapture), 让事件在捕获阶段就触发(由外到内),默认是在冒泡阶段触发(由内到外)。
快排和归并排序一致,用的是分治思想。区别在于,快速排序不会把真的数组分割开来再合并到一个新数组中去, 而是直接在原有数组内部进行排序。
首先, 选择(数组中间)一个基准值(pivot)。
接着,左右指针指向数组的两端。先移动左指针,直到找到一个不小于基准值的值为止(左边都比基准值小);然后移动右指针,直到找到一个不大于基准值的值为止。
然后不断的重复上述过程,即递归。
// 快速排序入口
function quickSort(arr, left = 0, right = arr.length - 1) {
// 定义递归边界,若数组只有一个元素,则没有排序必要
if(arr.length > 1) {
// lineIndex表示下一次划分左右子数组的索引位
const lineIndex = partition(arr, left, right)
// 如果左边子数组的长度不小于1,则递归快排这个子数组
if(left < lineIndex-1) {
// 左子数组以 lineIndex-1 为右边界
quickSort(arr, left, lineIndex-1)
}
// 如果右边子数组的长度不小于1,则递归快排这个子数组
if(lineIndex<right) {
// 右子数组以 lineIndex 为左边界
quickSort(arr, lineIndex, right)
}
}
return arr
}
// 以基准值为轴心,划分左右子数组的过程
function partition(arr, left, right) {
// 基准值默认取中间位置的元素
let pivotValue = arr[Math.floor(left + (right-left)/2)]
// 初始化左右指针
let i = left
let j = right
// 当左右指针不越界时,循环执行以下逻辑
while(i<=j) {
// 左指针所指元素若小于基准值,则右移左指针
while(arr[i] < pivotValue) {
i++
}
// 右指针所指元素大于基准值,则左移右指针
while(arr[j] > pivotValue) {
j--
}
// 若i<=j,则意味着基准值左边存在较大元素或右边存在较小元素,交换两个元素确保左右两侧有序
if(i<=j) {
swap(arr, i, j)
i++
j--
}
}
// 返回左指针索引作为下一次划分左右子数组的依据
return i
}
// 快速排序中使用 swap 的地方比较多,我们提取成一个独立的函数
function swap(arr, i, j) {
[arr[i], arr[j]] = [arr[j], arr[i]]
}
由以上代码看出, 我们没有像归并排序分割原数组再合并到一个新数组,而是直接在原有的数组内部进行排序。
时间复杂度的好坏,由基准值决定。
最好时间复杂度:每次选择基准值,刚好是当前子数组的中间数。这时,可以确保每一次分割都能将数组分为两半,进而只需要递归 log(n) 次。这时,快速排序的时间复杂度分析思路和归并排序相似,最后结果也是 O(nlog(n))
。
最坏时间复杂度:每次划分取到的都是当前数组中的最大值/最小值。大家可以尝试把这种情况代入快排的思路中,你会发现此时快排已经退化为了冒泡排序,对应的时间复杂度是 O(n^2)
平均时间复杂度: O(nlog(n))
本次面试两题都是传统题,面试官其实手下留情了,并没有考动规啥的。
还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。
老规矩,也给我们团队的辅导服务打个广告。
原文作者:旅梦开发团
原文地址:https://juejin.cn/post/7344908667039432719