写在前面
自己在最近面试的过程中以及同事面试给的建议整理。
--2022年2月26日更新:又要跳槽了,重新整理一波 精致版
- 说话成体系,能自圆其说,逻辑有体系。在回答问题前,可以先停顿几秒。(先思考,再回答,控制好语速)
- 说说你在项目中碰到的最大困难是什么,怎么解决的。(实践中对项目的理解、总结)
- 职业规划是怎样的(短期?长期?)
- 这段时间有什么可以总结的?(方法论?技术沉淀?业务思考?)
- 你如何在团队中发挥自己的影响力?(技术团队中的作用,项目中的作用,横向部门间的作用,比如主动帮同事review代码、调研新技术并推进实践、主动发现问题解决问题,作为项目PM推进项目进度)
- 大原则:实事求是。(知之为知之,是知也)
- 为什么选择将要应聘的这家公司?
- 你为什么从上家离职?(不要说公司的坏话,表达要委婉)
- 三个关键词形容自己(能突出自己的品质特点)
- 你有什么要问的?(不要不问(比如:我没有什么问题了),能提问最好。比如贵公司使用什么技术栈?业务模式?问些实际的问题,显得自己的意愿比较强)
自我思考
- 工作中或者项目中遇到的问题和解决方案
- 介绍一下自己
- 未来的发展规划
基础概念
浏览器缓存机制
get和post请求区别
1.url 体现区别,get请求能在URL上体现,post不可以。
2.长度限制,get请求有长度限制,但并非HTTP协议的限制,是浏览器和web服务器的限制,因此不同浏览器和web服务器限制成都不一样。
3.缓存区别, get请求一般用于查询,可以不用每次都与数据库交互,利用缓存机制,post请求一般用于增删改操作,必须与数据库就行操作,所以不可用于缓存。
跨域和解决方法
指浏览器不能执行其他网站的脚本,源于浏览器的同源策略,同源指的是协议相同,主机和端口均相同,更详细解答
解决:
Jsonp: 利用<scirpt>没有跨域限制,达到网页可以从其他来源动态产生json数据,需要服务器支持。
webpack 配置proxy,原理是webpack内部起了node服务正向代理,服务器 之间不需要代理
nginx 反向代理, nginx 配置跳板机, 前端请求跳板机,nginx 转发请求目标服务器。
Cors: 后端需要在目标服务器http相应头 配置允许跨域: Access-control-Allow-origin: '允许跨域的域名或者*'开启Cors;
前端发起请求时会分为“简单请求”和“复杂请求”,复杂请求时会在正式请求前发一个option请求来知道服务端是否允许跨域。
模块化发展历程
作用:主要用于抽离公共代码,隔离作用域,避免变量冲突等。
IIFE:立即执行函数,在一个独立的作用域中执行代码,不会污染全局作用域和避免变量冲突。
AMD: 用requireJS 来编写模块化,特点:依赖必须提前声明。
CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。
CommonJS: nodeJS 内置的模块化。
ES6的modules: es6引入的模块化。
TCP三次握手
第一次:client -> server 服务端知道客户端有发送能力
第二次:server -> client 客户端知道服务端游发送能力
第三次:client -> server 让服务端知道客户端接受能力没问题
其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的ACK都会对这个seq进行加一来进行确认。
null 和 undefined 区别
简单点理解: undefined表示值不存在,null表示值存在但是为空,没有意义;
null == undefined // true
null === undefined // fasle
// typeof null 为 Object, typeof undefined 为 undefined
null: 作为对象原型链的终点
以下几种情况均为undefined
1.当定义一个变量没赋值。2.当调用一个对象还没添加的属性时候。3.当调用一个没有返回值的函数时。
let a; // undefined
let b = {};
b.name; // undefined
function c() {};
let d = c(); // undefined
函数柯里化
将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
节流防抖
场景:1.向服务端频繁发请求,增加服务器压力。2.频繁dom操作影响浏览器性能
解释: 防抖: 每次有请求是继续延长时间,如:输入框输入请求,延迟1秒钟,一秒内还有请求继续延迟 节流:固定个时间请求。
function debounce(fn, interval, immediate) {
//fn为要执行的函数
//interval为等待的时间
//immediate判断是否立即执行
var timeout; //定时器
return function () { //返回一个闭包
var context = this, args = arguments; //先把变量缓存
var later = function () { //把稍后要执行的代码封装起来
timeout = null; //成功调用后清除定时器
if (!immediate) fn.apply(context, args); //不立即执行时才可以调用
};
var callNow = immediate && !timeout; //判断是否立即调用,并且如果定时器存在,则不立即调用
clearTimeout(timeout); //不管什么情况,先清除定时器,这是最稳妥的
timeout = setTimeout(later, interval); //延迟执行
if (callNow) fn.apply(context, args); //如果是第一次触发,并且immediate为true,则立即执行
};
};
var throttle = function (fn, interval = 500) { //fn为要执行的函数,interval为延迟时间
var _self = fn, //保存需要被延迟执行的函数引用
timer, //定时器
firstTime = true; //是否第一次调用
return function () { //返回一个函数,形成闭包,持久化变量
var args = arguments, //缓存变量
_me = this;
if (firstTime) { //如果是第一次调用,不用延迟执行
_self.apply(_me, args);
return firstTime = false;
}
if (timer) { //如果定时器还在,说明上一次延迟执行还没有完成
return false;
}
timer = setTimeout(function () { //延迟一段时间执行
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval);
};
};
讲讲Redux原理
三大原则: 单一数据源: 整个应用的state都被存储到一个状态树(store)里面,并且这个状态树,只存在于唯一的store中;
状态只读:state是只读的,唯一改变state的方法就是触发action,action是一个用于描述以发生时间的普通对象;
数据改变只能通过纯函数: 使用纯函数来执行修改,为了描述action如何改变state的,你需要编写reducers
store: 存放数据的仓库,通过redux提供的createStore生成stroe
state: 数据源一个store可以有多一个state
action: 一个对象,其中必须包括type属性,用来告诉store如何操作state
store.dispatch: 接受一个action, 告诉store改变state
reducer: reducer是一个纯函数,store 接受到action后要返回一个新的state,这个计算过程就叫做reducer.
箭头函数和普通函数区别
1.this指向, 箭头函数不绑定this, 会自动捕获上下文的this
2.箭头函数不能作为构造函数,也就不能new
3.箭头函数不绑定arguments,用...解决
4.箭头函数没有原型属性
5.箭头函数不能当作Generator函数,不能使用yield关键字
javascript 单线程
问:说一下javascript的单线程模型
答:js只在一个线程上运行,也就是说JavaScript同时只能执行一个任务。 好处就是执行起来相对简单,坏处就是会造成任务堵塞,和CPU资源浪费。
问:javascript为什么采用单线程
答:历史原因,js在语言设计之初就定下的规范,js的设计者在设计这门语言的就不想让js过于复杂,举个例子,如果js多线程的话可能会出现两个线程同时操作一个dom,这时候浏览器又要判断执行哪一个,是否又要引用锁的机制,所以避免过于复杂,JavaScript一开始就是单线程模型。
说一下JavaScript 的事件循环
JS引擎单线程,浏览器提供多个辅助线程来执行js主线程中遇到的异步任务。
先说一下js的单线程模型,事件循环是JS的一个机制,并不是JS内部的一个方法什么的,首先JS除了主线程之外,JS引擎提供了多个任务队列来执行主任务的一些异步任务,主线程在执行的过程中遇到遇到异步任务的话,会放到异步队列中去执行,然后继续执行主任务,直到当前的主任务执行完了,再去看异步队列中有没有完成的任务,有的话,继续拿到主任务中去执行,在这个过程中又遇到异步的话,继续放到异步队列中,知道任务执行完。这样一个过程就叫事件循环。
前面说到有多个任务队列,是根据异步任务产生的,异步任务也有优先级的,所有任务队列分为两种,一个是微任务队列,一个是宏任务队列,微任务的优先级要高于宏任务,所以当主任务执行完,先执行微任务,在执行宏任务,微任务主要是Promise,MutationObserver(监听dom变化),剩下的定时器,ajax等都是宏任务。
说一下JavaScript 的原型链
首先原型链是JS实现继承的一种方式。 一个实例对象要调用属性或方法时,JS会先从自身的属性和方法上找,然后在其构造函数的原型上找,在在原型的原型上,直至找到object.prototype为止。
比如:let a = [], 调用a.toString, 若a对象下没有toString方法则 -> a.proto.toString -> Object.toString为止,这样的链式调用就叫做原型链
es6新特性
let/const 、变量解构、Promise、Set和map数据类型 、 class、模块化、anysc/await、symbol、generator函数
generator函数
- 一种异步编程解决方案
- 语法上,Generator 函数是一个状态机,封装了多个内部状态 - yield/return 返回内部状态
- 执行 Generator 会返回一个遍历器对象,是一个遍历器对象生成函数,可以遍历函数内的每一个状态
Promise
- 异步编程的一个解决方案,可以让异步操作以同步的流出表达出来。传统的方案就是回调函数一级DOM事件
- 语法上讲Promise是一个对象,提供了统一的API进行异步操作
特点:
- 对象的状态不受外界影响,只有通过异步操作改变其状态
- pendding: 进行中
- fulfilled:已成功(resolve)
- reject:已失败
- 状态一旦发生(pendding -> fulfilled/reject)就不可改变。
关于微前端:
什么是微前端,解决了什么
目前开发的前端产品主要有两个方向:MPA和SPA。
MPA: 应用独立通过域名实现应用之间的跳转,优点天生具备了技术栈无关,独立部署,独立开发的优势。缺点:应用跳转会导致浏览器重刷,流程体验上就会造成断点。
SPA:天生体验上的优势,应用内的跳转,无需刷新。缺点就是技术栈强耦合,以及项目会越积越大。
微前端可以说是这两者优势的合集,微前端架构可以吧一个应用拆分成多个互相独立的子应用,保证了应用的独立性,同时也保证了产品的完整体验流程。
解决的问题:
- 业务不断扩大,业务模块之间耦合加剧
- 开发团队面临拆分、解耦才能达到并行开发
- 新的框架、方案如何适用现有的工程环境(构建工具)等
- 旧的框架如何平稳升级
微前端中应用的预加载
qiankun提供了开启预加载的字段(prefetch),通过setDefaultMountApp方法设置需要默认加载的字应用。
预加载的思路: 在浏览器提供的requsetIdleCallback事件里面执行import-html-entry。 加载的资源会根据url缓存起来。
子应用的加载
服务通信
首先微前端不建议应用之间过多的的通信。
1.localStorage
2.qiankun内部提供了action通信(观察者模式)
qiankun提供initGlobalState方法来在全局注册一个实例用于通信。这个实例有三个方法:
- setGlobalState: 来修改全局的状态(globalState),内部会对globalState进行浅比较,如果发生改变,会通知所有的观察者。
- onGlobalStateChange: 注册观察者的函数,子组件通过这个方法注册成观察者,当全局的globalState发生变化,就会触发该函数。
- offGlobalStateChange: 取消成为观察者。
下图可以看出整个流程就是,子组件通过注册成为观察者到观察者池中,通过修改globalState通知观察者函数,达到组件之间通信的效果。
ps:自己画一下图会更好理解,哪怕看着别人的画)
3.shared通信 思路:主应用通过基于redux维护一个状态池,暴露一些方法给子应用。
微前端中菜单路由的管理
qiankun是基于singe-spa实现的:主的思路是主应用里面维护一个路由规则表,每个子应用对应一个唯一的路由(比如商品应用goods),qiankun会劫持window.histroy相关的路由跳转事件,每次发生路由跳转时根据路由规则匹配到对应的子应用。匹配到后的路由交给子应用自己的路由系统。
为什么选择qiankun而不是飞冰
1.之前是因为飞冰的文档没乾坤全,乾坤可以参考singe-spa(现在的飞冰文档比乾坤好很多)。
2.沙盒模型: 在应用加载前对window对象做一个快照(拷贝),在应用卸载的时候恢复这个快照。这样可以实现每次进入应用都有一个全新的window对象环境。其实大致原理就是记录window对象在子系统运行期间新增、修改和删除的属性和方法,然后会在子系统卸载的时候复原这些操作。
qiankun: 在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。
ice: 可在主应用钩子函数中按需记录全局状态
<AppRouter
onAppEnter={(appConfig) => {
// 按需记录全局状态
}}
onAppLeave={(appConfig) => {
// 按需恢复全局状态
}}
>
// {...}
</AppRouter>
React
说一下React生命周期
三个阶段:
- 挂载时: 首次实例化组件被挂载时,主要的生命周期有:constructor, render,componentDidMount
- 更新时: 当组件的props或者state 更新时执行, componentDidUpdate和一个静态方法,返回最新的state
- 卸载时: 当组件卸载或销毁时,componentsWillUnmount
React Hooks
useState: 接受一个初始值返回一个新的state和一个更新state的函数。
useEffect: 接受两个参数,一个包含命令式和有可能有副作用代码的函数以及一个依赖值数组。 执行时机:在浏览器绘制之后并且保证在任何新的渲染之前执行。如果想在绘制之前执行可以使用useLayoutEffect代替
useContext: 接受一个context对象(Racet.creatContext返回值)返回该context的当前值。
额外的Hooks(如下):大多数是useState和useEffect的变体,在某些特殊情况下用起来更好。
useReducer: 类似于redux机制,接受一个reducer和一个initialState,返回一个state和dispatch方法。 在某些场景下代替uesState,比如:state逻辑复杂并且包含多个子值,或者下一个state依赖于上一个state.
useMemo和uesCallback: 作用:用法类似都是用于减少渲染次数,达到优化性能的作用。 useMemo(() => fn, [deps]) = useCallback(fn, [deps]) 他们唯一的区别是:useCallback是根据依赖项(deps)缓存第一个参数callback。useMemo是根据依赖项(deps)缓存第一个参数callback执行后的值。
说一下虚拟DOM
虚拟dom本质就是一个JS对象,虚拟dom存在一的意义在于最小化dom改变,然后批处理dom变化,在必要的时候在重新渲染页面。
diff 分为三类比较:
1.相同类型节点比较:只会对属性进行重设,从而实现节点的转变
2.不同类型节点比较:会直接删除旧的节点,创建新的节点代替。
3.列表节点比较:开发手动添加key,帮助react定位到正确的节点,减少dom操作次数。
单项数据流
我所理解的单项数据流是:数据是不可逆的,父组件向子组件通过props传递数据,子组件不可修改props, 只能通过触发事件通知父组件修改数据,在派发给子组件。
保证了数据了单一流向性,以及高维护性。
react的fiber
背景: react 是通过虚拟 (Virtual) dom 进行UI的描述, 通过对比新旧两个dom来最小化一个dom 的更新。 这个对比的过程最核心的就是diff算法, diff算法的计算并不是免费的,当遇到大量的dom更新时会,造成动画丢帧以及用户操作不流畅等问题。
造成以上问题的原因: 在react 16之前进行UI渲染核心实现是采用递归的方式,我们知道,递归是无法被打断的,当需要更新的时候,会从需要更新的地方一直进行diff。当diff 时间过长,js 的主线程就会造成堵塞,影响UI响应。
react fiber 如何解决这个问题: 核心是基于浏览器提供的一个原生api -> requestIdlecallback: 让浏览器在空闲时候执行脚步。
那fiber要做的事就是,让diff的过程可中断/恢复,其次要有一个优先级的调度策略。这样就可以实现在浏览器空闲时候按优先级顺序去执行操作。
常问的一些总结
React和Vue的区别
1.监听数据变化的实现不同
vue:通过劫持getter/setter等方法,准确的知道数据的变化。
react: 通过浅比较引用的方式,可能会造成大量不必要的渲染,当然也可以通过(pureComponet/shouldComponentUpdate/useMemo/useCallback)等进行优化
2.数据流的不同
vue: 双向数据绑定。
1.x版本中父子组件props可双向绑定,组件和dom通过v-model也可双向绑定。
2.x版本后不建议对props进行改变,只保留了组件和dom的双向绑定
react: 提倡单项数据流,自顶向下。 日常项目中一般使用vuex/redux等单项数据流状态管理框架,所以这一块可能感受不是很大。
3.hoc和mixins
vue 一直采用mixins, react最早也采用mixins,后面改成hoc。
原因:react组件和vue组件的区别。react组件本身是一个纯粹的函数,高阶函数对react来说 会简单很多。
vue组件就本身是一个被包装过的组件,比如模版是如何被编译,以及props如何被传递,这些都是vue在创建实例的时候隐式做的。如果使用hoc包装后可能就无法执行了。
4.组件通信
和react是父组件通过props给子组件传递数据。
vue最大的特点就是子组件可以通过自定义事件给父组件传递数据
5.模版渲染的区别
写法上,react通过jsx渲染模版,vue通过一种拓展的HTML语法渲染。
本质的区别: react 在写法上通过原声的JS实现一些插值、if、循环等操作。 vue通过v-if,v-for,等指令去操作。 更喜欢react做法,贴近原生
react的render是支持闭包的,所以在引用其他组件时可以直接拿来用,vue的话还需要在components下声明一下。