[TOC] 实现 promise.all 实现 promise.retry 将一个同步 callback 包装成 promise 形式 写一个函数,可以控制最大并发数

css 布局实现

1. 两栏布局

要求:垂直两栏,左边固定右边自适应。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>flex实现左定宽右自适应</title>
    <style>
      .container {
        display: flex;
        height: 300px;
      }
      .left {
        background: red;
        flex: 0 0 200px;
      }
      .right {
        background: blue;
        flex: 1 1 auto;
      }
    </style>
  </head>
  <body>
    <!-- 左右两栏,左边固定,右边自适应 -->
    <div class="container">
      <div class="left">左边</div>
      <div class="right">右边</div>
    </div>
  </body>
</html>
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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>浮动实现</title>
    <style>
      .box > div {
        height: 300px;
      }
      .left {
        width: 200px;
        float: left;
        background-color: brown;
      }
      .right {
        margin-left: 200px;
        background-color: aquamarine;
      }
    </style>
  </head>
  <body>
    <div class="box">
      <div class="left">left</div>
      <div class="right">right</div>
    </div>
  </body>
</html>
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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>绝对定位实现</title>
    <style>
      .box {
        height: 300px;
        position: relative;
      }
      .left,
      .right {
        height: 100%;
      }
      .left {
        width: 200px;
        background-color: bisque;
      }
      .right {
        position: absolute;
        left: 200px;
        top: 0;
        right: 0;
        width: calc(100% - 200px);
        background-color: aquamarine;
      }
    </style>
  </head>
  <body>
    <div class="box">
      <div class="left">left</div>
      <div class="right">right</div>
    </div>
  </body>
</html>
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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>绝对定位实现2</title>
    <style>
      .box {
        height: 300px;
        position: relative;
      }
      .left,
      .right {
        height: 100%;
      }
      .left {
        width: 200px;
        background-color: blanchedalmond;
        position: absolute;
      }
      .right {
        margin-left: 200px;
        background-color: aquamarine;
      }
    </style>
  </head>
  <body>
    <div class="box">
      <div class="left">left</div>
      <div class="right">right</div>
    </div>
  </body>
</html>
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

2. 三栏布局

要求:垂直三栏布局,左右两栏宽度固定,中间自适应。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>flex实现</title>
    <style>
      .box {
        display: flex;
      }
      .box > div {
        height: 300px;
      }
      .left {
        width: 200px;
        background-color: aquamarine;
      }
      .right {
        width: 200px;
        background-color: antiquewhite;
      }
      .middle {
        background-color: brown;
        flex: 1 1 auto;
      }
    </style>
  </head>
  <body>
    <!-- 三栏布局 左右固定 中间自适应 -->
    <div class="box">
      <div class="left">left</div>
      <div class="middle">middle</div>
      <div class="right">right</div>
    </div>
  </body>
</html>
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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>浮动实现</title>
    <style>
      .box > div {
        height: 300px;
      }
      .left {
        width: 200px;
        float: left;
        background-color: aquamarine;
      }
      .right {
        width: 200px;
        float: right;
        background-color: antiquewhite;
      }
      .middle {
        background-color: brown;
        margin: 0 200px;
      }
    </style>
  </head>
  <body>
    <!-- 三栏布局 左右固定 中间自适应 -->
    <div class="box">
      <!-- 左右盒子浮动导致父元素高度塌陷 -->
      <div class="left">left</div>
      <div class="right">right</div>
      <!-- 右栏部分要写在中间内容之前 这种实现方式要把middle这个div放到最后面。相当于先把两边的div布局好,然后中间的div嵌入进去。-->
      <div class="middle">middle</div>
    </div>
  </body>
</html>
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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>绝对定位实现</title>
    <style>
      .box {
        position: relative;
      }
      .box > div {
        height: 300px;
      }
      .left {
        position: absolute;
        width: 200px;
        background-color: aquamarine;
      }
      .right {
        width: 200px;
        position: absolute;
        right: 0;
        top: 0;
        background-color: bisque;
      }
      .middle {
        margin: 0 200px;
        background-color: blueviolet;
      }
    </style>
  </head>
  <body>
    <!-- 三栏布局 左右固定 中间自适应 -->
    <div class="box">
      <div class="left">left</div>
      <div class="middle">middle</div>
      <div class="right">right</div>
    </div>
  </body>
</html>
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

3. 圣杯布局和双飞翼布局

flex 实现圣杯布局

用 flex 来实现圣杯布局特别简单。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>flex圣杯布局</title>
    <style>
      html,
      body {
        height: 100%;
        overflow: hidden; /*默认显示一屏*/
        margin: 0;
        padding: 0;
      }
      .container {
        height: 100%;
        display: flex;
        flex-direction: column;
      }
      header {
        background-color: aqua;
        flex: 0 0 50px;
        text-align: center;
        line-height: 50px;
      }
      footer {
        background-color: blueviolet;
        flex: 0 0 50px;
      }
      section {
        background-color: blanchedalmond;
        flex: auto;
        display: flex;
      }
      .left {
        background-color: aquamarine;
        flex: 0 0 200px;
      }
      .right {
        background-color: blue;
        flex: 0 0 200px;
      }
      .center {
        background-color: brown;
        flex: auto;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <header>头部</header>
      <section>
        <div class="left">left</div>
        <div class="center">center</div>
        <div class="right">right</div>
      </section>
      <footer>底部</footer>
    </div>
  </body>
</html>
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

绝对定位实现圣杯布局

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>绝对定位实现圣杯布局</title>
    <style>
      html,
      body {
        height: 100%;
        overflow: hidden;
        margin: 0;
        padding: 0;
      }
      .container {
        position: relative;
      }
      .center {
        background-color: aquamarine;
        height: 400px;
        margin: 0 200px;
      }
      .left,
      .right {
        width: 200px;
        height: 400px;
        position: absolute;
        top: 0;
      }
      .left {
        background-color: blue;
        left: 0;
      }
      .right {
        background-color: brown;
        right: 0;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="left">左边</div>
      <div class="center">中间</div>
      <div class="right">右边</div>
    </div>
  </body>
</html>
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

浮动实现圣杯布局

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>浮动-圣杯布局</title>
    <style>
      html,
      body {
        height: 100%;
        overflow: hidden; /* 默认显示一屏 */
        margin: 0;
        padding: 0;
      }
      header,
      footer {
        height: 50px;
        background-color: yellow;
        text-align: center;
        line-height: 50px;
      }
      .container {
        padding: 0 200px;
        overflow: hidden; /*触发BFC,清除浮动*/
      }
      .center,
      .left,
      .right {
        float: left;
      }
      .left,
      .right {
        width: 200px;
        height: 400px;
      }
      .center {
        width: 100%;
        background-color: aquamarine;
        height: 400px;
      }
      .left {
        background-color: blueviolet;
        margin-left: -100%;
        position: relative;
        left: -200px;
      }
      .right {
        background-color: red;
        margin-left: -200px;
        position: relative;
        left: 200px;
      }
    </style>
  </head>
  <body>
    <header>头部</header>
    <section class="container">
      <div class="center"></div>
      <div class="left"></div>
      <div class="right"></div>
    </section>
    <footer>底部</footer>
  </body>
</html>
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

思路:

  1. 在 html 中,先定义好 header 和 footer 的样式,使之横向撑满;
  2. 在 container 中的三列设为浮动,center 要放在最前面;
  3. 三列的左右两列分别定宽 200px 和 200px,中间部分 center 设置 100%撑满;
  4. 这样因为浮动的关系,center 会占据整个 container,左右两块区域被挤下去了;
  5. 接下来设置 left 的 margin-left: -100%;,让 left 回到上一行最左侧;
  6. 但这会把 center 给遮住了,所以这时给外层的 container 设置padding: 0 200px;,给 left 空出位置;
  7. 这时 left 并没有在最左侧,给 left 盒子设置相对定位和left: -200px;,把 left 拉回最左侧;
  8. 同样的,对于 right 区域,设置margin-left: -200px;,把 right 拉回第一行;
  9. 这时右侧空出了 200px 的空间,所以最后给给 right 盒子设置相对定位和right: -220px;把 right 区域拉到最右侧就行了。

浮动实现双飞翼

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>浮动实现双飞翼</title>
    <style>
      html,
      body {
        height: 100%;
        overflow: hidden;
        margin: 0;
        padding: 0;
      }
      .container,
      .left,
      .right {
        float: left;
      }
      .container {
        width: 100%;
      }
      .container .center {
        height: 400px;
        background-color: aquamarine;
        margin: 0 200px;
      }
      .left,
      .right {
        width: 200px;
        height: 400px;
      }
      .left {
        background-color: blueviolet;
        margin-left: -100%;
      }
      .right {
        background-color: brown;
        margin-left: -200px;
      }
    </style>
  </head>
  <body>
    <!-- 双飞翼布局 center 最先且 多包一层 div -->
    <div class="container">
      <div class="center"></div>
    </div>
    <div class="left"></div>
    <div class="right"></div>
  </body>
</html>
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

4. 实现一个三角形

利用盒模型的 border 属性上下左右边框交界处会呈现出平滑的斜线这个特点,通过设置不同的上下左右边框宽度或者颜色即可得到三角形或者梯形。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>实现一个三角形</title>
    <style>
      div {
        width: 0;
        height: 0;
        border: 100px solid;
        margin-bottom: 10px;
      }
      .box {
        border-color: transparent transparent greenyellow transparent; /*等腰直角*/
      }
      .box2 {
        border-color: transparent transparent greenyellow green; /*等腰直角*/
      }
      .box3 {
        border-width: 100px 80px;
        border-color: transparent transparent yellow transparent; /*等腰三角形*/
      }
      .box4 {
        border-width: 100px 173px; /*173由勾股定理计算得来*/
        border-color: greenyellow yellow greenyellow red; /*等边三角形*/
      }
      .box5 {
        border-width: 100px 90px 80px 70px;
        border-color: transparent transparent yellow transparent; /*其它三角形*/
      }
    </style>
  </head>
  <body>
    <!-- 通过border实现 -->
    <div class="box"></div>
    <div class="box2"></div>
    <div class="box3"></div>
    <div class="box4"></div>
    <div class="box5"></div>
  </body>
</html>
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

利用 CSS3 的 clip-path 属性

.triangle {
  width: 30px;
  height: 30px;
  background: red;
  clip-path: polygon(
    0px 0px,
    0px 30px,
    30px 0px
  ); // 将坐标(0,0),(0,30),(30,0)连成一个三角形
  transform: rotate(225deg); // 旋转225,变成下三角
}
1
2
3
4
5
6
7
8
9
10
11

实现梯形

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>梯形</title>
    <style>
      .box {
        width: 100px;
        border-bottom: 100px solid greenyellow;
        border-left: 80px solid transparent;
        border-right: 80px solid transparent;
      }
    </style>
  </head>
  <body>
    <div class="box"></div>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

5. 实现正方形

使用 css 实现一个宽高自适应的正方形:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>正方形</title>
    <style>
      div {
        margin-bottom: 10px;
      }
      /* 都是像对于屏幕宽度的比例 */
      .square1 {
        width: 10%;
        height: 10vw;
        background-color: aquamarine;
      }
      /*或者 vw会把视口的宽度平均分为100份*/
      .square1 {
        width: 10vw;
        height: 10vw;
        background: red;
      }
      /* 利用margin/padding的百分比计算是相对于其父元素的width属性 */
      .square2 {
        width: 20%;
        height: 0; /*防止内容撑开多余的高度*/
        padding-top: 20%;
        background-color: blueviolet;
      }
      /* 通过子元素 margin */
      .square3 {
        width: 30%;
        overflow: hidden; /* 触发 BFC */
        background-color: brown;
      }
      .square3::after {
        /* 高度相对于 square3 的 width */
        content: '';
        display: block;
        margin-top: 100%;
      }
    </style>
  </head>
  <body>
    <div class="square1">square1</div>
    <div class="square2"></div>
    <div class="square3"></div>
  </body>
</html>
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

6. 画一个扇形

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>画一个扇形</title>
    <style>
      div {
        margin-bottom: 10px;
      }
      /* 通过 border 和 border-radius 实现 1/4 圆 */
      .sector1 {
        width: 0;
        height: 0;
        border: 100px solid;
        /* border-color: aquamarine yellow yellowgreen goldenrod; */
        border-color: transparent transparent yellowgreen transparent;
        border-radius: 50%;
      }
      /* .sector2 {
            width: 200px;
            height: 200px;
            border: 100px solid;
            border-color: aquamarine yellow yellowgreen goldenrod;
            border-color: transparent transparent yellowgreen transparent;
            border-radius: 50%;
        } */
      /* 类似三角形的做法加上父元素 overflow: hidden; 也可以实现任意弧度圆 */
      .sector2 {
        height: 100px;
        width: 200px;
        border-radius: 100px 100px 0 0;
        overflow: hidden;
      }
      .sector2::after {
        content: '';
        display: block;
        height: 0;
        width: 0;
        border-style: solid;
        border-width: 100px 58px 0;
        border-color: tomato transparent;
        transform: translate(42px, 0);
      }
      /* 通过子元素 rotateZ 和父元素 overflow: hidden 实现任意弧度扇形(此处是60°) */
      .sector3 {
        height: 100px;
        width: 100px;
        border-top-right-radius: 100px;
        overflow: hidden;
        /* background: gold; */
      }
      .sector3::after {
        content: '';
        display: block;
        height: 100px;
        width: 100px;
        background: tomato;
        transform: rotateZ(-30deg);
        transform-origin: left bottom;
      }
      /* 通过 skewY 实现一个60°的扇形 */
      .sector4 {
        height: 100px;
        width: 100px;
        border-top-right-radius: 100px;
        overflow: hidden;
      }
      .sector4::after {
        content: '';
        display: block;
        height: 100px;
        width: 100px;
        background: tomato;
        transform: skewY(-30deg);
        transform-origin: left bottom;
      }
      /* 通过渐变设置60°扇形 */
      .sector5 {
        height: 200px;
        width: 200px;
        background: tomato;
        border-radius: 50%;
        background-image: linear-gradient(150deg, transparent 50%, #fff 50%),
          linear-gradient(90deg, #fff 50%, transparent 50%);
      }
    </style>
  </head>
  <body>
    <div class="sector1"></div>
    <div class="sector2"></div>
    <div class="sector3"></div>
    <div class="sector4"></div>
    <div class="sector5"></div>
  </body>
</html>
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
88
89
90
91
92
93
94
95
96
97

7. 实现水平垂直居中

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>实现水平垂直居中</title>
    <style>
      .container {
        width: 500px;
        height: 500px;
        background-color: bisque;
        position: relative;
        margin-bottom: 10px;
      }
      .child {
        width: 100px;
        height: 100px;
        background-color: aquamarine;
      }
      /*flex实现*/
      .container1 {
        display: flex;
        justify-content: center;
        align-items: center;
      }
      /*绝对定位 + transform child宽高可以未知*/
      .child2 {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
      }
      /*
            绝对定位 + margin
            缺点:需要知道子盒子的宽高
        */
      .child3 {
        position: absolute;
        left: 50%;
        top: 50%;
        /* margin-left: -50%; 这里不能用-50%,会按照父元素的宽度的50%来计算 */
        margin-left: -50px;
        margin-top: -50px;
      }
      /*绝对定位 + margin 居中 前提是子元素宽高已知*/
      .child4 {
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
        margin: auto;
      }
    </style>
  </head>
  <body>
    <div class="container container1">
      <div class="child">flex实现</div>
    </div>
    <div class="container">
      <div class="child child2">transform</div>
    </div>
    <div class="container">
      <div class="child child3">margin</div>
    </div>
    <div class="container">
      <div class="child child4">margin居中</div>
    </div>
  </body>
</html>
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

8. 清除浮动

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>清除浮动</title>
    <style>
      .parent {
        background-color: bisque;
        margin-bottom: 10px;
      }
      /*触发BFC*/
      .parent1 {
        overflow: hidden;
      }
      /*伪元素实现*/
      .parent2::after {
        content: '';
        display: block;
        clear: both;
      }
      .child {
        width: 100px;
        height: 100px;
        background-color: aquamarine;
        float: left;
      }
    </style>
  </head>
  <body>
    <div class="parent parent1">
      <div class="child"></div>
    </div>
    <div class="parent parent2">
      <div class="child"></div>
    </div>
  </body>
</html>
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

9. 实现一个对话框

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>弹出框</title>
    <style>
      html,
      body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      .bg {
        height: 100%;
        background-color: aquamarine;
        font-size: 60px;
        text-align: center;
      }
      .dialog {
        position: fixed;
        z-index: 999;
        background: rgba(0, 0, 0, 0.5);
        top: 0;
        bottom: 0;
        right: 0;
        left: 0;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .dialog .content {
        background-color: #fff;
        width: 500px;
        min-height: 300px;
        border-radius: 10px;
        border: 1px solid #ebeef5;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
      }
    </style>
  </head>
  <body>
    <div class="bg">页面内容呢</div>
    <div class="dialog">
      <div class="content">这里是对话框</div>
    </div>
  </body>
</html>
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

10. 导航栏实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>水平滚动导航栏</title>
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
      }
      /* .nav {
            display: flex;
            padding: 3px;
            border: 1px solid #000;
            overflow-x: auto;
        }
        .item {
            height: 30px;
            flex: 0 0 200px;
            background: gray;
            margin-right: 5px;
            text-align: center;
            line-height: 30px;
        } */
      .nav {
        width: auto;
        height: 30px;
        padding: 3px;
        border: 1px solid #000;
        overflow-x: auto;
        white-space: nowrap;
      }
      .nav::-webkit-scrollbar {
        display: none;
      }
      .item {
        display: inline-block;
        width: 200px;
        height: 30px;
        margin-right: 5px;
        background: gray;
      }
    </style>
  </head>
  <body>
    <div class="nav">
      <div class="item">item1</div>
      <div class="item">item2</div>
      <div class="item">item3</div>
      <div class="item">item4</div>
      <div class="item">item5</div>
      <div class="item">item6</div>
      <div class="item">item7</div>
      <div class="item">item8</div>
      <div class="item">item9</div>
    </div>
  </body>
</html>
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

js 手写代码

1. Object.create 实现

Object.create(obj)基本原理:接收一个 obj 对象,然后创建一个空对象,让空对象的__proto__指向 obj,最终返回这个空对象。 实现思路:

  1. 接收一个要作为原型的对象;
  2. 返回一个原型指向该对象的空对象
Object.create = function (obj) {
  let o = {};
  // 理论上是可以的,但是__proto__在ie中不支持
  o.__proto__ = obj;
  return o;
};
1
2
3
4
5
6

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto

// 模拟Object.create
Object.create = function (proto) {
  function Fn() {}
  Fn.prototype = proto;
  // new Fn()就是一个空对象,因为没有任何私有属性和方法
  return new Fn();
};
1
2
3
4
5
6
7

2. new 操作符实现

new 操作符做了什么:

  1. 创建了一个全新的对象。
  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
  3. 使得 this 指向新创建的对象。
  4. 通过 new 创建的每个对象将最终被[[Prototype]]链接到这个函数的 prototype 对象上。
  5. 如果函数没有返回对象类型 Object(包含 Functoin,Array,Date,RegExg,Error),那么 new 表达式中的函数调用会自动返回这个新的对象。

步骤:

  1. 新建一个对象
  2. 将新建对象的原型指向构造函数的原型
  3. 获取构造函数参数并执行构造函数获取其结果
  4. 根据构造函数执行结果返回对应的对象
function myNewOperator(ctor) {
  if (typeof ctor !== 'function') {
    throw new Error('newOperator function the first param must be a function');
  }
  // new.target是指向构造函数的
  myNewOperator.target = ctor;
  // 新建一个对象,并指向构造函数原型
  const newObj = Object.create(ctor.prototype);
  const args = Array.prototype.slice.call(arguments, 1);
  // 获取构造函数的结果
  const res = ctor.call(newObj, ...args);
  // 判断构造函数返回结果是否为函数或者对象类型
  // 返回基本数据类型的时不受影响
  const isObject = typeof res === 'object' && res !== null;
  const isFunction = typeof res === 'function';
  // 构造函数结果为函数或者对象类型,直接返回该结果
  if (isObject || isFunction) {
    return res;
  }
  // 否则返回新建的实例对象
  return newObj;
}

// 测试
function Person(name, age) {
  this.name = name;
  this.age = age;
  // return 123;
  // return {};
}

const p = myNewOperator(Person, 'lisi', 12);
console.log(p); // Person { name: 'lisi', age: 12 }
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
function myNew(fn, ...args) {
  if (typeof fn !== 'function') {
    throw new Error('newOperator function the first param must be a function');
  }
  myNew.target = fn;
  const obj = Object.create(fn.prototype);
  const res = fn.call(obj, ...args);
  // 如果构造函数返回函数或者对象,则返回对应的函数或者对象,而不是返回新创建的obj
  if (
    typeof res !== null &&
    (typeof res === 'object' || typeof res === 'function')
  ) {
    return res;
  }
  return obj;
}

function Person(name, age) {
  this.name = name;
  this.age = age;
  // return {name: '111'};
  // return function() {console.log(123)};
}

// const p = new Person('lisi', 12);
// console.log(p);

const p = myNew(Person, 'wangwu', 12);
console.log(p);
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

具体参考-JavaScript 深入之 new 的模拟实现

js 继承实现

// ES5
function Parent(name, age) {
  this.name = name;
  this.age = age;
}
Parent.prototype.say = function () {
  console.log('I am' + this.name);
};

function Child(name, age, sex) {
  Parent.call(this, name, age);
  this.sex = sex;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ES6
class Parent {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

class Child extends Parents {
  constructor(name, age, sex) {
    super(name, age);
    this.sex = sex; // 必须先调用super,才能使用this
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 参考 You Dont Know JavaScript 上卷
// 基类
function Base() {}
// 派生类
function Derived() {
  Base.call(this);
}
// 将派生类的原型的原型链挂在基类的原型上
Object.setPrototypeOf(Derived.prototype, Base.prototype);
1
2
3
4
5
6
7
8
9

实现 instanceOf

instanceOf 的作用:判断一个对象是不是某个类型的实例。其原理就是通过原型链查找,一直向上查找左侧(实例对象)的隐式原型__ptoto__是否等于右侧(构造函数)的显式原型,原型链的尽头是 null,没找到就返回 false。

/**
 * 模拟实现instanceof
 * @param {*} left 实例对象(取隐式原型)
 * @param {*} right 构造函数(取显示原型)
 */
function myInstanceof(left, right) {
  let proto = left.__proto__;
  const prototype = right.prototype;
  while (true) {
    if (proto === null) return false;
    if (proto === prototype) return true;
    proto = proto.__proto__;
  }
}

class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}
const p = new Person('lisi');
console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
console.log(myInstanceof(p, Person)); // true
console.log(myInstanceof(p, Object)); // 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

更好的写法:使用 getInstanceof 方法代替__proto__。采用Object.getPrototypeOf的写法:

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left);
  let prototype = right.prototype;
  while (true) {
    if (proto === null) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}
1
2
3
4
5
6
7
8
9

实现 call/apply/bind

  1. 三者都可以改变函数的 this 对象指向。
  2. 三者第一个参数都是 this 要指向的对象,如果没有这个参数,默认指向全局 window。
  3. 三者都可以传参,但是 apply 是数组,而 call 是单个参数有顺序的传入。
  4. bind 是返回对应函数,便于稍后调用;apply 和 call 都是立即执行,返回对应函数的执行结果。

call

call 做了什么:

  • 将函数设为对象(this 上下文)的属性
  • 执行&删除这个函数
  • 指定 this 到函数并传人给定参数执行函数
  • 如果不传入参数,默认指向为 window
// context为绑定的this指向
// args是参数集合
Function.prototype.myCall = (context, ...args) => {
  if (typeof this !== 'function') {
    throw new TypeError('call must be called on a function');
  }
  context = context || window;
  context.fn = this;
  const res = context.fn(...args);
  delete context.fn;
  return res;
};
1
2
3
4
5
6
7
8
9
10
11
12
// 实现call方法
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    // 调用call的如果不是函数则报错
    throw TypeError('not a function');
  }
  // 当call的第一个参数不存在或者为null,this指向window
  context = context || window;
  // 给context添加一个属性(将函数设为对象(this上下文)的属性)
  // getInfo.call(obj, 'lisi', 22) => obj.fn = getInfo
  context.fn = this;
  // 获取context后面参数
  const args = [...arguments].slice(1); // ['lisi', 22]
  const result = context.fn(...args);
  // 删除属性fn
  delete context.fn;
  return result;
};

// 验证
const obj = {
  name: 'lisi',
  age: 22,
};

function getInfo() {
  console.log(this.name + '--' + this.age);
}

getInfo.myCall(obj);
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

apply

Function.prototype.myApply = (context, args) => {
  if (typeof this !== 'function') {
    throw new TypeError('apply must be called on a function');
  }
  context = context || window;
  context.fn = this;
  let res;
  // 兼容不传递参数的情况
  if (args && args.length) {
    res = context.fn(...args);
  } else {
    res = context.fn();
  }
  delete context.fn;
  return res;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实现apply方法
Function.prototype.myApply = function (context) {
  if (typeof this !== 'function') {
    throw TypeError('not a function');
  }
  context = context || window;
  context.fn = this;
  let result;
  // 判断是否存在第二个参数,如果存在,则第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
};

const obj = {
  name: 'lisi',
  age: 22,
};

function getInfo() {
  console.log(this.name + '--' + this.age);
}

getInfo.myApply(obj, [1, 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

bind

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数
  2. 可以传入参数

实现 bind 要做什么?

  • 返回一个函数,绑定 this,传递预置参数。
  • bind 返回的函数可以作为构造函数使用。特别注意:作为构造函数时,提供的 this 值会被忽略,但是传入的参数依然有效。

bind 实现:

  • 箭头函数的 this 永远指向它所在的作用域
  • 函数作为构造函数用 new 关键字调用时,不应该改变其 this 指向,因为 new 绑定的优先级高于显示绑定和硬绑定
Function.prototype.myBind = function (context, ...bindArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('bind must be called on a function');
  }
  const self = this; // 获取绑定函数
  const fNOP = function () {};
  const fBound = function (...args) {
    const allArgs = [...bindArgs, ...args];
    return self.apply(this instanceof fBound ? this : context, allArgs);
  };
  fNOP.prototype = self.prototype;
  fBound.prototype = new fNOP(); // 将返回函数的原型指向绑定函数的原型,使得实例对象可以访问原型上的属性
  return fBound;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Bind must be called on a function');
  }
  // 获取除context之外的其它参数
  const args = Array.prototype.slice.call(arguments, 1),
    // 调用bind的函数
    self = this,
    fNOP = function () {},
    fBound = function () {
      // 获取fBound调用时的参数
      const bindArgs = Array.prototype.slice.call(arguments);
      // this instanceof fBound为true时表示fBound被当做构造函数调用了
      // 当作为构造函数时,this指向实例,self指向绑定函数,因为下面一句`fBound.prototype = this.prototype;`,已经修改了fBound.prototype为绑定函数的prototype,此时结果为true,当结果为true的时候,this指向实例。
      // 当作为普通函数时,this指向window,self指向绑定函数,此时结果为false,当结果为false的时候,this指向绑定的 context。
      return self.apply(
        this instanceof fBound ? this : context,
        args.concat(bindArgs) // 将fBound的参数和args进行结合
      );
    };
  // 修改返回函数的prototype为绑定函数的prototype,实例就可以继承函数原型中的值
  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  // 上面的一行代码使fBound.prototype是fNOP的实例
  // 因此,返回的fBound函数若作为构造函数调用,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
  return fBound;
};
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

需要注意:构造函数效果的优化实现,不直接将fbound.prototype = this.prototype,因为假如我们直接修改 fbound.prototype 的时候,也会直接修改函数的 prototype。这个时候,我们可以通过一个空函数来进行中转。具体参考-JavaScript 深入之 bind 的模拟实现

应用场景

// 求数组的最大值和最小值
const arr = [1, 2, 23, 34];
console.log(Math.max.apply(null, arr)); // 34
console.log(Math.min.apply(null, arr)); // 1
1
2
3
4
// 将类数组转化为数组
const arr = Object.prototype.slice.call(arrayLike);
1
2
// 判断变量类型
function isArray(obj) {
  return Object.prototype.toString.call(obj) === '[object Array]';
}
1
2
3
4
// 对象冒充实现继承
function Parent(name, age) {
  this.name = name;
  this.age = age;
}

function Child(name, age) {
  Parent.call(this, name, age);
}
1
2
3
4
5
6
7
8
9

实现防抖函数(debounce)和节流函数(throttle)

防抖与节流函数是一种最常用的高频触发优化方式,能对性能有较大的帮助。

  • 防抖 (debounce): 将多次高频操作优化为只在最后一次执行s,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
  • 节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms 执行一次即可。

原理都是利用闭包保存变量。

  • 防抖是任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行,一般用于输入框实时校验或者搜索;
  • 节流是规定函数在指定的时间间隔内只执行一次,一般用于 scroll 事件。

防抖函数(debounce)

防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。

image-20200618151335093
/*
 * 函数调用后不会被立即执行,之后连续delay时间段没有调用才会执行
 * 用法:用户输入校验
 */
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer); // 如果在定时器未执行期间又被调用 该定时器将被清除 并重新等待 delay 秒
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(fn, delay) {
  let timer = null;
  return function () {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}
1
2
3
4
5
6
7
8
9

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次;
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能。

生存环境请用lodash.debounce

节流函数(throttle)

节流函数原理:规定在单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

/*
 * 节流函数 限制函数在指定时间段只能被调用一次
 * 用法 比如防止用户连续执行一个耗时操作 对操作按钮点击函数进行节流处理
 */
function throttle(fn, delay) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        fn(...args);
      }, delay);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 节流函数
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    // delay期间只能执行一次
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, cycle) {
  let start = Date.now();
  let now;
  let timer;
  return function () {
    now = Date.now();
    clearTimeout(timer);
    if (now - start >= cycle) {
      fn.apply(this, arguments);
      start = now;
    } else {
      timer = setTimeout(() => {
        fn.apply(this, arguments);
      }, cycle);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

写一个通用的事件侦听器函数

const EventUtils = {
  // 视能力分别使用dom0||dom2||IE方式 来绑定事件
  // 添加事件
  addEvent: function (element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent('on' + type, handler);
    } else {
      element['on' + type] = handler;
    }
  },

  // 移除事件
  removeEvent: function (element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent('on' + type, handler);
    } else {
      element['on' + type] = null;
    }
  },

  // 获取事件目标
  getTarget: function (event) {
    return event.target || event.srcElement;
  },

  // 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
  getEvent: function (event) {
    return event || window.event;
  },

  // 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
  stopPropagation: function (event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  },

  // 取消事件的默认行为
  preventDefault: function (event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  },
};
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

函数 currying

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术,是高阶函数的一种用法。比如求和函数 add(1,2,3), 经过柯里化后变成 add(1)(2)(3)

function currying(fn, ...args) {
  if (fn.length <= args.length) {
    return fn(...args);
  }
  return function (...args1) {
    return currying(fn, ...args, ...args1);
  };
}
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 6
var curryingAdd = currying(add);
curryingAdd(1)(2)(3); // 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14

实现一个 function sum 达到一下目的:

sum(1, 2, 3, 4, 5).valueOf(); //15
sum(1, 2, 3, 4)(5).valueOf(); //15
sum(1, 2, 3)(4)(5).valueOf(); //15
sum(1, 2)(3)(4)(5).valueOf(); //15
sum(1, 2)(3, 4)(5).valueOf(); // 15
1
2
3
4
5
/**
 *分析:
 * 1. sum可以分步传参
 * 2. sum返回一个函数
 * 3. 调用sum的valueOf方法返回值,需要重写valueOf()方法求和。
 */
function sum() {
  var args = [].slice.apply(arguments);
  var fn = function () {
    args.push.apply(args, [].slice.apply(arguments));
    return arguments.callee;
  };
  fn.valueOf = function () {
    return args.reduce(function (pre, cur) {
      return pre + cur;
    });
  };
  return fn;
}

console.log(sum(1, 2, 3, 4, 5).valueOf());
console.log(sum(1, 2, 3, 4)(5).valueOf());
console.log(sum(1, 2, 3, 4)(5).valueOf());
console.log(sum(1, 2)(3)(4)(5).valueOf());
console.log(sum(1, 2)(3, 4)(5).valueOf());
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

手写 jsonp

const jsonp = ({ url, params, callbackName }) => {
  const generateURL = () => {
    let dataStr = '';
    for (let key in params) {
      dataStr += `${key}=${params[key]}&`;
    }
    dataStr += `callback=${callbackName}`;
    return `${url}?${dataStr}`;
  };
  return new Promise((resolve, reject) => {
    // 初始化回调函数名称
    callbackName = callbackName || Math.random().toString.replace(',', '');
    // 创建 script 元素并加入到当前文档中
    let scriptEle = document.createElement('script');
    scriptEle.src = generateURL();
    document.body.appendChild(scriptEle);
    // 绑定到 window 上,为了后面调用
    window[callbackName] = (data) => {
      data ? resolve(data) : reject('error');
      // script 执行完了,成为无用元素,需要清除
      document.body.removeChild(scriptEle);
      window[cbFn] = null;
    };
  });
};
// 后端服务
const express = require('express');
const app = express();
app.get('/', function (req, res) {
  let { a, b, callback } = req.query;
  console.log(a); // 1
  console.log(b); // 2
  // 注意哦,返回给script标签,浏览器直接把这部分字符串执行
  res.end(`${callback}('数据包')`);
});
app.listen(3000);
// 使用
jsonp({
  url: 'http://localhost:3000',
  params: {
    a: 1,
    b: 2,
  },
}).then((data) => {
  // 拿到数据进行处理
  console.log(data); // 数据包
});
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
// 全局回调函数
// globalFn函数将会被调用并传入后台返回的数据
function globalFn(data) {
  console.log('通过jsonp获取后台数据:', data);
}

/**
 * 通过动态创建一个script标签发送一个 get 请求
 * 并利用浏览器对 <script> 不进行跨域限制的特性绕过跨域问题
 */
(function jsonp() {
  const head = document.getElementsByTagName('head')[0];
  const script = document.createElement('script');
  script.src =
    'http://domain:port/testJSONP?name=lisi&age=20&callback=globalFn'; // 设置请求地址
  head.appendChild(script); // 这一步会发送请求
})();

// 后台代码
// 因为是通过script标签调用的 后台返回的相当于一个js文件
// 根据前端传入的 callback 的函数名直接调用该函数
function testJSONP(callback, name, age) {
  const data = { name, age };
  return `${callback}(${data})`;
}
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

手写原生 ajax

// Asynchronous Javascript And XML
function ajax(options) {
  // 选项
  var method = options.method || 'GET',
    params = options.params,
    data = options.data,
    url =
      options.url +
      (params
        ? '?' +
          Object.keys(params)
            .map((key) => key + '=' + params[key])
            .join('&')
        : ''),
    async = options.async === false ? false : true,
    success = options.success,
    headers = options.headers;

  var request;
  if (window.XMLHttpRequest) {
    request = new XMLHttpRequest();
  } else {
    request = new ActiveXObject('Microsoft.XMLHTTP');
  }

  request.onreadystatechange = function () {
    /**
    readyState:
      0: 请求未初始化
      1: 服务器连接已建立
      2: 请求已接收
      3: 请求处理中
      4: 请求已完成,且响应已就绪

    status: HTTP 状态码
    **/
    if (request.readyState === 4 && request.status === 200) {
      success && success(request.responseText);
    }
  };

  request.open(method, url, async);
  if (headers) {
    Object.keys(headers).forEach((key) =>
      request.setRequestHeader(key, headers[key])
    );
  }
  method === 'GET' ? request.send() : request.send(data);
}
// e.g.
ajax({
  method: 'GET',
  url: '...',
  success: function (res) {
    console.log('success', res);
  },
  async: true,
  params: {
    p: 'test',
    t: 666,
  },
  headers: {
    'Content-Type': 'application/json',
  },
});
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

数组方法实现

reduce 的实现

Array.prototype.myReduce = function (cb, initialValue) {
  const { length } = this;
  let result = initialValue;
  let startIndex = 0;
  // 检查initialValue是否为undefined。如果是,则将数组的第一项设置为初始值,并将startIndex设置为1。
  if (initialValue === undefined) {
    result = this[0];
    startIndex = 1;
  }
  for (let i = startIndex; i < length; i++) {
    // 每次迭代,reduce方法都将回调函数的结果保存在累加器中,然后在下一次迭代中使用
    result = cb(result, this[i], i, this);
  }
  return result;
};

const arr = [1, 2, 3, 4, 5];
// accumulator为初始值,没有初始值的话为数组的第一个元素
const res = arr.myReduce(
  (accumulator, currentValue, currentIndex, sourceArray) => {
    return accumulator + currentValue;
  }
);
const res2 = arr.myReduce(
  (accumulator, currentValue, currentIndex, sourceArray) => {
    return accumulator + currentValue;
  },
  10
);

console.log(res); // 15
console.log(res2); // 25
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

实现 generator 的自动执行器

function func(data, cb) {
  console.log(data);
  console.log(cb); // next函数
  cb();
}

function* generatorTest() {
  let a = yield Promise.resolve(1);
  console.log(a);
  let b = yield Promise.resolve(2);
  console.log(b);
  yield func.bind(null, a + b);
}

function run(gen) {
  const g = gen();
  function next(data) {
    const result = g.next(data);
    // console.log(result);
    if (result.done) return result.value;
    // console.log(result.value);
    if (result.value instanceof Promise) {
      result.value.then((data) => next(data));
    } else {
      result.value(next);
    }
  }
  next();
}

run(generatorTest);
/**
output:
1
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

手写 async/await

我们都知道 async 是 generaotr 的语法糖。generator 函数是不会自动执行的,每一次调用它的 next 方法,会停留在下一个 yield 的位置。利用这个特性,我们只要编写一个自动执行的函数,就可以让这个 generator 函数完全实现 async 函数的功能。

const getData = () =>
  new Promise((resovle, reject) => {
    setTimeout(() => {
      resovle('data');
    }, 1000);
  });

function* testGen() {
  const data = yield getData();
  console.log('data111:', data);
  const data2 = yield getData();
  console.log('data222:', data2);
  return 'success';
}

// const gen = testGen();
// console.log(gen.next());
// console.log(gen.next());
// console.log(gen.next());

// 其实就是写一个自动执行generator函数的函数
// 基于generator实现async函数,接收一个generator函数
/**
 * @param  {any} genFn
 * @return
 */
function generatorToAsync(genFn) {
  return function () {
    const gen = genFn.apply(this, arguments);
    return new Promise((resovle, reject) => {
      function step(key, args) {
        let genResult;
        try {
          genResult = gen[key](args);
        } catch (error) {
          return reject(error);
        }
        const { done, value } = genResult;
        // 执行结束
        if (done) {
          return resovle(value);
        } else {
          return Promise.resolve(value).then(
            (val) => step('next', val),
            (err) => step('throw', err)
          );
        }
      }
      step('next');
    });
  };
}

const test = generatorToAsync(testGen);
test().then((data) => console.log(data));
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

手写 async await 的 20 行最简实现

深拷贝和浅拷贝

浅拷贝

function simpleClone(obj) {
  var result = {};
  for (var i in obj) {
    result[i] = obj[i];
  }
  return result;
}
1
2
3
4
5
6
7

简单版深拷贝

const newObj = JSON.parse(JSON.stringify(oldObj));
1

局限性 :

  1. 无法实现函数、RegExp 等特殊对象的克隆
  2. 会抛弃对象的 constructor,所有的构造函数会指向 Object
  3. 对象有循环引用,会报错

深拷贝(递归拷贝)

function deepClone(obj) {
  let result;
  if (typeof obj == 'object') {
    result = isArray(obj) ? [] : {};
    for (let i in obj) {
      // isObject(obj[i]) ? deepClone(obj[i]) : obj[i]
      result[i] =
        isObject(obj[i]) || isArray(obj[i]) ? deepClone(obj[i]) : obj[i];
    }
  } else {
    result = obj;
  }
  return result;
}
function isObject(obj) {
  return Object.prototype.toString.call(obj) == '[object Object]';
}
function isArray(obj) {
  return Object.prototype.toString.call(obj) == '[object Array]';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

完整版深拷贝

// 递归拷贝
// hash = new WeakMap 解决循环引用问题
// WeakMap 弱引用,不能用Map,会导致内存泄露
function deepClone(value, hash = new WeakMap()) {
  // 先把特殊情况全部过滤掉
  // null == undefined // true
  // 排除null和undefined
  if (value == null) {
    // null和undefined 是不需要拷贝的,直接返回
    return value;
  }
  if (value instanceof RegExp) {
    // 处理正则
    return new RegExp(value);
  }
  if (value instanceof Date) {
    // 处理日期
    return new Date(value);
  }
  // 函数是不需要拷贝的
  // 排除不是对象类型,包括函数和基本数据类型
  if (typeof value !== 'object') {
    return value;
  }
  // 根据constructor来区分对象和数组
  let obj = new value.constructor();
  // 说明是一个对象类型
  if (hash.get(value)) {
    // 有拷贝的就直接返回
    return hash.get(value);
  }
  hash.set(value, obj); // 制作一个映射表,解决循环引用问题
  // 区分对象和数组
  for (let key in value) {
    // 不拷贝原型链上的属性
    if (value.hasOwnProperty(key)) {
      // 递归拷贝
      obj[key] = deepClone(value[key], hash);
    }
  }
  return obj;
}
// let obj = {name: 'lisi', age: {num: 10}};
let obj = [[1, 2, 3]];
let obj1 = deepClone(obj);
// obj.age.num = 100;
console.log(obj); // { name: 'lisi', age: { num: 100 } }
// obj1.age.num = 1000;
console.log(obj1); // { name: 'lisi', age: { num: 1000 } }

let o = {};
o.x = o; // 循环引用,死循环了
let o1 = deepClone(o); // 如果这个对象拷贝过了,就返回那个拷贝的结果就可以了
console.log(o1); // RangeError: Maximum call stack size exceeded
// { x: [Circular] }

// 判断类型 typeof instanceof constructor
// Object.prototype.toString.call()

// 对象深拷贝需要注意的问题
// 1. 属性为函数
// 2. 属性为null或者undefined
// 3. 属性为Date
// 4. 属性为正则
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

3. 观察者模式实现

class Subject {
  constructor(name) {
    this.name = name;
    this.observers = []; // 存放观察者
    this.state = '心情很美丽';
  }
  // 被观察者添加观察者的方法(观察者和被观察者建立关系)
  attach(observer) {
    this.observers.push(observer);
  }
  // 更改被观察者的状态
  setState(newState) {
    this.state = newState;
    this.notify(); // 被观察者状态发生变化时,通知观察者
  }
  notify() {
    this.observers.forEach((o) => {
      o.update(this.name, this.state);
    });
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  update(subject, newState) {
    console.log(`${this.name}说:${subject}${newState}`);
  }
}
// 创建一个被观察者
const sub = new Subject('小公主');
// 创建两个观察者
const observer1 = new Observer('爸爸');
const observer2 = new Observer('妈妈');
// 被观察者添加观察者
sub.attach(observer1);
sub.attach(observer2);
// 被观察者更新状态,并通知观察者
sub.setState('不开心了');
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

Vue 的依赖收集就是基于观察者模式(基于 watcher)。观察者模式包含发布订阅模式。

4. 发布订阅模式实现

实现 eventEmitter

观察者模式是我们工作中经常能接触到的一种设计模式。用过 jquery 的应该对这种设计模式都不陌生。eventEmitter 是 node 中的核心,主要方法包括on、emit、off、once

class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 订阅
  on(name, cb) {
    if (!this.events[name]) {
      this.events[name] = [cb];
    } else {
      this.events[name].push(cb);
    }
  }
  // 发布
  emit(name, ...arg) {
    if (this.events[name]) {
      this.events[name].forEach((fn) => {
        fn.call(this, ...arg);
      });
    }
  }
  // 取消订阅
  off(name, cb) {
    if (this.events[name]) {
      this.events[name] = this.events[name].filter((fn) => {
        return fn !== cb;
      });
    }
  }
  // 一个事件订阅多次但只执行一次
  once(name, fn) {
    var onlyOnce = () => {
      fn.apply(this, arguments);
      this.off(name, onlyOnce);
    };
    this.on(name, onlyOnce);
    return this;
  }
}
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

实现 Event (event bus)

event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。 简单版:

class EventEmeitter {
    constructor(){
        this._events = this._events || new Map(); //储存事件/回调键值对
        this._maxListeners = this._maxListeners || 1o;//设立监听上限
    }
}

//触发名为type的事件
EventEmeitter.prototype.emit = function(type,...args){
    let hander;
    //从储存事件键值对的this._events中获取对应事件回调函数
    handler = this._events.get(type);
    if (args.length > 0) {
        hander.apply(this,args);
    }else{
        handler.call(this);
    }
    return true;
};

//监听名为type事件
EventEmeitter.prototype.addListener = function(type,fn) {
    //将type事件以及对应的fn函数放入this._events中储存
    if (!this._events.get(type)) {
        this._events.set(type,fn);
    }
};
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

具体参考

class Events {
  constructor() {
    this._evnets = Object.create(null);
  }

  on(event, fn) {
    if (Array.isArray(event)) {
      for (let i = 0; i < event.length; i++) {
        this.on(evnet[i], fn);
      }
    } else {
      (this._evnets[event] || (this._evnets[event] = [])).push(fn);
    }
  }

  emit(event, ...args) {
    const cbs = this._evnets[event];
    if (cbs) {
      for (let i = 0; i < cbs.length; i++) {
        cbs[i].apply(this, args);
      }
    }
  }

  off(event, fn) {
    if (!arguments) {
      this._evnets = Object.create(null);
      return this;
    }
    if (Array.isArray(event)) {
      for (let i = 0; i < event.length; i++) {
        this.off(event[i], fn);
      }
      return this;
    }
    if (!fn) {
      this._evnets[event] = null;
      return this;
    }
    const cbs = this._evnets[event];
    let i = cbs.length;
    while (i--) {
      const cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }

  once(evnet, fn) {
    function on() {
      this.off(evnet, on);
      fn.apply(this, arguments);
    }
    on.fn = fn;
    this.on(evnet, on);
    return this;
  }
}
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

迭代器实现

function myIterator(items) {
  let i = 0;
  return {
    next() {
      const done = i >= items.length;
      const value = !done ? items[i++] : undefined;
      return {
        done, // 是否全部迭代完成
        value, // 返回迭代的值
      };
    },
  };
}

const interator = myIterator([1, 2, 3]);
interator.next();
interator.next();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

实现一个 sleep 函数

sleep 函数的作用就是延迟指定时间后再执行接下来的函数。

function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
  });
}
1
2
3
4
5

手写 Promise

class Promise {
  constructor(executor) {
    this.status = 'pending';
    this.value;
    this.reason;

    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'resolved';
        this.value = value;
      }
    };
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
      }
    };
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  then(onresolved, onrejected) {
    if (this.status === 'resolved') {
      onresolved(this.value);
    }
    if (this.status === 'rejected') {
      onrejected(this.reason);
    }
  }
}

// new Promise((resolve, reject) => {
//     // resolve(123);
//     reject('出错了');
// }).then(data => {
//     console.log(data);
// }, err => {
//     console.log(err);
// });

module.exports = Promise;
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

实现 Promise.all

Promise.all 需要等到所有的 promise 的状态都变成 fulfilled 之后才 resolve, 但只要有一个 promise 失败即返回失败的结果。

Promise.all = (promiseArr) => {
  if (!Array.isArray(promiseArr)) {
    throw new TypeError('arguments must be a array');
  }
  return new Promise((resolve, reject) => {
    const resArr = [];
    let count = 0;
    function processData(index, data) {
      resArr[index] = data;
      count++;
      if (count === promiseArr.length) {
        resolve(resArr);
      }
    }
    for (let i = 0; i < promiseArr.length; i++) {
      Promise.resolve(promiseArr[i]).then((data) => {
        processData(i, data);
      }, reject); // 有一个失败了就走reject
    }
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

promise.retry

promise.retry 的作用是执行一个函数,如果不成功最多可以尝试 times 次。传参需要三个变量,所要执行的函数,尝试的次数以及延迟的时间。

Promise.retry = function (fn, times, delay) {
  return new Promise(function (resolve, reject) {
    var error;
    var attempt = function () {
      if (times == 0) {
        reject(error);
      } else {
        fn()
          .then(resolve)
          .catch(function (e) {
            times--;
            error = e;
            setTimeout(function () {
              attempt();
            }, delay);
          });
      }
    };
    attempt();
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Promise.race

Promise.race = (promiseArr) => {
  for (let i = 0; i < promiseArr.length; i++) {
    Promise.resolve(promiseArr[i]).then(resolve, reject);
  }
};
1
2
3
4
5

将一个同步 callback 包装成 promise 形式

同步的 callback 用的最多的是在 node 的回调中,例如下面这种,包装完之后就可以愉快的使用 .then 了。

nodeGet(param, function (err, data) {});
// 转化成promise形式
function nodeGetAysnc(param) {
  return new Promise((resolve, reject) => {
    nodeGet(param, function (err, data) {
      if (err !== null) return reject(err);
      resolve(data);
    });
  });
}
1
2
3
4
5
6
7
8
9
10

按照上面的思路,即可写出通用版的形式。

function promisify(fn, context) {
  return (...args) => {
    return new Promise((resolve, reject) => {
      fn.apply(context, [
        ...args,
        (err, res) => {
          return err ? reject(err) : resolve(res);
        },
      ]);
    });
  };
}
1
2
3
4
5
6
7
8
9
10
11
12

Callback 与 Promise 间的桥梁 —— promisify

写一个函数,可以控制最大并发数

function handleFetchQueue(urls, max, callback) {
  const urlsCount = urls.length;
  const res = []; // 结果数组
  const requestQueue = []; // 请求队列
  let i = 0;
  function fetch(url) {
    return new Promise((resolve, reject) => {
      const time = Math.round(Math.random() * 1e4);
      console.log(`start request ${url}`);
      setTimeout(() => {
        resolve(url);
        console.log(`end request ${url}`);
      }, time);
    });
  }
  function handleRequest(url) {
    const req = fetch(url)
      .then((data) => {
        res.push(data);
      })
      .catch((err) => {
        res.push(err);
      })
      .finally(() => {
        // 不管请求成功还是失败都算一次请求,都要从请求队列中去掉一次请求记录
        if (res.length < urlsCount && i < urlsCount) {
          // 每次请求完成一次,就可以重新发起一个请求
          requestQueue.shift();
          handleRequest(urls[i++]);
        } else if (res.length === urlsCount) {
          typeof callback === 'function' && callback();
        }
      });
    // 如果请求队列中请求小于最大值
    if (requestQueue.push(req) < max) {
      handleRequest(urls[i++]);
    }
  }
  handleRequest(urls[i++]);
}

// 假设有15个请求,最高并发为10
const urls = Array.from({ length: 15 }, (item, index) => index);

handleFetchQueue(urls, 10, () => {
  console.log('run callback');
});
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

参考 微信小程序最一开始对并发数限制为 5 个,后来升级到 10 个,如果超过 10 个会被舍弃。后来微信小程序升级为不限制并发请求,但超过 10 个会排队机制。也就是当同时调用的请求超过 10 个时,小程序会先发起 10 个并发请求,超过 10 个的部分按调用顺序进行排队,当前一个请求完成时,再发送队列中的下一个请求。

function concurrentPoll() {
  this.tasks = [];
  this.max = 10;
  setTimeout(() => {
    this.run();
  }, 0);
}

concurrentPoll.prototype.addTask = function (task) {
  this.tasks.push(task);
};

concurrentPoll.prototype.run = function () {
  if (this.tasks.length === 0) {
    return;
  }
  // 最多10个
  var min = Math.min(this.tasks.length, max);
  for (var i = 0; i < min; i++) {
    // this.max--;
    // 队列,先进先出
    // shift会改变原数组
    var task = this.tasks.shift();
    task()
      .then((res) => {
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
      })
      .finally(() => {
        // this.max++;
        this.run();
      });
  }
};
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

lazyMan 实现

// 原题如下:
实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")输出:
Hi! This is Hank!LazyMan("Hank").sleep(10).eat("dinner")输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~LazyMan("Hank").eat("dinner").eat("supper")输出
Hi This is Hank!
Eat dinner~
Eat supper~LazyMan("Hank").sleepFirst(5).eat("supper")输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
 
以此类推。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这道题主要考察的是链式调用、任务队列、流程控制等。关键是用手动调用 next 函数来进行下次事件的调用,类似 express 中间件和 vue-router 路由的执行过程。

function _LazyMan(name) {
  this.nama = name;
  this.queue = [];
  this.queue.push(() => {
    console.log('Hi! This is ' + name + '!');
    this.next();
  });
  setTimeout(() => {
    this.next();
  }, 0);
}

_LazyMan.prototype.eat = function (name) {
  this.queue.push(() => {
    console.log('Eat ' + name + '~');
    this.next();
  });
  return this;
};

_LazyMan.prototype.next = function () {
  var fn = this.queue.shift();
  fn && fn();
};

_LazyMan.prototype.sleep = function (time) {
  this.queue.push(() => {
    setTimeout(() => {
      console.log('Wake up after ' + time + 's!');
      this.next();
    }, time * 1000);
  });
  return this;
};

_LazyMan.prototype.sleepFirst = function (time) {
  this.queue.unshift(() => {
    setTimeout(() => {
      console.log('Wake up after ' + time + 's!');
      this.next();
    }, time * 1000);
  });
  return this;
};

function LazyMan(name) {
  return new _LazyMan(name);
}
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

判断是否为 Promise 对象

function isPromise(p) {
  return p && typeof p.then === 'function' && typeof p.catch === 'function';
}
// 判断是否是Generator对象
function isGenerator(obj) {
  return (
    obj && 'function' === typeof obj.next && 'function' === typeof obj.throw
  );
}
1
2
3
4
5
6
7
8
9

一个 VDOM 对象,写一个 render 函数来让其变成一颗 DOM 树

/**
 * {
	type: 'h1',
	props: {
		className: "",
		style: "",
	},
	children: [] // 嵌套节点
}
*/

function setAttr(el, key, value) {
  switch (key) {
    case 'value': {
      if (el.tagName.toLowerCase === 'input') {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    }
    case 'style': {
      node.style.cssText = value;
      break;
    }
    default: {
      node.setAttribute(key, value);
      break;
    }
  }
}

function render(elObj) {
  const { type, props, children } = elObj;
  const el = document.createElement(type);
  for (let key in props) {
    setAttr(el, key, props[key]);
  }
  children.forEach((child) => {
    child =
      child instanceof Element ? render(child) : document.createTextNode(child);
    el.appendChild(child);
  });
  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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

7. 实现(5).add(3).minus(2),使其输出结果为 6

/**
 * 实现(5).add(3).minus(2),使其输出结果为6
 */
// 实现一个闭包
~(function () {
  // 每一个方法执行完,都要返回Number这个类的实例,这样才可以继续调用Number类原型中的方法(链式操作)
  function checkNum(n) {
    n = Number(n);
    return isNaN(n) ? 0 : n;
  }
  function add(n) {
    return this + n;
  }
  function minus(n) {
    return this - n;
  }
  ['add', 'minus'].forEach((method) => {
    // eval(method)得到对应函数定义
    Number.prototype[method] = eval(method);
  });
})();

console.log((5).add(3).minus(2)); // 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

实现一个 add 方法

/**
 * add(1)(2,3)(4, 5).value() => 10
 */

function add(...args) {
  let fn = (...innerArgs) => {
    return add.apply(this, args.concat(innerArgs));
  };
  fn.value = () => {
    return args.reduce((pre, cur) => {
      return pre + cur;
    });
  };
  return fn;
}

console.log(add(1)(2, 3)(4, 5).value());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

千分位分割

将'10000000000'形式的字符串,以每 3 位进行分隔展示'10.000.000.000'。

let str = '10000000000';

function fn(str) {
  let len = str.length - 1;
  let count = 0;
  let res = '';
  // 从后往前遍历
  while (len >= 0) {
    if (count === 3) {
      res = '.' + res;
      count = 0;
      continue;
    }
    res = str[len] + res;
    len--;
    count++;
  }
  return res;
}

console.log(fn(str));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

实现 new Queue()函数

new Queue()
  .task(1000, () => console.log(1))
  .task(2000, () => console.log(2))
  .task(3000, () => console.log(3))
  .start();
1
2
3
4
5

实现如下:

/**
 * new Queue().task(1000,()=>console.log(1)).task(2000,()=>console.log(2)).task(3000,()=>console.log(3)).start()
 */

class Queue {
  constructor() {
    this.tasks = [];
  }
  task(delay, cb) {
    this.tasks.push(() => {
      setTimeout(cb, delay);
    });
    return this;
  }
  start() {
    this.tasks.forEach((cb) => {
      cb();
    });
  }
}

new Queue()
  .task(1000, () => console.log(1))
  .task(2000, () => console.log(2))
  .task(3000, () => console.log(3))
  .start();
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
class Queue {
  constructor() {
    this.tasks = [];
  }
  task(delay, cb) {
    this.tasks.push({ cb, delay });
    return this;
  }
  start() {
    let time = 0;
    this.tasks.forEach(({ cb, delay }) => {
      time += delay;
      setTimeout(cb, time);
    });
  }
}

// 要求1秒后输出1 再过2秒输出2 再过3秒输出3
// 相当于1秒后输出1 3秒后输出2 6秒后输出3
new Queue()
  .task(1000, () => console.log(1))
  .task(2000, () => console.log(2))
  .task(3000, () => console.log(3))
  .start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

数据类型判断函数实现

Object.prototype.toString.call();
1

数组相关

70 个数组元素生成一个二维数组(reduce 实现)

reduce 用法:

  • 如果没有提供 initialValue,那么 reduce 的第一轮回调函数中的 prev 就是 arr[0],cur 就是 arr[1],index 就是 1;
  • 如果提供 initialValue,那么 reduce 的第一轮回调函数中的 prev 就是 initialValue,cur 就是 arr[0],index 就是 0。
const arr = Array.from({ length: 70 }, (k, v) => v + 1);

let temp = [];
let res = [];
arr.reduce((prev, cur, index) => {
  if (temp.length === 10) {
    res.push([...temp]);
    temp = [];
  }
  temp.push(cur);
}, 0); // 这里要有初始值,保证index从0开始,cur就是数组中每一项值

res.push(temp);

console.log(res);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

数组乱序(洗牌算法)

使用 Array 的 sort 方法:

const arr = [1, 2, 3, 4, 5];

Array.prototype.shuffle = function () {
  this.sort((a, b) => {
    return Math.random() - 0.5;
  });
};
arr.shuffle();
console.log(arr);
1
2
3
4
5
6
7
8
9

更高效的方法,时间复杂度为 n:

// const arr = [1, 2, 3, 4, 5, 6, 7, 8];
const arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'];

Array.prototype.shuffle = function () {
  const res = this;
  for (let i = arr.length; i; i--) {
    // 核心思想,遍历数组元素,在前i项中随机取一项,与第i项交换
    let j = Math.floor(Math.random() * i); // j是小于i的
    // i-1可能等于j
    [arr[i - 1], arr[j]] = [arr[j], arr[i - 1]];
  }
  return res;
};

console.log(arr.shuffle()); // [ 4, 8, 5, 7, 3, 6, 2, 1 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

「前端进阶」数组乱序 JavaScript 专题之乱序

实现简单的数组求和

eval([1, 2, 3].join('+'));
// 或者
arr.reduce();
1
2
3

实现简单的数组去重

// 基于hash
function removeDup(arr) {
  const result = [];
  const hashMap = {};
  for (let i = 0; i < arr.length; i++) {
    const temp = arr[i];
    if (!hashMap[temp]) {
      hashMap[temp] = true;
      result.push(temp);
    }
  }
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

基础数据结构数组:

[...new Set([...])]或Array.from(new Set(array))(set返回的结构不是数组类型)
1

对象数组类型数组:

// 方法:
//定义常量res,值为一个Map对象实例
const res = new Map();
//返回arr数组过滤后的结果,结果为一个数组
//过滤条件是,如果res中没有某个键,就设置这个键的值为1
return arr.filter((a) => !res.has(a) && res.set(a, 1));
// 方法2:利用reduce方法遍历数组,reduce第一个参数是遍历需要执行的函数,第二个参数是item的初始值
var obj = {};
arr = arr.reduce((item, next) => {
  obj[next.key] ? '' : (obj[next.key] = true && item.push(next));
  return item;
}, []);
1
2
3
4
5
6
7
8
9
10
11
12

map

let arr = ['1', '2', '3', '1', 'a', 'b', 'b'];
const unique = (arr) => {
  let obj = {};
  arr.map((value) => {
    obj[value] = 0;
  });
  return Object.keys(obj);
};
console.log(unique(arr)); // ['1','2','3','a','b']
1
2
3
4
5
6
7
8
9

filter

let arr = ['1', '2', '3', '1', 'a', 'b', 'b'];
const unique = (arr) => {
  return arr.filter((ele, index, array) => {
    return index === array.indexOf(ele);
  });
};
console.log(unique(arr)); // ['1','2','3','a','b']
1
2
3
4
5
6
7

set

let arr = ['1', '2', '3', '1', 'a', 'b', 'b'];
const unique = (arr) => {
  return [...new Set(arr)];
};
console.log(unique(arr)); // ['1','2','3','a','b']
1
2
3
4
5

数组交集/并集/差集

① 直接使用 filter、concat 来计算

var a = [1, 2, 3, 4, 5];
var b = [2, 4, 6, 8, 10];
//交集
var c = a.filter(function (v) {
  return b.indexOf(v) > -1;
});
//差集
var d = a.filter(function (v) {
  return b.indexOf(v) == -1;
});
//补集
var e = a
  .filter(function (v) {
    return !(b.indexOf(v) > -1);
  })
  .concat(
    b.filter(function (v) {
      return !(a.indexOf(v) > -1);
    })
  );
//并集
var f = a.concat(
  b.filter(function (v) {
    return !(a.indexOf(v) > -1);
  })
);
console.log('数组a:', a);
console.log('数组b:', b);
console.log('a与b的交集:', c);
console.log('a与b的差集:', d);
console.log('a与b的补集:', e);
console.log('a与b的并集:', f);
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

② 借助扩展运算符(...)以及 Set 的特性实现相关计算,代码也会更加简单些

var a = [1, 2, 3, 4, 5];
var b = [2, 4, 6, 8, 10];
console.log('数组a:', a);
console.log('数组b:', b);
var sa = new Set(a);
var sb = new Set(b);
// 交集
let intersect = a.filter((x) => sb.has(x));
// 差集
let minus = a.filter((x) => !sb.has(x));
// 补集
let complement = [
  ...a.filter((x) => !sb.has(x)),
  ...b.filter((x) => !sa.has(x)),
];
// 并集
let unionSet = Array.from(new Set([...a, ...b]));
console.log('a与b的交集:', intersect);
console.log('a与b的差集:', minus);
console.log('a与b的补集:', complement);
console.log('a与b的并集:', unionSet);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

数组中最大差值

map

let arr = [23, 4, 5, 2, 4, 5, 6, 6, 71, -3];
const difference = (arr) => {
  let min = arr[0],
    max = 0;
  arr.map((value) => {
    if (value < min) min = value;
    if (value > max) max = value;
  });
  return max - min;
};
console.log(difference(arr)); // 74
1
2
3
4
5
6
7
8
9
10
11

max、min

let arr = [23, 4, 5, 2, 4, 5, 6, 6, 71, -3];
const difference = (arr) => {
  let max = Math.max(...arr),
    min = Math.min(...arr);
  return max - min;
};
console.log(difference(arr)); // 74
1
2
3
4
5
6
7

数组平均数

/**
 * 数组平均数
 * 使用reduce()将每个值添加到累加器,初始值为0,总和除以数组长度。
 * @param {*} arr
 * array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
 * total 必需。初始值, 或者计算结束后的返回值。
 * currentValue	必需。当前元素
 * currentIndex	可选。当前元素的索引
 * arr	可选。当前元素所属的数组对象。
 */
const averge = (arr) =>
  arr.reduce((total, currentValue) => total + currentValue, 0) / arr.length;

console.log(averge([1, 2, 3, 4]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14

数组扁平化并去重排序

将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组。

var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
1

实现如下:

const arr = [
  [1, 2, 2],
  [3, 4, 5, 5],
  [6, 7, 8, 9, [11, 12, [12, 13, [14]]]],
  10,
];

// sort函数返回排序后的数组
const res = Array.from(new Set(arr.flat(Infinity))).sort((a, b) => a - b);
console.log(res); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
1
2
3
4
5
6
7
8
9
10

数组乱序

// 取巧的一种算法,但是每个位置乱序的概率不同
function mixArr(arr) {
  return arr.sort(() => {
    return Math.random() - 0.5;
  });
}
1
2
3
4
5
6
// 著名的Fisher–Yates shuffle 洗牌算法
function shuffle(arr) {
  let m = arr.length;
  while (m > 1) {
    let index = parseInt(Math.random() * m--);
    [arr[index], arr[m]] = [arr[m], arr[index]];
  }
  return arr;
}
1
2
3
4
5
6
7
8
9

数组 filter 方法实现

Array.prototype.filter = function (fn, context) {
  if (typeof fn != 'function') {
    throw new TypeError(`${fn} is not a function`);
  }
  let arr = this;
  let reuslt = [];
  for (var i = 0; i < arr.length; i++) {
    let temp = fn.call(context, arr[i], i, arr);
    if (temp) {
      result.push(arr[i]);
    }
  }
  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

数组 flat 实现

数组 flat 方法是 ES6 新增的一个特性,可以将多维数组展平为低维数组。如果不传参默认展平一层,传参可以规定展平的层级。

// 展平一级
function flat(arr) {
  var result = [];
  for (var i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flat(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
//展平多层
function flattenByDeep(array, deep) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    if (Array.isArray(array[i]) && deep >= 1) {
      result = result.concat(flattenByDeep(array[i], deep - 1));
    } else {
      result.push(array[i]);
    }
  }
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
Array.prototype.flat = function () {
  return this.toString()
    .split(',')
    .map((item) => +item);
};
1
2
3
4
5

字符串相关

解析 URL Params 为对象

let url =
  'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url);

/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/
1
2
3
4
5
6
7
8
9
10
11

转化为驼峰命名

var s1 = 'get-element-by-id';
// 转化为 getElementById
var f = function (s) {
  return s.replace(/-\w/g, function (x) {
    return x.slice(1).toUpperCase();
  });
};
1
2
3
4
5
6
7

JSON.stringify&JSON.parse

JSON.stringify

JSON.stringify: 将对象转为 json 字符串

function jsonStringify(obj) {
  const type = typeof obj;
  if (type !== 'object') {
    if (type === 'string') {
      obj = '"' + obj + '"';
    }
    return String(obj);
  } else {
    const json = [];
    const arr = Array.isArray(obj);
    for (const k in obj) {
      let v = obj[k];
      const type = typeof v;
      if (type === 'string') {
        v = '"' + v + '"';
      } else if (v === null) {
        // 处理null情况
        v = null;
      } else if (/function|undefined/.test(type)) {
        // 原生方法会移除function和undefined,其实我们可以不移除
        delete obj[k];
      } else {
        v = jsonStringify(v); // 递归
      }
      json.push((arr ? '' : '"' + k + '":') + String(v));
    }
    return (arr ? '[' : '{') + String(json) + (arr ? ']' : '}');
  }
}

const obj = {
  a: 'a1',
  b: [1, 2, 3],
  c: 22,
  d: function () {},
  e: Date.now(),
  f: null,
  g: /str/gi,
  h: undefined,
};

const str = jsonStringify(obj);
// {"a":"a1","b":[1,2,3],"c":22,"e":1562815128952,"f":null,"g":{}}
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

JSON.parse

JSON.parse: 将字符串格式的对象转为对象

function jsonParse(str) {
  return new Function('return ' + str)(); // return后有一个空格
}

很神奇有木有,直接在字符串前面加上'return '关键字就可以转为对象。
在组件精讲小册里有一个实例,在线vue单文件编辑器。
原理就是将编辑器内的vue单文件字符串使用正则分割,
js部分将‘export default’替换为'return '。
通过new Function转为js对象使用。

const sum = new Function('a','b','return a + b');
sum(1, 2); // 3

const str = '{"a":"a1","b":[1,2,3],"c":22,"e":1562815128952,"f":null,"g":{}}';
jsonParse(str); //
a: "a1",
b: [1, 2, 3],
c: 22,
e: 1562815128952,
f: null,
g: {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

setInterval 实现

function mySetInterval(fn, interval) {
  const now = Date.now;
  let startTime = now();
  const loop = () => {
    const timer = requestAnimationFrame(loop);
    if (now() - startTime >= interval) {
      startTime = now();
      fn(timer);
    }
  };
  loop();
}
/*
一般来说是不建议使用setInterval的,如内部函数复杂就不能保证一定在规定时间内自动执行。
一般是通过setTimeout模仿setInterval。
那为什么要实现setInterval?
因为它内部的实现是使用requestAnimationFrame实现的,
该方法自带函数节流。
如有持续的动画需要执行,
基本会保证在16.6毫秒内执行一次,
提高动画性能并延时也是精确的。
*/

mySetInterval((timer) => {
  console.log('a');
  // cancelAnimationFram(timer) 可以取消当前定时器
});
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

实现 printf 函数

主要考察 replace 函数和正则表达式:js replace 方法第二个参数,远不止你想的那么强大

function printf(str, info) {
  str = str.replace(/\$\{([a-z]+)\}/g, function (word) {
    // console.log(word); // ${name}或者${city}
    // console.log(arguments);
    return info[arguments[1]];
  });
  return str;
}

const str = 'My name is ${name}, I am from ${city}';
const info = {
  name: 'AaDerBrane',
  city: 'GungZhou',
};
console.log(printf(str, info));
// My name is AaDerBrane, I am from GuangZhou
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log(arguments);
// 输出:
[Arguments] {
  '0': '${name}',
  '1': 'name',
  '2': 11,
  '3': 'My name is ${name}, I am from ${city}' }
[Arguments] {
  '0': '${city}',
  '1': 'city',
  '2': 30,
  '3': 'My name is ${name}, I am from ${city}' }
1
2
3
4
5
6
7
8
9
10
11
12
  • param1: 匹配到的字符串
  • param2: 匹配的子字符串
  • param3: 匹配到的字符串在字符串中的位置
  • param4: 原始字符串

js 统计一个字符串出现频率最高的字母/数字

let str = 'asdfghjklaqwertyuiopiaia';
const strChar = (str) => {
  let string = [...str],
    maxValue = '',
    obj = {},
    max = 0;
  string.map((value) => {
    obj[value] = obj[value] == undefined ? 1 : obj[value] + 1;
    if (obj[value] > max) {
      max = obj[value];
      maxValue = value;
    }
  });
  return maxValue;
};
console.log(strChar(str)); // a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

翻转字符串

let str = 'Hello Dog';
const reverseString = (str) => {
  return [...str].reverse().join('');
};
console.log(reverseString(str)); // goD olleH
1
2
3
4
5

不借助临时变量,进行两个整数的交换

数组解构

let a = 2,
  b = 3;
[b, a] = [a, b];
console.log(a, b); // 3 2
1
2
3
4

算术运算(加减)

// 输入a = 2, b = 3, 输出 a = 3,b = 2
let a = 2,
  b = 3;
const swop = (a, b) => {
  b = b - a;
  a = a + b;
  b = a - b;
  return [a, b];
};
console.log(swop(2, 3)); // [3,2]
1
2
3
4
5
6
7
8
9
10

逻辑运算(异或)

let a = 2,
  b = 3;
const swap = (a, b) => {
  a ^= b; // x先存x和y两者的信息
  b ^= a; // 保持x不变,利用x异或反转y的原始值使其等于x的原始值
  a ^= b; // 保持y不变,利用x异或反转y的原始值使其等于y的原始值
  return [a, b];
};
console.log(swap(2, 3)); // [3, 2]
1
2
3
4
5
6
7
8
9

排序 (从小到大)

冒泡排序

let arr = [43, 32, 1, 5, 9, 22];
const sort = (arr) => {
  let res = [];
  arr.map((v, i) => {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] > arr[j]) {
        [arr[i], arr[j]] = [arr[j], arr[i]];
      }
    }
  });
  return arr;
};
console.log(sort(arr)); // [1, 5, 9, 22, 32, 43]
1
2
3
4
5
6
7
8
9
10
11
12
13

参考文档

  1. [译]非常有用的 48 个 JavaScript 代码片段,值得收藏!
  2. 同学,你会手写代码吗?
  3. 22 道高频 JavaScript 手写面试题及答案
  4. 「中高级前端面试」JavaScript 手写代码无敌秘籍