2023春招/暑期美团面经大总结
2023春招/暑期美团面经大总结
Hello~ Everyone, 我在牛客上收集了一些团子的面经,统一做了一个总结。
如果有帮到你的话,能帮我点个赞么~
HTML 部分
1. 你对 SPA 的理解
单页面应用的核心思想是不同于传统的多页面应用,整个应用只有一个页面。页面初始化时会加载应用所需的所有资源,之后所有的内容切换、动态更新、交互等操作都在这个页面中进行。
在单页面应用中,网址的变化并不会导致页面的刷新或跳转。这是因为单页面应用会通过JS监听URL的变化,并响应变化而进行DOM操作,完成新内容的展示和旧内容的隐藏等更新操作。这种方式提高了应用的性能和用户体验,并避免了页面切换时的视觉闪烁。
虽然单页面应用相对于传统应用有许多优点,如响应速度快、用户体验好、能够进行离线浏览等等,但是它也有一些缺点,比如SEO问题、对浏览器前进/后退功能的兼容性等等。因此在应用场景上需要根据具体问题进行评估和选择。
2. 怎么做SEO优化?
- 使用服务端渲染
对于一些需要SEO优化的SPA应用,可以考虑在服务端进行渲染,把生成的HTML代码发送给客户端浏览器,这样就可以消除搜 索引擎对于渲染JavaScript生成的DOM的识别问题,也可以提高应用的性能和搜索引擎的可识别性。
- 合理的meta标签与title标签
在单页面应用中设置合理的meta标签和title标签对SEO也是非常重要的,这些标签需要详细记录页面内容的关键信息,包括关键字、页面描述等等。这样可以方便搜索引擎对页面进行分析和识别。
3. html语义化标签,为什么用语义化标签,
- HTML 语意化有啥
-
- article
- section
- header
- footer
- nav
- sider
- 好处
网页加载慢导致 CSS 文件还未加载时(没有 CSS),页面依然清晰、可读、好看。
有利于 SEO,和搜索引擎建立良好沟通,有利于爬虫抓取更多的有效信息。
方便其他设备(如屏幕阅读器、盲人阅读器、移动设备)更好的解析页面。
使代码更具可读性,便于团队开发和维护。
CSS 部分
1. css:两栏布局
.outer {
display: flex;
height: 100px;
}
.left {
width: 200px;
background: tomato;
}
.right {
flex: 1;
background: gold;
}
2. 对 flex 属性的了解
flex 布局是 CSS3新增的一种布局方式,可以通过将一个元素的 display 属性值设置为 flex 从而使它成为一个 flex 容器,它的所有子元素都会成为它的项目。一个容器默认有两个轴:一个是水平的主轴,一个是垂直的交叉轴。可以使用 flex-direction 来指定主轴的方向。可以使用 justify-content 来指定元素在主轴上的排列方式,使用 align-items 来指定元素在交叉轴上的排列方式。还可以使用 flex-wrap 来规定当一行排列不下的换行方式。对于容器中的项目,可以使用 order 属性来指定项目的排列顺序,还可以使用 flex-grow 来指定当排列空间有剩余的时候,项目的放大比例,还可以使用 flex-shrink 来指定当排列空间不足时,项目的缩小比例。
3. position的属性以及相对谁定位
- relative:元素的定位永远是相对于元素自身位置的,和其他元素没关系,也不会影响其他元素。
- fixed:元素定位是相对于 window(或者 iframe)边界的,和其他元素没有关系,但是它具有破坏性
- absolute:如果 absolute 设置了 top、left,浏览器会递归查找该元素的所有父元素,如果找到了一个设置了 position: relative/absolute/fixed 的元素,就以该元素为基准定位,如果没找到,就以浏览器边界定位。
- static:默认值,没有定位,元素出现在正常的文档流中,会忽略 top,bottom,left,right 或者 z-index 声明,块级元素从上往下纵向排布,行级元素从左向右排列。
- inherit:规定从父元素继承 position 属性的值。
4. css选择器如何工作的
- 内联样式 ==> 1000
- id 选择器 ==> 100
- 类选择器、伪类选择器、属性选择器 ==> 10
- 标签选择器、伪元素选择器 ==> 1
- 相邻兄弟选择器、子选择器、后代选择器、通配符选择器 ==> 0
5. display 的属性值
-
none ==> 元素不显示,并且会从文档流中移除
-
block ==> 块元素。默认宽度为父元素宽度,可设置宽高,换行显示。
-
inline ==> 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。
-
inline-block ==> 默认宽度为内容宽度,可以设置宽高,同行显示。
-
list-item ==> 像块类型元素一样显示,并添加样式列表标记。
-
table ==> 次元素会作为块级表格来表示。
-
inherit ==> 规定应该从父元素继承 display 属性的值
6. 清除浮动的方式有哪些
- 给父元素增加高度 ==> 可以解决问题,但不够彻底
- clear: both
在父元素的子元素最后添加一个块级元素,设置 style="clear: both";必须是一个块级元素,否则没法撑开父元素高度。
- 伪元素清除浮动
- 使用 BFC 布局
添加 overflow: hidden
- 使用 br 标签
<br clear="all">
7. CSS 如何实现垂直居中
- relative + absolute
.wrapper {
position: relative;
}
.box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
- flex
.wp {
display: flex;
justify-content: center;
align-items: center;
}
- grid
.wp {
display: grid
}
.ot {
justify-content: center;
align-items: center;
}
- absolute + transform
.wp {
position: relative;
}
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%)
}
- calc
.wp {
position: relative;
}
.box {
position: absolute;
top: calc(50% - 50px); <!--50 是父盒子的高度-->
left: calc(50% - 50px);
}
JS 部分
1. 事件循环一
setTimeout(() => {
new Promise(res => {
console.log(2);
res();
}).then(() => {
console.log(1);
});
console.log(4);
}, 0);
setTimeout(() => {
console.log(3);
});
console.log(5);
// ans : 5 2 4 3 1
2. sum(1)(2)(3) 如何实现
- 整体是一个柯里化的思路。
// 一个有局限的写法,需要规定参数数量
function curry(fn) {
let params = []
return function newFn(...restParams) {
params = [...params, ...restParams]
if (params.length === fn.length) {
return fn(...params)
} else {
return newFn
}
}
}
function reduce(a, b, c) {
return [...arguments].reduce((a, b) => a + b, 0)
}
const sum = curry(reduce);
console.log(sum(1)(2)(3));
// 这个写法就没有那么多局限
function curry(fn) {
let args = []
return function newFn(...restArgs) {
if (restArgs.length > 0) {
args = [...args, ...restArgs]
return newFn
} else {
// 这里传入的 args 不需要打开
const val = fn.apply(this, args)
args = []
return val
}
}
}
// 基础功能
function reduce(...args) {
return args.reduce((x, y) => x + y)
}
// 调用这两个函数
let add = curry(reduce)
console.log(add(1)(2, 3)()) // 6
3. 说一下进程和线程的概念吧
进程和线程的概念
进程和线程都是 CPU 工作时间片的一个描述:
- 进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
- 线程是进程中的更小单位,描述了执行一段指令所需的时间。
进程和线程的特点
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
- 线程之间共享进程的数据。
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
- **进程之间的内容相互隔离。**进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个程序如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要用于进程间通信的机制了。
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
进程和线程的区别
- 进程可以看作独立应用,线程不能。
- 资源:进程是 CPU 资源分配的最小单位(是能拥有资源和独立进行的最小单位);线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 通信方面:线程间可以直接共享同一进程的资源,而进程通信需要借助进程间通信。
- 调度:进程切换比线程切换的开销要大。线程是 CPU 调度的基本单位,同一进程内的线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器的内容,开销较小。
4. 浏览器中有哪些进程线程
5. 你知道死锁产生的原因么
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,他们都将无法再向前推进。
产生原因:
(1)竞争资源
-
- 产生死锁的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程 p1 使用,假定 p1 已占用了打印机,若 p2 继续要求打印机打印,打印机将阻塞)
- 产生死锁中的竞争资源的另外一种资源指的是竞争资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生后死锁。
(2)进程间推进顺序非法
若 p1 保持了资源 R1,P2保持了资源 R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如:当 p1 运行到 p1:Request(R2)时,将因 R2 已被 P2 占用而阻塞;当 P2 运行到 P2:Request(R1)时,也将因 R1 已被 P1 占用而阻塞,于是发生进程死锁
6. flat手撕
const flatten = (arr) => {
return arr.toString().split(',')
}
const flatten = (arr) => {
return arr.reduce((perv, cur) => prev.concat(Array.isArray(cur) ? flatten(cur) : cur),[])
}
7. 手撕节流函数
function throttle(fn, delay) {
let t1 = 0
return function() {
let t2 = new Date()
if (t2 - t1 > delay) {
fn.apply(this, arguments)
t1 = t2
}
}
}
8. 手写并发控制
class Schedular {
constructor(limit) {
this.limit = limit
this.queue = []
this.number = 0
}
addTask(name, timeout) {
this.queue.push([name, timeout])
}
start() {
if (this.number < this.limit && this.queue.length) {
const [fn, time] = this.queue.shift()
this.number++
setTimeout(() => {
fn()
this.numebr--
this.start()
}, time * 1000)
this.start()
}
}
}
9. 实现一个打点计时器
let count = 0;
function tick() {
count++;
console.log("tick: " + count);
}
let intervalId = setInterval(tick, 1000); //每秒打点一次,返回计时器 ID
//在一定时间(比如 10 秒)后停止计时器
setTimeout(function() {
clearInterval(intervalId);
console.log("timer stopped");
}, 10000);
10. 用JS给对象提供去重方法
JS中可以利用 Set
对象的特性(自动去重)为对象添加去重的方法。以下是一个例子:
//定义一个对象
const obj = {
a: 1,
b: 2,
c: 1,
d: 2
}
//为对象添加一个去重方法
obj.removeDuplicateProperties = function() {
const duplicateValues = new Set(); //Set对象用于存储重复的值
const propertiesToBeRemoved = []; //存储需要被删除的重复属性名称
for (const prop in this) {
if (this.hasOwnProperty(prop)) {
const propValue = this[prop];
if (duplicateValues.has(propValue)) { //如果值已经存在于Set中,表示属性值重复
propertiesToBeRemoved.push(prop); //将该属性名称推入数组,用于删除
} else {
duplicateValues.add(propValue); //否则将该值存入Set,以便后续判断
}
}
}
//遍历删除重复的属性
propertiesToBeRemoved.forEach(prop => {
delete this[prop];
})
}
//测试去重方法
console.log("原始对象:", obj);
obj.removeDuplicateProperties();
console.log("去重后的对象:", obj);
11. 手写 apply/call/bind
// 实现一个 apply
Function.prototype.apply = function(context, args) {
context = context ? context : window
args = args || []
const key = Symbol()
context[key] = this
const result = context[key](args)
delete context[key]
return result
}
// 实现一个 call
Function.prototype.call = function(context, ...args) {
context = context ? context : window
args = args || []
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
// 实现一个 bind
Function.prototype.bind = function(context, ...args) {
const self = this
return function(...newArgs) {
return self.apply(context, [...args, ...newArgs])
}
}
12. 红绿灯
function loadSource(target) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(target.color)
}, target.delay)
})
}
async function lightCycle(targetList) {
while(true) {
for(let i=0;i<targetList.length;i++) {
const res = await loadSource(targetList[i])
console.log(res)
}
}
}
lightCycle([
{color:"red",delay:1000},
{color:"green",delay:2000},
{color:"yellow",delay:3000}
])
13. 深拷贝
function deepClone1(target, map = new WeakMap()) {
if (typeof target === 'target') {
const newTarget = Array.isArray(target) ? [] : {}
if (map.get(target)) return map.get(target)
for (const key in target) {
newTarget[key] = deepClone(target[key])
}
map.set(target, newTarget)
return newTarget
} else {
return target
}
}
14. 实现一个 Promise.all
Promise.all = function(promises) {
let count = 0
let ansArr = []
return new Promise(() => {
promises.forEach((item, idx) => {
Promise.resolve(item).then(res => {
ansArr[idx] = res
count ++
if (count === promises.length) return resolve(ansArr)
})
})
})
}
Vue
1. vue的$nexttick有啥用?
1.1为什么存在 nextTick
因为 Vue 采用的「异步更新策略」,当监听到数据发生变化的时候不会立即去更新 DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更;
这种做法的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数。
1.2nextTick 的作用
nextTick 等待下一次 DOM 更新刷新的工具方法。{接收一个回调函数作为参数,并将这个回调函数延迟到 DOM 更新后才执行。}
「使用场景」:
- 想要操作 基于最新数据生成的 DOM 时,就将这个操作放在 nextTick 的回调中;「created 的时候想要提前获取 DOM 树」
1.3nextTick 的实现原理
将传入的回调函数包装成异步任务,异步任务又分为微任务和宏任务,为了尽快执行所以优先选择微任务;
- nextTick 提供了四种异步方法: Promise.then、MutationObserver、setImmediate、setTimeout(fn, 0)
- setImmediate 有兼容性的问题,必须要 IE10 以上才支持。
- setTimeout 主要是有时延问题,大概是 4ms 左右。
2. Vue和React的区别
- 从写法上来说Vue 倾向于提供自己的单文件组件(.vue)格式,以后端开发者和前端开发者共同开发;而 React 通过 JSX,可以在 JavaScript 中编写组件,也可以在外部文件中定义组件。
- 数据绑定:Vue 和 React 都通过虚拟 DOM 实现数据绑定和更新。在 Vue 中,通过其双向数据绑定技术,在模板中直接渲染数据。而在 React 中,由于其单向数据流,需要通过一些扩展库来实现类似的效果。
3. DIFF 算法
4. computed、watch区别
- 首先 computed 是计算属性,watch 是用来监听的。
computed 是会有依赖项的,从此派生出数据,最常见的方式,就是设置一个函数,返回计算之后的结果,computed 和 methods 区别在于缓存性,computed 依赖的数据如果不改变,那么返回值是固定不变的,而 watch 则监听一个数据,如果数据改变了,那么就执行对应回调函数中的 DOM操作 或异步操作。
一般来说,computed 使用在我们需要简化模版中值的部分,比如在 template 中要展示一个复杂表达式计算出来的值,我们直接写在上面会显得臃肿不好维护,使用计算属性会更加的简便好看; watch 的话则**不会有返回值,**仅仅是监听数据的变化做一些异步或者 DOM 操作。
除此之外,computed 还可以传入一个对象,获得可读可写的返回值,如果正常传入一个函数,那就是只读的,watch 也可以传入带有 deep 或者 immediate 的属性来达到深层监听 或者 立刻执行的目的。
计算机基础部分
1. 说一下响应状态码 100、101、200、301、302、304、403、404、500的含义
- 100 Continue:服务器已收到请求的初始部分,并且客户端应该继续发送其余部分。
- 101 Switching Protocols:服务器已经理解并接受客户端请求,并将切换到不同的协议来完成处理。
- 200 OK:请求成功。这是最常见的状态码,表示请求已成功处理。
- 301 Moved Permanently:所请求的资源已永久移动到新位置。浏览器会自动重定向到新位置。
- 302 Found:所请求的资源临时移动到新位置。浏览器会自动重定向到新位置。
- 304 Not Modified:资源未修改。使用缓存的版本,客户端应继续使用缓存。
- 403 Forbidden:服务器理解请求,但拒绝执行。通常表示客户端没有权限访问该资源。
- 404 Not Found:所请求的资源不存在。服务器无法找到请求的资源。
- 500 Internal Server Error:服务器在执行请求时遇到了错误。这是一般性的服务器错误状态码。
2. HTTP 加密流程
- 当客户端向服务器端发起连接请求时,服务器会发送包含自己公钥的数字证书给客户端。
- 客户端收到数字证书后会进行验证,确保证书是由可信的证书颁发机构颁发的,并且证书没有被篡改。这就保证了服务器端的身份。
- 如果验证成功,客户端会生成一个随机密钥,并使用公钥加密这个密钥,再发送给服务器端。
- 服务器端用自己的私钥解密客户端发来的消息,同时使用解密后的随机密钥对数据进行加密。
- 客户端通过使用刚才协商的随机密钥解密服务器端发来的数据。
3. http 和 https 区别
- 安全性
HTTP 是一种明文协议,即数据传输过程是不加密的,数据容易被第三方窃取或篡改,不具备安全性。而 HTTPS 则是一种加密的协议,可以确保数据传输的安全性,通过 SSL/TLS 加密协议将数据进行加密传输,从而大大降低了数据被盗用或篡改的风险。
- 监听端口
HTTP 协议的默认端口为 80,而 HTTPS 协议的默认端口为 443。因此,当 Web 应用程序使用 HTTPS 协议进行数据传输时,需要申请数字证书,对服务器进行认证,确保用户可以安全地访问应用程序。
- 抓包和缓存方式
HTTP 协议的数据传输是明文的,可以被第三方窃取或篡改,因此容易被抓包工具获取信息。而 HTTPS 协议的数据传输通过 SSL/TLS 加密协议进行加密传输,数据内容不易被窃取或篡改,安全性较高。同时,HTTPS 协议不允许缓存,以保证数据的一致性和安全性。
综上所述,HTTP 和 HTTPS 在安全性、监听端口、抓包和缓存方式等方面都有明显差异。建议在保护用户隐私、保护应用程序数据安全等方面,使用 HTTPS 协议进行数据传输。
4. OSI模型,以及各层都能有什么功能
5. 三次握手四次挥手
刚开始客户端处于 Closed 状态,服务端处于 Listen 的状态
- 第一次握手:客户端给服务器发送一个 SYN 报文,并指明客户端的初始化序列号是 ISN,此时客户端处于 SYN_SEND 状态
首部的同步位 SYN=1,初始化序列号 seq=x,SYN=1 的报文段不能携带数据,但要消耗掉一个序号。
- 第二次握手:第二次握手:服务器收到客户端的 SYN 报文后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN-REVD 的状态。
在确认报文段中 SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y
- 第三次握手:客户端收到了 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样吧服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段 ACK=1,确认号 ack=y+1,序号 seq=x+1 (初始为 seq=x,在第二个报文段的基础上 +1 ),ACK 报文段可以携带数据,不携带数据则不消耗序号。
- 第一次挥手:客户端会发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段 (FIN=1;序号seq=u) ,并停止再发送数据,主动关闭 TCP 连接,进入 FIN_WAIT1(终止状态),等待服务器确认。
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且吧客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文,此时服务端处理 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后发出确认报文段(ACK=1,确认号ack=u+1,序号 seq=v),服务端进入 CLOSE_WAIT (关闭等待)状态,此时的 TCP 处于半关闭状态,客户端到服务器的连接释放。
客户端收到服务端的确认后,进入 FIN_WAIT2(终止状态2)状态,等待服务端发出的连接释放报文段。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入 LAST_ACK(最后确认)状态,等待客户端的确认。
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为报答,且把服务端的序列号 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入 TIME_WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计时器设置的时间 2MSL 后,客户端才进入 CLOSED 状态。
6. http1.0 http2.0 http3.0
- 连接方面,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。
- 资源请求方面,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- 缓存方面,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。
- http1.1 中新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。
- http1.1 相对于 http1.0 还新增了很多请求方法,如 PUT、HEAD、OPTIONS 等。
- **服务器推送:**HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。在 HTTP/2 当中,服务器已经不再是完全被动地接收请求,响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 连接建立之后,比如浏览器请求一个 HTML 文件,服务器就可以在返回 HTML 的基础上,将 HTML 中引用到的其他资源文件一起返回给客户端,减少客户端的等待。
- 多路复用:多路复用产生的原因是「HTTP队头阻塞」,而队头阻塞又由「HTTP请求-应答」模式所造成(在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。),后面我们使用了并发连接和域名分片去解决这个问题,但这并没有真正从 HTTP 本身的层面解决问题,只是增加了 TCP 连接,分摊风险而已。而且这么做也有弊端,多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流(Stream)。HTTP/2 用流来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。
所谓的乱序,指的是不同 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧一定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文和响应报文。当然,在二进制帧当中还有其他的一些字段,实现了优先级和流量控制等功能
-
二进制协议:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码){这个问题被字节一面面试官问过,还答错了,和数据体搞混了。},数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。
-
头信息压缩: HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表「HPACK 算法」,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
-
- HPACK 算法的优势
-
-
-
- 首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,...)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。
- 其次是对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。
-
-
-
7. TCP 和 UDP 的区别
优化部分
1. 说一下项目中的常见首屏优化
- 代码优化:减少不必要的代码和资源加载,压缩和合并 JavaScript、CSS 和 HTML 文件,以减少文件大小和网络请求次数。
- 图片优化:使用适当的图片格式(如 WebP),压缩图片大小,使用懒加载或渐进式加载,以减少页面加载时间。
- 异步加载:延迟加载非关键性 JavaScript 和 CSS 文件,将它们放在页面底部,以便首屏内容能够更快地加载和显示。
- 骨架屏:在页面加载过程中,显示一个简单的骨架屏或占位符,给用户一个加载进度的反馈,并提前展示页面布局。
- 缓存策略:设置适当的缓存策略,利用浏览器缓存,减少重复的网络请求,加快页面加载速度。
- 数据优化:根据页面的实际需求,减少请求的数据量,使用分页加载或滚动加载等方式,避免一次性加载大量数据。
- 服务端渲染 (SSR):对于需要搜索引擎优化或具有复杂的首屏内容的项目,考虑使用服务端渲染,以提前生成并发送完整的 HTML 内容。
- 延迟加载或按需加载组件:将页面中的某些组件延迟加载或按需加载,只在需要时加载,减少初始页面的加载负担。
- CDN 加速:使用内容分发网络(CDN)来加速静态资源的传输,让用户从离他们更近的服务器获取资源,提高加载速度。
- 压缩和优化字体:减少使用的字体种类和文件大小,使用字体子集和压缩字体文件,以减少字体的加载时间。
2. 说一下图片懒加载是如何实现的
- element.offsetTop - document.documentElement.scrollTop <= window.innerHeight
- getBoundingClientRect
- IntersectionObserver
懒加载是一种常见的应用场景,它主要用于提高页面加载速度和性能的优化。常见的懒加载有两种实现方法:使用 getBoundingClientRect
和使用 IntersectionObserver
。
- 使用
getBoundingClientRect
getBoundingClientRect
是一个基本的 DOM API,用于获取某个元素相对于窗口左上角的坐标以及宽高等信息。通过监听窗口滚动事件,可以判断某个元素是否在可视区域,从而触发懒加载。
具体实现方法如下:
<img src="default.jpg" data-src="actual.jpg" alt="description" />
const lazyLoad = () => {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
if (img.getBoundingClientRect().top < window.innerHeight) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
};
window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
window.addEventListener('orientationchange', lazyLoad);
lazyLoad(); // 首屏图片直接显示,无需滚动就加载
通过设置 img
标签的 data-src
属性提前保存图片真正的 URL 地址,当图片进入可视区域时,将 img
标签的 src
属性设置为 data-src
的值,从而触发图片加载。
- 使用
IntersectionObserver
IntersectionObserver
是新的 API,用于观察元素与其祖先元素或 viewport 交叉的状态。它可以非常精确地监听可视变化,从而实现懒加载。
<img src="default.jpg" data-src="actual.jpg" alt="description" />
const lazyLoad = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
};
const observer = new IntersectionObserver(lazyLoad, {
rootMargin: '0px 0px 100% 0px'
});
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
observer.observe
Webpack
1. loader和plugin的区别
- Loader
Loader 是 Webpack 实现文件逐个进行处理的前置条件,它将文件作为输入,将处理后的文件作为输出。可以理解为一个转换器,用于对模块的源代码进行转换处理。
例如,在构建过程中,如果需要使用 SCSS 编写样式,我们就需要使用 SCSS Loader 将 SCSS 文件进行编译处理,并将其转换为 CSS 文件,方便 Webpack 进一步进行处理和打包。
在使用 Loader 的时候,需要在 Webpack 配置文件中进行配置,它的配置格式如下:
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
]
}
- Plugin
相比于 Loader,Plugin 更加强大,它可以具有更加广泛的功能,对 Webpack 触发的构建事件进行监听,执行自定义的构建任务,可以用于文件压缩、代码优化、资源管理等方面的工作。
Plugin 的使用方法需要先引入对应的模块,然后在 Webpack 配置文件中进行配置,它的配置格式如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new CleanWebpackPlugin()
]
};
上述代码中,HtmlWebpackPlugin
用于生成 HTML 页面,而 CleanWebpackPlugin
用于清除缓存文件、残留文件等。
总的来说,Loader 和 Plugin 是 Webpack 的重要组成部分,Loader 用于对源代码进行转换和处理,Plugin 用于扩展 Webpack 的功能,通过对构建事件的监听和处理,实现文件压缩、代码优化、资源管理等功能。虽然两个概念有所区别,但均是 Webpack 实现前端自动化构建的重要组成部分。
2. Webpack 热启动原理
webpack热启动(Hot Module Replacement,HMR)是一个高效的模块热更新技术,其原理可以简单概括为:首先,webpack 构建后生成对应的模块映射表,通过 webpack-dev-server 启动时会加载这些资源;其次,使用 HMR,让应用在运行中替换、添加和删除模块,从而实现模块的动态更新,减少了开发者在开发过程中的重复性操作,提高了开发效率。
在使用 webpack-dev-server 的过程中,HMR 依赖于以下几个核心概念:
- HMR Runtime:在 webpack 构建时,会将 HMR Runtime 注入到输出的 bundle.js 文件中,该文件负责将更新的模块内容从 devserver 推送到客户端,同时进行模块更新。当有更新时 HMR Runtime 将通知代码进行更新。
- 更新模块:当开发者对代码进行了修改之后,系统会触发 bundler 重新编译,然后使用 HMR Runtime 推送更新,更新的代码段会被替换掉老的代码。
- HMR Server:HMR Server 会将对应的代码输出到客户端进行模块热替换(HMR)更新。
在进行 HMR 的实现时,webpack-dev-server 会将修改的模块转化为原生的代码,然后通过 Webpack-dev-server 的服务端返回给客户端进行处理,以达到更新模块、自动刷新的效果。
在使用 HMR 技术时,注意以下几点:
-
需要在 webpack 配置文件中添加
hot
选项进行开启 HMR 的功能。 -
HMR 支持的模块类型只有部分,如处理样式的 CSS Loader、处理 React 的 Babel Loader 等,对于其他类型的模块可能需要进行特殊的配置。
-
如果使用了 React,则需要借助
react-hot-loader
进行配置,实现组件级别的热刷新。 -
HMR 使用并不复杂,但需要遵守一些原则,如开发中最好只有一个 webpack-dev-server,避免混乱;其次,避免使用诸如
require
等语句,这会使 HMR 失效。
总体而言,webpack HMR 技术的兴起大大提高了应用程序的开发效率,便于开发人员进行快速迭代和操作,让开发更快速、更高效,也更能投入更多的时间和精力到应用程序的功能上。
Git
1. git cherry-pick怎么用
git cherry-pick
是一个命令,用于选择某一分支中的一个或多个提交,并将其应用到另外一个分支中。
git cherry-pick
的语法如下:
git cherry-pick <commit-hash>
其中<commit-hash>
是要选择的提交的哈希值。
使用git cherry-pick
可以将指定的提交应用到当前分支中,并生成一个新的提交。
例如,假设我们想要从master
分支上选取一个提交,并将其应用到feature
分支上。我们可以执行以下命令:
git checkout feature
git cherry-pick <commit-hash>
其中<commit-hash>
需要替换为要选取的提交的哈希值。执行后,该提交会被应用到feature
分支的当前状态上,并生成一个新的提交。
除了单个提交外,git cherry-pick
还支持选择多个提交。例如:
git cherry-pick <commit-hash-1> <commit-hash-2> <commit-hash-3>
这个命令将会将<commit-hash-1>
、<commit-hash-2>
和<commit-hash-3>
这三个提交都应用到当前分支上,并生成三个新的提交。
需要注意的是,使用git cherry-pick
命令后会生成一个新的提交,可能会有冲突需要解决。如果遇到冲突,需要手动解决完冲突后再进行新的提交。此外,在 cherry-pick
过程中一定要留意是否合并成功,否则可能会出现意想不到的难以排查的问题。
浏览器
1. cookie中用什么字段来限制跨域
在 Cookie 中,用来限制跨域请求的字段是 SameSite
。该字段表示 Cookie 的属性值是否允许跨域请求。具体来说,它是一个枚举型的属性,它有 three 个值:
Strict
: 表示完全禁止第三方站点访问 Cookie,即跨站点时,只有在当前网页的 URL 与请求目标一致时才可以访问 Cookie。Lax
: 表示允许一些情况下的第三方站点访问 Cookie,例如,在通过链接的方式跳转到下一个网页时,只要该链接是 GET 请求,并具有良好的用户体验,也可以允许第三方站点进行访问。None
: 表示允许所有第三方站点访问 Cookie,这种情况下必须是使用 HTTPS 协议来进行请求。
当前主流的浏览器都已经对 Cookie 的 SameSite 进行了支持,所以在开发使用 Cookie 的时候,建议在数据隐私保护和防止 CSRF 攻击的层面,合理配置 SameSite 属性,提高站点安全性能。
2. 跨域是浏览器还是服务端限制的
跨域是由浏览器实现的安全策略来限制的,是一种浏览器的安全行为。
同源策略是浏览器实现的安全策略之一,它指的是一种安全机制,它要求在同一域名或主机名下的网页才能够互相进行数据交互,否则就会出现跨域问题。同源策略是由浏览器实现的,而不是由服务端实现的。
Web 应用中,浏览器和服务器之间的数据交互通常使用 HTTP 协议,而 HTTP 协议是一种基于请求和响应的模式,对于浏览器发起的任何请求,服务器都会返回相应的数据,但是服务器并不会检查该请求是否来自同一源,而是由浏览器在发起请求时进行同源检查。如果不满足同源规则,浏览器就不会将数据返回给 JavaScript,从而阻止了跨域问题。
总的来说,跨域是由浏览器实现的同源策略来限制的,是浏览器的内置机制。解决跨域问题的方法通常需要配合服务端的相关技术,如 CORS、JSONP、代理等,以避免同源策略的限制问题。
3. 跨域报错之后前端怎么根据报错信息去分析是哪里出了问题?
跨域报错通常会出现以下两种形式:
- console 报错信息
在浏览器的控制台中,会抛出类似以下的跨域错误信息:
Access to XMLHttpRequest at 'http://xxx.xxx.com/api/test' from origin 'http://yyy.yyy.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
- 网络请求报错信息
在浏览器的开发者工具中,可以查看到当前请求的状态以及 Response 数据,如果存在跨域问题,会显示类似以下的错误:
Failed to load resource: Origin 'http://yyy.yyy.com' is not allowed by Access-Control-Allow-Origin.
常考算法题
1. 归并排序
function mergeSort(arr) {
let len = arr.length
if (len <= 1) return arr
const mid = Math.floor( len / 2 )
let arr1 = mergeSort(arr.slice(0, mid))
let arr2 = mergeSort(arr.slice(mid))
const ans = mergeAdd(arr1, arr2)
return ans
}
function mergeAdd(arr1, arr2) {
let l1 = 0, l2 = 0
let len1 = arr1.length, len2 = arr2.length
let ans = []
while (l1 < len1 && l2 < len2) {
if (arr1[l1] > arr2[l2]) {
ans.push(arr2[l2++])
} else {
ans.push(arr1[l1++])
}
}
return ans.concat(l1 === len1 ? arr2.slice(l2) : arr1.slice(l1))
}
mergeSort([2, 3, 1, 6, 8, 2, 0])
2. 快速排序
function quickSort(arr) {
if (arr.length <= 1) return arr
const equalArr = []
const higherArr = []
const lowerArr = []
const val = arr[0]
arr.forEach(item => {
if (item === val) equalArr.push(item)
else if(item > val) higherArr.push(item)
else lowerArr.push(item)
})
return quickSort(lowerArr).concat(equalArr).concat(quickSort(higherArr))
}
quickSort([2, 3, 1, 6, 8, 2, 0])
3. 判断两棵树是否相同
var isSubtree = function(root, subRoot) {
// corner case: if root is null/undefined we don't go deep
if (!root) return false
// first we deal root and subRoot
const result = subTree(root, subRoot)
if (result) return true
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot)
function subTree(root, subRoot) {
if (!root && !subRoot) return true
if (!root || !subRoot) return false
if (root.val !== subRoot.val) return false
return subTree(root.left, subRoot.left) && subTree(root.right, subRoot.right)
}
};
4. 求两个字符串相同的连续子串的最大长度及子串。
function findLongestCommonSubstring(s1, s2) {
const m = s1.length;
const n = s2.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
let maxLen = 0;
let maxI = 0;
let maxJ = 0;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s1[i-1] === s2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
if (dp[i][j] > maxLen) {
maxLen = dp[i][j];
maxI = i;
maxJ = j;
}
}
}
}
return maxLen > 0 ? s1.slice(maxI - maxLen, maxI) : '';
}
const s1 = 'ABABC';
const s2 = 'BABCA';
const [maxLen, substring] = findLongestCommonSubstring(s1, s2);
console.log(`Max length: ${maxLen}, substring: ${substring}`);
最长公共子序列
var longestCommonSubsequence = function(text1, text2) {
const l1 = text1.length, l2 = text2.length
const dp = new Array(l1 + 1).fill(0).map(() => new Array(l2 + 1).fill(0))
for (let i=1;i<=l1;i++) {
const c1 = text1[i - 1]
for (let j=1;j<=l2;j++) {
const c2 = text2[j-1]
if (c1 === c2) {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j], dp[i-1][j-1])
}
}
}
return dp[l1][l2]
};
5. 求乱序数组中出现频率前m的数。要求复杂度O(n),n为数组长度
function topMFrequentNumbers(nums, m) {
const freqMap = new Map();
// 统计数字出现的频率
for (let i = 0; i < nums.length; i++) {
if (freqMap.has(nums[i])) {
freqMap.set(nums[i], freqMap.get(nums[i]) + 1);
} else {
freqMap.set(nums[i], 1);
}
}
// 将Map转换为数组并按照频率排序
const freqArray = Array.from(freqMap);
freqArray.sort((a, b) => b[1] - a[1]);
// 选取出现频率前m的数字
const result = [];
for (let i = 0; i < m; i++) {
result.push(freqArray[i][0]);
}
return result;
}
函数topMFrequentNumbers
输入乱序数字数组和选取的前m个数字,输出出现频率前m的数字。
首先使用Map
来统计数组中每个数字出现的频率,得到freqMap
。然后将freqMap
转换为数组freqArray
,按照频率从高到低进行排序。最后选取前m个频率最高的数字,并返回结果。
6. combination sum III
var combinationSum3 = function(k, n) {
// 用 k 个数合成 n
let min = 0, max = 0
for (let i=0;i<k;i++) {
min += i + 1
max += 9 - i
}
console.log(min, max)
if (n < min || n > max) return []
const res = []
const DFS = (idx, val, sum, obj) => {
if (sum > n || idx > k) return
if (idx === k && sum === n) {
res.push(val)
}
for (let i=1;i<=9;i++) {
if (obj[i]) return
obj[i] = true
val.push(i)
sum += i
DFS(idx + 1, val.slice(), sum, obj)
sum -= i
val.pop()
obj[i] = undefined
}
}
DFS(0, [], 0, {})
return res
};
7. 实现0、1、2组成的字符串序列与0-9,a-z组成字符串序列的互转,规则自定,要求生成的0-9和a-z组成的序列尽可能短。
const BASE36_ENCODE_TABLE = '0123456789abcdefghijklmnopqrstuvwxyz';
function convertToBase36(str) {
let num = 0;
for (let i = 0; i < str.length; i++) {
const digit = Number(str[i]);
num += digit * Math.pow(3, str.length - i - 1);
}
return num.toString(36);
}
function convertToBase10(str) {
let num = 0;
for (let i = 0; i < str.length; i++) {
const digit = BASE36_ENCODE_TABLE.indexOf(str[i]);
num += digit * Math.pow(36, str.length - i - 1);
}
return num;
}
function convertToBase36String(str) {
const base10Number = convertToBase10(str);
let base36String = '';
while (base10Number > 0) {
const remainder = base10Number % BASE36_ENCODE_TABLE.length;
base36String = BASE36_ENCODE_TABLE[remainder] + base36String;
base10Number = Math.floor(base10Number / BASE36_ENCODE_TABLE.length);
}
return base36String;
}
function convertToDigitsAndLetters(str) {
const base10Number = Number(convertToBase10(str));
const digitsAndLetters = base10Number.toString(36);
return digitsAndLetters;
}
convertToBase36(str)
函数用于将0、1、2组成的字符串序列转换为base36编码的字符串序列
convertToBase10(str)
函数用于将base36编码的字符串序列转换为10进制数字
convertToBase36String(str)
函数用于将10进制数字转换为base36编码的字符串序列
convertToDigitsAndLetters(str)
函数用于将base36编码的字符串序列转换为0-9和a-z组成的字符串序列
可以根据需要进行调用。需要注意的是,由于JavaScript中Number类型的精度限制,处理超过Number.MAX_SAFE_INTEGER
的数据时可能会有误差,需要特别注意。
8. lc652
9. lc17变形
var letterCombinations = function(digits) {
if (!digits) return []
const map = ["", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
const res = []
const DFS = (n, s) => {
if (n === digits.length) {
res.push(s)
return
}
for (let i=0;i<map[Number(digits[n]) - 1].length;i++) {
let str = s
s += map[Number(digits[n]) - 1][i]
DFS(n + 1, s)
s = str
}
}
DFS(0, "")
return res
};
19. lc139
var wordBreak = function(s, wordDict) {
// 使用一个 dp 用来记录我们可以处理的范围
const len = s.length
const wordSet = new Set(wordDict)
const dp = new Array(len + 1).fill(false)
dp[0] = true
for (let i=1;i<=len;i++) {
for (let j=0;j<i;j++) {
if (dp[j] && wordSet.has(s.slice(j, i))) {
dp[i] = true
}
}
}
return dp[len]
};
20. leetcode.200 岛屿数量
var numIslands = function(grid) {
const dx = [0, 0, 1, -1]
const dy = [1, -1, 0, 0]
let ans = 0
const l1 = grid.length
const l2 = grid[0].length
const DFS = (x, y) => {
if (x >= l1 || y >= l2 || x < 0 || y < 0 || grid[x][y] === '0') return
grid[x][y] = '0'
for (let i=0;i<4;i++) {
DFS(x + dx[i], y + dy[i])
}
}
for (let i=0;i<l1;i++) {
for (let j=0;j<l2;j++) {
if (grid[i][j] === '1') {
ans ++
DFS(i, j)
}
}
}
return ans
};
21. leetcode.121 买卖股票只能一次
var maxProfit = function(prices) {
// first i need a minumize value and a maxVal and there a order
let prev = Infinity, ans = 0
for (let i=0;i<prices.length;i++) {
prev = Math.min(prev, prices[i])
ans = Math.max(ans, prices[i] - prev)
}
return ans
};
23. leetcode.122 买卖股票可以多次
var maxProfit = function(prices) {
const len = prices.length
// 这里注意值需要是 -Infinity
const dp = new Array(len + 1).fill(-Infinity).map(() => new Array(2).fill(-Infinity))
dp[0][0] = 0
prices.unshift(0)
// 这里需要能够相等
for (let i=1;i<=len;i++) {
dp[i][0] = Math.max(dp[i][0], dp[i-1][1] + prices[i])
dp[i][1] = Math.max(dp[i][1], dp[i-1][0] - prices[i])
for (let j=0;j<2;j++) {
dp[i][j] = Math.max(dp[i][j], dp[i-1][j])
}
}
return dp[len][0]
};
24. 每日温度
一个单调栈的题目
var dailyTemperatures = function(temperatures) {
const lineQueue = []
const res = []
for (let i=0;i<temperatures.length;i++) {
if(!lineQueue.length) {
lineQueue.push([temperatures[i], i])
} else {
while (lineQueue.length > 0 && temperatures[i] > lineQueue[lineQueue.length-1][0]) {
res[lineQueue[lineQueue.length-1][1]] = i - lineQueue[lineQueue.length-1][1]
lineQueue.pop()
}
lineQueue.push([temperatures[i], i])
}
}
while(lineQueue.length) {
res[lineQueue[lineQueue.length-1][1]] = 0
lineQueue.pop()
}
return res
};
25. topK
var topKFrequent = function(nums, k) {
const map = new Map()
for (let i=0;i<nums.length;i++) {
if (!map.has(nums[i])) {
map.set(nums[i], 1)
} else {
map.set(nums[i], map.get(nums[i]) + 1)
}
}
const arr = []
for(let a of map) arr.push(a)
const ans = []
arr.sort((a, b) => b[1] - a[1]).forEach((item, idx) => {
if (idx < k) {
ans.push(item[0])
}
})
return ans
};
转载自:https://juejin.cn/post/7232856799170445372