2023春招/暑期美团面经大总结

1,222 阅读20分钟

2023春招/暑期美团面经大总结

Hello~ Everyone, 我在牛客上收集了一些团子的面经,统一做了一个总结。

如果有帮到你的话,能帮我点个赞么~

HTML 部分

1. 你对 SPA 的理解

单页面应用的核心思想是不同于传统的多页面应用,整个应用只有一个页面。页面初始化时会加载应用所需的所有资源,之后所有的内容切换、动态更新、交互等操作都在这个页面中进行。

在单页面应用中,网址的变化并不会导致页面的刷新或跳转。这是因为单页面应用会通过JS监听URL的变化,并响应变化而进行DOM操作,完成新内容的展示和旧内容的隐藏等更新操作。这种方式提高了应用的性能和用户体验,并避免了页面切换时的视觉闪烁。

虽然单页面应用相对于传统应用有许多优点,如响应速度快、用户体验好、能够进行离线浏览等等,但是它也有一些缺点,比如SEO问题、对浏览器前进/后退功能的兼容性等等。因此在应用场景上需要根据具体问题进行评估和选择。

2. 怎么做SEO优化?

  1. 使用服务端渲染

对于一些需要SEO优化的SPA应用,可以考虑在服务端进行渲染,把生成的HTML代码发送给客户端浏览器,这样就可以消除搜 索引擎对于渲染JavaScript生成的DOM的识别问题,也可以提高应用的性能和搜索引擎的可识别性。

  1. 合理的meta标签与title标签

在单页面应用中设置合理的meta标签和title标签对SEO也是非常重要的,这些标签需要详细记录页面内容的关键信息,包括关键字、页面描述等等。这样可以方便搜索引擎对页面进行分析和识别。

3. html语义化标签,为什么用语义化标签,

  1. HTML 语意化有啥
    • article
    • section
    • header
    • footer
    • nav
    • sider
  1. 好处

网页加载慢导致 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的属性以及相对谁定位

  1. relative:元素的定位永远是相对于元素自身位置的,和其他元素没关系,也不会影响其他元素。
  2. fixed:元素定位是相对于 window(或者 iframe)边界的,和其他元素没有关系,但是它具有破坏性
  3. absolute:如果 absolute 设置了 top、left,浏览器会递归查找该元素的所有父元素,如果找到了一个设置了 position: relative/absolute/fixed 的元素,就以该元素为基准定位,如果没找到,就以浏览器边界定位。
  4. static:默认值,没有定位,元素出现在正常的文档流中,会忽略 top,bottom,left,right 或者 z-index 声明,块级元素从上往下纵向排布,行级元素从左向右排列。
  5. inherit:规定从父元素继承 position 属性的值。

4. css选择器如何工作的

  • 内联样式 ==> 1000
  • id 选择器 ==> 100
  • 类选择器、伪类选择器、属性选择器 ==> 10
  • 标签选择器、伪元素选择器 ==> 1
  • 相邻兄弟选择器、子选择器、后代选择器、通配符选择器 ==> 0

5. display 的属性值

  • none ==> 元素不显示,并且会从文档流中移除

  • block ==> 块元素。默认宽度为父元素宽度,可设置宽高,换行显示。

  • inline ==> 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。

  • inline-block ==> 默认宽度为内容宽度,可以设置宽高,同行显示。

  • list-item ==> 像块类型元素一样显示,并添加样式列表标记。

  • table ==> 次元素会作为块级表格来表示。

  • inherit ==> 规定应该从父元素继承 display 属性的值

6. 清除浮动的方式有哪些

  1. 给父元素增加高度 ==> 可以解决问题,但不够彻底
  2. clear: both

在父元素的子元素最后添加一个块级元素,设置 style="clear: both";必须是一个块级元素,否则没法撑开父元素高度。

  1. 伪元素清除浮动
  2. 使用 BFC 布局

添加 overflow: hidden

  1. 使用 br 标签

<br clear="all">

7. CSS 如何实现垂直居中

  1. relative + absolute
.wrapper {
    position: relative;
}
.box {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}
  1. flex
.wp {
  display: flex;
  justify-content: center;
  align-items: center;
}
  1. grid
.wp {
    display: grid
}
.ot {
    justify-content: center;
    align-items: center;
}
  1. absolute + transform
.wp {
  position: relative;
}
.box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%)
}
  1. 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 工作时间片的一个描述:

  1. 进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
  2. 线程是进程中的更小单位,描述了执行一段指令所需的时间。

进程和线程的特点

  1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
  2. 线程之间共享进程的数据。
  3. 当一个进程关闭之后,操作系统会回收进程所占用的内存。当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
  4. **进程之间的内容相互隔离。**进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个程序如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要用于进程间通信的机制了。

进程是资源分配的最小单位,线程是 CPU 调度的最小单位。

进程和线程的区别

  • 进程可以看作独立应用,线程不能。
  • 资源:进程是 CPU 资源分配的最小单位(是能拥有资源和独立进行的最小单位);线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
  • 通信方面:线程间可以直接共享同一进程的资源,而进程通信需要借助进程间通信。
  • 调度:进程切换比线程切换的开销。线程是 CPU 调度的基本单位,同一进程内的线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器的内容,开销较小。

4. 浏览器中有哪些进程线程

image.png

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 更新后才执行。}

「使用场景」:

  1. 想要操作 基于最新数据生成的 DOM 时,就将这个操作放在 nextTick 的回调中;「created 的时候想要提前获取 DOM 树」

1.3nextTick 的实现原理

将传入的回调函数包装成异步任务,异步任务又分为微任务和宏任务,为了尽快执行所以优先选择微任务;

  • nextTick 提供了四种异步方法: Promise.then、MutationObserver、setImmediate、setTimeout(fn, 0)
  • setImmediate 有兼容性的问题,必须要 IE10 以上才支持。
  • setTimeout 主要是有时延问题,大概是 4ms 左右。

2. Vue和React的区别

  1. 从写法上来说Vue 倾向于提供自己的单文件组件(.vue)格式,以后端开发者和前端开发者共同开发;而 React 通过 JSX,可以在 JavaScript 中编写组件,也可以在外部文件中定义组件。
  2. 数据绑定:Vue 和 React 都通过虚拟 DOM 实现数据绑定和更新。在 Vue 中,通过其双向数据绑定技术,在模板中直接渲染数据。而在 React 中,由于其单向数据流,需要通过一些扩展库来实现类似的效果。

3. DIFF 算法

juejin.cn/post/722966…

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 加密流程

  1. 当客户端向服务器端发起连接请求时,服务器会发送包含自己公钥的数字证书给客户端。
  2. 客户端收到数字证书后会进行验证,确保证书是由可信的证书颁发机构颁发的,并且证书没有被篡改。这就保证了服务器端的身份。
  3. 如果验证成功,客户端会生成一个随机密钥,并使用公钥加密这个密钥,再发送给服务器端。
  4. 服务器端用自己的私钥解密客户端发来的消息,同时使用解密后的随机密钥对数据进行加密。
  5. 客户端通过使用刚才协商的随机密钥解密服务器端发来的数据。

3. http 和 https 区别

  1. 安全性

HTTP 是一种明文协议,即数据传输过程是不加密的,数据容易被第三方窃取或篡改,不具备安全性。而 HTTPS 则是一种加密的协议,可以确保数据传输的安全性,通过 SSL/TLS 加密协议将数据进行加密传输,从而大大降低了数据被盗用或篡改的风险。

  1. 监听端口

HTTP 协议的默认端口为 80,而 HTTPS 协议的默认端口为 443。因此,当 Web 应用程序使用 HTTPS 协议进行数据传输时,需要申请数字证书,对服务器进行认证,确保用户可以安全地访问应用程序。

  1. 抓包和缓存方式

HTTP 协议的数据传输是明文的,可以被第三方窃取或篡改,因此容易被抓包工具获取信息。而 HTTPS 协议的数据传输通过 SSL/TLS 加密协议进行加密传输,数据内容不易被窃取或篡改,安全性较高。同时,HTTPS 协议不允许缓存,以保证数据的一致性和安全性。

综上所述,HTTP 和 HTTPS 在安全性、监听端口、抓包和缓存方式等方面都有明显差异。建议在保护用户隐私、保护应用程序数据安全等方面,使用 HTTPS 协议进行数据传输。

4. OSI模型,以及各层都能有什么功能

image.png

image.png

5. 三次握手四次挥手

image.png 刚开始客户端处于 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 报文段可以携带数据,不携带数据则不消耗序号。

image.png

  • 第一次挥手:客户端会发送一个 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 的区别

image.png

优化部分

1. 说一下项目中的常见首屏优化

  • 代码优化:减少不必要的代码和资源加载,压缩和合并 JavaScript、CSS 和 HTML 文件,以减少文件大小和网络请求次数。
  • 图片优化:使用适当的图片格式(如 WebP),压缩图片大小,使用懒加载或渐进式加载,以减少页面加载时间。
  • 异步加载:延迟加载非关键性 JavaScript 和 CSS 文件,将它们放在页面底部,以便首屏内容能够更快地加载和显示。
  • 骨架屏:在页面加载过程中,显示一个简单的骨架屏或占位符,给用户一个加载进度的反馈,并提前展示页面布局。
  • 缓存策略:设置适当的缓存策略,利用浏览器缓存,减少重复的网络请求,加快页面加载速度。
  • 数据优化:根据页面的实际需求,减少请求的数据量,使用分页加载或滚动加载等方式,避免一次性加载大量数据。
  • 服务端渲染 (SSR):对于需要搜索引擎优化或具有复杂的首屏内容的项目,考虑使用服务端渲染,以提前生成并发送完整的 HTML 内容。
  • 延迟加载或按需加载组件:将页面中的某些组件延迟加载或按需加载,只在需要时加载,减少初始页面的加载负担。
  • CDN 加速:使用内容分发网络(CDN)来加速静态资源的传输,让用户从离他们更近的服务器获取资源,提高加载速度。
  • 压缩和优化字体:减少使用的字体种类和文件大小,使用字体子集和压缩字体文件,以减少字体的加载时间。

2. 说一下图片懒加载是如何实现的

  • element.offsetTop - document.documentElement.scrollTop <= window.innerHeight
  • getBoundingClientRect
  • IntersectionObserver

懒加载是一种常见的应用场景,它主要用于提高页面加载速度和性能的优化。常见的懒加载有两种实现方法:使用 getBoundingClientRect 和使用 IntersectionObserver

  1. 使用 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 的值,从而触发图片加载。

  1. 使用 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的区别

  1. Loader

Loader 是 Webpack 实现文件逐个进行处理的前置条件,它将文件作为输入,将处理后的文件作为输出。可以理解为一个转换器,用于对模块的源代码进行转换处理。

例如,在构建过程中,如果需要使用 SCSS 编写样式,我们就需要使用 SCSS Loader 将 SCSS 文件进行编译处理,并将其转换为 CSS 文件,方便 Webpack 进一步进行处理和打包。

在使用 Loader 的时候,需要在 Webpack 配置文件中进行配置,它的配置格式如下:

module: {
  rules: [
    {
      test: /\.scss$/,
      use: [
        'style-loader',
        'css-loader',
        'sass-loader'
      ]
    }
  ]
}
  1. 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 技术时,注意以下几点:

  1. 需要在 webpack 配置文件中添加 hot 选项进行开启 HMR 的功能。

  2. HMR 支持的模块类型只有部分,如处理样式的 CSS Loader、处理 React 的 Babel Loader 等,对于其他类型的模块可能需要进行特殊的配置。

  3. 如果使用了 React,则需要借助 react-hot-loader 进行配置,实现组件级别的热刷新。

  4. 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 个值:

  1. Strict: 表示完全禁止第三方站点访问 Cookie,即跨站点时,只有在当前网页的 URL 与请求目标一致时才可以访问 Cookie。
  2. Lax: 表示允许一些情况下的第三方站点访问 Cookie,例如,在通过链接的方式跳转到下一个网页时,只要该链接是 GET 请求,并具有良好的用户体验,也可以允许第三方站点进行访问。
  3. None: 表示允许所有第三方站点访问 Cookie,这种情况下必须是使用 HTTPS 协议来进行请求。

当前主流的浏览器都已经对 Cookie 的 SameSite 进行了支持,所以在开发使用 Cookie 的时候,建议在数据隐私保护和防止 CSRF 攻击的层面,合理配置 SameSite 属性,提高站点安全性能。

2. 跨域是浏览器还是服务端限制的

跨域是由浏览器实现的安全策略来限制的,是一种浏览器的安全行为。

同源策略是浏览器实现的安全策略之一,它指的是一种安全机制,它要求在同一域名或主机名下的网页才能够互相进行数据交互,否则就会出现跨域问题。同源策略是由浏览器实现的,而不是由服务端实现的。

Web 应用中,浏览器和服务器之间的数据交互通常使用 HTTP 协议,而 HTTP 协议是一种基于请求和响应的模式,对于浏览器发起的任何请求,服务器都会返回相应的数据,但是服务器并不会检查该请求是否来自同一源,而是由浏览器在发起请求时进行同源检查。如果不满足同源规则,浏览器就不会将数据返回给 JavaScript,从而阻止了跨域问题。

总的来说,跨域是由浏览器实现的同源策略来限制的,是浏览器的内置机制。解决跨域问题的方法通常需要配合服务端的相关技术,如 CORS、JSONP、代理等,以避免同源策略的限制问题。

3. 跨域报错之后前端怎么根据报错信息去分析是哪里出了问题?

跨域报错通常会出现以下两种形式:

  1. 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.
  1. 网络请求报错信息

在浏览器的开发者工具中,可以查看到当前请求的状态以及 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
};