总结一下
面试前要精心做好准备,简历上写的知识点和原理都需要准备好,项目上多想想难点和亮点,这是面试时能和别人不一样的地方。
还有就是表现出自己的谦虚好学,以及对于未来持续进阶的规划,企业招人更偏爱稳定的人。
万事开头难,但是程序员这一条路坚持几年后发展空间还是非常大的,一切重在坚持。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
为了帮助大家更好更高效的准备面试,特别整理了《前端工程师面试手册》电子稿文件。
前端面试题汇总
1. JS内存泄漏如何检测?场景有哪些?
1.1 垃圾回收 GC
垃圾回收是一种自动管理内存的机制,它负责在运行时跟踪内存的分配和使用情况,并在不再需要的对象上进行自动内存释放,常用的垃圾回收算法:
标记清除 (Mark-and-Sweep):最常用的垃圾回收算法,垃圾回收器会从根对象(如全局对象、调用栈)出发,标记所有可以访问到的对象,扫描整个堆内存,清除那些未被标记的对象;
引用计数 (Reference Counting):通过记录对象的引用次数来判断对象是否可达。当对象的引用次数为零时,该对象被视为垃圾并进行回收。引用计数算法的缺点是难以解决循环引用问题。
JS闭包属于内存泄漏吗?
内存泄漏是指不再需要的对象(非预期的对象)仍然占用内存,导致系统资源浪费和性能下降。严格意义上说闭包并不算内存泄漏,使用闭包是预期相关对象会继续存在在内存中,闭包的数据不能被垃圾回收更合适,因为它依然保持对外部变量的引用。
1.2 JS内存泄漏如何检测?
主要有如下途径:
- 利用浏览器开发者工具,如Chrome DevTools包含 Memory 面板,可以监控内存使用情况、快照内存状态、查看对象分配情况等;
- 借助的第三方工具,如 Heap Profiler 工具可以捕获 JavaScript 堆栈中的快照,并分析对象的分配情况、引用关系等,从而发现潜在的内存泄漏问题;
- 当然也可以在开发过程中手动检测,避免导内存泄漏的情况出现,例如监听时间处理函数未处理、循环引用的存在等。
1.3 至于内存泄漏的场景(Vue为例)
被全局变量、函数引用,组件销毁时未清除
被全局事件、定时器引用,组件销毁时未清除
被自定义事件引用,组件销毁时未清除
扩展:WeakMap WeakSet
WeakMap和WeakSet是JavaScript的内置对象,它们提供了一种存储对象的弱引用的机制,被广泛用于解决内存泄漏和垃圾回收的问题。他们的特点类似:
WeakMap:
- 一种键值对的集合,其中键必须是对象,值可以是任意类型。键是对象的弱引用,即当对象没有其他引用时,垃圾回收机制可以自动清理该键值对(Vue 3的响应系统中也是用了WeakMap这一特性避免内存泄漏);
- 没有提供遍历方法,也不能获取其大小或键列表;
- 由于键是弱引用,所以 WeakMap 是不可迭代的,也不支持常规的对象方法,如 size、forEach 等;
- 主要用于存储额外的数据或元数据,这些数据与对象的生命周期相关,当对象被销毁时,与之关联的额外数据也会被自动清除。
WeakSet:
- 一种存储对象的集合,其中的对象是弱引用,同样在没有其他引用时,垃圾回收机制可以自动清除对象。 只能存储对象,不能存储原始值;
- 不可迭代,也没有提供查询大小、遍历等功能;
- 主要用于存储一组对象,并且需要确保这些对象不会产生内存泄漏;
- 与 Set 不同,WeakSet 中的对象是弱引用,因此它们不会阻止垃圾回收器销毁这些对象。
2. 浏览器和node.js的事件循环有什么区别?
2.1 浏览器事件循环
JS是单线程的(无论浏览器还是nodejs),浏览前中JS执行和DOM渲染共用一个线程,这就需要使用异步编程模型,实现非阻塞的操作,提高程序的性能和响应速度;而事件循环机制就是来处理异步操作的机制,这些异步任务又被分为宏任务和微任务。
宏任务是一个独立的、完整的任务单元,比如setTimeout SetInterval 网络请求
微任务通常是一些更小粒度的任务,如Promise async/await
微任务会被插入到微任务队列(microtask queue)中,在每个宏任务执行完成后,事件循环会检查微任务队列,然后依次执行微任务,直到微任务队列为空
console.log('start')
setTimeout(() => {
console.log('timeout')
})
Promise.resolve().then(() => {
console.log('promise then')
})
console.log('end')
// start
// end
// promise then
// timeout
// 微任务在页面渲染前触发,宏任务是在页面渲染之后触发
const p = document.createElement('p')
p.innerHTML = 'new paragraph'
document.body.appendChild(p)
const list = document.getElementsByTagName('p')
console.log('length----', list.length)
console.log('start')
setTimeout(() => { // 渲染之后触发
const list = document.getElementsByTagName('p')
console.log('length on timeout----', list.length)
alert('阻塞 Timeout')
})
// DOM 渲染
Promise.resolve().then(() => { // 渲染前触发
const list = document.getElementsByTagName('p')
console.log('length on promise.then----', list.length)
alert('阻塞 promise')
})
console.log('end')
// length---- 67
// start
// end
// length on promise.then---- 67
// length on timeout---- 67
参考下图,浏览器事件循环机制首先执行左上角的同步代码,遇到异步任务会放入Callback Queue(现在已经分为Macro Queue 和Micro Queue)中,等同步代码执行完成之后,通过Event Loop机制执行队列中的任务
2.2 Node.js事件循环
Nodejs同样使用ES语法,也是单线程,也需要异步;异步任务也分为:宏任务 + 微任务
Node.js中事件循环实现是依靠libuv 引擎事件循环中表现的状态与浏览器大致相同。Node.js事件循环的大致执行流程也是先执行同步代码,再执行微任务(process.nextTick),按顺序执行6个类型的宏任务(每当开始之前都执行当前的微任务)
但是,Node.js的宏任务和微任务,分不同类型,有不同的优先级
Node.js中宏任务的类型和优先级(由高到低):
- Timers - setTimout setInterval
- pending callbacks:执行延迟到下一个循环迭代的 I/O 回调
- Idle,prepare - 闲置状态(Node.js内部使用)
- Poll轮询 - 执行poll中的I/O队列
- Check检查 - 存储setImmediate回调
- Close callbacks - 关闭回调,如socket.on(‘clos’)
Node.js 微任务类型和优先级:
包括: Promise,async/await,process.nextTick,其中process.nextTick优先级最高
总之,在一次事件循环中,先执行当前执行栈中的代码,然后检查是否有微任务需要执行,如果有,则依次执行微任务,直到微任务队列为空;接着从宏任务队列中提取下一个宏任务进行执行。这样的设计保证了微任务能够在宏任务执行结束之后立即得到执行,从而更及时地处理异步操作的结果。
注:
任务队列是任务的集合,而不是队列。因为事件处理模型会选取第一个可执行任务开始执行,而不是队首的任务。
MDN上最新描述已经不再使用宏任务,沿用它是为了更容易理解
Node.js新版本,推荐使用setImmediate代替process.nextTick。
3. 遍历数组,for和forEach那个更快?
先上代码验证下,结论是用for通常会比forEach更快
const arr = []
for(let i = 0; i < 100*10000 ; i++) {
arr.push(i)
}
const length = arr.length
console.time('for')
let n1 = 0
for (let i = 0; i < length; i++) {
n1++
}
console.timeEnd('for') // 3.838134765625 ms
console.time('forEach')
let n2 = 0
arr.forEach(() => n2++)
console.timeEnd('forEach') // 14.0830078125 ms
forEach每次都要创建一个函数来调用(函数需要对应的作用域,会有额外的性能开销),而for不会创建函数。
除此之外,使用for的优点还有:
- 更多控制:使用for循环可以更灵活地控制循环过程,可以通过控制索引来实现跳过、中断、反向等操作。
- 可中断性:for循环可以通过break语句或return语句随时中断循环,而forEach方法不支持中断循环,只能通过抛出异常来模拟中断。 虽然for循环在性能上可能更快,但在某些情况下,使用forEach方法可能更具可读性和简洁性
3. 虚拟DOM(vdom)真的很快吗?
虚拟DOM(virtual dom) 一种用于简化 UI 更新和渲染逻辑的技术, 主要优点在于简化了对真实 DOM 的频繁操作。我们知道Vue React等框架的核心价值在于实现了数据视图分离,数据驱动视图。这样前端程序员coding时只关注业务数据,而不用再关心DOM变化,它们实现数据驱动视图的技术方案就是vdom。
实际上,vdom并不快,JS直接操作DOM才是最快的;但是"数据驱动视图"需要合适的技术方案,不能全部DOM重建;vdom就是目前最合适的技术方案(并不是因为它快,而是合适)。
扩展:svelte框架就不用vdom
不同于React和Vue等框架,使用vdom来跟踪和管理应用程序的状态和视图更新。Svelte框架采用了一种称为“编译时”方法,通过在编译时将组件代码转换为高效的JavaScript代码,避免了传统vDOM框架中的运行时开销。这种方法使得Svelte成为一个快速、轻量级的前端框架选择。
5. Node.js如何开启多进程?进程如何通讯?
进程(Process) VS 线程 (Thread)
进程是指正在运行的程序的实例。
每个进程都有自己的地址空间、内存、文件描述符、打开的文件等资源。进程是操作系统进行资源分配和调度的基本单位,可以独立运行、拥有自己的生命周期,并与其他进程隔离。
线程是进程的一个实体,是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。一个进程可以包含多个线程,这些线程共享进程的资源,包括内存、文件句柄等。线程共享同一地址空间,可以访问相同的数据,因此线程之间通信更加方便高效。线程也被称为轻量级进程,因为创建和切换线程所需的开销比进程小得多。
JS是单线程的,但是可以开启多个进程执行,如WebWorker
为何需要多进程?
多核CPU,更适合处理多进程,而多进程同时执行多个任务,可以充分利用多核处理器的并行计算能力,提高系统的整体性能和效率
内存较大,多个进程才能更好的利用(单进程有内存上限,一般2G左右);
此外,多进程还可以实现任务隔离,提高安全性和稳定性
总之,"压榨"机器资源,更快,更节省
通过child_process.fork开启子进程
/**
* 主进程
*/
const http = require('http')
const fork = require('child_process').fork
const server = http.createServer(() => {
if(req.url === '/get-sum') {
console.info('主进程 id', process.pid)
// 开启子进程
const computeProcess = fork('./computer.js')
computeProcess.send('开始计算')
computeProcess.on('message', data => {
console.info('主进程接收到信息:', data)
res.end('sum is ' + data)
})
computeProcess.on('close', data => {
console.info('主进程因报错而退出')
computeProcess.kill()
res.end('error')
})
}
})
server.listen(3000, () => {
console.info('localhost: 3000')
})
/**
* computed.js 子进程,计算
*/
function getSum() {
let sum = 0
for (let i = 0; i <10000; i++) {
sum += i
}
return sum
}
//子进程可以通过这种自定义事件的方式来监听
process.on('message', data => {
console.log('子进程 id', process.pid)
console.log('子进程接收到的信息‘, data)
const sum = getSum()
// 发送消息给主进程
process.send(sum)
})
//localhost: 30000
// 主进程 id: 80780
// 子进程 id: 80781
// 子进程接收到的信息: 开始计算
// 主进程接收到的信息: 49995000
使用cluster方式开启多进程
/**
* cluster:多子进程,多个服务
* 根据CPU核数派生子进程,每个子进程创建一个Http服务
*/
const http = require('http')
const cpuCoreLength = require('os').cpus().length //获取CPU核数
// 引入cluster
const cluster = require('cluster')
if(cluster.isMaster) { // 是cluster的主进程
for (let i = 0; i < cpuCoreLength; i++) {
cluster.fork() // 开启子进程
}
cluster.on('exit', worker => { // worker表示当前进程的实例
console.log('子进程退出')
cluster.fork() // 进程守护
})
} else {
// 多个子进程会共享一个TCP连接,提供一份网络服务
const server = http.createServer((rep, res) => {
res.writeHead(200)
res.end('done')
})
server.listen(3000)
}
总结:Node.js中开启子进程child_process.fork和cluster.fork;使用send和on传递消息
扩展:工作中实际使用PM2 完成进程守护和开启多个进程 PM2 是一个针对Node.js应用的进程管理工具,可以用于进程守护、负载平衡、日志管理等。通过PM2可以方便地启动、停止、重启应用程序,并监控应用程序的运行状态
6. 请描述js-bridge的实现原理?
什么是JS Bridge?
JSBridge是一种在Webview和Native之间进行通信的技术。JS 无法直接调用native API,需要通过一些特定"格式" 来调用,这些"格式"统称为JS-Bridge ,例如微信的JSSDK。基本实现原理:
- 在Native端中创建一个Javascript对象,通过WebView的addJavascriptInterface()方法将该对象注入到WebView中。
- 在Web端中通过Javascript代码调用该对象的方法,从而实现与Native的通信。
- 在Native端中捕获Javascript代码的调用请求,解析请求参数并执行相应的操作(可执行一些特定操作,如打开摄像头、访问设备传感器等)。
- 将操作结果通过Javascript代码的回调函数返回给Web端。
- 在Web端中通过回调函数获取到Native端返回的数据,并进行相应的处理。
通过这种方式,JSBridge实现了Webview和Native之间的双向通信,可以使得Web应用和Native应用之间实现无缝的交互。
JS Bridege的常见实现方式
WebView方式:通过WebView加载一个网页,然后在网页中通过JavaScript代码调用Native代码来实现功能。这种方式适用于需要在网页中嵌入Native功能的场景
URL Scheme:通过定义特定的 URL Scheme,JavaScript 可以通过修改页面链接的方式触发原生应用执行相应操作。原生应用可以通过拦截特定的 URL Scheme 来响应 JavaScript 的请求。
// 封装JS-bridge
const sdk = {
invoke(url, data = {}, onSuccess, onError){
const iframe = document.createElement('iframe')
iframe.style.visibility = 'hidden'
document.body.appendChild(iframe)
iframe.onload = () => {
const content = iframe1.contentWindow.document.body.innerHTML
onSuccess(JSON.parse(content))
iframe.remove()
}
iframe.onError = () => {
onError()
iframe.remove()
}
iframe.src = `my-app-name://${url}?data=${JSON.stringify(data)}`
},
fn1(data, onSuccess, onError) {
this.invoke('api/fn1',data, onSuccess, onError)
},
fn2(data, onSuccess, onError) {
this.invoke('api/fn2',data, onSuccess, onError)
},
}
sdk.fn1()
7. requestIdleCallbcak和requestAnimationFrame有什么区别?
requestIdleCallback并不是一个常用的API,由于React 16版本之后引入React fiber引起关注。React fiber可以将组件树转换为链表,可以分段渲染;渲染时可以暂停,去执行其他高优先级任务,空闲时再继续渲染,这样就会存在如何判断空闲的问题?
requestIdleCallback该方法会在浏览器空闲时调用回调函数,即在主线程上没有其他高优先级任务需要执行时。它会尽量在空闲时间内执行,以避免对用户交互和页面渲染的干扰。
结尾
学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。
tml5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。