# 2020年裸辞后面试记录笔记

在裸辞了四个月后,山月在元旦前找工作,面试了几个大小公司,记录下面试题。一起加找工作的也可以加个微信交流: shanyue94

# 01 如何实现选中复制的功能

更多描述: 在一些博客系统,如掘金的博客中,可以复制代码,它是如何实现的

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 01 如何实现选中复制的功能 (opens new window)

它一般可以使用第三方库 clipboard.js (opens new window) 来实现,源码很简单,可以读一读

主要有两个要点

  1. 选中: Selection API
  2. 复制: document.execCommand

# 选中: Selection API

选中主要利用了 Selection API (opens new window)

选中的代码如下

const selection = window.getSelection();
const range = document.createRange();

range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);

selectedText = selection.toString();

取消选中的代码如下

window.getSelection().removeAllRanges();

它有现成的第三方库可以使用: select.js (opens new window)

# 复制: execCommand

复制就比较简单了,execCommand

document.exec('copy')

# 02 随着 http2 的发展,前端性能优化中的哪些传统方案可以被替代

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 02 随着 http2 的发展,前端性能优化中的哪些传统方案可以被替代 (opens new window)

  1. 雪碧图
  2. 资源文件合并

# 03 既然 http 是无状态协议,那它是如何保持登录状态

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 03 既然 http 是无状态协议,那它是如何保持登录状态 (opens new window)

通过 cookie 或者 Authorization header 来传递凭证,在服务端进行认证

# 04 http 响应头中的 Date 与 Last-Modified 有什么不同,网站部署时需要注意什么

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 04 http 响应头中的 Date 与 Last-Modified 有什么不同,网站部署时需要注意什么 (opens new window)

  • Date: 报文在源服务器的产生时间,由此可查看报文已缓存了多久时间
  • Last-Modified: 源服务器上资源的上次修改时间

LM-Factor 与它俩有关。

简而言之,一个静态资源没有设置 Cache-Control 时会以这两个响应头来设置强制缓存时间:(Date - LastModified) * n,而非直接进行协商缓存。在涉及到 CDN 时,表现更为明显,体现在更新代码部署后,界面没有更新。

# 05 关于 JSON,以下代码输出什么

更多描述:

const obj = {
  a: 3,
  b: 4,
  c: null,
  d: undefined,
  get e () {}
}

console.log(JSON.stringify(obj))

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 05 关于 JSON,以下代码输出什么 (opens new window)

const obj = {
  a: 3,
  b: 4,
  c: null,
  d: undefined,
  get e () {}
}

console.log(JSON.stringify(obj))

输出什么?

{"a":3,"b":4,"c":null}

对其中的 undefinedfunction 将在 JSON.stringify 时会忽略掉

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 06 如何删除一个 cookie (opens new window)

通过把该 cookie 的过期时间改为过去时即可删除成功,具体操作的话可以通过操作两个字段来完成

  1. max-age: 将要过期的最大秒数,设置为 -1 即可删除
  2. expires: 将要过期的绝对时间,存储到 cookies 中需要通过 date.toUTCString() 处理,设置为过期时间即可删除

很明显,max-age 更为简单

// max-age 设置为 -1 即可成功
document.cookie = 'a=3; max-age=-1'
> document.cookie
< ""

> document.cookie = 'a=3'
< "a=3"

> document.cookie
< "a=3"

// 把该字段的 max-age 设置为 -1
> document.cookie = 'a=3; max-age=-1'
< "a=3; max-age=-1"

// 删除成功
> document.cookie
< ""

# 07 有没有使用过 css variable,它解决了哪些问题

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 07 有没有使用过 css variable,它解决了哪些问题 (opens new window)

@Cicelychen 赞!而且它与 less/sass 相比,更加灵活,因为它很容易通过 JS 来控制。根据它来做主题切换简直得心应手。

# 08 no-cache 与 no-store 的区别是什么

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 08 no-cache 与 no-store 的区别是什么 (opens new window)

no-cacheno-store 用作控制缓存,被服务器通过响应头 Cache-Control 传递给客户端

# no-store

永远都不要在客户端存储资源,每次永远都要从原始服务器获取资源

# no-cache

可以在客户端存储资源,但每次都必须去服务器做新鲜度校验,来决定从服务器获取最新资源 (200) 还是从客户端读取缓存 (304),即所谓的协商缓存

当服务器资源返回 304 时与那些 HTTP 响应头有关 (opens new window)

一般情况下对于 index.html 或者现代构建环境下不加 hash 的静态资源都需要设置 Cache-Control: no-cache,用来强制每次在服务器端的新鲜度校验。

相当于以下响应头

Cache-Control: max-age=0, must-revalidate

# 相关问题

# 09 如何判断当前环境时移动端还是PC端

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 09 如何判断当前环境时移动端还是PC端 (opens new window)

判断 navigator.userAgent,对于 Android/iPhone 可以匹配以下正则

const appleIphone = /iPhone/i;
const appleIpod = /iPod/i;
const appleTablet = /iPad/i;
const androidPhone = /\bAndroid(?:.+)Mobile\b/i; // Match 'Android' AND 'Mobile'
const androidTablet = /Android/i;

当然,不要重复造轮子,推荐一个库: https://github.com/kaimallea/isMobile (opens new window)

import isMobile from 'ismobilejs'

const mobile = isMobile()

# 10 http2 中的首部压缩的实现原理是什么

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 10 http2 中的首部压缩的实现原理是什么 (opens new window)

HPACK 协议,其中原理是哈夫曼编码索引表 (看来常用的数据结构及算法要有所了解),以下内容摘自 谷歌开发者文档:http2 (opens new window)

每个 HTTP 传输都承载一组标头,这些标头说明了传输的资源及其属性。 在 HTTP/1.x 中,此元数据始终以纯文本形式,通常会给每个传输增加 500–800 字节的开销。如果使用 HTTP Cookie,增加的开销有时会达到上千字节。 (请参阅测量和控制协议开销。) 为了减少此开销和提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据,这种格式采用两种简单但是强大的技术:

  1. 这种格式支持通过静态霍夫曼代码对传输的标头字段进行编码,从而减小了各个传输的大小。
  2. 这种格式要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。

实践出真知,通过 wireshark 抓包分析 http2 的报文对理解 http2 收益颇多。

你要有知识,你就得参加变革现实的实践。你要知道梨子的滋味,你就得变革梨子,亲口吃一吃。你要知道原子的组织同性质,你就得实行物理学和化学的实验,变革原子的情况。你要知道革命的理论和方法,你就得参加革命。

以下是关于抓包信息的截图

http2 通过 Settings 帧设置 header table size,进行首部压缩

HTTP2 Settings

http2 通过首部压缩后,:method 伪标头在索引表中的 Index 为 2

HTTP2 Index

关于常用的标头会存储在静态索引表固定的位置,详见 https://httpwg.org/specs/rfc7541.html#static.table.definition (opens new window)

# 11 简述 node/v8 中的垃圾回收机制

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 11 简述 node/v8 中的垃圾回收机制 (opens new window)

v8 中的垃圾回收机制分为三种

  1. Scavenge,工作在新生代,把 from space 中的存活对象移至 to space
  2. Mark-Sweep,标记清除。新生代的某些对象由于过度活跃会被移至老生代,此时对老生代中活对象进行标记,并清理死对象
  3. Mark-Compact,标记整理。

# 相关链接

  1. 主流的垃圾回收机制都有哪些? (opens new window)
  2. 各种编程语言的实现都采用了哪些垃圾回收算法 (opens new window)

# 12 如何删除项目中没有使用到的 package

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 12 如何删除项目中没有使用到的 package (opens new window)

可以采用 depcheck (opens new window) 来完成这件事

$ npm install depcheck -g

$ depcheck
Unused dependencies
* underscore
Unused devDependencies
* jasmine
Missing dependencies
* lodash

# 13 简述下 css specificity

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 13 简述下 css specificity (opens new window)

css specificity 即 css 中关于选择器的权重,以下三种类型的选择器依次下降

  1. id 选择器,如 #app
  2. classattributepseudo-classes 选择器,如 .header[type="radio"]:hover
  3. type 标签选择器和伪元素选择器,如 h1p::before

其中通配符选择器 *,组合选择器 + ~ >,否定伪类选择器 :not() 对优先级无影响

另有内联样式 <div class="foo" style="color: red;"></div>!important(最高) 具有更高的权重

:not 的优先级影响 - codepen (opens new window) 可以看出 :not 对选择器的优先级无任何影响

# 14 在浏览器中如何监听剪切板中内容

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 14 在浏览器中如何监听剪切板中内容 (opens new window)

通过 Clipboard API 可以获取剪切板中内容,但需要获取到 clipboard-read 的权限,以下是关于读取剪贴板内容的代码:

// 是否能够有读取剪贴板的权限
// result.state == "granted" || result.state == "prompt"
const result = await navigator.permissions.query({ name: "clipboard-read" })

// 获取剪贴板内容
const text = await navigator.clipboard.readText()

注: 该方法在 devtools 中不生效

相关问题: 【Q019】如何实现选中复制的功能 (opens new window)

# 15 如何避免 CDN 为 PC 端缓存移动端页面

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 15 如何避免 CDN 为 PC 端缓存移动端页面 (opens new window)

如果 PC 端和移动端是一套代码则不会出现这个问题。这个问题出现在 PC 端和移动端是两套代码,却共用一个域名。

使用 nginx 配置如下,根据 UA 判断是否移动端,而走不同的逻辑 (判断UA是否移动端容易出问题)

location / {
    // 默认 PC 端
    root /usr/local/website/web;
    
    # 判断 UA,访问移动端
    if ( $http_user_agent ~* "(Android|webOS|iPhone|iPad|BlackBerry)" ){ 
        root /usr/local/website/mobile;
    }
 
    index index.html index.htm;
}

解决方案通常使用 Vary 响应头,来控制 CDN 对不同请求头的缓存。

此处可以使用 Vary: User-Agent ,代表如果 User-Agent 不一样,则重新发起请求,而非从缓存中读取页面

Vary: User-Agent

当然,User-Agent 实在过多,此时缓存失效就会过多。

# 简答

使用 Vary: User-Agent,根据 UA 进行缓存。

Vary: User-Agent

但最好不要出现这种情况,PC 端和移动端如果是两套代码,建议用两个域名,理由如下

  1. nginx 判断是否移动端容易出错
  2. 对缓存不友好

# 16 简单介绍 requestIdleCallback 及使用场景

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 16 简单介绍 requestIdleCallback 及使用场景 (opens new window)

requestIdleCallback 维护一个队列,将在浏览器空闲时间内执行。它属于 Background Tasks API (opens new window),你可以使用 setTimeout 来模拟实现

window.requestIdleCallback = window.requestIdleCallback || function(handler) {
  let startTime = Date.now();
 
  return setTimeout(function() {
    handler({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50.0 - (Date.now() - startTime));
      }
    });
  }, 1);
}

以上实现过于复杂以及细节化,也可以像 swr (opens new window) 一样做一个简单的模拟实现,以下代码见 https://github.com/vercel/swr/blob/8670be8072b0c223bc1c040deccd2e69e8978aad/src/use-swr.ts#L33 (opens new window)

const rIC = window['requestIdleCallback'] || (f => setTimeout(f, 1))

rIC 中执行任务时需要注意以下几点:

  1. 执行重计算而非紧急任务
  2. 空闲回调执行时间应该小于 50ms,最好更少
  3. 空闲回调中不要操作 DOM,因为它本来就是利用的重拍重绘后的间隙空闲时间,重新操作 DOM 又会造成重拍重绘

React 的时间分片便是基于类似 rIC 而实现,然而因为 rIC 的兼容性及 50ms 流畅问题,React 自制了一个实现: scheduler (opens new window)

use-swr (opens new window) 中进行资源的 revalidate 时,也是通过 rIC 来提高性能

# 参考

强烈推荐 MDN 与 w3c 上的两篇介绍

# 17 在 js 中如何实现继承

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 17 在 js 中如何实现继承 (opens new window)

有以下两种方法可实现继承

# class/extends

class Animal {
  constructor (name) {
    this.name = name
  }

  hello () {
    console.log('hello')
  }
}

class Dog extends Animal {
  constructor (name, say) {
    super(name)
    this.say = say
  }
}

# function/new

function Animal (name) {
  this.name = name
}

Animal.prototype.hello = () => {
  console.log('hello')
}

function Dog (name, say) {
  // 01 继承属性
  Animal.call(this, name)
  this.say = say
}

// 02 通过连接原型链完成继承
Dog.prototype = Object.create(Animal.prototype)

// 03 再加上 constructor
Dog.prototype.constructor = Dog
// Reflect.defineProperty(Dog.prototype, "constructor", {
//  value: Dog,
//  enumerable: false, // 不可枚举
//  writable: true
// })

# 18 https 中证书的格式化信息有哪些

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 18 https 中证书的格式化信息有哪些 (opens new window)

在 TLS 握手过程中,服务器端需要给客户端提供证书,X.509 就是证书的标准格式。

image

以下是 github 的证书在 Mac 上显示的内容,可见:

  1. 序列号
  2. Subject Name
  3. Issuer Name
  4. 电子签名
  5. 签名算法
  6. 公钥
  7. 扩展

# 相关链接

# 19 在 TLS 层如何优化网站性能

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 19 在 TLS 层如何优化网站性能 (opens new window)

  1. OSCP Stapling
  2. TLS 1.3

# 20 实现一个函数用来解析 URL 的 querystring

更多描述:

示例,如

const url = 'https://shanyue.tech?a=3&b=4&c=5'

// 解析后得到 qs 如下
const qs = {
  a: 3,
  b: 4,
  c: 5
}

镜像问题: 【Q440】实现一个函数用来对 URL 的 querystring 进行编码 (opens new window)

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 20 实现一个函数用来解析 URL 的 querystring (opens new window)

关于路由中解析 querystring,无论前端开发还是后端开发都无时无刻在使用这项功能,即使几乎没有人手动解析过它。这里来实现一个简单粗暴的解析函数

  1. 如何使用正则解析 qs
  2. 如何正确转义汉字
  3. 如何正确处理数组
  4. 如何处理各种复杂的嵌套对象

关于如何实现复杂嵌套对象,边界条件过多,强烈推荐一个 npm 库 qs (opens new window)

为此总结出以下用例用以检查解析函数的正确性

// {}
'https://shanyue.tech' 

// {a: ''}
'https://shanyue.tech?a' 

// {name: '山月'}
'https://shanyue.tech?name=%E5%B1%B1%E6%9C%88'    

// {name: '山月', a: 3}                
'https://shanyue.tech?name=%E5%B1%B1%E6%9C%88&a=3'            

// {name: '山月', a: [3, 4]}
'https://shanyue.tech?name=%E5%B1%B1%E6%9C%88&a=3&a=4'   

// {name: '山月', a: 3}
'https://shanyue.tech?name=%E5%B1%B1%E6%9C%88&a=3#hash' 

// {name: '1+1=2'}
'https://shanyue.tech?name=1%2B1%3D2' 

纯碎使用 javascript 完成解析函数,而不利用浏览器 DOM 特性 API,代码如下所示,细节在注释中体现

function parse(url) {

  // 一、夹杂在 ? 与 # 之前的字符就是 qs,使用 /\?([^/?#:]+)#?/ 正则来抽取
  // 使用正则从 URL 中解析出 querystring
  // 二、通过 Optional Chain 来避免空值错误
  const queryString = url.match(/\?([^/?#:]+)#?/)?.[1]

  if (!queryString) { return {} }

  queryObj = queryString.split('&').reduce((params, block) => {
    // 三、如果未赋值,则默认为空字符串
    const [_k, _v = ''] = block.split('=')
    // 四、通过 decodeURIComponent 来转义字符,切记不可出现在最开头,以防 ?tag=test&title=1%2B1%3D2 出错
    const k = decodeURIComponent(_k)
    const v = decodeURIComponent(_v)

    if (params[k] !== undefined) {
      // 处理 key 出现多次的情况,设置为数组
      params[k] = [].concat(params[k], v)
    } else {
      params[k] = v
    }
    return params
  }, {})
  return queryObj
}

如果引入浏览器特性 API,问题就简单很多迎刃而解,所涉及到的 API 有两个,这里不做展开

  1. new URL(url)
  2. new URLSearchParams(paramsString)

# 21 如何实现一个数组洗牌函数 shuffle

Issue (opens new window) 或者我的网站 (opens new window)中交流与讨论: 21 如何实现一个数组洗牌函数 shuffle (opens new window)

const shuffle = (list) => list.sort((x, y) => Math.random() - 0.5)

# 面试邀请

最后附上我的个人网站,如果有不错的面试机会可以与我联系

# 日问

欢迎关注个人网站: 互联网大厂面试每日一题 (opens new window),内含大厂内推机会、面经大全及若干面试题,每天学习五分钟,一年进入大厂中。


# 博客

欢迎关注 GitHub 山月行博客: shfshanyue/blog (opens new window),内含我在实际工作中碰到的问题、关于业务的思考及在全栈方向上的学习

Last Updated: 1/17/2021, 2:02:56 PM