2022前端面试八股文汇总
互联网寒冬,面试八股牢记于心。话虽如此,巩固并吃透一个知识点才是正道。 技术飞速迭代的今天,当你学不动的时候,停下脚步歇歇,回头看看也未尝不可。 作为一个正在接受社会毒打的前端工程师,以下是个人收集并归纳总结的一些面试常见问题, 问题的相关细节会放在括号中,方便拓展和举一反三。
JavaScript 核心
javascript 有哪些原始类型
Undefined,Null,Boolean,Number,String,Symbol,Bigint (一项新的提案让这个答案可能增加 Record 和 Tuple 这两个不可变数据类型)
执行上下文和作用域链
变量或者函数的执行上下文决定了他们可以访问那些数据,每个函数都有自己的执行上下文,当函数被执行的时候, 它的上下文会被推入 执行栈,并创建一个作用域链,执行时沿着作用域链查找变量, 函数执行完毕后,执行上下文也会被弹出。
闭包
闭包是指那些引用了另一个函数作用域中的变量的函数,通常在嵌套函数中实现。内部的函数作为参数传递或结果返回, 仍能访问到其所在的外部函数的变量。闭包形成的原因是当一个函数执行完毕时,将会销毁其执行上下文以及附带的活 动对象, 但由于外部函数的活动对象已被添加到内部函数的作用域链中,故无法被销毁,仍然保留在内存中,供内部函 数使用。 闭包的使用场景包括防抖、节流、单次调用函数等。
尾调用优化
尾调用优化是 ES6 规范新增的内存管理优化机制(严格模式下开启),当一个函数的返回值是是它内部返回值的时候, 通过重用执行栈栈帧的方式优化内存管理(内部函数执行上下文执行完毕后不会直接弹出,而是先判断它的外部栈帧 是否有存在必要)。
构造函数
任何函数只要通过new操作符调用就是构造函数(一个函数也可以通过new.target来判断自己是否最为构造函数
被调用)。当使用new操作符执行构造函数时,js 解释器会在内存中新建一个对象,并将该对象的__proto__属性
赋值为构造函数的原型对象prototype,然后构造函数内部的this被赋值为这个新对象,执行完内部代码后返回
这个对象(如果构造函数直接返回了一个对象,除非手动操作否则原型链会断掉)。
原型和原型链
每个函数(包括 ES6 类)都会创建一个prototype属性指向它的原型对象,原型对象的constructor属性也会
指向这个函数(或类),当这个函数作为构造函数使用,实例化出一个对象时,这个实例对象的__proto__属性也会
指向构它的原型对象。当一个函数的原型对象是另一个构造函数的实例时,就会形成原型链。
为什么 0.1+0.2 不等用 0.3
任何使用了 IEEE754 浮点规范的语言都会存在这个问题,双精度浮点数的可靠位为 15 位,16 位之后的可能是对不上的。
0.1 和 0.2 储存值都比实际值要大一些,所以结果不等于 0.3,比较小数是否相当,应该使用两者的差值与 ES6 新增的
Number.EPSILON属性比较:
if (0.1 + 0.2 - 0.3 < Number.EPSILON) {
console.log(`0.1+0.2=0.3`);
}
javascript 如何实现继承
原型链、借用构造函数、组合继承、原型式继承、寄生式继承、寄生式组合继承。详见这里
自己实现call或者apply
Function.prototype.myApplay = function (newThis, argArray) {
const tempObj = newThis ?? window;
const funcSymb = Symbol("tempFunc");
// 当一个函数作为对象的属性调用时,函数内this指向这个对象
// 利用这点来达到绑定传入的newThis的目的
tempObj[funcSymb] = this;
tempObj[funcSymb](...argArray);
};
自己实现bind
Function.prototype.myBind = function (newThis) {
const self = this;
// 利用闭包,绑定this
return function () {
self.apply(newthis, arguments);
};
};
浏览器与 javaScript
HTTP、HTTPS 和 HTTP2
- HTTPS = HTTP + SSL/TLS,(TLS 是 SSL 标准化后的产物)
- SSL 使用非对称加密,对称指的是加密解密使用同一密钥,非对称使用不同密钥
- HTTPS 证书中包含了公钥,发送数据时会使用该公钥加密,接受端使用私钥解密,加大了破解成本,提高安全性
- HTTPS 加密和解密过程会有一定程度的性能损耗
- 目前广泛使用的时 HTTP 协议版本为 1.1
- HTTP2 建立在 HTTPS 协议的基础上,安全
- HTTP2 通过二进制分帧来进行数据传输,高效
- HTTP2 一个域建立一次 TCP 链接,使用多路复用和连接共享,没有 HTTP1 中同个域的并发限制 (得益于分帧机制,帧可以乱序发送,不再依赖多个 TCP 实现并行)
HTTP 缓存机制
浏览器每次发起请求,都会现在浏览器缓存中查找该请求的结果以及缓存标识,且每次拿到结果,都会将该结果
和缓存标识存入浏览器缓存中,而这个缓存过程又分为强制缓存和协商缓存。服务端控制缓存规则的字段包括
Expires(HTTP1.0 的字段,客户端对比时间存在缺陷,已被后者取代)和Cache-Control(优先级更高),
Cache-Control的取值有:
- public 所有内容都被缓存
- private 默认值,所有内容只有客户端可以缓存
- no-cache 客户端缓存内容,但是否使用缓存要经过协商缓存验证
- no-store 所有内容都不会被缓存
- max-age=time 缓存内容在 time 秒后失效
当发送请求时,不存在缓存结果或上述标志,则直接向服务器发起请求,如果有这是缓存标志,且存在缓存结果, 则直接返回缓存结果(强制缓存生效,取缓存先内存后硬盘),如果缓存结果失效,则携带缓存标志向服务器请求, 服务器可能会返回 304(协商缓存生效), 浏览器直接使用缓存,也可能返回 200(协商了决定给新的), 浏览器使用新的结果。
协商缓存的过程中,请求会携带一些信息帮助浏览器判断:
- last-modified-since 上一次请求的返回体中的 last-modified 值
- if-none-match 上一次请求的返回体中的 etag 标识
跨域
协议、域名、端口任一不同即为跨域,浏览器的同源策略,不限制跨域请求的发送,但会拦截请求响应, 常用的跨域解决办法:
- CORS,服务端设置跨域原资源共享
- JSONP,浏览器告诉服务一个返回函数的名称,服务在返回的 script 里调用这个回调函数,同时传进客户端需要的数据, 这样返回的代码就能在浏览器上执行了
- 子域名跨父域名时,通过更改
document.domain为父域名即可 - 嵌套 iframe 跨父窗口,父域无法访问不同源的 iframe 内容,但 iframe 可以向上调用
window.parent, 两者间也可以通过postMessageAPI 来通信
cookie
默认情况下跨域 ajax 请求不会携带cookie,除非设置withCredentials属性为true
返回报文的Set-Cookie可以设置浏览器cookie:
- name=value 普通键值对
- Domain 指定 cookie 有效的域名,发送到这个域名的请求都会携带 cookie,可以指定是否包含子域名
- Path 指定使用 cookie 的路径,请求 URL 包含这个路径才会携带 cookie
- Expires 过期时间,即什么时间之后就不要携带 cookie 了
- Secure 安全标志,设置之后只有使用了 SSL 安全连接的情况才会携带 cookie
- SameSite 可设置三个值
- Strict 完全禁止第三方 cookie
- Lax 默认值,仅发送连接、预加载还有 GET 请求的 cookie
- None 关闭该设置
事件循环
JavaScript 设计之初就是一门处理浏览器网页交互的脚本语言,这决定了它注定是一门单线程语言
(多个线程同时操作 ui 是很麻烦的事情)。浏览器式多线程的,当 js 主线程调用setTimeout和
addEventLisenter之类的方法时,会触发其他线程(定时器触发线程、事件触发线程),以此来实现
异步非阻塞。其他线程执行完毕后会将对应的额回调函数较为任务队列维护,当 js 主线程执行栈为空时,
从这个队列中依次去除任务(回调函数)执行。新一轮的执行若有异步任务则重复以上步骤,这个过程称之为
事件循环。
宏任务与微任务
- 宏任务:I/O、setTimeout、setInterval、setImmediate、requestAnimationFrame、 requestIdleCallback(浏览器空闲时段调用)、ajax
- 微任务:process.nextTick(node 中)、promise、MutationObserver、MessageChannel
首屏性能优化
- 尽量减少请求次数
- gzip 压缩静态资源
- dns 预连接
<link rel="dns-prefetch" href="url"> - cdn 加速资源
- 优化缓存机制(更新频率低的可以加大缓存时间)
- 使用 http2
- 图片懒加载
- 优化 dom 结构,减少无用节点,避免页面重绘
- 提高首屏加载代码的覆盖率,想办法剔除为使用的代码
浏览器渲染过程
- HTML->DOM 树,CSS->CSSOM 规则树
- DOM 树+CSSOM 规则树=渲染树
- 遍历渲染树,先开始布局
- 绘制节点
浏览器加载 js
- 默认情况下同步加载,阻塞渲染
-
defer表示延迟执行脚本,解析到 script 标签时立即下载,等待渲染完成后再执行,并保证按出现顺序执行 -
async表示异步执行脚本,解析到 script 标签时立即下载,下载完就执行(不管浏览器是否渲染完成,可能造成阻塞) 不保证执行顺序 -
type="module"表示 ES6 模块,浏览器会异步加载,解析到 script 标签时立即下载,等待渲染完成后再执行, (它会受到async的影响,可能阻塞渲染)。
框架
MVC MVP MVVM…
- MVC
- Model 业务数据,在更新时会通知相应的观察者(View)
- View 视图,负责 UI 交互,是 Model 的可视化表示,它会观察相应的 Model
- Controller 控制器,时 Model 和 View 的中介,用户操作 View 时,它通常负责更新 Model
- MVP
- View 和 Model 不再练习,通过 Presenter 传递
- View 转换为被动形态,没有任何业务逻辑
- Presenter 表示器,几乎所有的逻辑都在这里
- MVVM
- 与 MVP 基本一致,Presenter 变成了 ViewModel
- ViewModel 视图模型,采用了双向绑定,View 的变动自动更新到了 ViewModel,反之亦然
- Model 更多是由 ViewModel 负责
Vue2
Vue 源码结构
自底向上分析,以下是 Vue 导出的全过程
- Vue 是一个函数类,构造函数内只有一个
this._init(options)(开发模式下会判断是否作为构造函数调用,直接调用Vue方法会报错) - 调用各种混入方法,传入
Vue,增强(或者说组合)Vue.prototype能力:-
initMixin,该混入声明了Vue.prototype._init,该方法负责一系列的初始化操作 (以下过程发生在实例化中):initLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')
-
stateMixin,加入了数据响应相关的能力:-
Vue.prototype上创建data和props选项的代理访问$data和$props -
Vue.prototype上增加$set,$delete,$watch
-
-
eventsMixin,事件相关的能力:Vue.prototype.$onVue.prototype.$onceVue.prototype.$offVue.prototype.$emit
-
lifecycleMixin,生命周期:Vue.prototype._updateVue.prototype.$forceUpdateVue.prototype.$destroy
-
renderMixin,渲染相关:-
Vue.prototype上安装渲染辅助工具,(挂载了原型上的_o,_n,_s之类的属性上) Vue.prototype.$nextTickVue.prototype._render
-
-
- 调用
initGlobalApi(Vue)加入 Vue 全局方法或属性(函数类的静态方法或者属性):-
Vue.config,全局配置 -
Vue.util,包含warn,extend,mergeOptions,defineReactive方法 -
Vue.set,Vue.delete,Vue.nextTick,与原型上的是一样的 -
Vue.observeable,2.6 版本的 API,时一个对象可响应 -
Vue.options,初始化了components,directives,filters空值选项,以及_base=Vue, 并增加内置组件KeepAlive - 调用
initUse,增加插件拓展能力Vue.use(判断插件是否有install方法以及是否重复安装) - 调用
initMixin,增加混入能力Vue.mixin(实际上就是将混传入的options与当前options, 会影响所有vm实例,通常在插件中使用,Vue.use和Vue.mixin方法都返回了this,支持链式调用) - 调用
initExtend,增加继承能力Vue.extend(用于创建Vue的子类,会在内部缓存) - 调用
initAssetRegisters,增加Vue.component,Vue.directives,Vue.filters这三个资源注册能力
-
- 增加
Vue.protoype环境相关标记$isServer,$ssrContext, 以及全局标记Vue.FunctionalRenderContext和Vue.version
以上为核心 Vue 的导出过程,接下来是导出完整的运行时版本 Vue,从这一步开始,就会区分 web 和 weex 平台了 (各平台的视图相关操作不尽相同),以下以 web 为例:
-
Vue.config上继续添加一些相关标记 - 添加平台内置的指令
v-model,v-show和组件Transition,TransitionGroup - 判断是否浏览器环境,绑定
patch方法到Vue.prototype.__patch__ - 增加
Vue.prototype.$mount方法(其实就是调用mountCommponent方法)
如果是待编译器的版本,会在上述基础上继续:
- 将运行时
Vue.prototype.$mount方法取出暂存,重写$mount:- 检查挂载的根元素,不能是
html或者body - 检查渲染函数,没有渲染函数时检查
template并转换为渲染函数,没有template则取挂载元素的outerHTML作为template - 继续调用原本的
$mount
- 检查挂载的根元素,不能是
- 增加
Vue.compile = compileToFunctions
Vue 的实例化过程
Vue 的构造函数中只调用了this._init,new Vue()这个过程详细展开为:
- 合并配置项,将默认配置与构造函数传入配置合并,赋值给
vm.$options -
initLifecycle(vm)初始化生命周期:- 挂载父节点和跟节点
- 初始化
vm.$children=[]和vm.$refs={} - 初始化
vm上生命周期的相关标记,_watcher,_inactive,_directInactive,_isMounted,_isDestroyed,_isBeingDestroyed
-
initEvents(vm)初始化事件:- 初始化
vm._events和vm._hasHookEvent - 判断父组件是否有事件监听,有的话进行相应的更新
- 初始化
-
initRender(vm)初始化渲染:- 初始化
vm._vnode和vm._staticTrees标记 - 初始化
vm.$slot和vm.$scopedSlots - 挂
createElement到vm._c和vm.$createElement(alwaysNormalize参数不同) - 调用
defineReactive注册vm实例响应对象vm.$attrs和vm.$listeners
- 初始化
-
callHook(vm, 'beforeCreate')触发 beforeCreate 钩子 -
initInjections(vm) // resolve injections before data/props- 读取
vm.$options上的injection配置 - 关闭全局
shouldObserve开关 - 遍历
injection,对每个key使用defineReactive - 打开全局
shouldObserve开关
- 读取
-
initState(vm)初始化状态:- 初始化
vm._watchers=[] -
initProps- 判断是否根节点,不是的话关闭全局
shouldObserve开关 - 遍历
propsOptions- 判断响应的是否
propsData是否满足条件(类型,验证等) -
defineReactive注册响应对象 - 创建
vm的props访问代理vm._props.xxx
- 判断响应的是否
- 打开全局
shouldObserve开关
- 判断是否根节点,不是的话关闭全局
-
initMethods,遍历metheds里的方法,对每个方法使用bind,绑定vm执行上下文 (这也是方法中this能访问到vm的原因) -
initData- 创建
vm的data访问代理vm._data.xxx - 观察数据,
observe(data,true)
- 创建
-
initComputed- 初始化
vm._computedWatcher - 遍历计算属性,调用
defineComputed,非 ssr 情况会生成新的watcher赋值给vm._computedWatcher.xxx
- 初始化
-
initWatch,遍历watch创建相应的watcher
- 初始化
initProvide(vm) // resolve provide after data/props-
callHook(vm, 'created')触发 created 钩子 - 判断
vm.$options.el是否存在,是则调用vm.$mount进行挂载
Vue 的挂载过程
- 检查
vm.$options是否有render,没有的话调用createEmptyVNode生成空节点 - 触发
beforeMount钩子 - 声明
updateComponent回调函数updateComponent = () => { vm._update(vm._render(), hydrating)}-
vm._render()->vm.$createdElement->createElement
-
- 实例化
Watcher,传入updateComponent回调,并注入beforeUpdate钩子(初始化时,默认会执行`updateComponent 钩子) - 标记
vm._isMounted,触发mounted钩子
响应式原理
再实例挂载的过程中,创建了一个watcher,他的回调updateComponent会立即执行一次,调用vm._render()
的过程中会触发响应式数据的getter,进行一次依赖收集,当有更新时则会派发更新。
响应式的核心是Watcher、Observer、Dep(经典的观察者模式),大致的流程为
Vue 的实例化过程中,对props,data等都进行了defineReactive操作,该操作就是给定义一个响应式对象,
给对象动态添加getter和setter
在defineReactive创建响应式对象的过程中,会实例化一个Dep,Dep类实际上是对Watcher的一种管理,
同时Dep类上有一个静态属性target,指向一个全局唯一的Watcher,Watcher类也会维护需要更新的
Dep队列,当响应式对象的getter触发时,会把自己的dep实例加入到目标watcher的dep实例队列,
同时也会把watcher实例加入到dep的subs队列,这个过程则是依赖收集。
实际上Watcher类维护了两个dep实例数组,一个表示新添加的,另一个表示上一次添加的
当修改响应式对象时,则会触发相应的setter,它会调用dep的notify方法,然后依次调用def实例中的
subs也就是watcher实例数组的update方法,将需要更新的回调加到一个队列里,在nextTick时后执行
diff 算法
vue 和 react 类似,在对比新旧节点树时都不会整棵树比较,而是同级比较。同层的比较采用的双端比较的算法… // TODO
Vue 是怎么实现组件化的
从底层代码来看,我觉得组件化的核心是Vue.extend,
自定义指令
配置钩子 bind -> inserted -> updated -> componentUpdated -> unbind
v-model
v-model本质上是语法糖:传入value属性,并在事件(具体事件因空间类型而异)中触发是更新相应的值。
如果要给自定义的组件添加v-model,配置组件的model: {prop: PropName, event: EventName}对象即可
插槽原理
普通插槽是在父组件和渲染阶段生成 vnodes,故数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的vnodes;
作用域插槽在父组件的编译和渲染阶段都不会直接生成vnodes,而是在父节点中保留scopedSlots对象,该对象储存者不同
名称的插槽以及对应的渲染函数,在编译和渲染子组件阶段执行渲染函数生成vnodes,由于是在子组件环境中执行的,所以对应的
数据作用域是子组件。来自 vue 技术揭秘
keep-alive
<keep-alive>是内置的抽象组件,该组件是通过插槽和自定义渲染函数来实现的,并且该组件会缓存vnode,在patch的过程中
已缓存的组件不会再次触发mounted钩子,而是activated和deactivated,可以通过inclue和exclude来控制缓存行为
vue-router 原理
vuex 原理
Vue3
diff 算法
编译模板时,会打上pathFlag,标记静态节点以及各种不同情况,优化 diff 效率
React
服务端渲染
Node
打包&构建&工程化
聊一聊前端工程化
很开放的问题,不太属于八股文范畴了,以下是网上冲浪加上一些自己理解的总结:
- git 版本控制
- 规范约束
- 分支规范
- 版本规范
- CSS 规范(BEM、ATOM)
- lint 校验、prettier 风格统一、husky 钩子流程做预检
- 文档
- 单元测试
- 模块化
- 自动化流程:打包、构建、发布、部署、性能测试
- 环境区分:开发、测试、预发、线上
- 线上监控报警
- bug 修复流程
webpack
文件指纹
文件指纹主要用于版本迭代管控以及浏览器的缓存访问行为,文件指纹分为:
- Hash 与整个项目有关,
compilation实例变化就会改变 - ChunkHash 和
entry相关,不同的模块会有不同的ChunkHash - ContentHash 和文件内容有关,文件保持自己的独立更新