📣 每日复习内容

Last updated on 2023-10-18 14:46

📚 html 和 h5

1. h5新增了哪些新特性√

  1. 语义化标签,例如header,footer,section,article等。 语义化标签的作用:提升页面的阅读性(结构性增强),更有利于SEO,对于使用屏幕阅读器的人来说会更友好(有明显的语气差别,例如strong标签内的内容会重读);还新增了一些状态标签、列表标签、文本标签。
  2. 新增媒体元素,audio、video标签能够很容易的输出音频或视频流,提供便利的获取文件信息的API
  3. 新增的表单控件:calendar、date、time、email、url、search
  4. 用于绘画的canvas属性 Canvas API 提供了一个通过JavaScript 和 HTML的canvas元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
  5. 新增本地存储方式:sessionStorage、localStorage。sessionStorage 用于存储会话级别的数据,会话关闭,数据消失,不可设置过期时间。localStorage 用于存储需要进行持久化存储的数据,只要不主动删除,数据不会消失。
  6. 新的技术:webworker、websocket。 webworker:用于多线程编程;websocket:客户端与服务端双向数据通信协议

2.localstorage、sessionstorage可以跨域吗✓

本地存储和会话存储都存储键值对。

本地存储和会话存储的主要区别在于在关闭浏览器后存储在会话存储中的键值对会丢失。下面是基本的使用(两者API一样,这里列举的是会话存储):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 保存键值对、获取键值对
sessionStorage.setItem('Name1', 'uiu');
sessionStorage.getItem('Name1');

// 保存数组,获取数据,如果不用JSON.parse(),则得到的是字符串
let ProgrammingLanguage = ['Java', 'Python', 'JavaScript', 'GO+']
sessionStorage.setItem('favoriteProgrammingLanguage',JSON.stringify(ProgrammingLanguage));
console.log(JSON.parse(sessionStorage.getItem('favoriteProgrammingLanguage')));

// 清除本地存储或会话存储
sessionStorage.clear()

// 移除某个键值对
sessionStorage.removeItem('Name1');

1.本地存储和会话存储的相同点、不同点

相同点:

1、存储大小均为5M左右
2、都有同源策略限制
3、仅在客户端中保存,不参与和服务器的通信

不同点:

1、生命周期 —— 数据可以存储多少时间

  • localStorage: 存储的数据是永久性的,除非用户人为删除否则会一直存在。
  • sessionStorage: 与存储数据的脚本所在的窗口的有效期是相同的。一旦窗口被关闭,那么所有通过 sessionStorage 存储的数据也会被删除。

2、作用域 —— 谁拥有数据的访问权

  • localStorage: 在同一个浏览器内,同源文档之间共享 localStorage 数据,可以互相读取、覆盖。
  • sessionStorage: 与 localStorage 一样需要同一浏览器同源文档这一条件。不仅如此,sessionStorage 的作用域还被限定在了窗口中,也就是说,只有同一浏览器、同一窗口的同源文档才能共享数据。

为了更好的理解sessionStorage,我们来看个例子:

例如你在浏览器中打开了两个相同地址的页面A、B,虽然这两个页面的源完全相同,但是他们还是不能共享数据,因为他们是不同窗口中的。但是如果是一个窗口中,有两个同源的iframe元素的话,这两个iframe的 sessionStorage 是可以互通的。

3. cookie有哪些属性,cookie和session的区别√

Cookie是一种在客户端(通常是Web浏览器)和服务器之间传输的小型文本文件。它存储在用户的计算机上,用于跟踪、识别和存储有关用户和其与网站的互动的信息。以下是Cookie的主要特点和作用:

1、Cookie 确实非常,它的大小限制为4KB左右

2、有状态性:Cookie使得Web服务器能够在多个请求之间保持用户状态信息,例如登录状态、购物车内容等。

3、一般由服务器生成,可设置失效时间。可以是会话级别(当用户关闭浏览器时删除)或永久性的(在指定日期之前保持有效)。如果在浏览器端生成Cookie,默认是关闭浏览器后失效。

4、每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题

5、存储在客户端:cookie存储在用户的计算机上,而不是服务器上。每当用户访问与Cookie相关的网站时,浏览器会将cookie发送回服务器。

用法(API)

服务端向客户端发送的cookie(HTTP头,不带参数):
Set-Cookie: <cookie-name>=<cookie-value> (name可选)

服务端向客户端发送的cookie(HTTP头,带参数):
Set-Cookie: <cookie-name>=<cookie-value>;(可选参数1);(可选参数2)

客户端设置cookie:

1
document.cookie = "<cookie-name>=<cookie-value>;(可选参数1);(可选参数2)"

可选参数:

下图是Chrome浏览器中的Cookie截图,属性分别有NameValueDomainPathExpires/Max-ageSizeHttpOnlySecureSameSitePriority

Domain=<domain-value>:指定cookie可以送达的主机域名,若一级域名设置了则二级域名也能获取。也就是决定在向该域发送请求时是否携带此Cookie,Domain的设置是对子域生效的,如Domain设置为 .a.com,则b.a.com和c.a.com均可使用该Cookie,但如果设置为b.a.com,则c.a.com不可使用该Cookie。Domain参数必须以点(“.”)开始。

Path=<path-value>:指定一个URL,和Domain类似,也对子路径生效,例如指定path=/docs,则 ”/docs” 、 ”/docs/Web/“ 、”/docs/Web/Http”均满足匹配条件。如Cookie1和Cookie2的Domain均为a.com,但Path不同,Cookie1的Path为 /b/,而Cookie2的Path为 /b/c/,则在a.com/b页面时只可以访问Cookie1,在a.com/b/c页面时,可访问Cookie1和Cookie2。Path属性需要使用符号“/”结尾。

Expires/Max-age
ExpiresMax-age均为Cookie的有效期,Expires是该Cookie被删除时的时间戳,格式为GMT,若设置为以前的时间,则该Cookie立刻被删除,并且该时间戳是服务器时间,不是本地时间!若不设置则默认页面关闭时删除该Cookie。
Max-age也是Cookie的有效期,但它的单位为秒,即多少秒之后失效,若Max-age设置为0,则立刻失效,设置为负数,则在页面关闭时失效。Max-age默认为 -1。

HttpOnlyHttpOnly值为 truefalse,若设置为true,则不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见,但在发送请求时依旧会携带此Cookie。

SecureSecure为Cookie的安全属性,若设置为true,则浏览器只会在HTTPS和SSL等安全协议中传输此Cookie,不会在不安全的HTTP协议中传输此Cookie。

SameSiteSameSite用来限制第三方 Cookie,从而减少安全风险。它有3个属性,分别是:

​ Strict:Scrict最为严格,完全禁止第三方Cookie,跨站点时,任何情况下都不会发送Cookie

​ Lax:Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

​ None:网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

关闭SameSite的方法

  • 操作方法谷歌浏览器地址栏输入:chrome://flags/
  • 找到:SameSite by default cookies、Cookies without SameSite must be secure设置上面这两项设置成 Disable

Priority
优先级,chrome的提案,定义了三种优先级,Low/Medium/High,当cookie数量超出时,低优先级的cookie会被优先清除。在360极速浏览器和FireFox中,不存在Priority属性,不清楚在此类浏览器中设置该属性后是否生效。

示例:

1
2
3
Set-Cookie: sessionid=aes7a8; HttpOnly; Path=/

document.cookie = "KMKNKK=1234;Sercure"

可选前缀:
__Secure-:以__Secure-为前缀的cookie,必须与secure属性一同设置,同时必须应用于安全页面(即使用HTTPS)

__Host-:以__Host-为前缀的cookie,必须与secure属性一同设置,同时必须应用于安全页面(即使用HTTPS)。必须不能设置domian属性(这样可以防止二级域名获取一级域名的cookie),path属性的值必须为”/“。

前缀使用示例:

1
2
3
4
5
Set-Cookie: __Secure-ID=123; Secure; Domain=example.com
Set-Cookie: __Host-ID=123; Secure; Path=/

document.cookie = "__Secure-KMKNKK=1234;Sercure"
document.cookie = "__Host-KMKNKK=1234;Sercure;path=/"

cookie的作用:

  1. 会话管理:最常见的用途之一是在Web应用程序中管理用户会话。通过Cookie,服务器可以跟踪用户是否已登录,以及用户的会话状态。
  2. 个性化体验:网站可以使用Cookie来存储用户的首选项和设置,以便在用户返回时提供个性化的体验。
  3. 购物车和电子商务:Cookie可用于存储购物车中的商品信息,使用户能够在购物过程中添加和删除商品。
  4. 跨页面跟踪:Cookie可以用于跟踪用户在网站上的活动,例如分析用户行为、广告定位和用户流量分析。
  5. 认证和授权:Cookie常用于存储会话令牌,以验证用户身份并授权其访问受限资源。
  6. 跟踪用户:虽然涉及隐私问题,但Cookie也被用于跟踪用户的互联网活动,以便广告商和网站可以提供更有针对性的广告和内容。

需要注意的是,由于Cookie涉及到用户隐私和安全问题,因此网站和应用程序必须谨慎处理Cookie,遵守隐私法规,并提供适当的控制选项供用户管理其Cookie设置。

2.Session

基本概念

Session是在无状态的HTTP协议下,服务端记录用户状态时用于标识具体用户的机制。它是在服务端保存的用来跟踪用户的状态的数据结构,可以保存在文件、数据库或者集群中。

在浏览器关闭后这次的Session就消失了,下次打开就不再拥有这个Session。其实并不是Session消失了,而是Session ID变了,服务器端可能还是存着你上次的Session ID及其Session 信息,只是他们是无主状态,也许一段时间后会被删除。

大多数的应用都是用Cookie来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在Cookie里面记录一个SessionID,以后每次请求把这个会话ID发送到服务器

与Cookie的关系与区别:

1、Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中,Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

2、Cookie安全性一般,他人可通过分析存放在本地的Cookie并进行Cookie欺骗。在安全性第一的前提下,选择Session更优。重要交互信息比如权限等就要放在Session中,一般的信息记录放Cookie就好了。

3、单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie。 当访问增多时,Session会较大地占用服务器的性能。考虑到减轻服务器性能方面,应当适时使用Cookie

4、Session的运行依赖Session ID,而Session ID是存在 Cookie 中的。也就是说,如果浏览器禁用了Cookie,Session也会失效(但是可以通过其它方式实现,比如在url中传递Session ID,即sid=xxxx)。

📚 css+c3

1.长度单位有哪些?√

A:答案如下:这里写了8个

  • CSS中的长度单位
  • CSS 有几种表示长度的不同单位。许多 CSS 属性接受“长度”值,诸如 widthmarginpaddingfont-size 等。长度是一个后面跟着长度单位的数字,诸如 10px2em 等。
  • 绝对单位有:
    • px 像素:我们的电脑屏幕是由一个一个“小点”组成的,每个“小点”,就是一个像素(px)。 一个像素的大小主要取决于显示器的分辨率,相同面积不同分辨率的显示屏,其像素点大小就不相同。 像素点越小,呈现的内容就越清晰、越细腻。
    • cm 厘米:可以用在网页设计,但是不太精细
    • nm 纳米
  • 相对单位有:
    • em:相对于font-size的大小,即为font-size的倍数。如果当前元素没有font-size,则往上一级一层一层的找,如果在根元素都没有找到,则选择浏览器默认的font-size。
    • rem:相对根元素的字体大小,即html的font-size
    • %:相对于父元素的某些属性
    • vm:相对于视口宽度大小的1%,1vm=视口宽度*1%
    • vh:相对于视口高度大小的1%,1vm=视口高度*1%
    • vmin:1vmin = 1vw 或 1vh,以较小者为准。
    • vmax:1vmax = 1vw 或 1vh,以较小者为准。

查看视口宽度的宽度:console.log(document.documentElement.clientWidth)

2.如何实现水平垂直居中?√

如果要在父元素中实现水平垂直居中:

  • 子元素为行内元素/行内块元素:(可以将行内元素和行内块元素当做文本处理)

    • 水平:text-align:center
    • 垂直:父元素设置行号:line-height=height,每个子元素加上:vertical-align:middle
      • 如果想要实现绝对的垂直居中,可以让父元素的字体大小为0,因为vertical-align本就是受字体大小的影响
  • 子元素为块元素:

    • ①使用margin,子元素需要高度

      • 水平:子元素设置margin:0 auto
      • 垂直:子元素设置margin-top:(父元素内容-子元素盒子总高) / 2
    • ②使用绝对定位(有3种)

      • 绝对定位+位移

        • 父元素设置相对定位

        • position: absolute;
          left: 50%;
          top: 50%;            
          transform: translate(-50%, -50%);
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10

          - 绝对定位+`margin`:2

          - ```css
          position: absolute;
          left:0;
          right:0;
          top:0;
          bottom:0
          margin: auto;
        • position: absolute;
          left: 50%;
          /*负的一半width*/
          margin-left: -25px; 
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11

          - ③ 弹性布局:2

          - 方式二:父容器开启 flex 布局,随后子元素 margin: auto

          - 加在父元素

          - ```JavaScript
          display: flex;
          justify-content: center;
          align-items: center;

3.CSS中隐藏元素有几种实现方式?√

参考:[css隐藏元素的六种方法_css hidden_muzidigbig的博客-CSDN博客](https://blog.csdn.net/muzidigbig/article/details/80967143#:~:text=css隐藏元素的六种方法 1 display%3Anone (通过隐藏盒子属性,脱标) 2 visibility%3Ahidden (通过隐藏盒子属性,不脱标) 3,(通过裁剪盒子,不脱标) 6 position%3Aabsolute%3B与clip%3Arect (0px 0px 0px 0px)配合 (通过裁剪绝对定位的盒子,脱标))

A:方式如下:

  • ① visibility: hidden

    • 特点:可以隐藏元素,占位
  • ② display:none

    • 特点:不占位
  • ③ 给元素加hidden属性,这是html5中的全局属性

    • 特点:和display一样,不占位
  • ④ opacity: 0

    • 特点:占位
  • ⑤ 绝对定位:

    • 主要是将其移出屏幕,优点:既能响应,也不影响布局
    • position:relative;
      left:-99999px;
      top:-90999px;
      
      1
      2
      3
      4
      5
      6
      7
      8
        
      - ⑥ z-index
      - 特点:不占据空间
      - ```JavaScript
      .hide{
      position:absolute;
      z-index:-1000;/* 不占据空间,无法点击 */
      }
  • ⑦ 位移:

    • 特点:占据空间
    • transform: scale(0,0)/* 占据空间,无法点击 */
方法比较 visibility: hidden opacity: 0 overflow:hidden; 绝对定位 位移 z-index display:none 加hidden属性
是否占位? × × ×
性能 会引起重绘,不会引起回流 只造成本元素重绘,性能消耗较少 用来隐藏元素溢出部分,无法响应点击事件 无法响应事件,引起页面回流与重绘,性能消耗大

image.png

4.css的常见布局方式

在这里总结一下CSS中的布局技巧,说明每个布局的应用场景并给出小案例。在CSS中的布局技巧有:浮动、定位、弹性盒子布局、grid布局、表格布局等。这里我们根据单列布局、两列布局和三列布局的标准来进行展开:

🏵 单列布局

在常见的单列布局中,常见的有两种:

  • 上中下等宽
  • 上下等宽、中间内容区偏窄

下面是单列布局的实现过程

实现第一种方式:可以将 header , content, footer 统一设置相等宽度,然后设置 margin:0 auto 即可实现居中:

实现第二种方式:可以将 headerfooter 的宽度设置为100%,然后设置内容宽度稍窄一些,然后对内容区进行居中即可:

以下是实现单列布局第一种方式的代码:

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
<!-- html结构代码 -->
<header></header>
<div class="content"></div>
<footer></footer>

<!-- css代码 -->
<style>
header{
margin:0 auto;
max-width: 960px;
height:100px;

}
.content{
margin: 0 auto;
max-width: 960px;
height: 400px;

}
.footer{
margin: 0 auto;
max-width: 960px;
height: 100px;
}
</style>

以下是实现单列布局第二种方式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- html结构代码 -->
<header></header>
<div class="content"></div>
<footer></footer>

<!-- css代码 -->
<style>
header{
width:100%;
height:100px;

}
.content{
margin: 0 auto;
max-width: 960px;
height: 400px;

}
.footer{
width:100%;
height: 100px;
}
</style>

应用场景:

目前淘宝、知乎等的首页就是采用单列布局。

🏵 两列布局

两列布局是一列固定宽度,另一列自适应宽度。
实现方式有:

  • 浮动
  • 定位
  • 弹性布局
  • grid布局

🌸 浮动

效果图如下:

该图是左列固定宽度,右边是自适应宽度,具体代码如下:

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
<!-- html结构代码 -->
<div class="container">
<div class="left">left</div>
<div class="right">right</div>
</div>
<!-- css代码 -->
<style>
.container {
width: 800px;
border: 1px solid black;
height: 300px;
text-align: center;
font-size: 30px;
}
.left {
width: 100px;
background-color: skyblue;
height: 300px;
line-height: 300px;
float: left;
}
.right {
background-color: rgb(230, 160, 230);
height: 300px;
line-height: 300px;
}
</style>

在采用浮动布局时需要注意,浮动会带来一些副作用:比如浮动元素的父元素高度会塌陷,会影响后面兄弟元素的位置。正是因为有这些副作用,因此还需要清除浮动,方法有很多,下面列举的都可以用来清除浮动影响:

方案一: 给父元素指定高度。

方案二: 给父元素也设置浮动,带来其他影响。

方案三:给父元素设置 overflow:hidden 。

方案四:在所有浮动元素的最后面,添加一个块级元素,并给该块级元素设置 clear:both 。

方案五:给浮动元素的父元素,设置伪元素,通过伪元素清除浮动,原理与方案四相同。===> 推荐使用

上面代码是给父元素设置高度来解决。

🌸定位

通过相对定位和绝对定位实现,需要三个div,其中一个div是父容器,包含两个子元素。
给父容器设置相对定位是因为可以让其设置绝对定位的子元素相对它进行移动;

两个子元素设置绝对定位,给上边的子元素设置宽度,下边的子元素设置left,值为上边子元素的宽度,再设置right:0。 给上边的子元素设置绝对定位可以让下边的子元素跟它在同一行。

🏵 三列布局

1.圣杯布局

2.双飞翼布局

5.什么是BFC?(√)

A:BFC是块格式化上下文,我将它理解为元素的一个“特异功能”。该特异功能在默认状态下是处于关闭的,当元素满足某些条件后,该“特异功能”被激活。所谓激活“特异功能”,通俗一点来说,就是元素开启了BFC。

元素开启BFC之后可以用来解决三个问题:第一个是元素开启BFC后,其子元素不会再产生margin塌陷问题;第二个是元素开启BFC后,自己不会被其他浮动元素所覆盖;第三个是元素开启BFC后,就算其子元素浮动,元素高度也不会塌陷。

简单来说:BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。

触发BFC特性呢只需要满足下面任一条件就可:第一:根元素;第二:浮动元素;第三:绝对定位、固定定位元素;第四:行内块元素;第五:表格元素;第六:overflow不为visible的元素;第七:伸缩项目;第八:多列容器。

6.介绍一下弹性布局(√)

A:弹性布局里包括伸缩容器和伸缩项目。① 给元素设置display:flex,该元素就变成了伸缩容器。② 伸缩容器所有子元素自动成为了:伸缩项目。仅伸缩容器的子元素成为了伸缩项目,孙子元素、重孙子元素等后代,不是伸缩项目。无论原来是哪种元素(块、行内块、行内),一旦成为了伸缩项目,全都会“块状化”。一个元素可以同时是:伸缩容器、伸缩项目。

弹性布局中的属性有:

① flex-direction:设置主轴方向,默认是水平的。取值有4个为:row row-reverse column column-reverse

注意:改变了主轴的方向,侧轴方向也随之改变。

主轴换行方式 flex-wrap,取值有3个:nowrap wrap wrap-reverse

主轴对齐方式justify-content,取值有6个:flex-start flex-end center space-between space-around space-evenly

侧轴对齐方式分为一行和多行:

一行align-items:取值有5个:flex-start flex-end center baseline stretch

多行align-content,取值有7个:flex-start flex-end center space-between space-around space-evenly stretch

弹性布局还可以用来实现水平垂直居中,有两种方式。

flex-grow(伸):定义伸缩项目的放大比例,默认为 0 ,即:纵使主轴存在剩余空间,也不拉伸(放大)。

规则:

  1. 若所有伸缩项目的 flex-grow 值都为 1 ,则:它们将等分剩余空间(如果有空间的话)。

  2. 若三个伸缩项目的 flex-grow 值分别为: 1 、 2 、 3 ,则:分别瓜分到: 1/6 、 2/6 、3/6 的空间。

flex-shrink(缩): 定义了项目的压缩比例,默认为 1 ,即:如果空间不足,该项目将会缩小。

📚 js和js进阶

1.js取整的方法,parseInt第二个参数是什么?(√)

1、丢弃小数部分,保留整数部分:

parseInt(d);
Math.trunc(d); 

两者的区别:parseInt 常常接收一个字符串作为参数,而 Math.trunc 则可以接收一个数字参数,所以如果要对数字取整,还是建议使用 Math.trunc。使用 parseInt 的时候,如果你传入的不是字符串,比如传入一个数字,parseInt 会先调用数字的 toString() 方法。

【知识点】parseInt()方法:字符串转数字

parseInt接收两个参数:

  • 第一个参数string:要被解析的字符串,如果不是字符串会被转换,忽视空格符
  • 第二个参数radix:要解析的数字的基数。该值介于2~36之间。默认值为10,表示十进制。这个参数表示将前面的字符从radix进制转化为十进制
    • 1.在没有指定基数,或者基数为0的情况下,parseInt()会根据string参数来判断数字的基数。
      • 如果字符串string以”0x”或者”0X”开头, 则基数是16 (16进制).
      • 如果字符串string以”0”开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
      • 如果字符串string以其它任何值开头,则基数是10 (十进制)。
    • 2.如果radix2 ~ 36之外会返回NaN。
1
2
3
4
5
6
7
8
9
10
// 例1
console.log(parseInt(3, 8)); // 3
// 例2
console.log(parseInt(3, 2)); // NaN
// 例3
console.log(parseInt(3, 0)); // 3
// 例4
console.log(parseInt(3, 1)); // NaN
// 例5
console.log(parseInt(123, 5)); // 结果为38

解析如下

  • 例1parseInt里面有两个参数,第二个参数是8,表示要将八进制的3转换为十进制的结果,八进制中有3,转化为十进制还是3,所以返回结果为3
  • 例2parseInt里面有两个参数,第二个参数是2,表示要将二进制的3转化为十进制,额…,不好意思,二进制中并没有3,所以返回NaN
  • 例3parseInt里面有两个参数,第二个参数是0,根据规则1,默认就是十进制,直接返回3
  • 例4parseInt里面有两个参数,第二个参数是1,根据规则2,1在2 ~ 36之外,直接返回NaN
  • 例5parseInt里面有两个参数,第二个参数是5,表示要将五进制的123转化为十进制,结果为38 => (1*5^2 + 2*5^1 + 3*5^0 = 38)

2、向上取整,有小数就整数部分加1:

1
2
3
// 11.1取整后得到12;
// -11.1取整后得到-11。
Math.ceil(d);

3、向下取整,正数舍弃小数位,负数整数位减一:

1
2
3
4
5
11.1取整后得到11;

-11.1取整后得到-12

Math.floor(d);

4、四舍五入:

1
Math.round(d);

2.数组转字符串、字符串转数组(√)

一、数组转字符串(3种方法)

同样是数组转字符串,toString(),toLocaleString(),join(),join(’,’)的区别是什么?

JavaScript 允许数组与字符串之间相互转换。其中 Array 方法对象定义了 3 个方法,可以把数组转换为字符串,如表所示。

数组方法 说明
toString() 将数组转换成一个字符串
toLocaleString() 把数组转换成本地约定的字符串
join() 将数组元素连接起来以构建一个字符串

1. join()方法用于把数组中的所有元素放入一个字符串

元素是通过指定的分隔符进行分隔的

join()指定的分隔符 说明
join() 可理解为直接变成字符串,默认逗号分隔
join(‘’) 空连接
join(’ ,’)或者 join(’ - ‘)或者 join(’ 。’) 中间这个逗号是手动添加的,也可以改成别的比如、。! -等等都可以

2. toString()方法可把一个逻辑值转换为字符串,并返回结果

1
2
3
4
5
6
7
var arr = [10, 20, 30, 40];
var str = arr.toString(); //把数组转换为字符串
console.log(str);
console.log(typeof str); //返回字符串string,说明是字符串类型
// 输出:
// 10,true,30,40
// string

toString()方法不可以指定分隔符,但是我们可以通过replace()方法指定替换

1
2
var str = arr.toString().replace(/,/gi, "-");
console.log(str); // 10-true-30-40

**3. toLocaleString()**:把数组转换成本地约定的字符串

1
2
3
4
5
var arr = [10, true, 30, 40];
var str = arr.toLocaleString(); //把数组转换为字符串
console.log(str); // 10,true,30,40
console.log(typeof str); //返回字符串string,说明是字符串类型

二、字符串转数组(2种方法)

字符串方法 说明
split() 方法 将字符串转换成一个数组
扩展运算符(…) es6里面的扩展运算符

1.split() 方法用于把一个字符串分割成字符串数组

同样是用于把一个字符串分割成字符串数组,split(’,’),split(),split(‘’)的区别是什么?

split()方法 说明
split(“,”) 以逗号分隔的转换为数组
split(‘’)空字符串 每个字符之间都会被分割
split() 可理解为直接变成数组
1
2
3
4
5
6
7
var str = "aa, bb, cc, dd";
var str1 = str.split(",");
var str2 = str.split("");
var str3 = str.split();
console.log(str1); // [ 'aa', ' bb', ' cc', ' dd' ]
console.log(str2); // ['a', 'a', ',', ' ','b', 'b', ',', ' ','c', 'c', ',', ' ','d', 'd']
console.log(str3); // [ 'aa, bb, cc, dd' ]

2.扩展运算符

1
2
3
var str = "aa,bb,cc,dd";
var str1 = [...str];
console.log(str1); // ['a', 'a', ',', 'b','b', ',', 'c', 'c',',', 'd', 'd']

3.数组常用方法(√)

一、改变原数组的方法7个

总结:

push、pop、unshift、shift、sort、reverse、splice

join、concat、slice、indexOf、lastIndexOf

新增:forEach、map、filter、every、some、reduce、find、Array.from、includes

1.push()末尾添加数据

语法: 数组名.push( 数据)

作用: 就是往数组末尾添加数据

返回值: 就是这个数组的长度

1
2
3
4
var arr = [10, 20, 30, 40]
res = arr.push(20)
console.log(arr);//[10,20,30,40,20]
console.log(res);//5

2.pop() 末尾处删除数据

语法: 数组名.pop()

作用: 就是从数组的末尾删除一个数据

返回值: 就是你删除的那个数据

1
2
3
4
var arr = [10, 20, 30, 40] 
res =arr.pop()
console.log(arr);//[10,20,30]
console.log(res);//40

3.unshift() 头部添加数据

语法: 数组名.unshift( 数据)

作用: 就是在数组的头部添加数据

返回值: 就是数组的长度

1
2
3
4
var arr = [10, 20, 30, 40]
res=arr.unshift(99)
console.log(arr);//[99,10,20,30,40]
console.log(res);//5

4.shift()头部删除数据

语法: 数组名.shift()

作用: 头部删除一个数据

返回值: 就是删除掉的那个数据

1
2
3
4
//shift复制代码 var arr = [10, 20, 30, 40]
res=arr.shift()
console.log(arr);[20,30,40]
console.log(res);10

5.reverse() 翻转数组

语法: 数组名.reverse()

作用: 就是用来翻转数组的

返回值: 就是翻转好的数组

1
2
3
4
//reverse复制代码var arr = [10, 20, 30, 40]
res=arr.reverse()
console.log(arr);//[40,30,20,10]
console.log(res);//[40,30,20,10]

6.sort() 排序

语法一: 数组名.sort() 会排序,会按照位排序

语法二: 数组名.sort(function (a,b) {return a-b}) 会正序排列

语法三: 数组名.sort(function (a,b) {return b-a}) 会倒序排列

1
2
3
4
5
6
7
var arr = [2, 63, 48, 5, 4, 75, 69, 11, 23]
arr.sort()
console.log(arr); // [ 11, 2, 23, 4, 48, 5, 63, 69, 75]
arr.sort(function(a,b){return(a-b)})
console.log(arr); // [2, 4, 5, 11, 23, 48, 63, 69, 75 ]
arr.sort(function(a,b){return(b-a)})
console.log(arr); // [75, 69, 63, 48, 23, 11, 5, 4, 2]

7.splice() 截取数组

语法一: 数组名.splice(开始索引,多少个)

​ 作用: 就是用来截取数组的

​ 返回值: 是一个新数组 里面就是你截取出来的数据

语法二: 数组名.splice(开始索引,多少个,你要插入的数据)

​ 作用: 删除并插入数据

​ 注意: 从你的开始索引起

​ 返回值: 是一个新数组 里面就是你截取出来的数据

1
2
3
4
5
6
7
8
9
10
var arr = [2, 63, 48, 5, 4, 75]
res = arr.splice(1,2)
console.log(arr); // [ 2, 5, 4, 75 ]
console.log(res); // [ 63, 48 ]
//******************************
//splice() 语法二
var arr = [2, 63, 48, 5, 4, 75]
res = arr.splice(1,1,99999,88888)
console.log(arr); // [2, 99999, 88888, 48, 5, 4, 75]
console.log(res); // [ 63 ]

二、不改变原数组的方法5个

1.concat() 合并数组

语法: 数组名.concat( 数据)

作用: 合并数组的

返回值: 一个新的数组

1
2
3
4
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.concat(20,"小敏",50)
console.log(arr); // [10, 20, 10, 30, 40, 50, 60]
console.log(res); // [10, 20, 10, 30, 40, 50, 60, 20,"小敏",50]

2.join() 数组转字符串

语法: **数组名.join(‘**连接符’)

作用: 就是把一个数组转成字符串

返回值: 就是转好的一个字符串

1
2
3
4
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.join("+")
console.log(arr); // var arr = [10, 20, 10, 30, 40, 50, 60]
console.log(res); // 10+20+10+30+40+50+60

3.slice() 截取数组的一部分数据

语法: 数组名.slice( 开始索引, 结束索引)

作用: 就是截取数组中的一部分数据

返回值: 就是截取出来的数据 放到一个新的数组中

注意: 包前不好后 包含开始索引不包含结束索引

1
2
3
4
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.slice(1,4)
console.log(arr); // [10, 20, 10, 30, 40, 50, 60]
console.log(res); // [20, 10, 30]

4.indexOf() 从左检查数组中有没有这个数值

语法一: 数组名.indexOf(要查询的数据)

作用: 就是检查这个数组中有没有该数据,如果有就返回该数据第一次出现的索引,如果没有返回 -1

语法二: 数组名.indexOf(要查询的数据, 开始索引)

1
2
3
4
5
6
7
8
9
10
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.indexOf(10)
console.log(arr); // [10, 20, 10, 30, 40, 50, 60]
console.log(res); // 0
//*************************************
//indexOf 语法二
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.indexOf(10,1)
console.log(arr); // [10, 20, 10, 30, 40, 50, 60]
console.log(res); // 2

5.lastIndexOf() 从右检查数组中有没有这个数值

语法一: 数组名.indexOf( 要查询的数据)

作用: 就是检查这个数组中有没有该数据

如果有就返回该数据第一次出现的索引

如果没有返回 -1

语法二: 数组名.lastIndexOf( 要查询的数据, 开始索引)

1
2
3
4
5
6
7
8
9
10
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.lastIndexOf(50)
console.log(arr)
console.log(res);
//*************************************
//lastIndexOf 语法二
var arr = [10, 20, 10, 30, 40, 50, 60]
res = arr.lastIndexOf(50,4)
console.log(arr)
console.log(res);

三、ES6新增的数组方法7个(都不改变原数组)

1. forEach() 用来循环遍历数组

语法: 数组名.forEach(function (item,index,arr) {})

  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 就是用来循环遍历数组的 代替了我们的for

1
2
3
4
5
6
7
8
9
10
var arr = [1, 2, 3, 4, 5]
var res = arr.forEach(function (item, index, arr) {
console.log(item, "------", index, "-------", arr);
})
// 输出:
// 1 ------ 0 ------- [ 1, 2, 3, 4, 5 ]
// 2 ------ 1 ------- [ 1, 2, 3, 4, 5 ]
// 3 ------ 2 ------- [ 1, 2, 3, 4, 5 ]
// 4 ------ 3 ------- [ 1, 2, 3, 4, 5 ]
// 5 ------ 4 ------- [ 1, 2, 3, 4, 5 ]

2.map() 映射数组

语法: 数组名.map(function (item,index,arr) {})

  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 就是用来数组映射

返回值: 必然是一个映射完毕的数组;这个数组和原数组长度一样,不改变原数组

1
2
3
4
5
6
7
8
9
var arr = [1, 2, 3, 4, 5]
var res = arr.map(function (item) {
return item*1000
})
console.log(arr);
console.log(res);
// 返回值:
// [ 1, 2, 3, 4, 5 ]
// [ 1000, 2000, 3000, 4000, 5000 ]

3.filter() 过滤数组

语法: 数组名.filter(function (item,index,arr) {})

  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 用来过滤数组的

返回值: 如果有就是过滤(筛选)出来的数据 保存在一个数组中;如果没有返回一个空数组

1
2
3
4
5
6
7
8
9
var arr = [1, 2, 3, 4, 5]
var res = arr.filter(function (item) {
return item > 2
})
console.log(arr);
console.log(res);
// 返回值:
// [ 1, 2, 3, 4, 5 ]
// [ 3, 4, 5 ]

4.every() 判断数组是不是满足所有条件

语法: 数组名.every(function (item,index,arr) {})

  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 主要是用来判断数组中是不是 每一个 都满足条件。只有所有的都满足条件返回的是true,只要有一个不满足返回的就是false

返回值: 是一个布尔值

注意: 要以return的形式执行返回条件

1
2
3
4
5
//every复制代码var arr = [1, 2, 3, 4, 5]
var res = arr.every(function (item) {
return item > 0
})
console.log(res);//打印结果 true

5.some() 数组中有没有满足条件的

语法: 数组名.some(function (item,index,arr) {})

  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 主要是用来判断数组中是不是 每一个 都满足条件。只有有一个满足条件返回的是true,只要都不满足返回的就是false

返回值: 是一个布尔值

注意: 要以return的形式执行返回条件

1
2
3
4
5
var arr = [1, 2, 3, 4, 5]
var res = arr.some(function (item) {
return item > 3
})
console.log(res); //true

6.find() 用来获取数组中满足条件的第一个数据

语法: 数组名.find(function (item,index,arr) {})

  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 用来获取数组中满足条件的数据

返回值: 如果有 就是满足条件的第一个数据;如果没有就是undefined

注意: 要以return的形式执行返回条件

1
2
3
4
5
var arr = [1, 2, 3, 4, 5]
var res = arr.find(function (item) {
return item > 3
})
console.log(res) //4

7.reduce() 累加后的效果

语法: 数组名.reduce(function (prev,item,index,arr) {},初始值)

  • prev :一开始就是初始值 当第一次有了结果以后;这个值就是第一次的结果
  • item : 这个表示的是数组中的每一项
  • index : 这个表示的是每一项对应的索引
  • arr : 这个表示的是原数组

作用: 就是用来累加的

返回值: 就是累加后的结果

注意: 以return的形式书写返回条件。如果不给初始值,那么prev就是第一个参数的值,并且从第二参数开始function。

1
2
3
4
5
var arr = [1, 2, 3, 4, 5]
var res = arr.reduce(function (prev, item) {
return prev *= item
}, 1)
console.log(res); //120

8.Array.from()

Array.from()方法可以将可迭代对象转换为新的数组。

  • 函数可接受3个参数(后两个参数可以没有):
    • 第一个表示将被转换的可迭代对象(如果只有一个参数就是把形参转变成数组)
    • 第二个是回调函数,将对每个数组元素应用该回调函数,然后返回新的值到新数组,
    • 第三个是回调函数内this的指向。
1
2
3
4
5
6
7
8
9
let arr = [1, 2, 3];
let obj = {
double(n) {
return n * 2;
}
}
console.log(Array.from(arr, function (n){
return this.double(n);
}, obj)); // [2, 4, 6]

9.includes()

参数:数值 ——– 返回值:true/false

includes()方法——是查看数组中是否存在这个元素,存在就返回true,不存在就返回false

1
2
3
let arr = [1,33,44,22,6,9]
let ary = arr.includes(22)
console.log(ary)

4.深拷贝和浅拷贝、赋值(√)

其他问法:数组深拷贝方法

参考:前端面试 第三篇 js之路 深拷贝与浅拷贝 - 掘金 (juejin.cn)JavaScript深拷贝和浅拷贝看这篇就够了 - 掘金 (juejin.cn)前端数组、对象的浅拷贝和深拷贝 - 掘金 (juejin.cn)浅拷贝与深拷贝 - 掘金 (juejin.cn)

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

浅拷贝: ① 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。② 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址里的内容,就会影响到另一个对象。(只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃);

image.png

深拷贝: ① 创建一个新对象,将原始对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,新旧对象不共享同一块内存,且修改新对象不会影响原对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样可以避免指针悬挂)

image.png

【默认情况下基本数据类型(number,string,null,undefined,boolean)都是深拷贝。】

赋值和深/浅拷贝的区别,比较的前提都是针对引用类型

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
  • 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
对原始数据的影响
和原数据是否指向同一对象 第一层数据为基本数据类型 原数据包含子对象(引用数据类型)
赋值 赋值后的数据改变,使原数据一同改变 赋值后的数据改变,使原数据一同改变
浅拷贝 浅拷贝后的数据改变,不会使原数据一同改变 浅拷贝后的数据改变,使原数据一同改变
深拷贝 浅拷贝后的数据改变,不会使原数据一同改变 浅拷贝后的数据改变,不会使原数据一同改变

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对象赋值
let obj1 = {
name: "Chen",
age: 18,
hobby: ["see a film", "write the code", "play basketball", "tourism"],
};
let obj2 = obj1;
obj2.name = "Forever";
obj2.hobby[1] = "swim";
obj2.hobby[2] = "alpinism";
console.log("obj1===>", obj1);
console.log("obj2===>", obj2);
// 输出为:
obj1===> {
name: 'Forever',
age: 18,
hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]
}
obj2===> {
name: 'Forever',
age: 18,
hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]
}
// 结论:对象中基本属性和引用属性都发生改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 浅拷贝
let obj1 = {
name: "Chen",
age: 18,
hobby: ["see a film", "write the code", "play basketball", "tourism"],
};
let obj3 = { ...obj1 };
obj3.name = "Forever";
obj3.hobby[1] = "swim";
obj3.hobby[2] = "alpinism";
console.log("obj1===>", obj1);
console.log("obj3===>", obj3);
// 输出为:
obj1===> {
name: 'Chen',
age: 18,
hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]
}
obj3===> {
name: 'Forever',
age: 18,
hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]
}
// 结论:浅拷贝时,对象中基本属性不变,引用属性发生变化

注意:当拷贝对象只有一层的时候,是深拷贝

浅拷贝的实现

  1. Object.assign()

参考资料:Object.assign()基本用法、注意点、用法

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象source复制到目标对象。

参数:第一个参数是目标对象,后面的参数都是源对象。

返回:目标对象target

1
2
3
4
5
const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }
const returnedTarget = Object.assign(target, source)
target // { a: 1, b: 4, c: 5 }
returnedTarget // { a: 1, b: 4, c: 5 }

注意

  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

  • 如果只有一个参数,Object.assign会直接返回该参数。

  • 如果该参数不是对象,则会先转成对象,然后返回。由于undefinednull无法转成对象,所以如果它们作为第一个参数,就会报错。如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefinednull不在首参数,就不会报错。其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let obj = {a: 1}
    Object.assign(obj, undefined) === obj // true
    Object.assign(obj, null) === obj // true

    const v1 = 'abc'
    const v2 = true
    const v3 = 10
    const obj = Object.assign({}, v1, v2, v3)
    obj // { "0": "a", "1": "b", "2": "c" }
  • Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

数组的处理 Object.assign可以用来处理数组,但是会把数组视为对象。

1
Object.assign([1, 2, 3], [4, 5])	// [4, 5, 3]

上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1

  1. 函数库lodash的_.clone方法

该函数库也有提供_.clone用来做浅拷贝,后面我们会再介绍利用这个库实现深拷贝。

1
2
3
4
5
6
7
8
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
  1. 扩展运算符…

提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

1
2
3
4
5
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
  1. Array.prototype.concat()
1
2
3
4
5
6
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
  1. Array.prototype.slice()
1
2
3
4
5
6
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

深拷贝的实现:

  1. JSON.parse(JSON.stringify())

利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

  1. 函数库lodash的_.cloneDeep方法
1
2
3
4
5
6
7
8
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
  1. jQuery.extend()方法

jquery 有提供一個$.extend可以用来做 Deep Copy

1
2
3
4
5
6
7
8
9
10
11
$.extend(deepCopy, target, object1, [objectN])  	//第一个参数为true,就是深拷贝

var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

  1. 手写递归方法

递归方法实现深度拷贝原理:遍历 对象或数组 直到里边都是基本数据类型,然后再去复制,就是深度拷贝

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

数组实现深拷贝可以使用以下方法

  • 使用slice()
  • 使用concat()
  • ES6扩展运算符[…str]
  • Array.from()

5.手写深拷贝函数 (√)

这里写了三种,参考资料:手写深浅拷贝

  1. 简单版(JSON)
1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
  1. 基础版(手写递归)

下面是一个实现 deepClone 函数封装的例子,通过 for in 遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
  1. 进阶版(递归实现)
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
// 判断一个对象是否为复杂数据类型,即对象或函数类型,且不为 null
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

// 定义深拷贝函数 deepClone,接受两个参数:obj 为要进行深拷贝的目标对象,hash 为已经拷贝过的对象的哈希表(用于解决循环引用问题)
const deepClone = function (obj, hash = new WeakMap()) {
// 1.如果目标对象是日期对象,则直接返回一个新的日期对象,避免修改原日期对象
if (obj.constructor === Date) {
return new Date(obj)
}
// 2.如果目标对象是正则对象,则直接返回一个新的正则对象,避免修改原正则对象
if (obj.constructor === RegExp){
return new RegExp(obj)
}
// 3.如果目标对象已经被拷贝过,则从 hash 中获取已经拷贝过的对象并返回,避免出现循环引用问题
if (hash.has(obj)) {
return hash.get(obj)
}
// 获取目标对象的所有属性描述符
let allDesc = Object.getOwnPropertyDescriptors(obj)
// 创建一个新对象 cloneObj,并将其原型链指向 obj 的原型对象
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
// 将目标对象和克隆对象的映射关系存入 hash 中,用于解决循环引用问题
hash.set(obj, cloneObj)
// 遍历目标对象的所有属性(包括字符串类型和 Symbol 类型的属性名)
for (let key of Reflect.ownKeys(obj)) {
// 如果目标对象的属性值是复杂数据类型(即对象或数组),则递归调用 deepClone 函数进行深拷贝,并将拷贝结果赋值给克隆对象的对应属性
if (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') {
cloneObj[key] = deepClone(obj[key], hash)
} else {
// 如果目标对象的属性值不是复杂数据类型,则直接将其赋值给克隆对象的对应属性
cloneObj[key] = obj[key]
}
}
// 返回深拷贝后的新对象
return cloneObj
}

6.call、apply、bind的作用和区别?(√)

其他问法:改变this的方法,apply和call最初设计的时候为什么要设计这两个,为什么apply参数是数组call不是

🍀 call 和 apply

作用:用来改变函数内部 this 的指向。

特点:

  • 任何函数都可以调用这两个方法,说明它们是添加在函数原型上的方法(Function.prototype)。
  • 调用 callapply 的函数会立即执行。callapply 的返回值就是函数的返回值。
  • 调用 callapply 指向 undefined 或者 null ,会将 this 指向 window
  • 调用 callapply 指向一个值类型, 会将 this 指向由它们的构造函数创建的实例。
  • 调用 callapply 指向一个引用类型, 会将 this 指向这个对象。

call 和 apply的区别

除了传参的形式不同没什么区别,传给fn的参数写法不同:

  • call 接收多个参数,第一个为函数上下文也就是 this ,后边参数为函数本身的参数。
  • apply 接收两个参数,第一个参数为函数上下文 this,第二个参数为函数参数只不过是通过一个 数组 的形式传入的。

🍀 bind

作用:也是用来改变函数内部 this 的指向。

bind 和 call/apply 的区别

1. 是否立刻执行

  • call/apply 改变了函数的 this 上下文后 马上 执行该函数。
  • bind 则是返回改变了上下文后的函数,不执行该函数

2. 返回值的区别:

  • call/apply 返回函数。
  • bind 返回函数的拷贝,指定了 函数 的 this 指向,保存了函数的参数。

7.轮播图实现思路 (√)

实现前端轮播图(也称为轮播幻灯片或轮播广告)通常涉及以下主要步骤和思路:

  1. HTML结构: 首先,在HTML中创建轮播图的基本结构。通常会使用一个包含图像或内容的容器,并为每个轮播项创建一个子元素。
1
2
3
4
5
6
7
8
9
<div class="carousel">
<div class="carousel-item">
<!-- 内容或图像1 -->
</div>
<div class="carousel-item">
<!-- 内容或图像2 -->
</div>
<!-- 更多轮播项 -->
</div>
  1. CSS样式: 使用CSS样式来定义轮播图容器的外观,包括宽度、高度、位置等。还可以设置轮播项的布局和过渡效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
.carousel {
width: 100%;
overflow: hidden;
position: relative;
}
.carousel-item {
width: 100%;
display: none; /* 隐藏所有项,除了当前活动的项 */
transition: opacity 1s; /* 过渡效果 */
}
.carousel-item.active {
display: block; /* 显示当前活动的项 */
}
  1. JavaScript交互: 使用JavaScript来实现轮播图的交互逻辑。以下是一些常见的实现思路:
    • 自动播放: 设置定时器,以一定的时间间隔自动切换到下一张轮播项。
    • 手动控制: 添加前进和后退按钮,以允许用户手动浏览轮播项。
    • 指示器: 创建轮播指示器,以显示当前轮播项的位置,并允许用户通过点击指示器来切换轮播项。
    • 循环播放: 当到达最后一张轮播项时,循环回到第一张。
    • 响应式设计: 确保轮播图在不同屏幕尺寸下适应,并且图像大小和数量能够自动调整。
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
let currentSlide = 0;
const slides = document.querySelectorAll('.carousel-item');

function showSlide(index) {
slides[currentSlide].classList.remove('active');
slides[index].classList.add('active');
currentSlide = index;
}

function nextSlide() {
const next = (currentSlide + 1) % slides.length;
showSlide(next);
}

function prevSlide() {
const prev = (currentSlide - 1 + slides.length) % slides.length;
showSlide(prev);
}

// 自动播放
setInterval(nextSlide, 5000);

// 手动控制按钮
const nextButton = document.getElementById('next-button');
const prevButton = document.getElementById('prev-button');

nextButton.addEventListener('click', nextSlide);
prevButton.addEventListener('click', prevSlide);

优化和性能: 对于包含大量图像的轮播图,应考虑性能优化,如懒加载图像,压缩图像大小等。

8.闭包的原理,有哪些应用?(√)

要理解闭包,首先必须理解Javascript特殊的变量作用域。变量的作用域无非就是两种:全局变量和局部变量。

  • 定义一个外层函数,外层函数内部再定义一个内层函数,这个内层函数和外层函数的变量之间存在的联系我们称为闭包。如果外层函数执行完之后,内层函数还存在,并且引用了外层函数的变量,这时就形成了闭包。外层函数的变量不会被销毁,而是继续存在于内部函数的作用域中。

闭包的应用

  1. 封装和模块化:闭包可以用于创建私有变量和函数,使其不被外部访问。将相关的功能封装在一个函数内部,只暴露必要的接口给外部,从而实现封装和模块化的代码结构。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function createCounter() {
    let count = 0;
    return function () {
    return ++count;
    };
    }

    const counter = createCounter();
    console.log(counter()); // 1
    console.log(counter()); // 2

2,保存状态:闭包可以用于保存函数执行时的状态,例如计数器、事件处理等。

1
2
3
4
5
6
7
8
9
10
11
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA()); // 1
console.log(counterB()); // 1
console.log(counterA()); // 2

3.函数柯里化:闭包可以用于函数柯里化,将一个多参数函数转化为一系列单参数函数。

1
2
3
4
5
6
7
8
function add(x) {
return function (y) {
return x + y;
};
}

const addFive = add(5);
console.log(addFive(3)); // 8

4.回调函数:在异步编程中,常常会使用闭包来处理回调函数,以保持上下文和访问相关的数据。

1
2
3
4
5
6
7
8
9
10
11
function fetchData(url, callback) {
// 异步操作获取数据
setTimeout(() => {
const data = "Some data from " + url;
callback(data);
}, 1000);
}

fetchData("https://example.com", function(data) {
console.log(data);
});

5.事件处理:DOM 事件处理函数常常使用闭包来访问事件发生时的上下文信息。

1
2
3
4
5
6
7
8
9
function addClickHandler(element) {
element.addEventListener("click", function() {
console.log("Clicked element: " + element.id);
});
}
const button1 = document.getElementById("button1");
const button2 = document.getElementById("button2");
addClickHandler(button1);
addClickHandler(button2);

定时器:在定时器中使用闭包可以保持定时器回调函数对特定数据的访问。

1
2
3
4
5
6
7
8
9
10
11
12
function startTimer() {
let count = 0;
const intervalId = setInterval(function() {
count++;
console.log("Count: " + count);
if (count >= 5) {
clearInterval(intervalId);
}
}, 1000);
}

startTimer();

总之,闭包是JavaScript中强大而灵活的概念,它在许多方面都有应用,包括封装数据、模块化开发、事件处理、回调函数和定时器等。理解闭包的原理可以帮助你更好地利用它来解决各种编程问题。

但要注意,过度使用闭包可能会导致内存泄漏,因此在使用闭包时需要小心管理内存。

9.手写promise

10.原型链的理解

11.对继承有什么了解吗?有几种方式?优劣?

12.对异步编程有什么了解?就回调函数、Promise,async/await,具体的是指什么?

13.执行上下文和执行上下文栈(√)

在介绍执行上下文之前,先说明一下变量提升和函数提升。

1.变量提升和函数提升

  1. 变量声明提升

    1. 通过var定义(声明)的变量,在定义语句之前就可以访问到
    2. 值:undefined
  2. 函数声明提升

    1. 通过function声明的函数,在之前就可以直接调用
    2. 值:函数定义(对象)
    1
    2
    3
    4
    5
    console.log(fn);			// [Function: fn]
    console.log(typeof fn); // function
    function fn() {
    console.log(1);
    }

    ① 变量只提升声明,赋值依旧在实际代码所在处;函数是声明和赋值都提升,且函数提升优先级高于变量提升。
    ② 变量提升不会覆盖同名函数提升,只有变量再次被赋值时,才会被覆盖

    **③ 同名函数提升会被后来者覆盖**
    

那么变量提升和函数提升是如何产生的?

2.执行上下文

我们知道代码分为全局代码和函数代码,全局代码有全局执行上下文,主要表现在:

  • 在执行全局代码前将window确定为全局执行上下文
  • 对全局数据进行预处理,这里包含3个步骤
    • var定义的全局变量给值undefined,添加为window的属性
    • function声明的全局函数==>赋值(fun),添加为window的方法
    • this赋值为window
  • 全局代码预处理完成之后开始执行全局代码

函数代码有函数执行上下文,主要表现为:

  • 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象
  • 对局部函数进行预处理,这里有5个步骤
    • 形参变量—>赋值(实参)—->添加为执行上下文的属性
    • arguments—>赋值(实参列表),添加为执行上下文的属性
    • var定义的局部变量—–》赋值为undefined,添加为执行上下文的属性
    • function声明的全局函数==》赋值(函数),添加为执行上下文的方法
    • this赋值为(调用函数的对象)
  • 预处理完成之后开始执行函数体代码

除了执行上下文,还有执行上下文栈。执行上下文栈也就是

  • 在全局代码执行前,JS引擎会创建一个栈来存储管理所有的执行上下文对象
  • 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
  • 在函数执行上下文创建后,将其添加到栈中(压栈)
  • 在当前函数执行完后,将栈顶对象移除(出栈)
  • 当所有的代码执行完后,栈中只剩下window

这就是我对执行上下文栈的理解。

1
2
3
4
5
6
console.log(bar); // f bar() { console.log(123) }
console.log(bar()); // undefined
var bar = 456;
function bar() {
console.log(123); // 123
}

上面代码的输出顺序为:

ƒ bar() {
console.log(123); // 123
}

123

undefined

【解析】因为先执行了函数,所以输出123,输出undefined是因为函数没有返回值。

14.作用域和作用域链、块级作用域(√)

作用域:全局作用域、函数作用域、块级作用域(ES6出的)

全局作用域:

  • 全局作用域是整个 JavaScript 程序的最外层作用域。在全局作用域中声明的变量和函数可以在代码的任何地方访问,它们通常被称为全局变量和全局函数。全局作用域在整个应用程序的生命周期中存在,直到应用程序关闭或页面刷新为止。

  • 全局作用域有一个全局对象windom,它代表的是一个浏览器的窗口,它由浏览器创建,我们可以直接使用

  • 在全局作用域中:创建的变量都会作为window对象的属性,创建的方法都会作为windom的方法

函数作用域:

  • 函数作用域是在函数内部声明的变量和函数所拥有的作用域。这意味着在函数内声明的变量只能在该函数内部访问,外部的代码无法直接访问这些变量。这样做的好处是有助于封装变量,防止命名冲突,并且可以提高代码的可维护性。
  • 我们在调用函数的时候会创建函数作用域,函数执行完毕以后,函数作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。
  • 需要注意的是:在函数作用域中可以使用window对象访问到全局作用域的变量,但在全局作用域中无法访问到函数作用域的变量。当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用如果没有则向上一级作用域中寻找,直到找到全局作用域。如果全局作用域中依然没有找到,则会报错ReferenceError。

块级作用域(Block Scope)

  • 块级作用域是在ES6中引入的新概念,它使得变量在 { } 块中声明时具有局部作用域,而不再仅限于函数作用域。块级作用域通常与 letconst 关键字一起使用,这样做的好处是有助于更细粒度地控制变量的作用域。
  • if 语句和 for 语句里面的 { } 也属于块作用域。

1. 作用域的作用有很多

  1. 比如:变量封装和隔离: 作用域允许你将变量和函数封装在一个特定的区域内,以防止命名冲突和不必要的全局污染。这意味着你可以在不同的作用域中定义具有相同名称的变量或函数而不会相互干扰。
  2. 还有生命周期管理: 作用域还控制了变量的生命周期,即它们存在的时间段。在离开作用域时,作用域内的变量通常会被销毁,从而释放内存资源。这样做的好处是有助于减少内存泄漏和提高代码的性能。
  3. 作用域链: 作用域链是一种嵌套的结构,允许内部作用域访问外部作用域中的变量。这种机制使得函数能够访问其定义范围之外的变量,如闭包(closure)中的概念。
  4. 代码组织和模块化: 作用域有助于将代码模块化,使得代码更易于组织、维护和重用。你可以将相关的变量和函数放在同一个作用域中,以创建更清晰和可维护的代码结构。

2. (不算块级作用域)有多少个作用域:n+1原则,n表示定义了几个函数,1表示全局作用域

3. 作用域和执行上下文的区别:

  • 区别1:
    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时。
    • 全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建的
    • 函数执行上下文环境是在调用函数时,函数体代码执行之前创建
  • 区别2:
    • 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
    • 上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会被释放
  • 联系
    • 上下文环境(对象)是从属于所在的作用域的
    • 全局上下文环境==>全局作用域
    • 函数上下文环境==>对应的函数作用域

作用域链

1.理解:多个上下级关系的作用域形成的链,它的方向是从内到外的。查找变量时就是沿着作用域链来查找的。

2.查找一个变量的查找规则:

  • 在当前作用域下的执行上下文中查找对应的属性,如果有则返回,否则进入2;
  • 在上一级作用域的执行上下文中查找对应的属性,如果有则返回,否则进行3;
  • 再次执行2的相同操作,直到全局作用域,如果还是找不到就抛出找不到的异常。

块级作用域

引入块级作用域的主要原因是提高 JavaScript 代码的可维护性、可读性和安全性。以下是引入块级作用域的一些重要原因:

  1. 避免变量污染(Variable Pollution): 在传统的函数作用域中,变量通常是函数级别的,这意味着在函数内部声明的变量在整个函数内都可见,容易导致变量污染问题,特别是在嵌套函数中。块级作用域允许我们在更小的范围内声明变量,避免了不必要的变量冲突。
  2. 更安全的代码: 块级作用域使得变量的作用范围更加明确和受限。这有助于减少意外的变量重写和不必要的副作用。在块级作用域内声明的变量仅在块内部可见,而不会泄漏到外部作用域。
  3. 更好的内存管理: 在块级作用域中声明的变量拥有更短的生命周期。一旦块执行结束,这些变量就会被销毁,从而帮助更有效地管理内存。这对于避免内存泄漏问题非常有帮助。
  4. 可读性和可维护性: 使用块级作用域可以更清晰地表达变量的作用范围,使代码更容易理解和维护。这对于大型项目和团队协作特别有价值。
  5. 解决闭包问题: 在传统函数作用域中,闭包可能会导致一些意外的问题,因为函数作用域的变量在整个函数范围内可见。块级作用域可以减少闭包问题的出现。

引入块级作用域的方式是使用 letconst 关键字来声明变量,它们具有块级作用域,而不是像 var 一样具有函数作用域。这样的改进使得 JavaScript 代码更加可控和可预测,有助于提高代码质量和安全性。

15.事件循环(√)

其他问法:给一段代码,事件循环结果输出是什么

事件循环的动画演示:浏览器EventLoop事件循环机制动画演示_哔哩哔哩_bilibili

JS是一个单线程的动态解释性语言,需要通过JS引擎(JS Engine)进行解释(翻译)成对应的字节码、机器码然后才会运行。随着网页复杂性和性能要求的提高,JS引擎也经历了从SpiderMonkeyV8(由google开发)的变革,而由谷歌开发的V8引擎最为出色,目前已被大多数现代浏览器等(Chrome、Edge、Safari)采用。同时JS也从以前浏览器单一的运行时(Runtime)演变到可以在服务端运行的NodeJS(基于V8)运行时,为它提供不同平台的运行时环境。

  • Runtime:由JavaScript的宿主环境提供额外的属性和方法,如浏览器提供了用户交互和一些异步任务的功能。
  • Engine:为JavaScript解析和执行提供环境条件(类似Java虚拟机),并完成内存分配和垃圾回收等等。

JS的是通过异步回调的方式解决单线程的执行阻塞问题,虽然JS引擎是单线程的,但它的宿主环境一般都是多线程的,如通过浏览器的定时任务线程、网络线程协调执行异步回调。所以常说的EventLoop是面向宿主环境的也就是Runtime,如浏览器和NodeJS。EventLoop主要是宿主环境实现的

这里我们需要先了解下浏览器的架构,本文以Chrome浏览器作为介绍:

它是多进程和多线程的架构,其内部包括:

  • Brower进程:提供浏览器URL、后退/前进、调度等全局作用
  • 网络进程:进行网络资源请求、安全策略等等
  • GPU进程:3D渲染、动画绘制等等
  • 渲染进程:负责每个Tab页面加载解析,JS、CSS、DOM等相关页面和用户操作
  • 插件进程:浏览器插件

除了以上列出的进程外,还有一些其它的进程。

这里主要来说下渲染进程,它是前端开发者最必要的关注点。Chrome为每个tab页面提供一个渲染进程。渲染进程会包括很多线程:

  1. 主线程:调度页面的加载解析,执行dom、css、js操作处理等等
  2. GUI线程:负责页面的渲染
  3. JS引擎线程:进行解析执行JS
  4. 定时器线程:处理异步定时任务
  5. 异步请求线程:进行网络请求
  6. 事件触发线程:监听执行事件callback
  7. WebWorker线程:独立的脚本,不影响页面渲染,通常用来执行密集复杂的计算

等等…

当加载页面时会从上到下解析文档,当遇到JS脚本(通常情况下)时会阻塞DOM的解析,也就是JS引擎的执行会阻塞GUI线程渲染的执行,这也符合JS是个单线程语言的特征。不过渲染进程也提供了不同的线程去处理异步任务,可以并行处理多个任务,如:定时器线程、网络请求线程等等,而不会影响页面的渲染推翻JS单线程的理念。

其实浏览器多线程执行异步任务的原理背后是基于事件驱动机制的。不同类型的事件触发不同的任务,如:点击事件、滚动事件等等。而事件循环机制(EventLoop)就是基于事件驱动机制的。当JS执行代码时,如果遇到异步代码如Ajax请求时,会交给别的线程去执行异步任务,然后主线程挂起当前任务,不会阻塞后面代码的执行。这些异步任务会由浏览器不同的线程进行负责,不会影响到主线程和JS引擎线程,当这些异步任务执行完毕后,会被存放到指定的任务队列中,等JS的执行栈中当前同步任务执行完毕后,会从这些任务队列中取出待执行的任务,而具体优先取哪一个这就是要取决于事件循环机制了。

通过上面的介绍你应该会了解到浏览器的多线程其实就是让JS拥有多线程并发处理异步任务的能力,主要负责点击等事件、定时任务、网络请求、脚本执行、用户交互和页面渲染之间的的调度。

JS的运行机制就是事件循环!事件循环(Event Loop)是一种处理异步操作的编程模型,通常在单线程的环境中使用,如浏览器的JavaScript引擎或Node.js。事件循环允许程序执行异步任务,而不会阻塞主线程,以确保应用程序的响应性和性能。

  1. 事件循环的基本原理
    • 事件循环是一个持续运行的循环,它不断检查任务队列(Task Queue)中是否有待处理的任务。
    • 任务可以是回调函数,例如定时器回调、事件处理器或Promise的then回调。
    • 当任务队列中有任务时,事件循环将逐个执行这些任务,并将它们从队列中移除。
  2. 事件循环的组成部分
    • 调用栈(Call Stack):用于执行同步代码,每个函数调用都会在栈上创建一个帧,帧中包含了 函数 的参数和局部变量。
    • 任务队列(Task Queue):用于存储待处理的异步任务,包括宏任务(如setTimeout)和微任务(如Promise)。所以任务队列里有宏任务队列和微任务队列。
    • 事件触发线程:用于监听外部事件,例如鼠标点击或HTTP请求,将相关任务推送到任务队列中。(也可以理解为web APIs)
  3. 事件循环的执行流程
    • 从调用栈开始,执行同步代码。
    • 遇到异步任务(宏任务或微任务),会委托给宿主环境执行。已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行。
    • 继续执行同步代码,直到调用栈为空。
    • 从任务队列中取任务,如果有微任务则先执行微任务,然后在执行宏任务,直到任务队列为空。
    • 重复上述步骤。

当主线程中的执行栈被清空后,JS主线程会从任务队列中读取异步任务的回调函数,放在执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又被称为EventLoop(事件循环)。

易错点:

  1. 如果是定时器任务,则会在宿主环境执行了定时器的时间之后再加入宏任务队列。比如设置1s,则会在1s后加入到宏任务队列;
  2. promise本身是一个同步的代码(只是容器),只有它后面调用的then()方法里面的回调才是微任务
  3. await右边的表达式还是会立即执行,表达式之后的代码才是微任务, await微任务可以转换成等价的promise微任务分析
  4. script标签本身是一个宏任务, 当页面出现多个script标签的时候,浏览器会把script标签作为宏任务来解析

参考:EventLoop事件循环机制(浏览器和Node EventLoop) 事件循环机制(Event Loop)的基本认知 - 掘金 (juejin.cn)

16.宏任务和微任务(√)

先来了解一下三个重要的概念主线程微任务(micro task)宏任务(macro task)

主线程

所有的同步任务都是在主线程里执行的,异步任务可能会在macrotask或者microtask里面

  • 同步任务: 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  • 异步任务: 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

微任务(micro task)

  • promise:promise本身是一个同步的代码(只是容器),只有它后面调用的then()方法里面的回调才是微任务
  • async/await:await右边的表达式还是会立即执行,表达式之后的代码才是微任务
  • process.nextTick(node)
  • mutationObserver(html5新特性)

宏任务(macro task)

  • script(整体代码):script标签本身是一个宏任务, 当页面出现多个script标签的时候,浏览器会把script标签作为宏任务来解析
  • setTimeout、setInterval
  • setImmediate
  • I/O(输入输出)
  • UI render
  • ajax
  • (DOM操作): 如没有阻塞的插入元素
  • 读取文件
  • navigation和history:导航和history操作
  • 常见的键盘、鼠标、Ajax、setTimeout、setInterval、操作数据库等都属于MacroTask Source

【注意】

  • 微任务是由ES6语法规定的

  • 宏任务是由浏览器规定的

  • 微任务在DOM渲染前触发,宏任务在DOM渲染后触发

在EventLoop中的每一次循环成一个tick,每一次tick都会先执行同步任务,然后查看是否有微任务,将所有的微任务在这个阶段执行完,如果执行微任务阶段再次产生微任务也会把他执行完(每次tick只会有一个微任务队列),接下来会可能会进行视图的渲染,然后再从MacroTask队列中选择一个合适的任务放入执行栈执行,然后重复前面的步骤不断循环

参考资料:事件循环机制(Event Loop)的基本认知 - 掘金 (juejin.cn)

17.对象的遍历(√)

要遍历对象中的键值对,你可以使用不同的方法,具体取决于你的需求和 JavaScript 的版本。以下是两种常见的遍历对象键值对的方法:

  1. 使用 for…in 循环(适用于普通对象):

    1
    2
    3
    4
    5
    6
    7
    8
    const myObject = { a: 1, b: 2, c: 3 };

    for (const key in myObject) {
    if (myObject.hasOwnProperty(key)) {
    const value = myObject[key];
    console.log(key + ": " + value);
    }
    }

    请注意,使用 hasOwnProperty 来检查属性是否属于对象是为了确保不会遍历到原型链上的属性。

  2. **使用 Object.keys() 和 forEach()**(适用于普通对象):

    1
    2
    3
    4
    5
    6
    const myObject = { a: 1, b: 2, c: 3 };

    Object.keys(myObject).forEach((key) => {
    const value = myObject[key];
    console.log(key + ": " + value);
    });

    这种方法使用 Object.keys() 获取对象的所有键,然后使用 forEach() 方法遍历键,以访问相应的值。

  3. 使用 for…of 循环(适用于 ES6 中的 Map 和 Set,以及可迭代对象):

    1
    2
    3
    4
    5
    6
    7
    8
    const myMap = new Map();
    myMap.set("a", 1);
    myMap.set("b", 2);
    myMap.set("c", 3);

    for (const [key, value] of myMap) {
    console.log(key + ": " + value);
    }

    这种方法适用于 Map、Set 和其他可迭代对象,它允许直接在循环中获取键和值。

18.继承

js常见的继承方式包括原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承,以及ES6新增的class继承。

参考:JavaScript实现继承的6种方式 - Leophen - 博客园 (cnblogs.com)

19.null和undefined的区别

1 实习内容介绍,你们的分页怎么实现,假如不给你total值,你可以实现分页吗,你觉得一个分页组件的核心要素是什么(有没有佬解答一下)
7渲染10w条数据怎么优化,虚拟列表技术上怎么实现,闪烁怎么解决
8常见性能优化手段
9假如用户打开了你的网站,但是是白屏你怎么解决
10说一下hashmap,常见数据结构
12nodejs有没有接触
13你对前端的看法,你更想做前端的哪个方向

📚 ES6

1.ES6模块化

2.Promise(√)

Promise 是 JavaScript 中用于处理异步操作的对象。它提供了一种更结构化和可管理的方式来处理异步代码,以避免回调地狱(Callback Hell)和提高代码的可读性。

异步操作有哪些?异步操作是指在程序执行过程中不会等待某个任务完成而是继续执行其他任务的一种编程模式。在计算机编程中,有许多情况下需要进行异步操作,以下是一些常见的异步操作的示例:

  1. 网络请求:从服务器获取数据通常需要一段时间,因此在发起网络请求后,通常会继续执行其他任务,而不会等待数据返回。
  2. 文件读写:读取大文件或者写入文件都可能需要花费一定时间,因此通常以异步方式执行。
  3. 定时任务:设置定时器执行某个任务,例如在一段时间后触发回调函数。
  4. 事件处理:监听事件,例如点击事件、键盘事件、鼠标事件等,当事件发生时执行相关的回调函数。
  5. 数据库操作:连接数据库、查询数据、写入数据等数据库操作通常是异步的。
  6. 动画和多媒体处理:在网页开发中,动画和多媒体处理通常需要异步执行,以确保界面流畅性。
  7. Promise 和异步函数:使用 JavaScript 中的 Promise 和异步函数(async/await)来管理和处理异步操作。
  8. 并发任务:同时执行多个任务,例如并发下载多个文件。
  9. 外部设备交互:与外部硬件设备(例如传感器、摄像头、打印机等)通信通常是异步的。
  10. 用户输入:等待用户输入数据,例如等待用户填写表单或选择选项。

异步操作是现代计算机编程中不可或缺的部分,因为它们允许程序在等待某些任务完成时继续执行其他任务,从而提高了程序的响应性和性能。在异步编程中,通常使用回调函数、Promise、异步函数(async/await)等机制来处理异步操作,以确保正确的执行顺序和错误处理。

以下是 Promise 的详细介绍:

1.Promise 的状态

  • Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
  • 当一个异步操作开始时,Promise 处于 pending 状态。
  • 当操作成功完成时,Promise 会从 pending 转变为 fulfilled,并传递一个结果值。
  • 当操作发生错误时,Promise 会从 pending 转变为 rejected,并传递一个拒绝原因。

2.Promise 的基本结构

1
2
3
4
5
6
7
8
const myPromise = new Promise((resolve, reject) => {
// 异步操作,根据结果调用 resolve 或 reject
if (/* 操作成功 */) {
resolve(result); // 成功时调用 resolve 并传递结果
} else {
reject(error); // 失败时调用 reject 并传递错误信息
}
});

3.Promise 的方法

  • .then(onFulfilled, onRejected): 用于注册回调函数,当 Promise 进入 fulfilled 状态时,调用 onFulfilled,当进入 rejected 状态时,调用 onRejected
  • .catch(onRejected): 用于捕获 Promise 的错误,相当于 .then(null, onRejected)
  • .finally(onFinally): 无论 Promise 的状态如何,都会执行 onFinally 回调,通常用于清理工作。
  • .all(iterable): 接受一个可迭代对象(通常是数组),并在所有 Promise 都成功时返回所有成功promise成功结果组成的数组,如果其中任何一个失败,失败的结果是里面失败的promise的结果。
  • .race(iterable): 接受一个可迭代对象,并返回一个新的 Promise,当其中任何一个 Promise 第一次实现成功或失败时,返回的 Promise 也相应地成功或失败。

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
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const randomNum = Math.random();
if (randomNum > 0.5) {
resolve(randomNum); // 模拟成功
} else {
reject("Error"); // 模拟失败
}
}, 1000);
});
};

fetchData()
.then((result) => {
console.log("Success:", result);
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
console.log("Cleanup");
});

Promise 是处理异步代码的一种强大工具,它使得异步操作更具可读性和可维护性。但要注意,Promise 本身不处理多个异步操作之间的依赖关系,这时可以结合使用 Promise.all 或其他异步控制工具来处理。

5.promise的优点

Promise 在处理异步操作时具有许多优点,其中一些主要优点包括:

  1. 可读性和可维护性:Promise 提供了一种结构化的方式来处理异步代码,通过链式调用 .then().catch() 方法,可以更清晰地表达代码的逻辑,减少了回调地狱(Callback Hell)的问题,使代码更易于理解和维护。
  2. 错误处理:Promise 具备良好的错误处理机制。你可以使用 .catch() 方法捕获 Promise 中的错误,这使得错误处理更加一致和可控。
  3. 顺序控制:Promise 允许你按照特定的顺序执行异步操作,而不需要深度嵌套回调函数。这使得代码的执行流程更容易理解,特别是在复杂的异步场景中。
  4. 并行执行:使用 Promise.all(),你可以并行执行多个异步操作,并在它们都完成后获得结果。这提高了性能,特别是在需要同时发起多个请求的情况下。
  5. 可组合性:Promise 非常容易组合,你可以将多个 Promise 链接在一起以执行复杂的异步任务,这种可组合性有助于构建模块化和可重用的异步代码。
  6. 更好的错误跟踪:由于 Promise 具有状态和错误处理机制,因此当出现问题时,可以更轻松地跟踪错误的来源和上下文,有助于快速诊断和修复问题。
  7. 标准化:Promise 是一种标准的异步处理方法,已被广泛采纳并集成到 JavaScript 和许多库和框架中。这意味着开发者可以在不同的环境中共享和理解 Promise 代码。
  8. 异步代码的统一接口:Promise 提供了一种统一的接口,不论是处理异步的浏览器 API、Node.js 回调风格的函数,还是其他异步操作,都可以很容易地包装成 Promise,使得异步操作的处理方式更加一致。

总之,Promise 是一种强大的工具,它提供了更好的异步编程体验,使得异步代码更易于编写、理解和维护,并提供了一种标准化的方式来处理异步操作。这些优点有助于改善代码质量和开发效率。

6.promise实现按照特定的顺序执行异步操作

要按照特定顺序执行异步操作,你可以使用 Promise 的链式调用方法 .then() 来实现。每个 .then() 方法返回一个新的 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
javascriptCopy code// 模拟异步操作,返回一个 Promise
function asyncOperation(order, delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Operation ${order} completed`);
resolve(order); // 模拟操作成功,传递订单号
}, delay);
});
}

// 按照特定顺序执行异步操作
asyncOperation(1, 1000)
.then((result) => {
return asyncOperation(2, 2000); // 等第一个操作完成后执行第二个操作
})
.then((result) => {
return asyncOperation(3, 500); // 等第二个操作完成后执行第三个操作
})
.then((result) => {
console.log("All operations completed");
})
.catch((error) => {
console.error("Error:", error); // 捕获任何操作中的错误
});

在上面的示例中,asyncOperation 函数模拟了异步操作,每个操作都有一个延迟时间。通过使用 .then() 方法,我们确保每个操作在前一个操作完成后才执行,从而按照特定顺序执行这些异步操作。

这种方法允许你构建异步操作的串行流程,确保它们按照你的期望顺序执行。如果任何一个操作失败,你也可以使用 .catch() 方法捕获错误。

7.promise的应用场景

Promise 在 JavaScript 中有广泛的使用场景,特别是在处理异步操作时,它们非常有用。以下是一些常见的 Promise 使用场景:

1. 网络请求:当你需要从服务器获取数据时,通常会使用 Promise。这可以包括使用 fetch API 或其他库(如 Axios)来执行 HTTP 请求。

1
2
3
4
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

2. 定时任务:使用 setTimeoutsetInterval 进行定时任务,通常结合 Promise 使用,以便在一定时间后执行某个操作。

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

delay(2000) // 等待 2 秒
.then(() => console.log('2 秒后执行的任务'));

3. 文件操作:读取和写入文件通常是异步操作,Promises 也用于处理这些操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascriptCopy codeconst fs = require('fs');

function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}

readFileAsync('file.txt')
.then(data => console.log(data))
.catch(error => console.error(error));

4.并行执行多个任务Promise.all 可用于并行执行多个异步任务,并等待它们全部完成。

1
2
3
4
5
javascriptCopy codeconst promises = [fetchData1(), fetchData2(), fetchData3()];

Promise.all(promises)
.then(results => console.log(results))
.catch(error => console.error(error));

5.处理事件:Promises 可以用于封装事件处理,以便在事件发生时执行异步操作。

1
2
3
4
5
6
7
8
9
10
javascriptCopy codeconst button = document.getElementById('myButton');

function onClick() {
return new Promise((resolve) => {
button.addEventListener('click', resolve, { once: true });
});
}

onClick()
.then(() => console.log('按钮被点击了'));

这些是一些常见的 Promise 使用场景,但实际上 Promises 可以用于几乎任何需要处理异步操作的情况。它们提供了一种更结构化和可管理的方式来处理异步代码,有助于提高代码的可读性和可维护性。

3.ES6、ES7、ES8、ES9、ES10新特性一览(√)

以下是一些 JavaScript 的主要 ECMAScript(ES)版本(ES6 到 ES10)中引入的新特性和改进:

  1. ES6(ECMAScript 2015):
    • 函数:箭头函数、参数默认值。
    • letconst:块级作用域变量声明。
    • symbol
    • 类:引入了类和面向对象编程的支持。
    • 模板字面量:字符串模板。
    • 解构赋值:数组解构、对象解构。
    • 剩余和扩展操作符:... 运算符用于处理可变数量的参数。
    • Promise:更好的异步编程支持。
    • 模块化:引入了 importexport 以支持模块化编程。
    • 对象:map和set、属性名简写、对象和数组新方法
  2. ES7(ECMAScript 2016):
    • Array.prototype.includes():用于检查数组是否包含某个元素的方法。
  3. ES8(ECMAScript 2017):
    • asyncawait:用于更简单的异步代码编写。
    • Object.values() 和 Object.entries():用于遍历对象的值和键值对。
    • 字符串填充:padStart()padEnd() 用于填充字符串。
  4. ES9(ECMAScript 2018):
    • 异步迭代器和 for-await-of 循环:更好的异步迭代支持。
    • Rest/Spread 属性:在对象上使用 ... 运算符。
    • 正则表达式增强:引入 s 修饰符以匹配换行符,并且支持命名捕获组。
  5. ES10(ECMAScript 2019):
    • Array.prototype.flat() 和 Array.prototype.flatMap():用于处理多维数组的新方法。
    • Object.fromEntries():将键值对数组转换为对象。
    • String.prototype.trimStart()String.prototype.trimEnd():去除字符串开头和结尾的空白字符。
    • 动态 import():异步加载模块。
    • 可选链式调用(Optional Chaining):安全地访问深层嵌套对象的属性。
    • 空值合并运算符(Nullish Coalescing):处理 nullundefined 值的默认值问题。

每个新版本都引入了一些语言特性和语法改进,以提高 JavaScript 的功能和可读性,并简化开发人员的工作。

4.变量和函数的声明提前(√)

1.变量:使用var关键字定义的变量会在所有代码执行之前被声明,但是不会赋值。

2.函数:使用函数声明形式创建的函数function 函数名{} 会在所有代码执行之前被创建,所以我们可以在函数声明前来调用函数,使用函数表达式创建的函数不会声明提升。

3.函数作用域也有声明提升的特性:

  • 使用var关键字声明的变量,会在函数中所有的代码执行之前被声明。
  • 函数声明也会在函数中所有代码执行之前执行。

注意:

①在函数中不使用var声明的变量都会变成全局变量

②定义形参相当于在函数作用域中声明了变量

5.let、const、var三者的区别(√)

我们在全局作用域中或局部作用域中,使用var关键字声明的变量,都会被提升到该作用域的最顶部,这就是我们常说的变量提升

let, const, 和 var 是 JavaScript 中用于声明变量的关键字,它们之间有一些重要的区别:

  1. 作用域:
    • letconst 声明的变量具有块级作用域(在 {} 内有效),是es6语法新提出的,不会变量提升。
    • var 声明的变量是函数级作用域的(在函数内有效),并且会变量提升至函数的顶部。(即使写在if代码块里也是看函数作用域的)
  2. 变量提升:
    • var 声明的变量会被提升到函数或全局作用域的顶部,即使在声明之前访问它,也不会报错,但值为 undefined
    • letconst 声明的变量没有变量提升,在访问之前会存在暂时性死区(Temporal Dead Zone,TDZ),访问时会报错,表示该变量存在但不可访问的状态。
  3. 重复声明:
    • 使用 var 可以多次声明同名变量,而不会抛出错误。
    • 使用 letconst 在同一作用域内重复声明同名变量会引发语法错误。
  4. 可变性:
    • var 声明的变量可以重新赋值,也可以不赋值。
    • let 声明的变量可以重新赋值,但不可以不赋值。
    • const 声明的变量必须在声明时初始化,并且不能重新赋值。
  5. 全局对象属性:
    • 使用 var 声明的全局变量会成为全局对象(例如,window 在浏览器中)的属性。
    • 使用 letconst 声明的全局变量不会成为全局对象的属性。
  6. 适用场景:
    • 推荐使用 letconst,因为它们更安全,避免了许多常见的问题,特别是 const 对于不需要重新赋值的情况。
    • 在需要支持旧版浏览器或在一些特定情况下,仍然可以使用 var

总结来说,letconst 是现代 JavaScript 推荐的变量声明方式,它们提供了更严格的作用域和变量行为,有助于减少错误。在编写新的代码时,应优先使用 letconst,只有在特殊情况下才使用 var

6.async/await(√)

async/await 是 JavaScript 中用于处理异步操作的语法糖,它们使得异步代码的编写和阅读更加清晰和直观。async 用于定义一个异步函数,而 await 用于等待一个异步操作完成。以下是关于 async/await 的详细解释以及如何使用它们:

async 函数

  • async 是一个关键字,用于定义一个异步函数,它在函数前面添加 async 关键字来表示这个函数将返回一个 Promise。
1
2
3
4
async function fetchData() {
// 异步操作
return result;
}

await 表达式

  • await 也是一个关键字,用于等待一个 Promise 完成并返回其结果。它只能在 async 函数内部使用。
1
2
3
4
async function example() {
const result = await fetchData(); // 等待 fetchData() 异步操作完成
console.log(result);
}

async/await 的作用

  1. 改善异步代码的可读性async/await 可以将异步操作的嵌套回调转化为更线性的代码,使得代码更容易理解。
  2. 更好的错误处理try/catch 结合 await 可以更容易地捕获和处理异步操作中的错误,提高了代码的可维护性。
  3. 等待异步操作完成await 让你可以等待异步操作的结果,而不需要回调函数或 Promise 的 .then() 方法来处理结果。

如何使用 async/await

1. 定义异步函数:首先,使用 async 关键字定义一个异步函数。

1
2
3
async function fetchUserData() {
// 异步操作
}

2. 在函数内使用 await:在函数内部,使用 await 关键字等待一个异步操作的完成。这个异步操作通常是返回一个 Promise 的函数调用。

1
2
3
4
5
async function fetchUserData() {
const response = await fetch('https://api.example.com/userdata');
const data = await response.json();
return data;
}

3. 调用异步函数:在其他地方调用异步函数时,可以使用 .then() 来处理 Promise 的结果,或者将其包装在另一个异步函数中并使用 await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fetchUserData()
.then((userData) => {
console.log(userData);
})
.catch((error) => {
console.error(error);
});

// 或者使用 async/await
async function main() {
try {
const userData = await fetchUserData();
console.log(userData);
} catch (error) {
console.error(error);
}
}

main();

async/await 是一种强大的工具,可以显著提高异步代码的可读性和可维护性,使得异步编程更加愉快和容易。但请注意,async/await 只能在支持 ECMAScript 2017(ES8)及更高版本的 JavaScript 环境中使用。

7.map 和 set (√)

Map对象用于保存键值对,任何JavaScript支持的值都可以作为一个键(key)或者一个值(value)。typeof 返回 object。 与对象不同的是

  1. object的键只能是字符串或ES6的symbol值,而Map可以是任何值。
  2. Map对象有一个size属性,存储了键值对的个数,而object对象没有类似属性。

Map介绍如下

Map 是一种键值对的集合,其中每个值都有一个相关联的键。以下是关于 Map 的详细介绍和使用方法:

特性和用途

  • 键可以是任意数据类型,包括对象、函数和原始数据类型。
  • 保持键值对的插入顺序,因此可以迭代它们的顺序。
  • 可以使用 size 属性来获取 Map 中键值对的数量。
  • Map 是一种灵活的数据结构,适用于许多不同的场景,例如缓存、数据存储等。

创建和基本操作

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
// 1.创建一个空的 Map
const myMap = new Map();
// Map还接收数组作为参数
let m1 = new Map([["name", "zhangsan"], ["age", 20]]);
let m2 = new Map([{0:"name",1:"zhangsan"},{"0":"age","1":20}]);
// 上面两行的输出为:
//Map(2) { 'name' => 'zhangsan', 'age' => 20 }
//Map(2) { 'name' => 'zhangsan', 'age' => 20 }
// 2.设置键值对
myMap.set('name', 'John');
myMap.set('age', 30);
// 3.获取值
console.log(myMap.get('name')); // 输出: "John"
// 4.检查是否包含某个键
console.log(myMap.has('age')); // 输出: true
// 5.删除键值对
myMap.delete('age');
// 6.迭代 Map
// 6.1 forEach
myMap.forEach((value, key) => {
console.log(key, value);
});
// 6.2 遍历键
let num = new Map([["one", 1], ["two", 2], ["three", 3]]);
for(let key of num.keys()){
console.log(key);
}
// one
// two
// three
// 6.3 遍历值
for(let value of num.values()){
console.log(value);
}
// 1
// 2
// 3
// 6.4 遍历条目
for(let item of num.entries()){
console.log(item[0], item[1]);
}
// one 1
// two 2
// three 3
// 将上面代码通过解构优化
for(let [key, value] of num.entries()){
console.log(key, value);
}
// one 1
// two 2
// three 3

// 7.清除所有的键值对
myMap.clear();

Set

Set 是一种 值的无序集合,其中每个值都是唯一的,不能重复。以下是关于 Set 的详细介绍和使用方法:

特性和用途

  • 存储唯一的值,去重效果非常高效。
  • 不允许重复的元素,因此不能有相同值的项。
  • 可以使用 size 属性来获取 Set 中值的数量。
  • Set 通常用于存储一组不重复的值,例如集合操作、去重数组等。

创建和基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个空的 Set
const mySet = new Set();
// 添加值
mySet.add(1);
mySet.add(2);
mySet.add(2); // 不会重复添加
// 检查是否包含某个值
console.log(mySet.has(2)); // 输出: true
// 删除值
mySet.delete(1);
// 迭代 Set
mySet.forEach((value) => {
console.log(value);
});
// 清除所有的键值对
mySet.clear();

总之,Map 适用于需要存储键值对的情况,而 Set 适用于需要存储一组唯一值的情况。这两种数据结构在 JavaScript 中提供了更多的灵活性和高效性,可以用于解决各种问题。

两者的比较 map set
创建一个空的 const myMap = new Map(); const mySet = new Set();
添加 myMap.set('name', 'John'); mySet.add(1);
获取 myMap.get('name') --
判断是否有某值 myMap.has('age'),返回布尔值 mySet.has(2),返回布尔值
删除单个键值对 myMap.delete('age'); mySet.delete(1);
清除所有 myMap.clear(); mySet.clear();

8.对象的新方法(√)

在 ES6 中,添加了Object.is()Object.assign()Object.keys()Object.values()Object.entries()等方法。

1. Object.is()

  • Object.is()方法用来判断两个值是否为同一个值,返回一个布尔类型的值。
1
2
3
4
5
6
7
8
const obj1 = {};
const obj2 = {};
console.log(Object.is(obj1, obj2)); // false

const obj3 = {};
const value1 = obj3;
const value2 = obj4;
console.log(Object.is(value1, value2)); // true

2. Object.assign()

  • Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,并返回目标对象。
1
2
3
4
5
6
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { a:5 , c: 3 };
//对象合并,把后面对像合并到第一个对象,对象里相同的属性会覆盖
Object.assign(obj1, obj2, obj3);
console.log(obj1); // { a: 5, b: 2 , c:3}

3.Object.keys()、Object.values()、Object.entries()

  • Object.keys() 返回对象所有属性
  • Object.values() 返回对象所有属性值
  • Object.entries() 返回多个数组,每个数组是 key–value 不解释直接看例子
1
2
3
4
5
6
7
8
9
10
11
12
let person = {
name: "admin",
age: 12,
language: ["java", "js", "css"],
};
console.log(Object.keys(person)); //[ 'name', 'age', 'language' ]
console.log(Object.values(person)); //[ 'admin', 12, [ 'java', 'js', 'css' ] ]
console.log(Object.entries(person)); /* [
["name", "admin"],
["age", 12],
["language", ["java", "js", "css"]],
]; */

📚 浏览器

1.浏览器缓存(强缓存,协商缓存)具体字段头是哪些,有何区别

2.前端缓存方法

3.输入URL到页面渲染的过程

4.同源策略和跨域

📚 计算机网络

1.介绍一下Http各个版本

参考:HTTP 面试题(2023最新版)-CSDN博客

HTTP 发展至今,总共经历了四个版本——HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2.0、HTTP/3

HTTP 0.9

① HTTP 0.9 是最早发布出来的一个版本,于1991年发布。

② 它只接受 GET 一种请求方法,没有在通讯中指定版本号,且不支持请求头。由于该版本不支持 POST 方法,因此客户端无法向服务器传递太多信息。

③ HTTP 0.9 具有典型的无状态性,每个事务独立进行处理,事务结束时就释放这个连接。HTTP 协议的无状态特点在其第一个版本中已经成型。

HTTP 1.0

① HTTP 1.0是HTTP协议的第二个版本,于1996年发布,如今仍然被广泛使用,尤其是在代理服务器中。

这是第一个在通讯中指定版本号的HTTP协议版本,具有以下特点:

  • 不仅仅支持 GET 命令,还支持 POST 和 HEAD 等请求方法。
  • HTTP 的请求和回应格式也发生了变化,除了要传输的数据之外,每次通信都包含头信息,用来描述一些信息。
  • 不再局限于 0.9 版本的纯文本格式。根据头信息中的 Content-Type 属性,可以支持多种数据格式,这使得互联网不仅仅可以用来传输文字,还可以传输图像、音频、视频等二进制文件。
  • 开始支持cache,就是当客户端在规定时间内访问同一网站,直接访问cache即可。
  • 其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。

1.0 版本的工作方式是每次 TCP 连接只能发送一个请求,当服务器响应后就会关闭这次连接,下一个请求需要再次建立 TCP 连接。 TCP 连接的建立成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。

HTTP 1.0 版本的性能比较差。随着网页加载的外部资源越来越多,这个问题就愈发突出了。为了解决这个问题,有些浏览器在请求时,即在请求头部加上 Connection 字段:

image.png

这个字段要求服务器不要关闭TCP连接,以便其他请求复用。服务器同样回应这个字段。

一个可以复用的TCP连接就建立了,直到客户端或服务器主动关闭连接。但是,这不是一个标准字段,不同实现的行为可能不一致,因此不是根本的解决办法。

HTTP 1.1

默认采用持续连接(Connection: keep-alive),能很好地配合代理服务器工作。

还支持以管道方式在同时发送多个请求,以便降低线路负载,提高传输速度。

HTTP 1.1 具有以下特点:

  • 新增了请求方式 PUT、PATCH、OPTIONS、DELETE 等。

  • 客户端请求的头信息新增了 Host 字段,用来指定服务器的域名。

  • 引入了持久连接(persistent connection)

    即 TCP 连接默认不关闭,可以被多个请求复用,不用声明 Connection: keep-alive。客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送 Connection: close,明确要求服务器关闭 TCP 连接。

  • 加入了管道机制

    在同一个 TCP 连接里,允许多个请求同时发送,增加了并发性,进一步改善了 HTTP 协议的效率。

    举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。

    管道机制则是允许浏览器同时发出 A 请求和 B 请求,但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求。

    一个 TCP 连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是 Content-length 字段的作用,声明本次回应的数据长度。

  • 分块传输编码

    使用 Content-Length 字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。

    更好的处理方法是,产生一块数据,就发送一块,采用”流模式”(stream)取代”缓存模式”(buffer)。

    因此,HTTP 1.1 版本规定可以不使用 Content-Length 字段,而使用”分块传输编码”(chunked transfer encoding)。只要请求或回应的头信息有 Transfer-Encoding 字段,就表明回应将由数量未定的数据块组成。

  • HTTP 1.1 支持文件断点续传,RANGE:bytes,HTTP 1.0 每次传送文件都是从文件头开始,即 0 字节处开始。RANGE:bytes=XXXX 表示要求服务器从文件 XXXX 字节处开始传送,断点续传。即返回码是 206(Partial Content)

HTTP/1.1 相比 HTTP/1.0 提高了什么性能?

HTTP/1.1 相比 HTTP/1.0 性能上的改进:

  • 使用持续连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
  • 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

但 HTTP/1.1 还是有性能瓶颈:

  • 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;
  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
  • 没有请求优先级控制;
  • 服务器是按请求的顺序响应的,如果服务器响应慢,会导致客户端一直请求不到数据,也就是队头阻塞;
  • 请求只能从客户端开始,服务器只能被动响应。

HTTP/2.0

于 2015 年 5 月作为互联网标准正式发布。

它具有以下特点:

  • 头信息压缩

    HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 CookieUser Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

    HTTP 2.0 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用 gzip 或c ompress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

  • 多工

    HTTP 2.0 复用 TCP 连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”(HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1大了好几个数量级)。

    举例来说,在一个 TCP 连接里面,服务器同时收到了 A 请求和 B 请求,于是先回应 A 请求,结果发现处理过程非常耗时,于是就发送 A 请求已经处理好的部分, 接着回应 B 请求,完成后,再发送 A 请求剩下的部分。

  • 服务器推送

    HTTP 2.0 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。意思是说,当我们对支持 HTTP 2.0 的 web server 请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。 服务器端推送的这些资源其实存在客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的。

  • 二进制协议

    HTTP 1.1 版的头信息肯定是文本(ASCII 编码),数据体可以是文本,也可以是二进制。

    HTTP 2.0 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”(frame):头信息帧和数据帧。

HTTP/2 做了什么优化?

HTTP/2 协议是基于 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。

那 HTTP/2 相比 HTTP/1.1 性能上的改进:

  • 头部压缩
  • 二进制格式
  • 并发传输
  • 服务器主动推送资源

1. 头部压缩

HTTP/2 会压缩头(Header)。如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

2. 二进制格式

HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)

这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率

比如状态码 200 ,在 HTTP/1.1 是用 ‘2’’0’’0’ 三个字符来表示(二进制:00110010 00110000 00110000),共用了 3 个字节,如下图

在 HTTP/2 对于状态码 200 的二进制编码是 10001000,只用了 1 字节就能表示,相比于 HTTP/1.1 节省了 2 个字节,如下图:

Header: :status: 200 OK 的编码内容为:1000 1000,那么表达的含义是什么呢?

image.png

  1. 最前面的 1 标识该 Header 是静态表中已经存在的 KV。(至于什么是静态表,可以看这篇:HTTP/2 牛逼在哪?)
  2. 在静态表里,“:status: 200 ok” 静态表编码是 8,二进制即是 1000。

3. 并发传输

我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。

而 HTTP/2 就很牛逼了,引出了 Stream 概念,多个 Stream 复用在一条 TCP 连接。

从上图可以看到,1 个 TCP 连接包含多个 Stream,Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成。Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体)。

针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应

比如下图,服务端并行交错地发送了两个响应: Stream 1 和 Stream 3,这两个 Stream 都是跑在一个 TCP 连接上,客户端收到后,会根据相同的 Stream ID 有序组装成 HTTP 消息。

4、服务器推送

HTTP/2 还在一定程度上改善了传统的「请求 - 应答」工作模式,服务端不再是被动地响应,可以主动向客户端发送消息。

客户端和服务器双方都可以建立 Stream, Stream ID 也是有区别的,客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。

比如下图,Stream 1 是客户端向服务端请求的资源,属于客户端建立的 Stream,所以该 Stream 的 ID 是奇数(数字 1);Stream 2 和 4 都是服务端主动向客户端推送的资源,属于服务端建立的 Stream,所以这两个 Stream 的 ID 是偶数(数字 2 和 4)。

再比如,客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返,如下图左边部分:

image.png

如上图右边部分,在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。

HTTP/2 有什么缺陷?

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

image.png

举个例子,如下图:

image.png

图中发送方发送了很多个 packet,每个 packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 packet 3 在网络中丢失了,即使 packet 4-6 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 packet 3 重传后,接收方的应用层才可以从内核中读取到数据,这就是 HTTP/2 的队头阻塞问题,是在 TCP 层面发生的。

所以,一旦发生了丢包现象,就会触发 TCP 的重传机制,这样在一个 TCP 连接中的所有的 HTTP 请求都必须等待这个丢了的包被重传回来

http/3.0

前面我们知道了 HTTP/1.1 和 HTTP/2 都有队头阻塞的问题:

  • HTTP/1.1 中的管道( pipeline)虽然解决了请求的队头阻塞,但是没有解决响应的队头阻塞,因为服务端需要按顺序响应收到的请求,如果服务端处理某个请求消耗的时间比较长,那么只能等响应完这个请求后, 才能处理下一个请求,这属于 HTTP 层队头阻塞。
  • HTTP/2 虽然通过多个请求复用一个 TCP 连接解决了 HTTP 的队头阻塞 ,但是一旦发生丢包,就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。

HTTP/2 队头阻塞的问题是因为 TCP,所以 HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP!

UDP 发送是不管顺序,也不管丢包的,所以不会出现像 HTTP/2 队头阻塞的问题。大家都知道 UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。

QUIC 有以下 3 个特点。

  • 无队头阻塞
  • 更快的连接建立
  • 连接迁移

1、无队头阻塞

QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。

QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。

所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。

2、更快的连接建立

对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。

HTTP/3 在传输数据前虽然需要 QUIC 协议握手,但是这个握手过程只需要 1 RTT,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。

但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是 QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,如下图:

甚至,在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

如下图右边部分,HTTP/3 当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到 0-RTT(下图的右下角):

3、连接迁移

基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。

那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接。而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

所以, QUIC 是一个在 UDP 之上的 TCP + TLS + HTTP/2 的多路复用的协议。

QUIC 是新协议,对于很多网络设备,根本不知道什么是 QUIC,只会当做 UDP,这样会出现新的问题,因为有的网络设备是会丢掉 UDP 包的,而 QUIC 是基于 UDP 实现的,那么如果网络设备无法识别这个是 QUIC 包,那么就会当作 UDP包,然后被丢弃。

HTTP/3 现在普及的进度非常的缓慢,不知道未来 UDP 是否能够逆袭 TCP。

总结

HTTP/0.9:功能捡漏,只支持GET方法,只能发送HTML格式字符串。

HTTP/1.0:增加POST、HEAD等方法,支持多种数据格式,增加头信息,每次只能发送一个请求(无持久连接)

HTTP/1.1:默认持久连接、请求管道化、增加缓存处理、增加Host字段、支持断点传输分块传输等。

HTTP/2.0:二进制分帧、多路复用、头部压缩、服务器推送

2.有哪些请求?

请求方法包括GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE以及扩展方法,当然并不是所有的服务器都实现了所有的方法,部分方法即便支持,处于安全性的考虑也是不可用的

3.get和post有什么区别?(√)

http协议是没有规定get请求参数有长度限制,但是服务器会因为url过长而无法处理。

根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。

根据 RFC 规范,POST 的语义是根据请求体对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在请求体中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。

GET 和 POST 方法都是安全和幂等的吗?

先说明下安全和幂等的概念:

  • 在 HTTP 协议里,所谓的「安全」是指请求方法不会「破坏」服务器上的资源。
  • 所谓的「幂等」,意思是多次执行相同的操作,结果都是「相同」的。

如果从 RFC 规范定义的语义来看:

  • GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签
  • POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。所以,浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签

做个简要的小结。

GET 的语义是请求获取指定的资源。GET 方法是安全、幂等、可被缓存的。

POST 的语义是根据请求负荷(报文主体)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 不安全,不幂等,(大部分实现)不可缓存。

注意, 上面是从 RFC 规范定义的语义来分析的。

但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:

  • 可以用 GET 方法实现新增或删除数据的请求,这样实现的 GET 方法自然就不是安全和幂等。
  • 可以用 POST 方法实现查询数据的请求,这样实现的 POST 方法自然就是安全和幂等。

如果「安全」放入概念是指信息是否会被泄漏的话,虽然 POST 用 body 传输数据,而 GET 用 URL 传输,这样数据会在浏览器地址拦容易看到,但是并不能说 GET 不如 POST 安全的。

因为 HTTP 传输的内容都是明文的,虽然在浏览器地址拦看不到 POST 提交的 body 数据,但是只要抓个包就都能看到了。

所以,要避免传输过程中数据被窃取,就要使用 HTTPS 协议,这样所有 HTTP 的数据都会被加密传输。

GET 请求可以带 body 吗?

RFC 规范并没有规定 GET 请求不能带 body 的。理论上,任何请求都可以带 body 的。只是因为 RFC 规范定义的 GET 请求是获取资源,所以根据这个语义不需要用到 body。

另外,URL 中的查询参数也不是 GET 所独有的,POST 请求的 URL 中也可以有参数的。

4.HTTP 中常用的状态码15个(✓)

参考:常用的HTTP状态码(面试常被问……)_面试常问的h状态码-CSDN博客

HTTP状态码负责表示客户端发送HTTP请求的返回结果、标记服务器的处理是否正常、通知出现的错误工作等。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。

状态码如200 OK,由3位十进制数字和原因短语组成。

  • 第一个十进制数字定义了状态码的类型
  • 后两个数字用来对状态码进行细分。

2XX 成功

  1. 200 ok(请求成功)

  2. 204 no content (请求成功,但是没有结果返回)

  3. 206 partial content (客户端请求一部分资源,服务端成功响应,返回限定范围内的资源)

3XX 重定向

  1. 301 move permanently (永久性重定向)
  2. 302 found (临时性重定向)
  3. 303 see other (查看其它地址。与301类似。使用GET和POST请求查看)
  4. 304 not modified (未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源)
  5. 307 temporary redirect (跟302一个意思)

4XX 客户端错误

  1. 400 bad request (客户端请求的语法错误,服务器无法理解)
  2. 401 unauthorized (需要认证(第一次返回)或者认证失败(第二次返回))
  3. 403 forbidden (服务器理解请求客户端的请求,但是拒绝执行此请求)
  4. 404 not found (服务器上无法找到请求的资源)

5XX 服务器错误

  1. 500 internal server error (服务端执行请求时发生了错误)
  2. 503 service unavailable (服务器正在超负载或者停机维护,无法处理请求)
  3. 504 Gateway Time-out (充当网关或代理的服务器,未及时从远端服务器获取请求)

5.强缓存和协商缓存

对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据,不必在通过网络获取服务器的响应了,这样的话 HTTP/1.1 的性能肯定肉眼可见的提升。

所以,避免发送 HTTP 请求的方法就是通过缓存技术,HTTP 设计者早在之前就考虑到了这点,因此 HTTP 协议的头部有不少是针对缓存的字段。

HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存

什么是强制缓存?

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。

如下图中,返回的是 200 状态码,但在 size 项中标识的是 from disk cache,就是使用了强制缓存。

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;
  • Expires,是一个绝对时间;

如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control 的优先级高于 Expires

Cache-control 选项更多一些,设置更加精细,所以建议使用 Cache-Control 来实现强缓存。具体的实现流程如下:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;
  • 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;
  • 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。

什么是协商缓存?

当我们在浏览器使用开发者工具的时候,你可能会看到过某些请求的响应码是 304,这个是告诉浏览器可以使用本地缓存的资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存。

上图就是一个协商缓存的过程,所以协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存

协商缓存可以基于两种头部来实现。

第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

  • 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;
  • 请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。

第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

  • 响应头部中 Etag:唯一标识响应资源;
  • 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

如果在第一次请求资源的时候,服务端返回的 HTTP 响应头部同时有 Etag 和 Last-Modified 字段,那么客户端再下一次请求的时候,如果带上了 ETag 和 Last-Modified 字段信息给服务端,这时 Etag 的优先级更高,也就是服务端先会判断 Etag 是否变化了,如果 Etag 有变化就不用在判断 Last-Modified 了,如果 Etag 没有变化,然后再看 Last-Modified。

为什么 ETag 的优先级更高?这是因为 ETag 主要能解决 Last-Modified 几个比较难以解决的问题:

  1. 在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
  2. 可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次;
  3. 有些服务器不能精确获取文件的最后修改时间。

注意,协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求

下图是强制缓存和协商缓存的工作流程:

当使用 ETag 字段实现的协商缓存的过程:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;
  • 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:
    • 如果没有过期,则直接使用本地缓存;
    • 如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;
  • 服务器再次收到请求后,会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较
    • 如果值相等,则返回 304 Not Modified,不会返回资源
    • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
  • 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。

6.什么是鉴权?

📚 性能优化

1.防抖和节流的实现方式

2.图片懒加载

📚 vue2+vue3

1.Vue diff算法(√)

Vue.js 使用了一种称为Virtual DOM(虚拟DOM)的机制来提高页面渲染的性能。Vue的Virtual DOM通过Diff算法来比较前后两个虚拟DOM树的差异,然后仅更新需要变化的部分,从而减少了页面重绘和重新渲染的开销。以下是Vue的Diff算法的简要工作原理:

  1. 生成虚拟DOM树: 当数据发生变化时,Vue首先会生成一个新的虚拟DOM树。这个虚拟DOM树是一个JavaScript对象树,它的结构与实际的DOM树相似,但只包含了需要渲染的元素和组件。
  2. 比较新旧虚拟DOM树: Vue会逐层比较新旧虚拟DOM树的节点,找出差异。这个比较过程是深度优先的,从根节点开始,递归地比较子节点。
  3. 标记差异: 在比较的过程中,Vue会标记出两个虚拟DOM树之间的差异。差异可以分为四种类型:添加、删除、属性修改和文本内容修改。
  4. 批量更新: 一旦标记出差异,Vue会将所有差异记录下来,然后一次性地应用这些差异,而不是立即更新实际的DOM。这个批量更新可以提高性能,因为实际的DOM操作是昂贵的。
  5. 更新视图: 最后,Vue会使用新的虚拟DOM树来更新实际的DOM,只更新那些发生变化的部分。这个过程通常是非常高效的,因为它只涉及到实际变化的部分,而不是整个页面。

Vue的Diff算法的性能优化是通过减少实际DOM操作的次数来实现的,这是因为实际DOM操作是相对较慢的,尤其是在复杂的页面上。通过比较虚拟DOM树并只更新需要变化的部分,Vue能够显著提高页面的性能和响应速度。

需要注意的是,Vue的Diff算法并不是唯一的实现方式,不同的前端框架可能使用不同的Diff算法来提高性能。但Diff算法的核心思想是相似的:通过比较前后两个状态来确定需要更新的部分,从而减少不必要的DOM操作。

2.Vue组件通信

包括父子组件通信:父传子,子传父,兄弟组件通信

3.闭包在vue中有哪些应用?(√)

以下是一些常见的 Vue.js 中闭包的应用场景:

1.事件处理函数:当你在 Vue 组件中绑定事件处理函数时,这些函数通常会捕获组件的作用域(即组件的数据和方法)。这是因为事件处理函数被绑定在组件实例上,形成了一个闭包,使得它们可以访问组件的数据和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<button @click="handleClick">点击我</button>
</template>

<script>
export default {
data() {
return {
count: 0,
};
},
methods: {
handleClick() {
// 这里的 this 指向组件实例,可以访问组件的数据
this.count++;
},
},
};
</script>

2.模块化开发:Vue 组件通常使用单文件组件(.vue 文件)来组织代码,这些文件中的数据、计算属性、方法等都被封装在组件的闭包中,以避免全局污染和命名冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
export default {
data() {
return {
message: 'Hello, Vue!',
};
},
computed: {
reversedMessage() {
return this.message.split('').reverse().join('');
},
},
methods: {
showMessage() {
alert(this.message);
},
},
};
</script>

3.路由守卫:在 Vue Router 中,路由守卫使用闭包来访问当前路由的信息、组件实例等,以执行导航守卫逻辑。

1
2
3
4
5
6
7
8
import router from './router';
router.beforeEach((to, from, next) => {
// 访问当前路由的信息
console.log(to.path);
// 访问组件实例
console.log(to.matched[0].instances.default);
next();
});

4.自定义指令:如果你编写自定义 Vue 指令,闭包可以用于存储和访问指令的局部状态和逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
Vue.directive('custom-directive', {
bind(el, binding) {
// 使用闭包访问局部状态
let count = 0;

el.addEventListener('click', () => {
// 在事件处理程序中使用局部状态
count++;
console.log(`Clicked ${count} times`);
});
},
});

5.计时器和异步操作:Vue 中常用的计时器(如 setIntervalsetTimeout)和异步操作(如 AJAX 请求)也会涉及到闭包。你可以在回调函数内部访问组件的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
data() {
return {
timer: null,
message: "Hello, Vue!",
};
},
created() {
// 使用闭包保存组件内部数据
this.timer = setInterval(() => {
console.log(this.message);
}, 1000);
},
destroyed() {
// 清除计时器以防止内存泄漏
clearInterval(this.timer);
},
};
</script>

6.作用域:在一些特殊情况下,你可能需要在 Vue 模板中使用闭包来访问外部作用域的数据。这通常涉及到在计算属性或指令中使用函数。

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
<template>
<div>
<p>{{ calculate() }}</p>
<button @click="updateMessage">更新消息</button>
</div>
</template>

<script>
export default {
data() {
return {
message: "Hello, Vue!",
};
},
methods: {
updateMessage() {
this.message = "New Message";
},
},
computed: {
calculate() {
// 使用闭包访问外部作用域的数据
const originalMessage = this.message;
return function () {
return `Length: ${originalMessage.length}`;
};
},
},
};
</script>

总之,闭包在 Vue.js 中被广泛用于访问组件的局部作用域、数据和方法,以及实现模块化开发、路由守卫、自定义指令等功能。Vue.js 利用闭包机制使得组件化开发更加强大和灵活。

4.vue中双向数据绑定(√)

Vue.js中的双向数据绑定是其核心特性之一,它使视图和模型之间的数据保持同步。Vue的双向绑定原理可以概括为以下几个步骤:

  1. 数据劫持(Data Observation)
    • 当你在Vue实例中声明数据时,Vue会通过对象的Object.defineProperty方法来将这些属性转化为”响应式属性”。
    • Vue会遍历数据对象的每个属性,并在每个属性上定义gettersetter方法。
    • getter负责追踪属性的依赖关系,当属性被读取时,会将观察者添加到依赖项列表中。
    • setter监听属性的变化,当属性被修改时,会通知所有依赖于该属性的观察者,并触发视图更新。
  2. 模板编译(Template Compilation)
    • Vue使用带有特殊语法的模板来定义视图。
    • 模板中的表达式会被解析并建立对数据属性的引用。
    • Vue会将模板编译成虚拟DOM(Virtual DOM)。
  3. 虚拟DOM与真实DOM的比较(Virtual DOM Diffing)
    • 每当数据发生变化时,Vue会生成一个新的虚拟DOM树。
    • Vue会将新的虚拟DOM与旧的虚拟DOM进行比较,找出两者之间的差异。
    • 这个过程叫做”虚拟DOM的Diff算法”,它可以高效地找出需要更新的部分,以最小化DOM操作。
  4. 更新视图(Reactivity)
    • Vue知道哪些属性在模板中被引用,以及它们之间的依赖关系。
    • 当数据改变时,Vue会触发相应属性的setter方法,通知相关的观察者进行更新。
    • 观察者接收到通知后,会通知虚拟DOM重新渲染视图,但只更新变化的部分,而不是整个视图。
  5. 用户交互与数据变更的同步(User Interaction and Data Mutation)
    • 当用户与页面交互,例如在表单输入框中输入内容时,输入框的值会被绑定到Vue实例的数据属性。
    • 数据属性的变化将触发更新,更新会反映在视图中,保持视图和数据的同步。

总的来说,Vue的双向绑定原理通过数据劫持、模板编译、虚拟DOM的比较和更新视图等机制,使数据与视图保持同步,从而实现了双向数据绑定。这个机制使得开发者可以更轻松地管理数据和用户界面的交互,提高了开发效率和应用的可维护性。

5.Vue 中的虚拟DOM和真实DOM(√)

vue.js 是一个流行的前端 JavaScript 框架,它使用虚拟 DOM(Virtual DOM)来提高性能和效率。理解虚拟 DOM 和真实 DOM 的概念对于理解 Vue.js 和其他一些前端框架的工作原理非常重要。

  1. 真实 DOM(Real DOM)
    • 真实 DOM 是浏览器中实际存在的文档对象模型。
    • 当页面中的数据发生变化时,浏览器会重新渲染整个页面,包括对应数据发生变化的部分。
    • 操作真实 DOM 需要消耗大量的计算资源,因此频繁的 DOM 操作可能导致性能下降。
  2. 虚拟 DOM(Virtual DOM)
    • 虚拟 DOM 是一个轻量级的 JavaScript 对象树,它是对真实 DOM 的抽象。
    • 当数据变化时,Vue.js 首先会生成一个新的虚拟 DOM 树,然后将新旧虚拟 DOM 树进行比较,找出差异。
    • 找到差异后,Vue.js 只更新必要的部分,而不是整个页面,以提高性能。
    • 这种方式可以减少对真实 DOM 的操作次数,从而提高应用的性能和响应速度。

虚拟 DOM 的工作流程如下:

  1. 初始渲染:Vue.js 使用模板和数据生成虚拟 DOM。
  2. 数据变化:当应用状态(数据)发生变化时,Vue.js 生成一个新的虚拟 DOM。
  3. 虚拟 DOM 比较:Vue.js 将新旧虚拟 DOM 树进行比较,找出差异。
  4. 更新真实 DOM:Vue.js 只更新必要的部分,以使真实 DOM 反映新的应用状态。

虚拟 DOM 的好处在于它可以最小化对真实 DOM 的直接访问和操作,从而提高了前端应用的性能和效率。这是因为真实 DOM 操作通常是昂贵的,而虚拟 DOM 可以将多个操作批量处理并最小化页面的重新渲染。

在使用 Vue.js 时,你通常不需要直接操作虚拟 DOM,框架会负责处理它。你只需关注数据的变化和视图的声明性描述,Vue.js 将自动处理虚拟 DOM 的创建和更新。这使得开发过程更简单,同时又能保持良好的性能。

6.vue3和vue2的区别(√)

Vue.js是一个流行的JavaScript框架,用于构建用户界面。Vue 3和Vue 2之间存在一些重要的区别,Vue 3引入了一些新功能和性能优化,以提高开发者的体验。以下是Vue 3和Vue 2之间的主要区别:

  1. 性能优化: Vue 3在性能方面进行了显著改进。其中一个关键的优化是虚拟DOM的升级,使其更高效。Vue 3还引入了懒编译,允许更小的包大小,因此加载时间更快。
  2. Composition API: Vue 3引入了Composition API,这是一种新的组织组件逻辑的方式。它允许开发者根据功能而不是选项对代码进行组织,使组件更容易理解和维护。
  3. Teleport: Vue 3引入了Teleport,这是一种新的方式来在DOM中移动元素,而无需改变其组件层次结构。这对于创建模态框、弹出菜单等非常有用。
  4. Fragments: Vue 3支持Fragments,允许你在组件中返回多个根元素,而不需要包装它们在一个额外的父元素中。
  5. 全局API的更改: Vue 3对一些全局API进行了更改。例如,Vue.observable()现在变成了Vue.reactive()Vue.nextTick()现在是Vue.nextTick()
  6. 自定义渲染器: Vue 3引入了自定义渲染器的能力,这意味着你可以在不同的目标(例如Web、原生移动应用、桌面应用)上渲染Vue组件。
  7. TypeScript支持: Vue 3更好地支持TypeScript,包括通过.d.ts文件提供类型定义。
  8. 模块化编译: Vue 3的编译器是模块化的,这意味着你可以只编译你实际用到的特性,而不是整个编译器。

需要注意的是,虽然Vue 3引入了许多改进和新功能,但它与Vue 2的核心思想仍然保持一致,因此如果你已经熟悉Vue 2,学习Vue 3应该相对容易。然而,对于新项目,特别是需要性能优化和更好的组织代码的项目,Vue 3通常是更好的选择。

vue2与vue3的区别-CSDN博客

1.vue2和vue3双向数据绑定原理发生了改变

2.Vue3支持碎片(Fragments):就是说在组件可以拥有多个根节点。

3.Composition API: Vue2使用选项类型API(Options API)对比Vue3组合API(Composition API)

4.生命周期钩子

Vue2————–vue3
beforeCreate -> setup()
created -> setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted

7.你有没有自己封装过组件?

有,分页器、日历

8.vue中的数据驱动

9.vue中组件的通信方式

项目用的vue2 问会不会vue3

vue3取消了mixin,用什么代替
适配是怎么实现的 为啥要适配
采用px-vw不会造成拉伸吗
axios发送的请求如何取消
404要怎么实现
如何在登录前防止用户访问
前后端分离相较前后端不分离的区别
用的什么打包工具
使用webpack做了什么
webpack如何打包import
封装过什么组件
对项目提出过什么改进

📚 面试题和笔试题带坑

1.console.log([“1”, “2”, “3”].map(parseInt));的输出值是多少?

来源:parseInt()函数绝不是你想的那么简单~~ - 掘金 (juejin.cn)

【知识点】:map()和parseInt()

【分析】map方法可以将一个数组映射为一个新数组。它接收一个callback回调函数作为参数,这个回调函数体现了将原数组映射成新数组的映射关系。原数组在循环遍历数组每一项时,都会调用一次callback回调函数,并传入三个参数:

  • 当前正在遍历的元素
  • 元素索引
  • 原数组本身 (这个参数基本不使用)

callback函数对当前遍历的元素进行包装执行,得到的返回值就是新数组中对应的结果

parseInt接收两个参数:

  • 第一个参数string:要被解析的字符串,如果不是字符串会被转换,忽视空格符
  • 第二个参数radix:要解析的数字的基数。该值介于2 ~ 36之间。默认值为10,表示十进制。这个参数表示将前面的字符从radix进制转化为十进制

把上面的代码的完整写法为:

1
2
3
4
5
6
console.log(
["1", "2", "3"].map(function (item, index, arr) {
console.log(item + "-----" + index);
return parseInt(item, index);
})
);

每次每一项给parseInt传入的值为:

1
2
3
1-----0
2-----1
3-----2

所以最终结果为:[ 1, NaN, NaN ]

2.在进行算术运算时会做,+号,数字隐式转换成字符串。其余的运算符号是字符串隐式转换成数字。

例如:JavaScript中定义var a=”40”,var b=7,则执行a%b会得到?会得到5

加号优先级高于 三目运算。低于括号。 所以括号中无论真假 加上前边的字符串都为 TRUE 三目运算为TRUE是 输出 define

3.typeof运算符:js中typeof的用法详解 - 知乎 (zhihu.com)

注意点:

① 变量未定义时:typeof str 为undefined。变量定义未赋值,console.log(str)为undefined。

所以console.log(typeof str == undefined)可以判断该变量是否定义。

② 如下:

1
2
3
alert(typeof(null));        // object
alert(typeof(eval));        // function
alert(typeof(Date));        // function

对于对象、数组、null 返回的值是 object ,那么如何判断是否是数组呢?JavaScript 判断是否为数组 - 知乎 (zhihu.com)

方法一:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

1
2
var arr = [1, 2 ,3]
console.log(arr instanceof Array) // true

方法二:原型链,但这都是有局限的

constructor属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数

方法三:(最通用)—-Object.prototype.toString

该方法通用。

1
2
3
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]'
}

方法四:Array.isArray

isArray() 方法是 ES5 标准提供的一个判断数组方法。

1
2
3
function isArray(arr) {
return Array.isArray(arr)
}

综上所述,提供一个完整可靠的方法,如下:

1
2
3
4
5
function isArray(arr) {
const toString = Object.prototype.toString
const isArray = Array.isArray || function (arg) { return toString.call(arg) === '[object Array]' }
return isArray(arr)
}

4.eval():用于执行()里的内容,里面可以是字符串或表达式。且将会返回对最后一个表达式的求值结果。

5.JavaScript单线程的,浏览器实现了异步的操作,整个js程序是事件驱动的,每个事件都会绑定相应的回调函数,

js是单线程,浏览器是多线程的 。Js是运行在浏览器的渲染主线程上的,渲染主线程是单线程事件循环执行机制,每一个异步任务会放在事件队列等待执行,这些任务在执行的过程中是以回调函数的形式被调用的。

JavaScript Promise 启示录 | AlloyTeam

6.this

①在事件中,this指向触发这个事件的对象(特殊的是:IE中的attachEvent中的this总是指向全局对象window)。

②this总是指向函数的直接调用者(而非间接调用者)

③new后面就是构造函数,构造函数中的this指向的就是当前的对象

7.js的全局属性:Infinity、NAN、undefined

js的全局函数:decodeURI()、decodeURIcomponent()、

​ encodeURI、encodeURIcomponent()、

​ escape()、eval()、isFinite()、isNAN()、

​ Number()、parseFloat()、parseInt()、String()、unescape()。

setTimeout是BOM的全局函数,不是ES core的全局函数

8.Object(1.0)就是将数字“1.0”封装成它对应的包装类的一个对象实例比如Number(1.0)

还有一点要注意的是,1++“2”是会报错的,+与+之间得有空格,但是+-是不会报错的,中间有无空格都无所谓

9.class和let一样都有暂时性死区,在被声明前无法访问

也就是在当前作用域能找到,但是要在声明后才能访问。es6中的class和let const一样都不存在提升
(实际存在提升,只是因为TDZ的作用,并不会像var那样得到undefined,而是直接抛出错误)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
原来的代码
var a = 1;
function test(){
//console.log(a) 位置A
class a {}
// console.log(a) 位置B
}
test();

实际上提升后的
var a = 1;
function test(){
console.log(a) 位置A //在test()作用域内找得到a是一个class但是存在TDZ暂时性死区,访问报错
class a {}
console.log(a) 位置B //a已经声明创建出来了
}
test()

7.javascript中[]转化为布尔值为true,即Boolean([])返回true

8.CSS中的行为——expression - 蓝色理想 (blueidea.com)

文末

其他:

PC端和移动端项目CSS的适配区别
\3. rem是怎么设置的,手动算的还是用了库
\4. rem和em的区别
\5. 屏幕大小变化的时候是如何适配的
\6. 直接写rem计算屏幕的宽高是怎么获取的
\7. 其他移动端适配的方法?
\8. 上一题提示:vw,vh
\9. 逻辑像素和物理像素的区别
\10. 浏览器渲染是如何解析渲染html文档的
\11. css下载的过程会阻塞js的下载吗
\12. 为什么阻塞?
\13. 项目性能优化的方式
\14. tree shaking的限制条件?比如模块化方式commonJS或ES6module的方法能实现吗
\15. 原生JS 类的实现方法,比如new一个函数的过程
\16. ES6的class编译完成后产物是什么样的,比如是函数or对象or数组
\17. 可以用var a = new
\18. ES6中继承的原理?
\19. 原型链
\20. function的prototype是什么
\21. 闭包原理
\22. ES5有哪些作用域,ES6呢
\23. 项目中用过的异步方法怎么实现的
\24. promise传的参数(resolve,reject)执行时机是异步还是同步的
\25. 函数里有error会怎么样
\26. 必须catch吗,用then呢
\27. then后面再有then,是会进入resolve还是reject的回调
\28. async await原理
\29. await后面跟一个1或者字符串可以吗
\30. Generator函数接触过吗
\31. 浏览器的Eventloop和node的Eventloop区别
\32. 提示:事件循环
\33. 异步任务挂起之后还可以给下一个用户提供服务吗,(挂起期间有新用户访问)
\34. git用到哪些命令多一些
\35. merge用过吗
\36. 跨域问题如何解决
\37. 跨域请求的时候带cookie怎么带
\38. react 了解过吗,如果让你上手做可以做吗
\39. hook听过吗

做题
\1. 最长连续递增数组,复杂度多少,能优化到多少
\2. 二叉树层数

304状态码是什么,说一下这个请求的过程

详细说下协商缓存

HTTP2.0的多路复用是什么

CSRF是什么

如果是第三方的链接,直接拒绝访问是不是也可以,就是CSRF怎么达到一种攻击的状态,攻击了用户的什么东西

CSRF如何防御

CORS跨域的请求响应过程

origin的请求跨域网站头,能放很多域名吗

以上的流程和细节多学习

Vue的源码

如何监听一个对象属性的改变

浏览器如何解析Vue的模板,最终在浏览器中如何使用

Vue模板会解析成什么样子的东西,又没有了解

Vue的diff算法是什么

写一个div,第一个子元素用v-if控制,如何第一个元素v-if=false,其中的子div会不会塌陷和挤压,结合diff算法来说

技术栈是react和Angluar,写游戏页面里的H5

面完十分钟秒挂,我说要是技术栈和业务方向 学历不匹配可以别面,别浪费彼此时间

webpack 原理?

Tree shaking 的原理?
- 想用 Tree Shaking 可以有哪些方式?
- 所有 ES6 写的都可以用 Tree Shaking 吗?
- Tree Shaking 有哪些限制?
- 如果我一个导入依赖另一个依赖 另一个依赖被 tree-shaking 掉了怎么办, CSS 引用复杂,CSS 没打进去怎么办?
- 微前端了解吗?
- 为什么需要微前端?
- 各种微前端的原理是什么?
- JS 全局隔离怎么做?
- 这种方法老版本兼容性不太好,有没有解决方案?
- webpack 的联邦模块要先加载 container,在去取 remotes,有性能浪费,能不能直接将主应用和子应用(remotes)同时加载,应该怎么做?
- B 端开发的时候有没有碰到什么问题、痛点?
- 实习问题、怎么解决的?
- 项目问,一个功能解决什么问题?
- 鉴权为什么要存 token?
- webworker 解决什么问题?
- Token 和 cookie ssession 方案?
- 这样会有什么安全问题?

js做数据循环用了哪些方法,如何实现的,这些方法哪种性能最好,哪种最不好
(我答的for最好,for…of最不好,也不知道对不对)
2、http2和http1.1的区别
3、axios库的原理
4、设计一个sdk实现前端发送的请求前加一个header,无论谁引用sdk,都会加header(偏场景题)
面试官给了一个aop埋点的思路
5、防抖和节流
6、vue双向绑定源码
7、promise常用的几种静态方法
8、react的jsx怎么转换成dom结构

美团(成都 到家事业群)前端面试记录

一面:
1 ts Partial
2 正则表达式匹配替换
3 Get是完全幂等的吗?
4 不适用js实现一个点击显示悬浮窗 active
5 webview上h5的屏幕适配
6 节流和防抖,应用场景
7 git代码出错标准处理
8 webpack打包优化和配置
9 babel和polyfill
10 盒模型 box-sizing
11 BFC
12 跨域和解决方案
13 dom操作 querySelectorAll(编程)
14 在Array原型链上添加flat()方法(编程)
15 驼峰命名转短横线命名(编程)

二面:
1 项目进度管理和质量保证
2 着重讲一下某个项目,遇到的具体难点是什么?
3 文件里面有一万个数,范围[0, 1000],缓存大小只有2k,怎么实现排序及其优化?
4 节流函数(编程)
5 数组的左移右移实现(编程)
6 state变化到页面变化的整个过程

OPPO前端面试记录

1 为什么学习前端?
2 你对前端行业的认识?
3 Vue和React区别
4 Vue3和React16的新功能
5 前端性能优化策略
6 前端错误定位?
7 浏览器性能调试api?
8 事件循环
9 手机端h5适配方法?
10 尾递归


📣 每日复习内容
https://luoynothing.github.io/2023/09/04/📣-面试八股文(持续更新中)/
Author
John Doe
Posted on
2023-09-04 17:50
Updated on
2023-10-18 14:46
Licensed under