目前在杭州工作,很多朋友都转战上海了,金九银十在朋友的鼓动下,投了上海的几家互联网公司,面试的基本都是中高级前端岗位,面试运气不错基本都拿到offer。面试也很久了,空闲下来记录下面试记录。
在文章里我不仅会列出面试题,还会给到一些答题建议,基本都是点到为止,面试的时候基本都有展开回答,篇幅有限就不展开了。个人能力有限,也不能保证我回答都正确,如果有错误,希望能纠我。题目排序不分先后,不区分公司,简单做了分类,就是纯记录下,都是非常常规的题目。
莉莉丝、米哈游、哔哩哔哩、小红书、微盟、得物等,本文主要记录面试过的二线互联网公司,当然还面过阿里、美团、拼多多等公司,姑且把他们算到一线阵营,这边就没有进行记录了。具体看下一篇哦!
判断图片所在位置是否在可视区内,图片移到可视区内进行加载,提供三种判断方法
- offsetTop < clientHeight + scrollTop
- element.getBoundingClientRect().top < clientHeight
- IntersectionObserver
跨域处理的方案是在太多太多了,我们公司主要是通过nginx进行转发,前端发起请求的地址是和页面地址一致的所以不存在跨域,nginx将请求转发到正确的服务。页面地址:web.taobao.com,请求接口地址web.taobao.com/api.taobao.com/***
表单提交是可以进行跨域的,不受浏览器的同源策略限制,估计是历史遗留原因,也有可能是表单提交的结果js是拿不到的,所以不限制问题也不大。但是存在一个问题,就是csrf攻击,具体不展开了,因为可以自动带上cookie造成攻击成功,而cookie的新属性SameSite就能用来限制这种情况
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断number, string, object, boolean, function, undefined, symbol 这七种类型,这种判断能帮助我们搞定一些问题,js在底层存储变量的时候会在变量的机器码的低位1-3位存储其类型信息(000:对象,010:浮点数,100:字符串,110:布尔,1:整数),但是null所有机器码均为0,直接被当做了对象来看待。
那么有没有更好的办法区分类型呢,一般使用
GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
- 多终端的出现,APP、小程序、PC端都需要相同的接口,但是又略有差异,常规接口需要提供几套,GraphQL的话只需要写好查询语句即可
- 天生的聚合接口,以前一个页面需要请求不同的数据,我们可以请求多个接口,我们可以让服务端进行聚合,有了GraphQL后我们可以自己去聚合想要的数据
- 不用被版本困扰,之前写接口的时候为了兼容老的项目可以正常被访问,尤其是APP,线上的项目,我们接口肯定是不能影响线上的,所以有比较大的改变的时候,只能升级版本,有了GraphQL后就无需关心版本问题了,接口还是那个接口查询语句变一下就好了
- 迁移很简单,服务端在之前的接口上稍加改造就好,前端写查询语句
事件委托是利用事件冒泡机制处理指定一个事件处理程序,来管理某一类型的所有事件
利用冒泡的原理,将事件加到父级身上,触发执行效果,这样只在内存中开辟一块空间,既节省资源又减少DOM操作,提高性能
动态绑定事件,列表新增元素不用进行重新绑定了
改变URL的目的是让js拿到不同的参数,进行不同的页面渲染,其实就是vue-router的原理
最简单的就是改变hash,改变hash是并不会刷新页面的,也会改变URL,也能监听hashchange事件进行页面的渲染
还有一种就是使用history.pushState()方法,该方法也可以改变url然后不刷新页面,但是该方法并不能够触发popstate事件,不过pushState使我们手动触发的,还能不知道url改变了么,其实这时候并不需要监听popstate我们就能够知道url改变拿到参数并渲染页面
使用requestAnimationFrame
css和js动画各有优劣
不占用JS主线程
可以利用硬件加速
浏览器可对动画做优化(元素不可见时不动画,减少对FPS的影响)
var event = new Event('build');
// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);
// Dispatch the event.
elem.dispatchEvent(event);
看团队吧,个人都能接受,对vue和react相对熟悉一点
jsx的灵活性更高,用写js的思路来写html,更加的高效
JSONP 是一种非正式传输协议,允许用户传递一个callback给服务端,然后服务端返回数据时会将这个callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。当GET请求从后台页面返回时,可以返回一段JavaScript代码,这段代码会自动执行,可以用来负责调用后台页面中的一个callback函数。
具体看文档就好了,实际中我也只是一个项目中使用过一次,并不常用
因人而异吧
使用最多的就是charles和chrome://inspect/#devices进行调试,当然实际开发中还是使用chrome的开发这工具,真机的时候是使用charles代理或者chrome://inspect/#devices代理,ios的话使用的是safari真机调试,当然也使用过vconsole,weinre等进行调试
flexible + rem进行适配
伪元素缩放
IE盒模型,常规盒模型以及其他衍生问题
这个我也没搞过,查了下有同学给了比较详细的方案:https://juejin.im/post/6844904122643120141
- API注入,原理其实就是 Native 获取 JavaScript环境上下文,并直接在上面挂载对象或者方法,使 js 可以直接调用,Android 与 IOS 分别拥有对应的挂载方式
- WebView 中的 prompt/console/alert 拦截,通常使用 prompt,因为这个方法在前端中使用频率低,比较不会出现冲突
- WebView URL Scheme 跳转拦截
知乎上面有一篇文章说的很清楚:https://zhuanlan.zhihu.com/p/71064826
展开来说可讲的东西太多了,把自己知道的都扯了一些,然后实际中使用到的也都扯了
同上
同上
强缓存协议缓存具体也不展开阐述了
具体也不展开阐述了,可以放到性能优化中去说
同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。 异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
回调函数、Generator、Promise、async/await
Promise是一种异步解决方案,Promise对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调resolve和失败时的回调reject;另外resolve的参数除了正常值以外, 还可能是一个Promise对象的实例;reject的参数通常是一个Error对象的实例。
- 优点就是更好的异步解决方案
- 缺点就是无法取消Promise,一旦新建它就会立即执行,无法中途取消
完全是有可能的,不然charles抓包是怎么做到的,当然前提是客户端上装了相应的证书
传输加密啊,具体流程是比较复杂的可以展开一篇文章来讲
非对称加密的加解密效率是非常低的,只作用在证书验证阶段,对称加密在数据传输阶段
xss、csrf、爬虫、薅羊毛等安全问题
传输加密、接口加签、环境变量、token、输入校验等
具体能讲的东西也很多这边也不展开来说了
xss前端主要是控制好输入,cookie设置http-only
csrf需要严格的cookie策略,增加token校验等
看情况而定,常规的请求参数:搜索关键词,分页参数没有任何加密的必要。特殊的字段:身份证校验肯定是需要加密的。同时前端游戏游戏数据也是必须要加密的,不仅仅需要加密还需要加签。
中间人攻击是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。
使用高版本的 Webpack、多线程/多实例构建、缩小打包作用域、充分利用缓存提升二次构建速度、DLL
loader,它是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务
项目中写过几个处理打包后文件的插件,就简单说了下
rollup等
npm install读取package.json以创建依赖关系列表,并使用package-lock.json告知要安装这些依赖关系的版本。如果依赖项不在package-lock.json中,它将由npm install添加。
npm ci(以持续集成命名)直接从package-lock.json安装依赖关系,并且仅使用package.json来验证没有不匹配的版本。如果缺少任何依赖项或版本不兼容,则将引发错误。
速度上ci明显比install快,线上发布打包的时候使用ci是优于install的
这种题目能说的东西很多,每一个点都可以展开,主要看自己对源码的熟悉程度,可以说Object.defineProperty,可以说Watcher,各个方面展开说一点即可,也可以从一个变量的变化如何渲染到真实的dom上去去阐述这个流程,能够清楚的说清楚就好
Proxy、CompositionAPI、TypeScript等
- 更好的代码组织,options api造成了代码的跳来跳去
- 逻辑复用更加的方便,虽然mixin也能够很好的复用代码,但是当mixin多了以后就不知道变量哪里来的了,还会造成命名冲突
- 没有让人捉摸不透的this
是有冲突的,其实官网上就有解释https://vuex.vuejs.org/zh/guide/forms.html
v-model会去修改state的值,但是vuex数据修改又必须经过mutation,这样就冲突了
简单的办法就是不要使用v-model,自己进行数据绑定即可
store里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载vue实例,store里面的数据就会被重新赋值初始化。理论上我们是不需要持久存储vuex的值的,因为请求我们会去接口拿数据,进行重新渲染,和第一次进入一样
但是如果非要保存上一次的临时状态,其实可以使用localStorage进行持久化存储,但是这个时候又得去处理和服务端数据同步的问题
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
当多个组件拥有同一个状态的时候,vuex能够很好的帮我们处理
可以很好的使用vue开发者工具调试vuex的状态
这些优势是localStorage不能够很好的模拟的
要在 beforeDestroy 手动销毁,否则如果在 mounted 中使用 addEventListeners,可能会多次重复注册导致内存泄漏。
简单描述下工作中使用的各种项目模板,使用的场景是啥样的,模板是怎么组织代码的,这样组织的原因是啥,以及技术选型,单页还是多页,template还是jsx,js还是ts等
适当加班可以理解,过度加班还不给工资不能接受
每个人遇到的情况都不一样,可能是技术问题,可能是项目推动问题,反正啥都说了一些
从一个产品的姿态去了解项目,从一个PM的姿态去跟进项目,从一个技术的角度去实现项目,从多个方面去描述自己如何去参与到一个项目中去,以及项目后续的数据跟踪,问题复盘,总结推广等
平时技术文档都是在熟悉了解需求文档的基础上去写,业务流程,技术选型,接口定义(有些前端定,有些后端定,我们没有太强的归属),然后和后端进行接口对接,定下来交互模式后进行开发
项目复盘主要是从项目遇到的问题,数据情况等进行复盘,说了挺多的
从自身情况阐述吧,是否写过组件库,是否参与过啥啥啥系统的开发,以及自己的角色
开放型题目,实际情况实际描述吧
防抖 一定时间内持续触发是不会重复调用,当超过一定时间后再回执行,主要应用在输入框这种地方,当需要查询一个东西的时候,持续输入是不会请求接口
function debounce(fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
节流表示的是不一直触发,一定时间触发一次,常用在滑动滚动或者视窗大小变化的控制
function throttle(fn, delay) {
let start = +Date.now()
let timer = null
return function(...args) {
const now = +Date.now()
if (now - start >= delay) {
clearTimeout(timer)
timer = null
fn.apply(this, args)
start = now
} else if (!timer){
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
}
class EventEmiter {
constructor() {
this.events = {}
}
emit(event, ...args) {
this.events[event].forEach(fn => {
fn.apply(this, args)
})
}
on(event, fn) {
if (this.events[event]) {
this.events[event].push(fn)
} else {
this.events[event] = [fn]
}
}
remove(event) {
delete this.events[event]
}
}
const eventHub = new EventEmiter()
eventHub.on('test', data => {
console.log(data)
})
eventHub.emit('test', 1)
console.log(2)
力扣第88题:https://leetcode-cn.com/problems/merge-sorted-array/,归并排序中的归并步骤拿来用就好了
function merge(left, right) {
let i = 0
let j = 0
const temp = []
while(i < left.length && j < right.length) {
if (left[i] < right[j]) {
temp.push(left[i])
i++
} else {
temp.push(right[j])
j++
}
}
while(i < left.length) {
temp.push(left[i])
i++
}
while(j < right.length) {
temp.push(right[j])
j++
}
return temp
}
力扣第70题: https://leetcode-cn.com/problems/climbing-stairs/,动态规划解题f(x)=f(x−1)+f(x−2)
var climbStairs = function(n) {
if (n <= 2) return n
let n1 = 1
let n2 = 2
let nn = 0
for (let i = 3; i <= n; i++) {
nn = n1 + n2
n1 = n2
n2 = nn
}
return nn
};
力扣第875题:https://leetcode-cn.com/problems/koko-eating-bananas/
function minEatingSpeed(piles, H) {
let left = 1;
let right = Math.max(...piles);
const canEat = (piles, speed, H) => {
let sumTime = 0;
for (let pile of piles) {
sumTime += Math.ceil(pile / speed);
}
return sumTime <= H;
};
while (left < right) {
let mid = Math.floor((right + left) / 2);
if (canEat(piles, mid, H)) {
right = mid;
} else {
left = mid + 1;
}
}
return right;
};
力扣第146题目:https://leetcode-cn.com/problems/lru-cache/,考keep-alive算法的时候喜欢问
class LRU {
constructor(max) {
this.max = max
this.cache = new Map()
}
get(key) {
const { cache } = this
const value = cache.get(key)
if (!value) return -1
cache.delete(key)
cache.set(key, value)
return value
}
set(key, value) {
const { cache, max } = this
if (cache.has(key)) {
cache.delete(key)
}
if (cache.size === max) {
cache.delete(cache.keys().next().value)
}
cache.set(key, value)
}
}
二叉树的遍历方式有很多种,前序、中序、后序以及层次遍历等,力扣上面都有原题,一般使用递归或者广度优先搜索即可
// 前序遍历
function preorderTraversal(root) {
const result = []
function preOrderTraverseNode(node) {
if (node) {
result.push(node.val)
preOrderTraverseNode(node.left)
preOrderTraverseNode(node.right)
}
}
preOrderTraverseNode(root)
return result
};
// 层次遍历
function levelOrder(root) {
const res = []
function dfs(node, step) {
if (node) {
if (res[step]) {
res[step].push(node.val)
} else {
res[step] = [node.val]
}
dfs(node.left, step + 1)
dfs(node.right, step + 1)
}
}
dfs(root, 0)
return res
}
力扣第647题:https://leetcode-cn.com/problems/palindromic-substrings/
function countSubstrings(s) {
const n = s.length;
let ans = 0;
for (let i = 0; i < 2 * n - 1; ++i) {
let l = i / 2, r = i / 2 + i % 2;
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
--l;
++r;
++ans;
}
}
return ans;
};
力扣第121题: https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
function maxProfit(prices) {
const len = prices.length
if (len < 2) return 0
let min = prices[0]
let dis = 0
for (let i = 1; i < len; i++) {
if (prices[i] < min) {
min = prices[i]
}
const disc = prices[i] - min
if (disc > dis) {
dis = disc
}
}
return dis
};
Promise.all = function(promises) {
const values = []
let count = 0
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
Promise.resolve(promise).then(res => {
count++
values[index] = res
if (count === promises.length) {
resolve(values)
}
}, err => {
reject(err)
})
})
})
}
Promise.allSeleted = function(promises) {
let count = 0
let result = []
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
Promise.resolve(promise).then(res => {
result[index] = {
value: res,
reason: null,
}
}, err => {
result[index] = {
value: null,
reason: err,
}
}).finally(() => {
count++
if (count === promises.length) {
resolve(result)
}
})
})
})
}
function maxRequest(fn, maxNum) {
return new Promise((resolve, reject) => {
if (maxNum === 0) {
reject('max request number')
return
}
Promise.resolve(fn()).then(value => {
resolve(value)
}).catch(() => {
return maxRequest(fn, maxNum - 1)
})
})
}