1. JavaScript 相关
1.1 基础概念类
JavaScript 中的数据类型
- 八大基本数据类型(含 ES6):Undefined Null Number String Boolean Object Symbol BitInt
- 原始类型:String Number Boolean Null Undefined Symbol
- 引用类型:Object Array Function RegExp Date
原型与原型链
重点:
- 对原型与原型链的理解
- 基于原型链的查找逻辑
- 显式原型与隐式原型的区别
- 为什么要设计原型与原型链机制
任务队列
重点:
- 为什么要设计异步
- 任务队列的执行过程
- 给一段代码,要求说出输出结果 练习题
new 一个对象发生了什么
- 创建一个新对象
- 新对象的隐式原型
__proto__
指向构造函数的显示原型prototype
- 将构造函数中的
this
指向新对象 - 执行构造函数
- 返回新对象(如果构造函数返回一个对象,返回该对象)
关联:[[面试中遇到的高频问题整理#实现 new 方法]]
宏任务与微任务
简单示例:
1 | setTimeout(() => alert("timeout")); |
执行顺序:
code
首先显示,因为它是常规的同步调用。promise
第二个出现,因为then
会通过微任务队列,并在当前代码之后执行。timeout
最后显示,因为它是一个宏任务。
要点:
- 微任务会在执行任何其他事件处理,或渲染,或执行任何其他宏任务之前完成
- 每个宏任务执行时都会去先检查微任务队列里是否有微任务,如果有则先清空微任务队列
注意,new Promise
的处理函数中执行的代码也是同步执行的:
1 | console.log("script start"); |
如果题目中使用了 async 和 await,可以改写为 Promise 链,这样就不迷糊了,如:
1 | async function async1() { |
等同于:
1 | function async1() { |
JavaScript 作用域
词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。看例子:
1 | let number = 42; |
上面代码可以看出无论 printNumber()
在哪里调用 console.log(number)
都会打印 42
。动态作用域不同,console.log(number)
这行代码打印什么取决于函数 printNumber()
在哪里调用。
箭头函数的 this 指向
在非显示绑定和 new 绑定时,普通函数中的 this 总是指向调用该函数的对象,但是在箭头函数中, this 的指向是根据外层作用域来决定的,换句话说,就是箭头函数被定义时的上下文中(词法作用域内)的 this。此外箭头函数也无法使用 call apply bind 来显式绑定 this。
示例:
1 | /** |
将一个对象的属性设置为一个只读的方式有哪些
使用 Object.defineProperty
设置 writeable
使用 Object.freeze
使用对象的 get
使用模块模式、闭包:
1 | var CONFIG = (function () { |
Map 和 WeakMap 的区别
- Map 的键可以是任意类型,WeakMap 只接受对象作为键(null 除外),不接受其他类型的值作为键;
- Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的;
- Map 可以被遍历, WeakMap 不能被遍历,weakMap 没有
keys
和values
的方法;
Map 与 WeakMap 对内存回收的展现示例:
1 | var a = {}; |
1 | var a = {}; |
1.2 实现类
实现一个类型判断的方法
1 | function getType(value) { |
如何实现一个深拷贝
1 | function isObject(obj) { |
重点:
- 深拷贝的意义
- 深拷贝与浅拷贝的区别
JSON.parse(JSON.stringify(obj))
的缺陷 参考- 至少能够熟练实现对 Object、Array、null、undefined 这几种数据类型的拷贝,写出完整的拷贝方法是加分项
用 JS 实现一个继承
重点:
- 参考文章中的八种继承方案必须全部理解
- 熟练掌握寄生组合式继承
1 | /** |
call apply bind 的实现
重点:
- 先理解 call apply bind 的区别,然后再理解他们各自的使用场景,最后再去实现
bind
方法在柯里化函数中的实践 参考
1 | function call2(context, ...args) { |
实现 Object.create
Object.create
会创建一个新的对象,并且将参数位的目标对象,与新创建的对象会进行原型链链接(newObject.__proto__ === targetObject)。
实现:
1 | function createObject(proto) { |
实现 new 方法
1 | function objectFactory(constructor, ...args) { |
实现 instanceof
1 | function instOf(subInstance, parentConstructor) { |
节流与防抖
1 | // 节流 |
共同点:
- 都需要使用闭包创建一个
timer
存放计时器 - 都在定时器执行完毕时才将
timer
置空
不同点:
- 在执行节流函数时如果检测到没有计时器就立即执行 fn,然而在执行防抖函数时是在定时器执行完毕后才执行 fn
- 节流函数不会去主动结束已有的定时器,而防抖函数在执行后总是会清除上一个定时器
Promise 各个静态方法实现
Promise.all:Promise.all()方法接受一个数组为参数,数组中是 promise,如果数组中的 promise 都是 resolve 状态,那么 Promise.all()正常返回 resolve,返回的数据为一个数组,就是参数中每个 promise 的结果组成的数组。如果 promise.all()中任何一个是 reject,那么 promise.all()直接 reject。
1 | // 假设我们已经实现了_Promise |
Promise.race:Promise.race() 静态方法接受一个 promise 可迭代对象作为输入,并返回一个 Promise。如果其中一个 promise 处于 fulfilled 或者 reject 后,Promise.race 所创建的 promise 会立刻被敲定,执行 then 或者 catch 逻辑。未敲定的 promise 仍会执行,但不会影响 Promise.race 所创建的 promise 了。
1 | Promise._race = (promises) => |
Promise.allSettled:Promise.allSettled()
可以获取数组中每个 promise
的结果,无论成功或失败。
1 | const rejectHandler = (reason) => ({ status: "rejected", reason }); |
1 | MyPromise.allSettled = function (values) { |
2. CSS 相关
元素居中的方案
- 垂直居中
- 设置 line-height,适用于文本或图片
- flex 布局
- 绝对定为 + margin auto,子元素需要有绝对宽高
- 父元素
display: table
,子元素display: table-cell; vertical-align: middle;
- 绝对定为 + transform
- padding 百分比
- calc
- display: inline-block + vertical-align: middle
纯 CSS 绘制三角形
CSS 选择器以及其优先级
重点:
- 优先级计算规则 参考
- 掌握常用的选择器
伪类与伪元素
重点:
- 伪类与伪元素的区别
inline 元素
- inline 和 inline-block 元素可以设置 padding,但是纵向的 padding 不会影响其文档流的对其方式
- inline 和 inline-block 元素可以设置横向的 margin,纵向的 margin 不会生效
display: none / visibility:hidden / opacity:0
displa: none
元素在页面上不渲染;visibility: hidden
元素被隐藏,但占用的位置还在opacity: 0
:元素是可以互动的,因为它们实际上是可见的,只是非常透明
3. 网络原理
HTTPS
- 什么是密码学、加密通信、数字签名,生动展示密码学基础、非对称加密的原理、非对称加密在 HTTPS 中的应用,对理解为什么要使用 HTTPS 以及其工作原理很有帮助
- 参考文章,专业但是不生动,作为视频补充观看
从输入 URL 到页面展示到底发生了什么
参考文章太多了,上面的是相对完整的,但是冗长,需要自己整理精简后理解,具体的概念去了解对应的专题。
不要仅局限于参考文章中列出的知识扩展,作者文字过于抽象(比如 TCP 三次握手),没到一个相关的知识扩展,建议去搜这个知识点相关更好更全的文章。
重点:
- 大体描述整个过程
- DNS lookup(向上查找)的过程
- TPC 的三次握手四次挥手,熟练说出整个过程,并明白每个步骤的意义,每次交互发送的报文内容(如 SYN = 1, seq = x)记不住可以不记,最好记住发送报文后服务器与客户端各处的状态。
- 命中协商缓存后的过程
- 浏览器渲染页面的过程,渲染阻塞的问题 参考
这道题是个经典题目,因为涉及的知识面广,每一个步骤都可以深入提问,因此只了解大致过程并没有什么用,很容易被面试管逼问更深层的内容而回答不上来导致减分。
这道题的回答策略是先跟面试官简述整体的过程,让面试官知道你有一个清晰的思路并且整体流程是正确的,然后再 主动 展开详细阐述每个过程的具体经过。如果不能完全掌握这道题的话,一定要努力把自己所知道的一切都倾倒在这个题中,也就是说能回答的多详细就回答多详细(这样还能主动拉长面试时间),把话语权掌握在自己手中,千万不要等着面试管主动向你提问关于这道题更深的内容,这样很容易翻车。
简单请求和非简单请求
简单请求:
- GET、POST、HEAD
- 没有自定义的请求头
- Content-Type的值只有以下三种:
text/plain
multipart/form-data
application/x-www-form-urlencoded
针对复杂请求,我们需要设置不同的响应头。因为在预检请求的时候会携带相应的请求头信息。
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin
的值为“*
”。
4. Vue
Vue 响应式原理
Vue2 响应式原理(必须掌握,代码跟着敲一遍)
Vue2 响应式原理基于 Object.defineProperty
,Vue3 响应式原理基于 Proxy,两者思想都是一样的,只不过具体实现不一样而已,先搞懂 Vue2,Vue3 的原理就会很快理解。而且目前Object.defineProperty
比 Proxy
应用更广,了解 Vue2 原理有助于对 Object 的理解。
重点:
- MVVM(数据双向绑定)的实现
- watch、computed 的原理
为什么说 Vue 的响应式更新精确到组件级别而 React 不行
Vue 在组件的依赖发生变化时,就会重新渲染对应的组件,在渲染过程中遇到子节点时会进行 DOM Diff,但是遇到子组件时只会对组件上的 props、listeners 等属性进行更新,而不会深入到组件内部进行更新。假如父组件向子组件传入的 props 发生了变更,那么子组件的 watcher 就会被触发,进而更新子组件。
Vue 每个组件都有自己的
渲染 watcher
,它掌管了当前组件的视图更新,但是并不会掌管ChildComponent
的更新。
而在 React 中,组件更新是自顶向下递归更新的,父组件的更新会引起子组件的重新渲染,因为 React 遵循 Immutable 的设计思想,永远不在原对象上修改属性,那么 Vue 的响应式依赖收集就无法实现,React 便无法得知子组件是否需要更新,因此只能将子组件全部重新渲染一遍,然后再使用 Diff 算法来决定哪一部分的视图需要更新。
v-if 和 v-for 为什么不能一起使用
在 Vue 生成 DOM 树的算法中,v-for
的优先级高于 v-if
,因此会先进行遍历出 DOM 节点,然后在判断元素是否显示,这会造成不必要的性能浪费。
Vue2 相比 Vue3
特性:
- 原生支持 TypeScript;
- 新增了 Composition API,与 Option API 相比代码可读性更好,代码复用更简明,TypeScript 支持更好;
- 支持多根节点;
- 支持 Teleport;
性能提升:
- Vue3 对不参与更新的元素,比如没有任何响应式数据参与的普通 DOM 节点,会在编译阶段进行静态提升,渲染时直接使用,并通过一个静态标记,不参与 diff 算法的对比中;
- Vue2 绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化,而 Vue3 事件监听会被缓存;
- SSR 优化,当静态内容大到一定量级时候,会用
createStaticVNode
方法在客户端去生成一个 static node,这些静态node
,会被直接innerHtml
,就不需要创建对象,然后根据对象渲染; - 代码体积更小,支持 Tree shaking;
- 使用 Proxy 实现响应式系统(无法兼容 IE),不需要在响应式对象创建时就进行深度遍历所有嵌套对象来挂载响应式,而是在嵌套对象被访问时才将其转化为一个响应式对象(Vue2 没有这么做是设计问题);
生命周期:
- Vue3 没有
beforeCreated
和created
生命周期函数; setup
在beforeCreated
前执行;- Vue3 中在 setup 调用的生命周期函数,如果在 options 中定义了相同类型的回调函数,那么 setup 中调用的声明函数更优先执行,比如
onMounted
在mounted
之前执行;
实现一个 Toast 弹窗组件
组件实现不再多讲,主要讲一下如何将 Toast 组件使用指令方式调用,如调用 showToast({...})
后显示在页面上,并且返回一个 destory
方法用于手动销毁 Toast。
Vue3 中可以用 createVNode
创建组件 VNode 实例,然后使用 render
函数渲染到目标 DOM 上:
1 | import { createVNode, h, render } from "vue"; |
此外,还可以使用 createApp
的方式来挂载组件到 DOM 中:
1 | export function useMountComponent(component: Component) { |
如果是 Vue2,可以使用 Vue.extend
的方式获得组件实例:
1 | import Modal from "./Modal.vue"; |
性能优化手段
- props 稳定性,尽量避免 props 频繁更新;
- 使用
v-once
可以让组件跳过后续渲染; - 使用
v-memo
可以有条件的跳过某些大型 DOM 结构的更新,甚至连虚拟 DOM 的创建都会被跳过; - 计算属性稳定性,尽可能让 computed 返回一个值类型,而不是引用类型的数据,这样 computed 值会尽可能的减少非必要的副作用触发;
- 使用虚拟列表;
- 使用
shallowRef
和shallowReactive
来绕开深度响应; - 避免过多的组件抽象,渲染组件比渲染普通 DOM 节点要昂贵得多;
常用的 Composition API
核心:
- ref
- reactive
- readonly
- computed
- watch
- watchEffect:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行
- watchPostEffect:
watchEffect()
使用flush: 'post'
选项时的别名。 - watchSyncEffect:
watchEffect()
使用flush: 'sync'
选项时的别名。
工具:
- isRef
- unref
- toRef 3.3+: 可以将值、refs 或 getters 规范化为 refs
- toValue 3.3+: 将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。
- toRefs: 将一个响应式对象转换为一个普通对象。
- 当从组合式函数中返回 reactive 对象时,toRefs 相当有用,可以使用 toRefs 包裹 reactive 对象让使用该组合式函数的地方可以通过解构来获取响应式值。
- isProxy:检查一个对象是否是由
reactive()
、readonly()
、shallowReactive()
或shallowReadonly()
创建的代理。 - isReactive
- isReadonly
进阶:
- shallowRef()
- triggerRef(): 强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。
- customRef(): 创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
- shallowReactive()
- shallowReadonly()
- toRaw()
- markRaw()
- effectScope()
- getCurrentScope()
- onScopeDispose()
5. React
常用的 Hook
- useState
- useReducer
- useEffect
- usrLayoutEffect
- useMemo
- useCallback
6. NodeJS
setTimeout(fn, 0) 和 setImmediate(fn) 哪个先触发?
浏览器环境下,哪个写在前面哪个先触发。而在 Node 环境下则不一定哪个先触发。
在时间循环过程中,定时器在 timers 阶段执行,而 setImmediate 在 check 阶段执行。在执行 setTimeout(fn, 0)
时会去创建一个定时器,即使事件我们传入了 0ms 但实际执行时可能会在 1ms,那么就有可能在执行到 event loop 的 timers 阶段时定时器任务并没有创建好,此时则会执行后面的流程,来到 check 阶段执行 setImmediate 创建的任务,setTimeout 创建的 0ms 任务则在下一个 event loop 执行。
此外,setTimeout 由于要创建定时器,其耗时要比 setImmediate 更多。
再看另一种情况:
1 | // 说说下边代码的执行顺序,先打印哪个? |
这个无论执行多少次,setImmediate 都会比 setTimeout 先执行,因为 IO 异步在 poll 阶段执行,然后执行 check 阶段,定时器将在下一个 event loop 执行。
7. 构建工具
ESM 和 CJS 的区别
- ES Module 输出的是值的引用,而 CommonJS 输出的是值的拷贝;
- ES Module 是编译时执行,而 CommonJS 模块是在运行时加载;
为什么 esm 支持 tree-shaking,而 commonjs 不支持
- ESM 是静态的,它在编译时(静态阶段)就能够确定模块的依赖关系和导出内容
- CommonJS 的加载是动态的,它是等到代码执行到时才加载模块,只能在运行时才能确定代码是否被使用
- 在 ESM 中,导出的内容是静态的,只能通过
export
关键字显式导出,这使得 tree shaking 更加容易实现。而在 CommonJS 中,导出的内容是通过module.exports
或exports
动态赋值的,这使得很难在编译时确定导出的内容,从而难以进行有效的 tree shaking。
Tree shaking 的实现
- Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中:
- 将模块的所有 ESM 导出语句转换为 Dependency 对象,并记录到 module 对象的 dependencies 集合;
- 所有模块都编译完毕后,触发 compilation.hooks.finishModules 钩子,开始执行 FlagDependencyExportsPlugin 插件回调;
- FlagDependencyExportsPlugin 插件从 entry 开始读取 ModuleGraph 中存储的模块信息,遍历所有 module 对象,所有 ESM 风格的 export 语句都会记录在 ModuleGraph 体系内,后续操作就可以从 ModuleGraph 中直接读取出模块的导出值;
- Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用:
- 触发 compilation.hooks.optimizeDependencies 钩子,开始执行 FlagDependencyUsagePlugin 插件逻辑;
- 在 FlagDependencyUsagePlugin 插件中,从 entry 开始逐步遍历 ModuleGraph 存储的所有 module 对象;
- 遍历 module 对象对应的 exportInfo 数组,确定其对应的 dependency 对象有否被其它模块使用;
- 被任意模块使用到的导出值,调用 exportInfo.setUsedConditionally 方法将其标记为已被使用;
- exportInfo.setUsedConditionally 内部修改 exportInfo._usedInRuntime 属性,记录该导出被如何使用;
- 打包阶段根据导出值的使用情况生成不同的代码:
- 打包阶段,调用 HarmonyExportXXXDependency.Template.apply 方法生成代码;
- 在 apply 方法内,读取 ModuleGraph 中存储的 exportsInfo 信息,判断哪些导出值被使用,哪些未被使用;
- 对已经被使用及未被使用的导出值,分别创建对应的 HarmonyExportInitFragment 对象,保存到 initFragments 数组;
- 遍历 initFragments 数组,生成最终结果;
- 生成产物时,若变量没有被其它模块使用则删除对应的导出语句:
- 由 Terser、UglifyJS 等 DCE 工具“摇”掉这部分无效代码,构成完整的 Tree Shaking 操作
webpack 不会对 dead code 进行删除,只是在
__webpack_require__.d
中不注册未使用的方法,是 Terser 将未使用的代码删除的。
Webpack 的流程
https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw
8. 算法
斐波那契数列
斐波那契数,指的是这样一个数列:1、1、2、3、5、8、13、21、……在数学上,斐波那契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*),用文字来说,就是斐波那契数列由 0 和 1 开始,之后的斐波那契数列系数就由之前的两数相加。
1 | function fib(num) { |
fib
方法利用闭包方式实现,但是如果 num
值过大,会造成递归函数嵌套过深,导致堆债溢出。perfFib
方法不使用递归实现可以避免堆栈溢出问题。
深度优先 & 广度优先
深度优先:
1 | let dfs = (node, nodeList = []) => { |
广度优先:
1 | let bfs = (node) => { |
扁平化数组
1 | function flat(arr) { |
求连续
随机生成一个长度为 10 的整数类型的数组,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20]
,将其排列成一个新数组,要求新数组形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]
。
思路:对目标数组进行去重和排序后,遍历目标数组,在每次遍历时取结果数组 acc
的最后一位 lastArr
(这是排的原因)及其最后一个元素 lastVal
,将当前项 cur
与其进行对比,如果紧邻则将其推入 lastArr
否则,向 acc
中新增一个元素。
1 | function formArray(arr: any[]) { |
字符串匹配
实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。
使用正则表达式 match:
1 | fucntion matchStr(str, targetStr){ |
遍历字符串:
1 | const find = (S, T) => { |
移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
1 | 输入: [0,1,0,3,12] |
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
需要注意数组塌陷,并且不能遍历到已经移动到尾部的 0。
1 | function sortZero(arr) { |
数组转树
以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:
1 | // 原始 list 如下 |
如果从上到下生成树,那么将会多次遍历原数组,时间复杂度为 O(2n),要想做到时间复杂度为 O(1) 那么整体思想就是去遍历一遍原数组在遍历的过程中去查找每个元素在树上的 parent,同时创建一个 map 来快速根据 parentId 来找到 parent 节点:
1 | function convert(list) { |
路径查找
有一个树形结构的数据,要求给出一个节点的 id,输出这个节点在树上的路径:
1 | const data = [ |
递归反转字符串
用 JavaScript 写一个函数,输入 int 型,返回整数逆序后的字符串。如:输入整型 1234,返回字符串“4321”。要求必须使用递归函数调用,不能用全局变量,输入函数必须只有一个参数传入,必须返回字符串。
解析:将数字拆解为左右两个字符串,如 1234
拆解为 1
和 234
,让后将两个字符串调换位置,同时对 234
字符串进行递归调用当前函数,当处理的字符串只有一个字符时跳出递归。
1 | function reverseNum(num) { |
如果 reverseNum
递归时必须接收 Number,则可以使用除模运算来将数字 1234
拆解为 123
和 4
进行反转。
9. 其他
遇到过内存溢出吗,如何排查
场景:
- Vue3 的 computed 占用 reactive 对象
- 全局 pina 没有销毁
什么情况会造成内存溢出:
- 全局变量未删除
- 对象的循环引用
面试官反问
- 您可以给我一些建议吗
- 您在团队里的职责是什么
- 团队使用的技术栈