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
, 两者间也可以通过postMessage
API 来通信
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/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
-
stateMixin
,加入了数据响应相关的能力:-
Vue.prototype
上创建data
和props
选项的代理访问$data
和$props
-
Vue.prototype
上增加$set
,$delete
,$watch
-
-
eventsMixin
,事件相关的能力:Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
-
lifecycleMixin
,生命周期:Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy
-
renderMixin
,渲染相关:-
Vue.prototype
上安装渲染辅助工具,(挂载了原型上的_o
,_n
,_s
之类的属性上) Vue.prototype.$nextTick
Vue.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 和文件内容有关,文件保持自己的独立更新