前端灵魂拷问

2020/9/3 面试

🌙 前端灵魂拷问

🌙 1.CSS相关

🌙 1.css3特性中的transform:translateZ(0)有什么作用?

GPU加速,优化前端性能

.example {
  /*开启硬件加速*/
  transform: translateZ(0);
  /*优化:告诉浏览器,哪些属性需要硬件加速,提前开启硬件加速*/
  will-change: transform, opacity;
  /*人为干扰复合层的排序,可以有效减少chrome创建不必要的复合层,提升渲染性能*/
  z-index: 1
}
1
2
3
4
5
6
7
8

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • will-change

在 CSS 动画中使用硬件加速(翻译) (opens new window)

CSS3 transform介绍 (opens new window)

CSS3硬件加速也有坑 (opens new window)

CSS3开启硬件加速的使用和坑 (opens new window)

CSS will-change 属性 (opens new window)

iScroll, smooth scrolling for the web (opens new window)

🌙 2.理解BFC

BFC 即 Block Formatting Contexts (块级格式化上下文)

  • 触发 BFC:

    • body 根元素
    • 浮动元素:float 除 none 以外的值
    • 绝对定位元素:position (absolute、fixed)
    • display 为 inline-block、table-cells、flex
    • overflow 除了 visible 以外的值 (hidden、auto、scroll)
  • BFC特性:

    • 盒子从顶部开始垂直排列
    • 两个相邻的盒子之间的垂直距离由外边距(既margin)决定
    • 块级格式化上下文中相邻的盒子之间的垂直边距折叠
    • 每个盒子的左外边与容器的左边接触(从右到左的格式化则相反),即使存在浮动也是如此,除非盒子建立了新的块格式化上下文
    • 形成了BFC的区域不会与float box重叠
    • 计算BFC的高度时,浮动子元素也参与计算

10 分钟理解 BFC 原理 (opens new window)

这一次,完全弄懂BFC (opens new window)

🌙 3.清除浮动

清除浮动主要是为了解决,父元素因为子级元素浮动引起的内部高度为0的问题: 当父元素不给高度的时候,内部元素不浮动时会撑开,而浮动的时候,父元素变成一条线

清除浮动的方法:

  • 1.额外标签法(在最后一个浮动标签后,新加一个标签,给其设置clear:both;)
  • 2.父级添加overflow属性(父元素添加overflow:hidden)或者父元素也浮动-利用BFC特性
  • 3.使用after伪元素清除浮动(推荐使用)
    .clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
        content: "";
        display: block;
        height: 0;
        clear:both;
        visibility: hidden;
    }
    .clearfix{
        *zoom: 1; /*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 4.使用before和after双伪元素清除浮动
    .clearfix:after,
    .clearfix:before {
        content: "";
        display: table;
    }
    .clearfix:after{
        clear: both;
    }
    .clearfix{
        *zoom: 1;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

详解 清除浮动 的多种方式(clearfix) (opens new window)

🌙 4. flex:1flex:auto 区别

flex: [flex-grow] [flex-shrink] [flex-basis]

.box {
  flex: none;   // flex: 0 0 auto;
  flex: auto;  // flex:1 1 auto;
  flex: 1;    //  flex:1 1 0%;
}
1
2
3
4
5

从上面可以看到flex:autoflex:1的区别只在于flex-basis这个属性,auto表示基准值(也就是设置的width),0%表示0无尺寸

flex的第一个值:flex-grow,默认是0,就是不占用多出来的空间。所有子节点宽度和小于父节点宽度时生效,按比例分摊剩余空间

flex的第二个值:flex-shrink,默认是1,所有子节点宽度和超过父节点宽度时生效。

flex的第三个值:flex-basis,如果声明,则会覆盖节点的width属性,flex-basis的优先级更高。如果所有子节点的宽度求和超过父节点宽度,则按照声明的宽度比例缩小填满父节点;但是宽度小于的时候,却不会自动填满

width属性和flex-basis属性同时存在的时候,flex-basis不为auto时,flex-basis值权限更高,否则width权限更高。

  • 缩小情况

先根据flex-basis的值计算出容器超出容器多少空间,但是其不是简单的根据flex-shrink的值计算出缩放比例,而是根据容器中某个元素的flex-basis值乘以flex-shrink的值占容器中所有子元素的flex-basis乘以flex-shrink的值之和来计算缩放比例的。

<div class="container">
    <div class="item-1"></div>
    <div class="item-2"></div>
    <div class="item-3"></div>
</div>

<style>
.container{
    width: 800px;
    height: 300px;
    display: flex;
    font-size: 20px;
}
.item-1 {
    width: 200px;
    flex: 0 2 auto;
    background: red;
}
.item-2 {
    width: 200px;
    /*这里flex-basis值相当于是800 * 100% = 800px*/
    flex: 0 3 100%;
    background: green;
}
.item-3 {
    width: 100px;
    /*这里flex-grow值为0,不放大,还是200px*/
    flex: 0 0 200px;
    background: blue;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
itme-1的flex-basis为auto,所以值和元素本身大小一致,即200px;

item-2的flex-basis为100%,其相对的是容器自身的大小,即800px * 100% = 800px;

item-3的flex-basis为设置的200px,这里需要注意的是,虽然width设置的为100px,但是width不会其作用,仍然会以flex-basis为基准。

首先根据flex-basis计算出超出空间 = (200px + 800px + 200px) - 800px = 400px;

由于超出空间为400px > 0,所以容器中的子元素需要缩小,由于item-3的flex-shrink值为0,所以item-3不会缩小,仍然以200px显示。

item-1的缩小值 = (2*200px / (2*200px + 3*800px + 0*200px)) * 400px = 57.14px;

item-2的缩小值 = (3*800px / (2*200px + 3*800px + 0*200px)) * 400px = 342.86px;

所以,最终:

item-1的大小 = 200px - 57.14px = 142.86px;

item-2的大小 = 800px - 342.86px = 457.14px;

item-3的大小 = 200px;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 放大情况

根据flex-basis的值计算出容器是否有剩余空间,如果有剩余空间,并且该容器中某个子元素是可以放大的,那么用该子元素的flex-grow值比上容器中所有子元素的flex-grow值之和计算出该容器子元素的放大系数,然后乘以剩余空间的大小即是该容器子元素需要放大的像素值。

<div class="container">
    <div class="item-1"></div>
    <div class="item-2"></div>
    <div class="item-3"></div>
</div>

<style>
.container{
    width: 800px;
    height: 300px;
    display: flex;
    font-size: 20px;
}
.item-1 {
    width: 200px;
    flex: 2 1 auto;
    background: red;
}
.item-2 {
    width: 200px;
    /*这里flex-basis值相当于是800 * 10% = 80px*/
    flex: 3 1 10%;
    background: green;
}
.item-3 {
    width: 100px;
    /*这里flex-grow值为0,不放大,还是220px*/
    flex: 0 1 220px;
    background: blue;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
itme-1的flex-basis为auto,所以值和元素本身大小一致,即200px;

item-2的flex-basis为10%,其相对的是容器自身的大小,即800px * 10% = 80px;

item-3的flex-basis为设置的220px,这里需要注意的是,虽然width设置的为100px,但是width不会其作用,仍然会以flex-basis为基准。

首先根据flex-basis计算出剩余空间 = 800px - (200px + 80px + 220px) = 300px;

由于剩余空间为300px > 0,所以容器中的子元素可以放大,由于item-3的flex-grow值为0,所以item-3不会放大,仍然以220px显示。

item-1的放大值 = 2 / (2 + 3) * 300px = 120px;

item-2的放大值 = 3 / (2 + 3) * 300px = 180px;

所以,最终:
item-1的大小 = 200px + 120px = 320px;

item-2的大小 = 80px + 180px = 260px;

item-3的大小 = 220px;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

🌙 5.回流(reflow)与重绘(repaint)

reflow:当render树中的一部分或者全部因为大小边距等问题发生改变而需要重建渲染树的过程叫做回流

repaint:当元素的一部分属性发生变化,如外观背景色不会引起布局变化而需要重新渲染的过程叫做重绘

注意:回流一定会触发重绘,而重绘不一定会回流

  • 1.浏览器渲染页面过程

    • 1.解析HTML,生成DOM树,解析CSS,生成CSSOM树
    • 2.将DOM树和CSSOM树结合,生成渲染树(Render Tree)
    • 3.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
    • 4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
    • 5.Display:将像素发送给GPU,展示在页面上。
  • 2.构建渲染树(渲染树只包含可见的节点)

    • 1.从DOM树的根节点开始遍历每个可见节点。
      • 一些不会渲染输出的节点,比如script、meta、link等
      • 一些通过css进行隐藏的节点。比如display:none。
      • 注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。
    • 2.对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
    • 3.根据每个可见节点以及其对应的样式,组合生成渲染树。
  • 3.何时发生回流重绘

    回流:通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

    重绘:通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素

  • 元素的位置发生变化

  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。

  • 页面一开始渲染的时候(这肯定避免不了)

  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

  • 4.减少回流和重绘

    • 最小化重绘和重排:由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。

    • 批量修改DOM:当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:

      • 第一步:使元素脱离文档流:
        • 1.隐藏元素,应用修改,重新显示
        • 2.使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
        • 3.将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
      • 第二步:对其进行多次修改
      • 第三步:将元素带回到文档中。

      该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。

    • 避免触发同步布局事件

    • 对于复杂动画效果,使用绝对定位让其脱离文档流

    • css3硬件加速(GPU加速) 划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

你真的了解回流和重绘吗 (opens new window)

🌙 6.CSS3动画

css3实现动画有几种方式? (opens new window)

🌙 2.浏览器和网络相关

🌙 1.列举三种禁止浏览器缓存的头字段,并写出响应的设置值?

/**
* 禁用浏览器缓存通过Cache-Control、pragma、expires 
*/
response.setHeader("Cache-Control","no-cache");
response.setHeader("pragma", "no-cache");
response.setDateHeader("expires", -1);
1
2
3
4
5
6

在现代的浏览器里,为了增强用户体验,浏览器一般都会把网页上所需的静态文件缓存到本地,再次刷新的时候则无需再重新加载,但是我们有时候就是不需要浏览器缓存这些文件,而是每次都从服务器端读取数据,可以用以下做法:

在html文件头部加上:

<meta HTTP-EQUIV="pragma" CONTENT="no-cache">
<meta HTTP-EQUIV="Cache-Control" CONTENT="no-store, must-revalidate">
<meta HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT">
<meta HTTP-EQUIV="expires" CONTENT="0">
1
2
3
4

然而这些还是不够的,有些浏览器还是缓存了文件,那么就必须给每个文件加个后缀时间戳,告诉浏览器这个是新文件,必须重新加载,浏览器就会从新到服务器端读取数据文件显示出来。

例如:

<link href="reset.css?v=20150127" rel="stylesheet">
1

那么,浏览器缓存有哪些呢?

  • 强缓存
  • 协商缓存

🌙 1.强缓存

  • HTTP1.0:Expires即过期时间,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。(坑:服务器的时间和浏览器的时间可能并不一致,那服务器返回的这个过期时间可能就是不准确的)

    Expires: Wed, 22 Nov 2019 08:41:00 GMT
    
    1
  • HTTP1.1:Cache-Control,采用过期时长来控制缓存,对应的字段是max-age

    # 代表这个响应返回后在 3600 秒,也就是一个小时之内可以直接使用缓存。
    Cache-Control: max-age=3600
    
    1
    2

    Cache-Control属性值:

    • max-age:过期时长s

    • private: 这种情况就是只有浏览器能缓存了,中间的代理服务器不能缓存。

    • no-cache: 跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段

    • no-store:非常粗暴,不进行任何形式的缓存。

    • s-maxage:这和max-age长得比较像,但是区别在于s-maxage是针对代理服务器的缓存时间。

    • must-revalidate: 是缓存就会有过期的时候,加上这个字段一旦缓存过期,就必须回到源服务器验证。

    值得注意的是,当ExpiresCache-Control同时存在的时候,Cache-Control会优先考虑。

    当然,还存在一种情况,当资源缓存时间超时了,也就是强缓存失效了,接下来怎么办?没错,这样就进入到第二级屏障——协商缓存了。

🌙 2.协商缓存

强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存

  • Last-Modified: 最后修改时间(单位s)(性能优于ETag)

在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。

浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。

服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:

  1. 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。

  2. 否则返回304,告诉浏览器直接用缓存。

ETag: 文件生成的唯一标识。(精准度优于Last-Modified)

服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。

浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:

  • 如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
  • 否则返回304,告诉浏览器直接用缓存。

🌙 3.缓存位置

  • Service Worker

Service Worker 借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存消息推送网络代理等功能。其中的离线缓存就是 Service Worker Cache

Service Worker 同时也是 PWA 的重要实现机制

  • Memory Cache 内存缓存,效率最快
  • Disk Cache 磁盘缓存
  • Push Cache 推送缓存(HTTP/2)

🌙 4.总结

首先通过 Cache-Control 验证强缓存是否可用

  • 如果强缓存可用,直接使用,不用发起HTTP请求

  • 否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的

    If-Modified-Since
    
    1

    或者

    If-None-Match
    
    1

    这些条件请求字段检查资源是否更新

    • 若资源更新,返回资源和200状态码
    • 否则,返回304,告诉浏览器直接从缓存获取资源

能不能说一说前端缓存? (opens new window)

关于浏览器缓存的控制cache-control,expires,last-modified,etag,及编程示例 (opens new window)

🌙 3.从输入url到页面展示发生了什么?

(超详细)从输入url到页面展示发生了什么? (opens new window)

经典面试题!从输入URL到页面展示你还不赶紧学起来? (opens new window)

「导航渲染流程」你真的知道从输入URL到页面展示发生了什么吗?(内附思维导图) (opens new window)

1.进程:浏览器进程(主进程) --- 网络进程 --- 渲染进程 --- GPU进程----穿插其他进程(插件进程等)

2.具体过程:

  • 1.用户在浏览器导航栏输入信息: 非url 是query--- 默认搜索引擎去搜索;url --- 浏览器进程间通信,交给网络进程处理

  • 2.网络进程会先看有没有缓存,否则进行 DNS解析获取IP地址建立TCP连接

  • 3.DNS解析:网络进程拿到URL后会请求DNS域名服务器,解析获取ip地址。

  • 4.建立TCP连接,三次握手,如果是https协议,还会建立SSL/TLS连接

  • 5.发送HTTP请求,四次挥手

  • 6.服务器响应:服务器收到请求信息后,会根据请求信息生成响应行响应头响应体,并发给网络进程。网络进程接受了响应信息之后,就开始解析响应头的内容。

  • 7.网络进程解析响应行和响应头信息的过程

    • 重定向:如果响应行状态码为301(永久重定向)和302(临时),那么说明需要重定向到其他url。这时候网络进程会从响应头中的Location字段里读取重定向的地址,并重新发起网络请求。
    • 响应数据处理:通过请求头的Content-type字段判断响应体数据的类型,从而决定怎么处理响应的数据。如果是文本或html用渲染进程渲染,否则如果是文件通知下载管理器;
  • 8.准备渲染进程:默认情况,每个页面一个渲染进程。但若处于同一站点(同根域名+协议),那么渲染进程就会复用

  • 9.提交文档:渲染进程准备好后,浏览器进程发出**“提交文档的消息”**。等数据传输完成了,渲染进程会告诉浏览器进程,确认文档提交,这时候浏览器会更新页面,安全状态,url,前进后退的历史。到这里导航结束,进入渲染阶段。(:当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为百度首页的页面。因为需要等待提交文档阶段,页面内容才会被替换。)

  • 浏览器的渲染过程

    • 1.浏览器无法直接读取html,需要先构建Dom树。

    • 2.把读取到的css,变成浏览器可以理解的cssom树。

      转换样式表中的属性值,使其标准化。2em 被解析成了 32px,red 被解析成了 rgb(255,0,0),bold 被解析成了 700……

      计算出 DOM 树中每个节点的具体样式。利用css继承、css优先级、css层叠规则等计算出来。

    • 3.浏览器会先从DOM树的根节点开始遍历每个可见节点,并把这些节点添加到渲染树中。不可见的节点不会添加到渲染树,比如css设置了display为none 属性的节点。

    • 4.根据生成的渲染树,进行布局(也可以叫做回流),得到各个节点在页面中的确切位置和大小。(自动重排)。布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小,所有相对的测量值也都会被转换为屏幕内的绝对像素值(重绘)。

    • 5.生成分层树,页面都是一层一层叠加在一起形成的。比如一些复杂的css动画,z-index等,渲染引擎会为他们生成专用的图层,并生成对应的图层树

    • 6.构建完图层之后,渲染引擎会对图层树中的每个元素进行绘制。合成线程会把分层树的图层变成图块

    • 7.GPU的栅格化把视窗附近的图块变成位图,然后保存在GPU的进程中。(因为一个页面可能很大,而用户只能看到视口中页面的一部分,如果全部绘制开销会很大,所以合成线程会按照视口附近的图块来优先生成位图)

      栅格化完成之后,浏览器进去GPU进程里取出页面内容显示在屏幕上,这就完成了渲染阶段

    • 当 渲染器进程 渲染结束(渲染结束意味着该页面内的所有的页面,包括所有 iframe 都触发了 onload 时),会发送 IPC 信号到 浏览器进程, UI线程会停止展示 tab 中的 spinner。

      总结:生成各种树,包括dom tree, css tree, layout tree, layer true, render tree,这些是渲染器进程中的GUI渲染线程做的事。其中js脚本的解析执行是同进程下的js引擎线程来做的,也就是大名鼎鼎的V8引擎。至于定时器回调,是定时器线程来计数,计数完毕会把回调推入事件触发线程维护的任务队列,当js线程空闲并且此线程维护的微任务队列无事件,才会去任务队列拿宏任务执行处理。)

🌙 4.浏览器工作原理

浏览器工作原理 (opens new window)

🌙 5.TCP三次握手

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。

TCP连接握手,握的是啥? 通信双方数据原点的序列号

  • 握手过程:

    • 1.第一次握手:Client将标志位SYN(synchronize)置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
    • 2.第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYNACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
    • 3.第三次握手:Client收到确认后,检查ack是否为J+1ACK(acknowledge)是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手。

    握手成功之后就可以愉快的进行数据传输了。

  • 为什么需要三次握手,两次或者四次可以吗?

    为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

    三次握手是在安全可靠的基础上,握手次数最少的方案。两次握手并不能保证可靠性。四次握手又浪费了效率,当然,有的需要更高安全性的地方,是可以有N次握手协议的,但那是特殊情况。

TCP 为什么是三次握手,而不是两次或四次? (opens new window)

🌙 6.TCP四次挥手

所谓四次挥手(Four-Way Wavehand )即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。

  • 挥手过程:

    • 1.第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态 。
    • 2.第二次挥手:Server收到FIN后 ,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态 。
    • 3.第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
    • 4.第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1 , Server进入CLOSED状态, 完成四次挥手。
  • 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

    这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACKSYN放在一个报文里发送给客户端。 而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACKFIN一般都会分开发送。

🌙 7.TCP与UDP

UDP(用户数据协议) TCP(传输控制协议)
是否连接 无连接 面向连接(三次握手)
是否可靠 不可靠传输,不使用流量控制和拥塞控制,不保证顺序 可靠传输,使用流量控制和拥塞控制,保证顺序
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输
  • TCP向上层提供面向连接的可靠服务 ,UDP向上层提供无连接不可靠服务。
  • 虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
  • 对数据准确性要求高,速度可以相对较慢的,可以选用TCP

🌙 8.Https和Http的区别

  • https协议需要到CA申请证书。

  • http是超文本传输协议,信息是明文传输;https 则是具有安全性的ssl加密传输协议。

  • http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

  • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

  • http默认使用80端口,https默认使用443端口

🌙 9.HTTP状态码301/302/303/307/408

https://www.cnblogs.com/wuguanglin/p/redirect.html

🌙 3.DOM相关

🌙 1. 精确获取页面元素位置的方式有哪些?

  • dom.getBoundingClientRect: 返回 一个TextRectangle对象,即使DOM里没有文本也能返回TextRectangle对象。

  • dom.getClientRects : 返回一个TextRectangle集合,就是TextRectangleList对象。

TextRectangle包含元素相对于视图窗口的左上角的位置(top, right, bottom,left, x, y)以及元素本身的属性(width, height)。

const dom = documnet.getElementById('#box');
// 元素的相对位置
let X = dom.getBoundingClientRect().left;
let Y = dom.getBoundingClientRect().top;
// 获取滚动条位置
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
let scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
// 再加上滚动距离,就可以得到绝对位置
let X = dom.getBoundingClientRect().left + scrollLeft;
let Y = dom.getBoundingClientRect().top + scrollTop;
1
2
3
4
5
6
7
8
9
10

getClientRects() 和 getBoundingClientRect() 的用法和区别 (opens new window)

用Javascript获取页面元素的位置 (opens new window)

🌙 4.defer和async

<script src="script.js"></script>
1
  • 没有 deferasync,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script>
1
  • async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
<script defer src="myscript.js"></script>
1
  • defer,加载后续文档元素的过程将和script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

🌙 4.JS相关

🌙 1.正则从2018-10-07T11:48:47 Asia/zh-cn 提取出来结果[2018,10,07,11,48,47]

const date = new Date().toString();
// 返回匹配的数组
const ans = date.match(/\d+/g); 
// 惰性:返回RegExpStringIterator
const ans = date.matchAll(/\d+/); 
1
2
3
4
5

🌙 2.如何判断object是数组类型?

alert(typeof 1);                // 返回字符串"number" 
alert(typeof "1");              // 返回字符串"string" 
alert(typeof true);             // 返回字符串"boolean" 
alert(typeof {});               // 返回字符串"object" 
alert(typeof []);               // 返回字符串"object " 
alert(typeof function(){});     // 返回字符串"function" 
alert(typeof null);             // 返回字符串"object" 
alert(typeof undefined);        // 返回字符串"undefined"
alert(typeof Symbol());        // 返回字符串""symbol""
1
2
3
4
5
6
7
8
9
  • 从原型入手,Array.prototype.isPrototypeOf(obj);利用isPrototypeOf()方法,判定Array是不是在obj的原型链中,如果是,则返回true,否则false。

  • 从原型入手,Object.prototype.toString.call(obj) === '[object Array]'

  • Array.isArray()方法

🌙 3.手动实现compose和pipe函数

// 使用reduce实现
const compose = (...args) => value => args.reduceRight((acc, fn) => fn(acc), value);
const pipe = (...args) => value => args.reduce((acc, fn) => fn(acc), value);
1
2
3
// 原生实现compose从右往左执行
function compose() {
    var fns = [].slice.call(arguments);
    return function(value) {
        var res = value;
        for(var i=fns.length - 1; i>=0;i--) {
            res = fns[i](res);
        }
        return res;
    }
}
//pipe从左往右执行
function pipe() {
    var fns = [].slice.call(arguments);
    return function(value) {
        var res = value;
        for(var i=0; i<fns.length;i++) {
            res = fns[i](res);
        }
        return res;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

实现compose的五种思路 (opens new window)

🌙 4. 实现add(1)(2)(3)(4) 输出 10(函数柯里化)

toString && valueOf && Symbol.toPrimitive 辨析 (opens new window)

聊一聊valueOf和toString (opens new window)

由Object.prototype.toString.call( )引发关于toString( )方法的思考 (opens new window)

How can I make var a = add(2)(3); //5 work? (opens new window)

toString()的几种用法 (opens new window)

// add(1)(2) = 3
const add = (a, b) => a + b;
const add2 = a => b => a + b;

// 多個
function add(x) {
    return function(y) {
        if (typeof y !== 'undefined') {
            x = x + y;
            return arguments.callee;
        } else {
            return x;
        }
    };
}

// add(1)(2)(3)(4)()

function add(n){
  var addNext = function(x) {
    return add(n + x);
  };

  addNext.valueOf = function() {
    return n;
  };

  return addNext;
}

// more
function add(a, b){
 return a && b ? a+b : function(c){return a+c;}
}

console.log(add(2, 3));
console.log(add(2)(3));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
add(1) // 1
add(1)(2) //3
add(1)(2)(3) // 6
add(1,2)(3) // 6
add(1,2,3) // 6

function add(...args) {
    let _add = function(..._args) {
        return add.apply(null, [...args, ..._args]);
    }
    _add.toString = function() {
        return args.reduce((a, b) => a+b)
    }
    return _add;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

前端经典面试题解密-add(1)(2)(3)(4) == 10到底是个啥? (opens new window)

实现柯里化函数:

function curry(fn, ...args) {
    function curried(..._args) {
        return curry.call(null, fn, ...args, ..._args);
    }
	// 重写toString
    curried.toString = function() {
        return fn.apply(null, args);
    }
    // 返回一个新函数
    return curried;
}


// 使用
const dynamicAdd = (...args) => args.reduce((prev, curr) => prev + curr, 0);
const curriedAdd = curry(dynamicAdd);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

🌙 5.求1234567890.32格式化为:1,234,567,890.32。

把一串数字表示成千位分隔形式——JS正则表达式的应用 (opens new window)

let n = 1234567890.32;
// 最简洁
n.toLocaleString()

// 正则
// 非单词边界\B,用来表示所匹配的这个空后面不能是一个单词边界
let reg = /\B(?=(\d{3})+(?!\d))/g;
let str = '' + n;
str.replace(reg, ",")
1
2
3
4
5
6
7
8
9

使用reduce:

function formatStr(str) {
    let[intStr,dotStr] = str.split('.');
    console.log(intStr);

    return intStr.split('').reverse().reduce((prev,next,index)=> {
        return ((index % 3) ? next : (next + ',')) + prev
    }) + '.' + dotStr;
}

1
2
3
4
5
6
7
8
9

🌙 6.、如何理解JavaScript原型链

  • JavaScript中的每个对象都有一个__proto__属性(函数对象即有__proto__,又有prototype,prototype指向自己的原型而__proto__指向父级的原型)我们称之为原型,而原型的值也是一个对象,因此它也有自己的原型,这样就串联起来了一条原型链,原型链的链头是Object.prototype._proto,它的值比较特殊,值为null。
  • 原型链的作用是用于对象继承,函数A的原型属性(prototype property)是一个对象,当这个函数被用作构造函数来创建实例时,该函数的原型属性将被作为原型赋值给所有对象实例,比如我们新建一个数组,数组的方法便从数组的原型上继承而来。
  • 当访问对象的一个属性时, 首先查找对象本身, 找到则返回; 若未找到, 则继续查找其原型对象的属性(如果还找不到实际上还会沿着原型链向上查找, 直至到根). 只要没有被覆盖的话, 对象原型的属性就能在所有的实例中找到,若整个原型链未找到则返回undefined;
function Foo(){}

const f1 = new Foo();
const f2 = new Foo();

const o1 = new Object();
const o2 = new Object();

// 请解释下列现象:
f1.constructor === Foo; // true;

f1.__proto__ === f2.__proto__; // true
o1.__proto__ === o2.__proto__; // true

Foo.prototype.constructor === Foo; // true
Object.prototype.constructor === Object;// true

// 为什么下列三者为true?
Foo.__proto__ === Object.__proto__;
Foo.__proto__ === Function.__proto__;
Object.__proto__ === Function.__proto__;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

解释:

// 实例(即对象)的constructor
f1.constructor === Foo;
f2.constructor === Foo;
o1.constructor === Object;
o2.constructor === Object;

// 1.实例(即对象)的__proto__指向构造函数的prototype
f1.__proto__ === Foo.prototype;
f2.__proto__ === Foo.prototype;
o2.__proto__ === Object.prototype;
o2.__proto__ === Object.prototype;

// 2.函数的prototype是一个对象,有两个属性:constructor和__proto__
// prototype的constructor指向函数本身
Foo.prototype.constructor === Foo;
// prototype的__proto__指向构造函数的prototype,而对象的构造函数是Object
Foo.prototype.__proto__ === Object.prototype;

// 3.Object的构造函数是Function
Object.prototype.constructor === Function;
// 4.Object的__proto__指向null(原型链的链头)
Object.prototype.__proto__ === null;

// 5.Function本身即是对象也是构造函数,那么就有__proto__和prototype
// 6.Function的构造函数是它本身
Function.__proto__ === Function.prototype;

// 函数的prototype是一个对象,有两个属性:constructor和__proto__
// prototype的constructor指向函数本身
Function.prototype.constructor === Function;
// prototype的__proto__指向构造函数的prototype,而对象的构造函数是Object
Function.prototype.__proto__ === Object.prototype;

// 7.Object本身即是对象也是构造函数,那么就有__proto__和prototype
// 8.Object的构造函数是Function
Object.__proto__ === Function.prototype;

// Object是函数,函数的prototype是一个对象,有两个属性:constructor和__proto__
// prototype的constructor指向函数本身
Object.prototype.constructor === Object;
Object.prototype.__proto__ === Function.prototype; // false 注意:此处不对
Object.prototype.__proto__ === null // true


// 综上所述:
Foo.__proto__ === Function.prototype;
Object.__proto__ === Function.prototype;
Function.__proto__ === Function.prototype;

// 那么
Foo.__proto__=== Object.__proto__; // true
Foo.__proto__ === Function.__proto__; // true
Object.__proto__ === Function.__proto__; // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

prototype和__proto__

🌙 7.什么是闭包?

掌握JavaScript面试:什么是闭包? (opens new window)

🌙 8.ES6转为ES5的过程?

ES6 转 ES5 目前行业标配是用 Babel,转换的大致流程如下:

  1. 解析:解析ES6代码字符串,生成 AST
  2. 转换:按一定的规则转换、修改 AST
  3. 生成:将修改后的 AST 转换成普通ES5代码

实现:将 let 转换为 var

// example.js

let a = 3; 
let b = 2;
console.log(a + b);


// app.js
const fs = require('fs');
const acorn = require('acorn'); //将JS代码转化为语法树模块
const walk = require('acorn-walk'); //JS语法树遍历各节点
const escodegen = require('escodegen'); //将JS语法树反编译成js代码模块


// 1. 获取JS代码
let code = fs.readFileSync('./example.js');
// 2. 用acorn将代码解析为语法树ast
let ast = acorn.parse(code, {
    ranges: true
});
// 3. 用walk操作语法树ast,输出node.value
walk.simple(ast, {
    VariableDeclaration(node) {
        if(node.kind === 'let'){  
            node.kind = 'var'; // 把let 变成 var
        }
    }
})

fs.writeFileSync('result.json', JSON.stringify(ast));  // 将修改后的语法树ast存储为result.json文件
fs.writeFileSync('result.js', escodegen.generate(ast, {comment: true})); // 用escodegen将语法树转换为最终代码,并存储为result.js
// result.js
var a = 3;
var b = 2;
console.log(a + b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

知识点延伸:

Acorn

acorn是常用的一个 JS 解析器,常用到的还有一个 Esprima,acorn在它之后诞生,两者相比,acorn实现代码更少,速度和Esprima相差无几。

二者都遵循 The Estree Spec 规范,也就是得到的结果在很大部分上是兼容的。对 ECMAScript 来说,社区有一个 AST 规范::ESTree Spec (opens new window)

解析 javascript 的三件套: acornacorn-walkescodegen

AST 节点是按照如下的格式定义的:

interface Node {
    type: string;
    loc: SourceLocation | null;
}
1
2
3
4

应用举例:

  1. webpack是使用acorn作为自己的Parser的基础库。
  2. babel, babylon.js也是fork的acorn实现的.

最后推荐一个在线实时查看AST的网站:https://astexplorer.net/

🌙 9.Event Loop (opens new window)

https://juejin.im/post/6892164887456251918

🌙 5.编程相关

32个手撕JS,彻底摆脱初级前端(面试高频) (opens new window)

🌙 1.已知数据结构users,请实现语法支持user.unique能够按照name字段去重,并输出结构为:[“a”,“b”,“c"]

var users=[{
   id:1,name:"a"
},{
   id:2,name:"a"
},{
   id:3,name:"b"
},{
   id:4,name:"c"
}]
1
2
3
4
5
6
7
8
9
// 要支持user.unique语法,需要在数组原型上做文章
Array.prototype.unique = function(key = '') {
    if(!key) return [...new Set(this)];
   return [...new Set(this.map(d => d[key]))];
}

1
2
3
4
5
6
 // 使用`reduce`:
Array.prototype.unique = function(key = '') {
   if(!key) return [...new Set(this)];
   return this.reduce((accumulator, currentValue) => {
       if(!accumulator.includes(currentValue[key])) {
            accumulator.push(currentValue[key]);
       }
       return accumulator;
   }, []);
}

users.unique(key)
1
2
3
4
5
6
7
8
9
10
11
12

🌙 2.已知如下对象,请基于es6proxy方法设计一个属性拦截读取操作的例子,要求实现去访问目标对象example中不存在的属性时,抛出错误:Property “$(property)” does not exist

const man={
    name:'jscoder',
    age:22
}
 //补全代码
const proxy = new Proxy(/*...*/)
proxy.name //"jscoder"
proxy.age //22
proxy.location //Property "$(property)" does not exist
1
2
3
4
5
6
7
8
9
const proxy = new Proxy(obj, {
    // getter拦截
    get: function(target, property) {
        if(property in target) {
            return target[property];
        } else {
            throw new Error(`Property $(property) does not exist.`);
        }
    },
    // setter拦截
    set: function(target, property, value) {
        target[property] = value;
    },
    has:function() {
        console.log('in操作符的拦截')
    },
    deleteProperty: function(){
        console.log('delete操作符的拦截')
    },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 考点es6Proxy 实例的方法
const p = new Proxy(target, handler)
1

Proxy.revocable() 方法可以用来创建一个可撤销的代理对象。Proxy.revocable(target, handler);

Proxy (opens new window)

🌙 3.给出如下虚拟dom的数据结构,如何实现简单的虚拟dom,渲染到目标dom树

//样例数据
let demoNode = ({
    tagName: 'ul',
    props: {'class': 'list'},
    children: [
        ({tagName: 'li', children: ['douyin']}),
        ({tagName: 'li', children: ['toutiao']})
    ]
});
1
2
3
4
5
6
7
8
9

构建一个render函数,将demoNode对象渲染为以下dom:

<ul class="list">
    <li>douyin</li>
    <li>toutiao</li>
</ul>
1
2
3
4

通过JavaScript,我们可以很容易构建它,如下:

var elem = Element({
    tagName: 'ul',
    props: {'class': 'list'},
    children: [
        Element({tagName: 'li', children: ['item1']}),
        Element({tagName: 'li', children: ['item2']})
    ]
});
1
2
3
4
5
6
7
8

下面实现Element类:

Element为一个构造函数,返回一个Element对象。为了更清晰的呈现虚拟DOM结构,我们省略了new,而在Element中实现

function Element({tagName, props, children}) {
    if (!(this instanceof Element)) {
        return new Element({tagName, props, children});
    }
	this.tagName = tagName;
	this.props = props;
	this.children = children;
}
1
2
3
4
5
6
7
8

现在可以通过Element我们可以任意地构建虚拟DOM树,那么怎么转为真正的DOM呢?

可以通过DOM操作,createElement创建元素节点,createTextNode创建文本节点:

// 使用DFS
Element.prototype.render = function() {
    // 创建父节点
    let el = document.createElement(this.tagName);
    for(let propName in this.props) {
        el.setAttribute(propName, this.props[propName]);
    }
    // 遍历子节点 -》 子节点-》...
    this.children.forEach(function(child) {
        let childEl = null;
        // 是Element类
        if(child instanceof Element) {
            childEl = child.render();
        // 不是Element类,但是{tagName, props, children}
        } else if (child.tagName) {
           childEl = Element(child).render();        
        } else {
           childEl = document.createTextNode(JSON.stringfy(child));
        }
        el.appendChild(childEl);
    });
    return el;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

🌙 4. 手写深拷贝

function deepClone(value) {
    if(value === null) return value;
    if(typeof value !== 'object') return value;
    if(value instanceof RegExp) return new RegExp(value);
    if(value instanceof Date) return new Date(value);
    // [] 或者{}
    let obj = new value.constructor();
    // 使用for ... in遍历自身属性
    for(let key in value) {
        // 只拷贝对象自身的属性
        if(value.hasOwnProperty(key)) {
            // 递归调用
             obj[key] = deepClone(value[key]);
        }
    }
    return obj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🌙 5.手写数组扁平化

数组多维转一维度(扁平化)的6种方法 (opens new window)

function flatten(arr, depth=1) {
   if(depth > 0) {
       return arr.reduce((acc, cur) => {
           return acc.concat(Array.isArray(cur) ? flatten(cur, depth-1) : cur)
       }, [])
   } else {
       return arr;
   }
}
1
2
3
4
5
6
7
8
9

🌙 6.手写new操作符

首先,理解new操作符干了什么?

  1. 创建了一个新对象
  2. 将this指向新对象
  3. 将创建的对象的原型指向构造函数的原型
  4. 返回一个对象。如果构造函数本身有返回值并且返回的也是对象,那么就返回这个返回值,否则否会创建的对象。
function _new(fn, ...args) {
    // 创建一个空对象,并将对象的__proto__指向fn的ptototype
    let obj = Object.create(fn.prototype);
    // 将fn的this指向obj,并获取返回值
    let ret = fn.apply(obj, args);
    // 如果fn返回一个新对象,就返回这个对象,否则返回刚创建的对象
    const isObject = typeof ret === 'object' && ret !== null;
    const isFunction = typeof ret === 'function';
    return (isObject || isFunction) ? ret : obj;
}
1
2
3
4
5
6
7
8
9
10

🌙 7.手写对象继承

js继承的几种方式 (opens new window)

function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    //用新创建的对象替代子类默认原型,设置Child.prototype.constructor = Child;保证一致性
    Child.prototype.constructor = Child;  
}
1
2
3
4
5
6
7

🌙 8. 手写防抖 debounce

Debouncing and Throttling Explained Through Examples (opens new window)

  • 简单实现
function debounce(fn, wait) {
    let timer = null;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, wait);
    };
};
1
2
3
4
5
6
7
8
9
  • 控制立即执行
function debounce(fn, wait=200, immediate=true) {
    let timer = null;
    return function(...args) {
        // 这里只是让定时器停下来,并没有置为null
        if(timer) { clearTimeout(timer) }
        if(immediate) {
            let notCalled = !timer;
            timer = setTimeout(() => {
              timer = null;
            }, wait)
            notCalled && fn.apply(this, args)
        } else {
          timer = setTimeout(() => {
            fn.apply(this, args)
          }, wait)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

🌙 9.手写节流 throttle

  • 简单实现
// 定时器版
function throttle() {
    let timer = null;
    return function(...args) {
        if(timer) return;
        timer = setTimeout(() => {
            fn.call(this, ...args);
            timer = null;
        }, wait);
    };
};

// 时间戳版
const throttle = (func, wait) => {
  let last = 0;
  return (...args) => {
    const current_time = +new Date();
    if (current_time - last_time > wait) {
      func.apply(this, args);
      last_time = +new Date();
    }
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • 节流或防抖切换
function throttleOrDebounce(fn, wait, isDebounce=true) {
    let timer = null;
    let prev = 0;
    return function(...args) {
        if(isDebounce) {
           if(timer) clearTimeout(timer);
           timer = setTimeout(() => {
               fn.apply(this, ...args);
           }, wait)
        } else {
            const now = +new Date();
            if(now - prev < wait) return;
            prev = now;
            fn.apply(this, args);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 控制立即执行
// leading: 延迟之前执行, trailing: 延迟之后执行
function throttle(fn: Function, ms = 200, options = {leading: true, trailing: true}) {
  let timer: NodeJS.Timeout | null = null;
  let prev = 0;

  return function (...args: any[]) {
    const now = Date.now();

    // 核心逻辑 1
    if (options.leading && !timer && now - prev >= ms) {
      fn.apply(this, args);
      prev = now;
    }

    if (!timer) {
      timer = setTimeout(() => {
        // 核心逻辑 2
        if (options.trailing && now - prev >= ms) {
          fn.apply(this, args);
        }
        timer = null;
        prev = now;
      }, ms);
    }
  };
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

🌙 10. 手写 call

  • 简易版(不考虑context非对象情况,不考虑Symbol\BigInt 不能 new.constructor( context )情况)
Function.prototype._call = function(context, ...args) {
    // null,undefined,空 --- context为window
    context = context == null ? window : context;
    
    // 将要被替换的fn放入context中
    context.fn = this;
    // 此时执行fn,其this自然指向了context
    const ret = context.fn(...args);
    // 用完卸载
    delete context.fn;
    // 返回执行结果
    return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 完善版(context必须对象类型,兼容Symbol等情况)
Function.prototype._call = function(context, ...args) {
    // null,undefined,空 --- context为window
    context = context == null ? window : context;
    
    // 必须保证 context 是一个对象类型
  	let contextType = typeof context;
    if (!/^(object|function)$/i.test(contextType)) {
   	 	// context = new context.constructor(context); // 不适用于 Symbol/BigInt
    	context = Object(context);
    }

    // 将要被替换的fn放入context中
    context.fn = this;
    // 此时执行fn,其this自然指向了context
    const ret = context.fn(...args);
    // 用完卸载
    delete context.fn;
    // 返回执行结果
    return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

🌙 11. 手写 apply

原理: 利用函数通过obj.func()执行的方式,将函数的this指向这个对象。

Function.prototype._apply = function(context, ...args) {
     context = context == null ? window : context;
    let contextType = typeof context;
  	if (!/^(object|function)$/i.test(contextType)) {
        // Object会将context包装成对应的包装对象
    	context = Object(context);
    }
    
    context.fn = this;
    // 此处参数是与call的区别
    let ret = context.fn(args);
    delete context.fn;
    
    return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

🌙 12. 手写 bind

Function.prototype._bind = function (context, ...args1) {
    // 缓存要处理函数的this
    let that = this;
    return function (...args2) {
        that.call(context, ...args1, ...args2);
        // 或者
        // that.apply(context, [..args1, ...args2]);
        // 或者在这里手动实现call/apply
    }
}
1
2
3
4
5
6
7
8
9
10

🌙 13.手写 Promise

最详细的Promise源码实现,再被面试问到轻松解答 (opens new window)

🌙 14.实现JS AOP之before

Function.prototype.before = function(fn, ...args) {
    var that = this;
    return function() {
        fn.apply(that, args);
        return that.apply(that, args);
    }
}
1
2
3
4
5
6
7

🌙 15.实现JS AOP之after

Function.prototype.after = function(fn, ...args) {
    var that = this;
    return function() {
        var ret = that.apply(that, args);
        fn.apply(that, args);
        return ret;
    }
}
1
2
3
4
5
6
7
8

🌙 6.前端工程化相关

🌙 6.1 babel转换ES6语法工作原理

  1. parsing阶段:将ES6代码通过babylon解析,生成AST。
  2. transforming阶段:用babel-transverse将上述ES6的AST进行遍历转译为ES5的AST。
  3. generating阶段:用babel-generating将ES5的AST生成ES5代码。

🌙 7.算法相关

🌙 1.二进制

🌙 1.判断一个整数转换成二进制后1的个数

  • 取巧
function ChargeOnesCountInNum(num) {
    return num.toString(2).split('1').length - 1;
}
1
2
3
  • 常规
function ChargeOnesCountInNum(num) {
    let count = 0;
    num = Math.abs(num);
    while(num > 0) {
       if(num % 2 > 0)  count++;
        num = Math.floor(num / 2);
    }
    return count;
}
1
2
3
4
5
6
7
8
9
  • 右位移

先将整数转换成正整数,再将该数与1进行与运算。若不将整数做取绝对值处理,当输入的数是负数时,每向右移动一位,高位会自动补1,就会导致死循环。








 




function ChargeOnesCountInNum(num) {
    let count = 0;
    num = Math.abs(num);
    while(num > 0) {
        // & 运算
       if(num & 1)  count++;
        // 向右移动一位
        num = num >> 1;
    }
    return count;
}
1
2
3
4
5
6
7
8
9
10
11
  • 左位移




 
 
 




function ChargeOnesCountInNum(num) {
    let count = 0;
    let flag = 1;
    num = Math.abs(num);
    while(flag < num) {
       if(num & flag)  count++;
        flag = flag << 1;
    }
    return count;
}
1
2
3
4
5
6
7
8
9
10
  • 与运算

将一个整数减去1之后,其对应的二进制中最右边的一个1会变为0,若其后存在0,则其之后的所有0都会变为1。基于此,设一个整数为n,则 n & (n-1)之后,会消掉n对应的二进制的最右边的1。因此,将一个数中所有1消掉所用的次数,即为该整数对应的二进制中1的个数。






 




function ChargeOnesCountInNum(num) {
    let count = 0;
    num = Math.abs(num);
    while(num > 0) {
       count++;
       num = num & (num - 1);
    }
    return count;
}
1
2
3
4
5
6
7
8
9
  • 相关的变形问题

    • 用一条语句判断一个整数是不是2的整数次方。参考答案:if(!(num & (num-1))){}
    • 输入两个整数m和n,计算m和n对应的二进制中有多少个不同的位。参考思路:先对m和n进行异或,然后计算异或后的二进制结果中1的个数。

🌙 2. 动态规划

动态规划从入门到精通系列 (opens new window)

经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解) (opens new window)

🌙 1.跳台阶问题

  • 普通跳台阶问题(菲波那切数列)

    一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。

n dp(n)
1 1
2 2
3 3
... ...
n f(n-1) + f(n-2)
// O(n) = 2^n
function dp(n) {
    if(n<2) return n;
    return dp(n-1) + dp(n-2);
}
1
2
3
4
5

优化(利用缓存):

function dp(n) {
   if(n<2) return n;
   let f = 1;
   let g = 1;
   while(n--) {
       g = g + f;
       f = g - f;
   }
   return f 
}

// 或者
function dp(n, pre=1, next=1) {
    if (n < 2) {
        return next;
    } else {
        return dp(n - 1, next, pre + next)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 拓展:变态跳台阶问题

题目:一个台阶总共有n级,如果一次可以跳1级,也可以跳2级......它也可以跳上n级。此时该青蛙跳上一个n级的台阶总共有多少种跳法?

分析:用f(n)表示青蛙跳上n阶台阶的跳法数,设定f(0) = 1;

当n = 1 时,只有一种跳的方式,一阶跳,f(1) = 1

当n = 2 时,有两种跳的方式,一阶跳和两阶跳,f(2) = f(1) + f(0) = 2

当n = 3 时,有三种跳的方式,第一次跳出一阶后,后面还有f(3-1)中跳法; 第一次跳出二阶后,后面还有f(3-2)中跳法;第一次跳出三阶后,后面还有f(3-3)中跳法,f(3) = f(2) + f(1) + f(0) = 4

当n = n 时,第一次跳出一阶后,后面还有f(n-1)中跳法; 第一次跳出二阶后,后面还有f(n-2)中跳法......第一次跳出n阶后,后面还有 f(n-n)中跳法,即:

f(n) = f(n-1) + f(n-2) + f(n-3) + ... + f(n-n) = f(0) + f(1) + f(2) + ... + f(n-1)

又因为 f(n-1) = f(0) + f(2) + f(3) + ... + f(n-2)

两式相减得:f(n) = 2 * f(n-1) ( n >= 2)

      0 (n = 0);
f(n)= 1 (n = 1);
      2*f(n-1) (n >= 2);
1
2
3
function dp(n) {
    if(n < 2) return n;
    return 2 * dp(n - 1);
}

// 优化(利用缓存)
function dp(n) {
    if(n < 2) return n;
    let ans = 1;
    for(let i=2;i<=n;i++) {
        ans = 2 * ans;
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🌙 2.剪绳子

给你一根长度为n的绳子,请把绳子剪成m段 (m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],…,k[m].请问k[0]k[1]…*k[m]可能的最大乘积是多少?

分析:

n dp(n)
0 0
1 1
2 1
3 2
4 4
n max(dp(i)*dp(n-i)) , i = 1,2,3 ... n/2

编码:

function dp(n) {
    if(n < 2) return n;
    if(n===2) return 1;
    if(n===3) return 2;
    
    /****长度为0、1、2、3的绳子的长度********/
    let arr = [0,1,2,3];
    let max = 0;
    for(let i=4;i<=n;i++) {
        // max(dp(i)*dp(n-i)) , i = 1,2,3 ... n/2,从1开始
        for(let j=1; j<=n/2;j++) {
            let t = arr[j] * arr[i - j];
            if(max < t) {
                max = t;
            }
            arr[i] = max;
        }
    }
    console.log(arr)
    return max;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

🌙 3.数字三角形 (opens new window)

             7					7··························三角形行数(i)·········[7]
           3  8					3	8······················当前行第2个数字(j)····[3,8]
         8  1  0		===>	8	1	0	····································[8,1,0]
        2  7  4  4   			2	7	4	4 ··································[2,7,4,4]
       4  5  2  6  5    		4	5	2	6	5·······························[4,5,2,6,5]

       DP三角形👉              	   
       7
       10	15	
       18	16	15
       20  25   20  19
       24  30   27  26  24  
1
2
3
4
5
6
7
8
9
10
11
12

在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99

分析

  • 用二维数组存放数据

  • D(i, j):第i行第j个数字(从0开始编号)

  • dp(i,j):从顶部走到D(i,j)处的最大路径和

  • DP方程:

    dp(i,j) = max(dp(i-1, j), dp(i-1,j-1)) + D(i,j)
    
    1
  • 边界条件:

    • 在三角形的顶部时(i=0),最大路径和就等于对应位置的元素值
    dp(0,0)=D(0,0);
    
    1
    • 第一列(最左边j=0)计算的时候,如果用dp(i-1, j-1)获取斜上方值时会出现下标越界
    dp(i,0) = dp(i-1,0) + D(i,0)
    
    1
    • 最右边也是只有一条转移路径(i=j)
    dp(i,j) = dp(i-1,j-1) + D(i,j)
    
    1
let dataset = [
    [7],
    [3,8],
    [8,1,0],
    [2,7,4,4],
    [4,5,2,6,5]
];
function getMaxSum(dataset) {
    let dp = [];
	let N = dataset.length;
	// 创建二维数组
    for(let i=0;i<N;i++) {
        let arr = [];
        for(let j=0;j<i+1;j++) {
           arr.push(0)
        }
        dp.push(arr);
    }	
    // 初始化dp[0][0],计算第一行
    dp[0][0] = dataset[0][0];
    // 从第二行开始,即i=1
    for(let i=1;i<N;i++) {
        // 计算第一列
        dp[i][0] = dp[i-1][0] + dataset[i][0];
        for(let j=1;j<i+1;j++) {
            dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + dataset[i][j]
        }
        // 最右侧
        dp[i][i] = dp[i - 1][i - 1] + dataset[i][i]
    }
    return Math.max(...dp[N-1])
}

getMaxSum(dataset) // 30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 动态规划-自底向上-降维
function getMaxSum(dataset) {
    let N = dataset.length;
    let dp = new Array(N + 1).fill(0);
    for(let i=N-1;i>=0;i--) {
        for(let j=0;j<dataset[i].length;j++) {
            dp[j]=Math.max(dp[j],dp[j+1]) + dataset[i][j];
        }
    }
    return dp[0];
}
1
2
3
4
5
6
7
8
9
10
11

🌙 4.八皇后问题 (opens new window)

设计一种算法,打印 N 皇后在 N × N 棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线。

八皇后 0 1 2 3 4 5 6 7
0 🔒 🔒 🔒 🔒 🔒 🔒 🔒
1 🔒 🔒 🔒 🔒 🔒 🔒 🔒
2 🔒 🔒 🔒 🔒 🔒 🔒 🔒
3 🔒 🔒 🔒 🔒 🔒 🔒 🔒
4 🔒 🔒 🔒 🔒 🔒 🔒 🔒
5 🔒 🔒 🔒 🔒 🔒 🔒 🔒
6 🔒 🔒 🔒 🔒 🔒 🔒 🔒
7 🔒 🔒 🔒 🔒 🔒 🔒 🔒

🔒 :表示不能存放👱‍♀

✅ :表示能存放的位置

伪代码如下:

(i,j) i-行 j-列
dfs(n) {
	// 如果x等于n了,说明每行的皇后都放置完毕
	if(i===n) {
		//将棋盘内容的快照保存下来
		return
	}
	
	for(let j=0;j<n;j++) {
		if (i,j)位置可以放置皇后(横竖撇捺都没有其他皇后) {
			   将棋盘[i,j]位置设置为 Q
            	dfs(i+1) 继续尝试下一行
            	将棋盘[i,j]位置还原
		}
	}
}

// 分析
横:i相同(i,j)(不用检查,我们是按行扫描的)
竖:j相同(i-1,j)
撇:(i-1,j-1) (y=x + c)
捺:(i-1,j+1) (y=-x + c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function solveQueens(n) {
    // 生成N*N的棋盘
    //填充棋盘,每个格子默认是"."表示没有放置皇后
    let arr = new Array(n).map(d => new Array(n).fill('.'));
    // 结果数组
    let ans = [];
    dfs(arr,0,n,ans);
    return ans;
}

function dfs(arr, row, n, ans) {
    // 从第一行开始(row=0),当row等于n,表示所有皇后都放置完,记录结果
    if(row===n){
        // 记录
        let res = [];
        for(let i=0;i<n;i++) {
            // //每一行拼成字符串 '...Q....'
            res[i] = arr[i].join('');
        }
        ans.push(res)
        return;
    }
    
    // 遍历每一列
    for(let col=0;col<n;col++) {
         //检查(row, col)这一坐标是否可以放置皇后
         //如果满足条件,就放置皇后,并继续检查下一行
        if(check(arr, row, col, n)) {
            // 放置皇后
            arr[row][col] = 'Q';
            // 递归
            dfs(arr,row+1,n,ans);
            // 初始化
            arr[row][col] = '.';
        }
    }
}

// 检查当前行和列是否可以放置皇后
function check(arr, row, col, n) {
    // 检查竖着的一列是否有皇后
    for(let i=0;i<row;i++) {
        if(arr[i][col] === 'Q') {
            return false;
        }
    }
    // 检查撇是否有皇后(左上到右下的斜边)
    let x = row - 1, y = col - 1;
    while(x >=0 && y>=0) {
        if(arr[x][y]==='Q') {
            return false;
        }
        x--;
        y--;
    }
    // 检查捺是否有皇后(左下到右上的斜边)
    x = row - 1;
    y = col + 1;
    while(x >=0 && y<n) {
        if(arr[x][y]==='Q') {
            return false;
        }
        x--;
        y++;
    }
    return true;
}


// 优化判断
// 判断能否将第row行第col列设置为皇后
function isValid(arr, row, col, n) {  
    //遍历[0, row - 1]行所出现的皇后位置
    for(let i = 0; i < row; i++) {
        for(let j = 0; j < n; j++) {
            if(arr[i][j] === 'Q' && (j === col || i + j === row + col || i - j === row - col)) {                    
                return false;
                /* 
                        j == col 代表当前位置与 已设置的皇后在同一列
                        i + j === row + col 代表当前位置与 已设置的皇后在同一副对角线(y=-x)
                        i - j === row - col 代表当前位置与 已设置的皇后在同一主对角线(y=x)
                    */
            }
        }
    }
    return true;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

🌙 5.背包问题

背包问题-笔记整理 (opens new window)

购物单 (opens new window)

算法--背包九讲(详细讲解+代码) (opens new window)

背包九讲——全篇详细理解与代码实现 (opens new window)

  • 首先来看一下经典背包问题zkkysqs

问题描述:有一个背包可以装物品的总重量为W,现有N个物品,每个物品重w[i],价值v[i],用背包装物品,能装的最大价值是多少?

定义状态转移数组dp[i][j],表示前i个物品,背包重量为j的情况下能装的最大价值。

例如:dp[3][4] = 6,表示前三个物品装入重量为4的背包所能获得的最大价值为6。

状态转移方程DP

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i])

dp[i-1][j]表示当前物品不放入背包,dp[i-1][j-w[i]]+v[i]表示当前物品放入背包,即当前第i个物品要么放入背包,要么不放入背包。

伪代码实现如下:

dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
    for j in range(1, n+1):
        if j - w[i] >= 0:
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
        else:
            dp[i][j] = dp[i-1][j]
return dp[m][n]
1
2
3
4
5
6
7
8
  • 购物车 购物车本质上还是0-1背包问题,只不过多了主件和附件。假设先不看附件,那么就和0-1背包一样了。附件不能单独出现,要依赖于主件。

对应于背包问题,主件的个数就是物品的个数,考虑每个主件时要考虑可能出现的情况。

输入例子:
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
1
2
3
4
5
6
7

在当前的例子当中物品的个数就是3。

考虑每个物品时要考虑每种可能出现的情况,1、主件,2、主件+附件1,3、主件+附件2,4、主件+附件1+附件2,不一定每种情况都出现,只有当存在附件时才会出现对应的情况。

w[i][k]表示第i个物品的第k种情况,k的取值范围0~3,分别对应以上4中情况,v[i][k]表示第i个物品对应第k种情况的价值,现在就把购物车问题转化为了0-1背包问题。

状态转移方程DP可以定义为:

dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i][k]]+v[i][k])

dp[i-1][j]表示当前物品不放入背包,w[i][k]表示第i个主件对应第k中情况,即当前第i个物品的4中情况中价值最大的要么放入背包,要么不放入背包。

需要注意:dp[i][j] = max(物品不放入背包,主件,主件+附件1,主件+附件2,主件+附件1+附件2)

伪代码实现如下:

dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
    for j in range(1,n+1):
        max_i = dp[i-1][j]
        for k in range(len(w[i])):
            if j-w[i][k]>=0:
                max_i = max(max_i, dp[i-1][j-w[i][k]]+v[i][k])
        dp[i][j] = max_i
print(dp[m][n])
1
2
3
4
5
6
7
8
9
/**
*@param m:总钱数
*@param n:物品数
*@param arr: [[v,p,q]](v-价格,p-重要度,q-主附件)
*/
let m = 1000;
let n = 5;
let arr = [
  [800,2,0],
  [400,5,1],
  [300,5,1],
  [400,3,0],
  [500,2,0],
];

buyGoods(m/10, n, arr) // 2200;

function buyGoods(m, n, arr) {
    // {v: 价格, w:价值}[]
    let goods = generateGoods(arr);
    let res = Array(m+1).fill(0);
    
    for(let i=0;i<goods.length;i++) {
        for(let j=m;j>=0;j--) {
            if(goods[i]) {
                goods[i].forEach(d => {
                    if(d.v <= j) {
                        // 买权重较高的物品
                        res[j] = Math.max(res[j], res[j-d.v] + d.w);
                    }
                })
            }
        }
    }
    console.log(res);
    return res[m] * 10;
}

function generateGoods(arr) {
    let res = [];
    for(let i=0;i<arr.length;i++) {
        // 是主件
        if(arr[i][2] === 0) {
            res[i] = [{v: arr[i][0]/10, w:arr[i][0]/10 * arr[i][1]}]
        // 是附件,同时必须购买相应的主件    
        } else {
            // res[arr[2]-1]对应主件的数据
            let add = res[arr[i][2]-1].map(d => ({
                v: arr[i][0]/10 + d.v,
                w: arr[i][0]*arr[i][1]/10 + d.w,
            }))
            res[arr[i][2]-1] = [...res[arr[i][2]-1], ...add];
        }
    }
    return res.filter(Boolean);
    // (3) [Array(4), Array(1), Array(1)]
    // 0: Array(4)
    // 0: {v: 800, w: 1600}
    // 1: {v: 1200, w: 3600}
    // 2: {v: 1100, w: 3100}
    // 3: {v: 1500, w: 5100}
    // length: 4
    // __proto__: Array(0)
    // 1: Array(1)
    // 0: {v: 400, w: 1200}
    // length: 1
    // __proto__: Array(0)
    // 2: Array(1)
    // 0: {v: 500, w: 1000}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

背包问题解法: 01 背包问题: 如果是 01 背包,即数组中的元素不可重复使用,外循环遍历 arrs,内循环遍历 target,且内循环倒序:

完全背包问题: (1)如果是完全背包,即数组中的元素可重复使用并且不考虑元素之间顺序,arrs 放在外循环(保证 arrs 按顺序),target在内循环。且内循环正序。 (2)如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序。

源自一套框架解决「背包问题」 (opens new window)

🌙 6.买卖股票的最佳时机

团灭股票买卖问题

leetcode-代码随想录 (opens new window)