2020-02-24字节跳动面试总结

对于凌驾于命运之上的人来说,信心是命运的主宰。

二面,leader 面。被问到了最薄弱的正则表达式了…


前言

在复习了 一面 回答的不好的点后,没过多久就迎来了二面。

第一个问题依然是自我介绍。


说一下前端缓存吧

不在同一个坑中摔倒两次,之前已经介绍过了 cookie 和 storage 的区别,这次主要说一下 强缓存协商缓存

HTTP 缓存

HTTP 缓存都是从 第二次请求 开始的。

第一次请求资源时,服务器返回资源,并在 respone header 头中回传资源的缓存参数;

第二次请求时,浏览器判断这些请求参数,命中强缓存就直接 200,否则就把请求参数加到 request header 头中传给服务器,看是否命中协商缓存,命中则返回 304,否则服务器会返回新的资源。

强缓存的实现原理

强缓存是利用 HTTP 头中的 Expires (HTTP 1.0) 和 Cache-Control (HTTP 1.1) 两个字段来控制的。当请求“再次”发起时,浏览器来检测 ExpiresCache-Control 来判断目标资源是否符合强缓存,若符合则直接从缓存中获取资源,不会再与服务端发生通信。

强缓存

Expires

在响应消息头指定一个 绝对的过期日期,当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。

不过由于我们 可以去更改客户端的时间,因此可以更改缓存命中的结果。因此这种方式很快在后来的 HTTP 1.1 版本中被抛弃了。故现在大多数使用 Cache-Control

1
Expires: Sat, 02 Nov 2019 01:40:44 GMT

Cache-Control

1
Cache-Control: max-age=31536000

max-age= 表示缓存内容将在 t 秒后失效(以秒为单位),在该时间内,客户端不需要向服务器发送请求。

Expires 的区别就是前者是绝对时间,而后者是相对时间。如果 Cache-ControlExpires 同时存在,那么优先考虑 Cache-Control

Cache-Control 的其他参数

no-cache 可以在本地和代理服务器缓存,但是这个缓存需要服务器验证才可以使用,即直接进入协商缓存阶段。

no-store 真正意义上的“不要缓存”,不进行任何形式的缓存,每次都从服务器获取。

private 设置客户端可以缓存,代理服务器不能缓存。这些响应通常只为单个用户缓存,因此不允许任何中间缓存对其进行缓存,例如,用户的浏览器可以缓存包含用户私人信息的 HTML 网页,但 CDN 不能缓存;

public 设置客户端和代理服务器都可以缓存;

must-revalidate 告诉缓存,在事先没有跟原始服务器进行再验证的情况下,不能提供这个对象的陈旧副本,缓存仍然可以随意提供新鲜的副本。如果在缓存进行must-revalidate新鲜度检查时,原始服务器不可用,缓存就必须返回一条504错误;

s-maxage 是针对代理服务器的缓存时间。

最佳 Cache-Control 策略

协商缓存的实现原理

协商缓存,也叫对比缓存。

本地缓存过期了并不意味着他和原始服务器目前处于活跃状态的文档有实际的区别,这只是意味着到了要进行核对的时间了,这种情况被称为协商缓存,说明 缓存需要询问原始服务器是否发生变化

  • 如果再验证显示内容发生了变化,缓存会获取一份新的文档副本,并将其存储在旧文档的位置上,然后将文档发送给客户端;
  • 如果再验证内容没有发生变化,缓存只需要获取新的首部,包括一个 新的过期日期,并对缓存中的首部进行更新就行了。

协商缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省 是它的优化点。

协商缓存是可以和强制缓存一起使用的,作为在强制缓存失效后的一种后备方案。实际项目中他们也的确经常一同出现。

协商缓存

Last-Modified & If-Modified-Since

Last-Modified 也是个时间戳,它会在我们首次请求的时候随着 Response Headers 返回,告诉浏览器我们最后一次修改时间是 19 年六月,随后我们每次请求都会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次请求时 Last-Modified 的值。

具体流程如下:

  1. 客户端第一次向服务器发起请求,服务器将最后的修改日期 (Last-Modified) 附加到所提供的资源上去
  2. 当再一次请求资源时,如果没有命中强缓存,在执行再验证时,会包含一个 If-Modifed-Since 首部,值为资源的 Last-Modified 日期,询问服务器该资源自从这个 Last-Modified 日期之后有没有被修改过;
  3. 如果内容被修改了,服务器回送新的资源,返回 200 状态码和最新的修改日期;
  4. 如果内容没有被修改,会返回一个 304 Not Modified ,告诉浏览器就用缓存就好啦。
但是:
  1. 如果我们编辑了文件,但是并没有对文件内容做修改,Last-Modified 时间戳也会变,不该重新请求的时候也去重新请求了。
  2. 当我们修改文件的手速巨快,由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的,该重新请求的时候也不会重新请求了。

ETag & If-None-Match

EtagLast-Modified 类似,当我们首次请求时,会在响应头里获取到一个最初的标识字符串,这个是服务器算出的 hash 值,比 Last-modified 更准确。

因为有些情况下仅使用最后修改日期进行再验证是不够的:

  1. 有些文档有可能会被周期性的重写(比如: 从一个后台进程中写入),但实际上包含的数据常常是一样的,尽管内容没有变化,但修改日期会发生变化;
  2. 有些文档可能被修改了,但所做修改并不重要。不需要让世界范围内的缓存都重装数据(比如填写注释);

有些服务器无法准确判定其页面的最后修改日期。

有些服务器提供的文档会在毫秒间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了;

因此 HTTP 允许用户对被称为实体标签的 ETag 的版本标识符进行比较。实体标签是附加到文档上的任意标签(引用字符串),服务器生成并返回的随机令牌通常是文件内容的哈希值或其他指纹。客户端不需要指纹是如何生成的,只需在下一次请求时将其发送至服务器。如果指纹仍然相同,则表示资源未发生变化,您就可以跳过下载。

流程和 If-Modified-Since 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件哈希值,把 If-Modified-Since 变成了 If-None-Match。服务器对比已缓存标签与服务器文档中的标签是否有所不同,相同返回 304, 不相同返回新资源和 200。


什么是 CORS

跨域资源共享 (CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。

当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。


CORS 时为什么会发送两次请求

浏览器将 CORS 请求分成两类:简单请求(simple request)和 非简单请求(not-so-simple request)。

只要同时满足以下三大条件,就属于简单请求:

  1. 请求方法是以下三种方法之一:HEAD、GET、POST;
  2. HTTP 的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID;
  3. Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不同时满足上面三个条件的,就属于非简单请求。

当请求存在跨域资源共享 (CORS) 并且是非简单请求,就会触发 CORS 的预检请求 (preflight),预检请求用的请求方法是 OPTIONS。

解决多次请求方法

实际开发过程中,后台采用 token 检验机制,前台发送请求必须将 token 放到 Request Header 中,那么就需要传输自定义 Header 信息、或则请求头中的 Content-Type="application/json",就会形成非简单请求。

预检请求以 OPTIONS 形式发送,当中同样包含域,并且还包含了两项 CORS 特有的内容:

  1. Access-Control-Request-Method:该项内容是实际请求的种类,可以是 GET、POST 之类的简单请求,也可以是 PUT、DELETE 等等;
  2. Access-Control-Request-Headers:该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。

显而易见,这个预检请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。

一旦预回应如期而至,所请求的权限也都已满足,才会发出真实请求,携带真实数据。

同时,面对这种跨域预检机制造成的多次请求问题,我们可以在后台设置 Access-Control-Max-Age 来控制浏览器在多长时间内(单位s)无需在请求时发送预检请求,从而减少不必要的预检请求。


什么是 postMessage()

以下资料来源于 MDN Web 文档

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443 为 https的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值) 时,这两个脚本才能相互通信。

window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

  1. 利用 postMessage() 不能和服务端交换数据,只能在两个窗口(iframe)之间交换数据;
  2. 两个窗口能通信的前提是,一个窗口以 iframe 的形式存在于另一个窗口,或者一个窗口是从另一个窗口通过 window.open() 或者超链接的形式打开的(同样可以用 window.opener 获取源窗口)。

302 状态是什么,收到 302 之后怎么办

302 重定向又称之为 302 代表暂时性转移 (Temporarily Moved)。也被认为是暂时重定向 (temporary redirect)。

详细来说,301 和 302 状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会 自动跳转到一个新的 URL 地址,这个地址可以从响应的 Location 首部中获取(用户看到的效果就是他输入的地址 A 瞬间变成了另一个地址 B),这是它们的共同点。

他们的不同在于,301 表示旧地址 A 的资源已经被 永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;

302 表示旧地址 A 的资源还在(仍然可以访问),这个重定向只是临时地从旧地址 A 跳转到地址 B,搜索引擎会抓取新的内容而保存旧的网址。

为什么 302 重定向和网址劫持有关系

从网址 A 做一个 302 重定向到网址 B 时,主机服务器的隐含意思是 网址 A 随时有可能改主意,重新显示本身的内容或转向其他的地方。大部分的搜索引擎在大部分情况下,当收到 302 重定向时,一般只要去抓取目标网址就可以了,也就是说网址 B。如果搜索引擎在遇到 302 转向时,百分之百的都抓取目标网址 B 的话,就不用担心网址 URL 劫持了。

问题就在于,有的时候搜索引擎,尤其是 Google,并不能总是抓取目标网址。

比如说,有的时候 A 网址很短,但是它做了一个 302 重定向到 B 网址,而 B 网址是一个很长的乱七八糟的 URL 网址,甚至还有可能包含一些问号之类的参数。很自然的,A 网址更加用户友好,而 B 网址既难看,又不用户友好。这时 Google 很有可能会仍然显示网址 A。由于搜索引擎排名算法只是程序而不是人,在遇到 302 重定向的时候,并不能像人一样的去准确判定哪一个网址更适当,这就造成了网址 URL 劫持的可能性。

也就是说,一个不道德的人在他自己的网址 A 做一个 302 重定向到你的网址 B,出于某种原因, Google 搜索结果所显示的仍然是网址 A,但是所用的网页内容却是你的网址 B 上的内容,这种情况就叫做网址 URL 劫持。你辛辛苦苦所写的内容就这样被别人偷走了。


编程 — 最大 / 最小子序和

简单 DP,最大最小思路完全一致,唯一要注意的是需要写出 O(n) 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const func = array => {
let max = -2147483648;
let min = 2147483647;
let maxSum = 0;
let minSum = 0;
for (let i = 0; i < array.length; i++) {
maxSum += array[i];
minSum += array[i];
max = Math.max(max, maxSum);
min = Math.min(min, minSum);
if (maxSum < 0) maxSum = 0;
if (minSum > 0) minSum = 0;
}
console.log(max);
console.log(min);
};

就用最大子序和来说明吧,为什么可以优化至 O(n):

  1. 如果暴力求出所有子序列,再求其和,会浪费掉很多时间;
  2. 如果当前的数是 x,且 x < 0,那么 x + y 一定小于 y;
  3. 子序列是连续的;

那么可以遍历一遍数组,将经过的值求和,记录下来最大值。且,如果当前的总和已经小于 0 了,那么完全可以 抛弃 这些总和,毕竟求的是 最大值,即当前的数 x 和它的下一个数 y,绝对不可能同时出现在答案中。

最小子序和同理,颠倒过来就行了,但大致思路原封不动。

面试的时候,被告知这道题我 WA 了,当时我想因为这道题是我大一的时候刷的,可能有些细节就是记不清了吧。时间紧,就没有去仔细回顾。但事后用完全相同的思路在 Leetcode 上交了一发 AC 了,现在满头黑线不知道什么鬼???(在做第三道编程时,被告知的 WA,当时脑子里都是正则表达式根本没回头去仔细看为什么 WA)

正则表达式专场

(呜呜呜呜呜…我的正则是真的一丢丢都没复习,用的也不是很多)

这里先贴上答案,这两天把正则重学了一遍,下次开一篇正则的 Blog --- 2020.02.28
  1. 可以验证所有邮箱的正则
1
2
3
const val = 'euphoria.wqh@gmail.com';
const regExp = /^(?:\w+\.?)+@[a-zA-Z]{2,10}(?:\.[a-z]{2,4}){1,3}/;
console.log(regExp.tst(val));
  1. 可以拆分出货币形式的正则
1
2
3
4
5
6
const val = "-12345678.90";
console.log(
val.replace(/^(-?)(\d+)((\.\d+)?)$/, (s, s1, s2, s3) => {
return s1 + s2.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,') + s3;
})
);
  1. 可以区别单词的正则
1
2
3
const val = 'My name is Euphoria, I am 20 years old.';
const regExp = /[a-zA-Z0-9]+(?=\b)/g;
console.log(val.match(regExp));
  1. 可以格式化日期的正则(这个感觉不简单,暂时没有很好的解决方法)

布局方式

这个只记得被问到了圣杯布局的变体,因为个人除了写移动端真的不常用 flex 布局,基本要求是这样的:

第一反应想到的就是:

  1. 这是个单页面,htmlbodyapp 宽高 100%
  2. headerfooter 绝对定位到上面和下面;
  3. 中间的容器宽高 100%
  4. 因为 headerfooter 脱离标准流了,中间的容器上下加 200px 的内边距;
  5. 中间左侧可以定位也可以浮动,宽度定死 200px
  6. 剩下的那一块,宽 100%,左侧 200px 内边距。

但是面试官感觉不是很开心,被告知希望用 flex 做(唉):

  1. 这是个单页面,htmlbodyapp 宽高 100%
  2. app 弹性盒,主轴方向竖直,headerfooter 定死,中间加一个 flex:1
  3. 中间的盒子也是弹性盒,主轴方向水平(默认);
  4. 左侧定死,右侧 flex:1 结束。
因为面试官是位 Leader,当时好像有挺着急的会议要开,被告知希望用 `flex` 做之后就结束了面试...

作业?

emmm 对,让我下去后照着 淘宝主页 抄一份,没用任何 UI 库,也权当是一次挑战。

讲真适配移动端把我弄了好一阵子。

发现一个有趣的事情,首页导航栏左侧的菜单栏,鼠标划入到 <li> 后,过了一阵子右侧的悬浮盒子才显示出来(假设之前并没有显示),并且 <li> 的过渡也有延迟,这里有两个细节(个人觉得):

<li> 的激活延迟

这些 <li> 的鼠标划入事件不是简单的 transition 就解决了,而是需要加上 mouseentermouseleave 事件,配合定时器延迟触发。

(个人觉得是右侧悬浮盒子的频繁更新渲染代价太高了,如果用户使劲在 <li> 上面划来划去要出事情)

最后重搞了个类,就像 Vue 中的 v-enter-active 那样给 <li> 加上,同时记录下这个标签中执行的定时器序号,当鼠标移出的时候也要删除这个定时器(因为这时可能还没有执行到)。

关于 opacitydisplay 一起使用的问题

观察淘宝主页可以发现,当鼠标移动到菜单栏的 <li> 上面时,右侧悬浮盒子是淡入出现的,但是在没有出现的时候也不会盖住下面的轮播图。显然消失的时候是 display: none,出现的时候是 display: block

然后可以配合 opacity 再加上 transition: opacity 可以完美解决,即:渐变出现。

真的是这样吗?

我们可以发现,当一起使用 displayopacity 时,不论如何这个盒子都是瞬间出现的,即使写成 transition: opacity 1200s 也达不到想要的效果。

内部的小机制

设置 display 不仅会破坏掉过渡,同时也会破坏掉 CSS 动画。

关于 display 为何会造成破坏,目前本人仍未找到相关资料来证明其内部机制,我的个人理解是,display 的操作会触发浏览器的 reflow 操作而 transition 支持的效果只是触发浏览器的 repaint 操作,但如果我们通过 visibility 属性来控制显示与隐藏,则不会破坏 transition 的效果。

所以,我暂时这么认为:reflowrepaint 的混合会破坏 transition 的动画效果!


总结

感觉没了,虽然大家都说一面 90 min,二面 70 min 还是 Leader 面的。但是真的被 Leader 面傻了(不知道怎么,就是贼慌),到最后竟然开始着急了(就那种,冒汗 + 心慌)。

已经做好 GG 的准备了,个人认真学习前端的时间讲真不长(毕竟是半道插进来的),2019 年 10 月的时候还压根没想过要走前端这条路(无奈)。

ACMer 标配结语,菜是原罪。

-------------本文结束 Euphoria 在此感谢您的阅读-------------

本文标题:2020-02-24字节跳动面试总结

文章作者:王钦弘

发布时间:2020年02月26日 - 16:35

最后更新:2020年02月28日 - 23:03

原始链接:https://www.wqh4u.cn/2020/02/26/2020-02-24%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E9%9D%A2%E8%AF%95%E6%80%BB%E7%BB%93/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

您的支持将鼓励 Euphoria 继续创作!
(如果你还是学生请千万不要打赏!留点钱在学习上啊!)