Web常见知识点

HTML

1. 如何理解HTML的语义化?

1、增加代码的可读性
2、让搜索引擎更容易读懂(SEO; )

HTML的基本结构

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>

2. DOCTYPE的作用

作用: 告诉浏览器以什么样的模式(标准模式; 兼容模式)来解析文档;

3. HTML5新特性

  1. 绘图 Canvas(有点类似于Python中的小乌龟)
  2. 用于媒介回放的video和audio元素
  3. 本地离线缓存localStorage、sessionStorage
  4. 语义化更好的内容元素: header、article、nav、section、footer等

4. 浏览器的渲染原理

  1. 生成DOM树
  2. 生成CSS规则树
  3. 构建渲染树
  4. 布局
  5. 绘制

5. 回流和重绘

  1. 回流;
  • 概念: 当DOM的变化影响了元素的几何信息, 浏览器需要重新计算元素的几何属性;
  • 表现: 重新布局,重新排列元素
  1. 重绘;
  • 概念: 某些元素的外观被改变;
  1. 注意点
  • 回流必定导致重绘,重绘不一定导致回流;
  • 回流成本 > 重绘
  • 减少回流: 使用position: absolute或fixed使得元素脱离文档流;

如何减少重排和重绘

  1. 使用transform和opacity属性:(transform属性和opacity属性不会触发重排,因此它们是创建动画和过渡效果的良好选择。使用这些属性来执行元素的平移、旋转、缩放和淡入淡出等动画效果。)
  2. 使用position: absolute和fixed:(当一个元素的位置使用position: absolute或position: fixed时,它会脱离文档流,不会影响其他元素的布局,因此不会触发重排。这对于创建浮动菜单、弹出框等元素非常有用。)
  3. 使用CSS动画库:(使用现成的CSS动画库(如Animate.css)可以简化动画创建过程,同时优化了动画的性能。)

6. Storage & cookie

sessionStorage,localStorage 和 cookie 的区别

  • 共同点:都是保存在浏览器端,且同源的
  • 区别:
    • cookie 在浏览器和服务器之间来回传递;而 sessionStorage 和 localStorage 不会自动把数据发送到服务器,仅在本地保存
    • 存储大小限制不同。cookie 不能超过 4K,因为每次 http 请求都会携带 cookie,所以 cookie 只适合保存很小的数据,如:会话标识。sessionStorage 和 localStorage 虽然也有存储大小限制,但比 cookie 大得多,可以达到 5M 或更大。
    • 数据有效期不同。sessionStorage 仅在当前浏览器窗口关闭之前有效;localStorage 始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie 只在设置的 cookie 过期时间之前有效。
    • 作用域不同。sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 和 cookie 在所有同源窗口中都是共享的。

6. token和cookie的区别

Token 和 Cookie 是用于在客户端和服务器之间进行身份验证和状态管理的两种不同机制,它们有一些区别:

  1. 位置和存储方式:
    • Cookie: Cookie 是存储在客户端(浏览器)的小型文本文件,由服务器在 HTTP 响应头中通过 Set-Cookie 设置,并被浏览器保存,每次请求时都会自动发送到服务器。
    • Token: Token 是一种用于身份验证的令牌,通常作为 HTTP 请求头中的一部分发送到服务器,可以存储在客户端的 Local Storage、Session Storage 或内存中。
  2. 安全性:
    • Cookie: Cookie 可能存在一些安全问题,比如可能会被窃取(跨站脚本攻击、跨站请求伪造攻击等),虽然可以通过设置 Cookie 的安全属性来增加安全性(比如设置 HttpOnly、Secure 和 SameSite 属性)。
    • Token: Token 的安全性取决于实现方式和存储位置,如果使用不当可能会有安全风险,但一般来说,在 HTTPS 协议下,将 Token 存储在客户端的 Local Storage 或者内存中相对较安全。
  3. 用途和灵活性:
    • Cookie: 主要用于客户端和服务器之间进行状态管理,比如用户身份验证、跟踪用户会话等。
    • Token: 通常用于身份验证和授权,特别适用于分布式系统中的身份验证,比如 JSON Web Token (JWT),可以方便地传递用户身份信息和授权信息,适用于跨域、移动端等场景。
  4. 跨域支持:
    • Cookie: 受同源策略的限制,不同域名之间的 Cookie 通常不共享。
    • Token: Token 通常可以跨域使用,在前后端分离、跨域等场景中更为灵活。
      总的来说,Cookie 是存储在客户端的标准机制,用于跟踪会话和存储状态信息;而 Token 作为一种认证方式,更多用于身份验证和授权,可以在分布式系统中更灵活地实现跨域认证和授权。在实际应用中,两者的选择取决于具体的需求和场景。

7. 块状元素&内联元素

1、块状元素; 特点: 独占一行; 代表元素: div、h1、table、ul、p;
2、内联元素; 特点: 不会独占一行; 代表元素: span、img、input、button

8. defer和async的区别

deferasync是用于控制HTML中<script>标签加载和执行JavaScript文件的属性,它们有以下区别:

  1. 执行顺序:
    • defer<script defer>会按照它们在HTML文档中的顺序进行加载,并在HTML解析完成后、DOMContentLoaded事件触发前依次执行。多个defer脚本会按照它们在HTML中的出现顺序执行。
    • async<script async>会在下载完成后立即执行,不会等待HTML文档解析,也不会等待其他资源的下载完成。多个async脚本的执行顺序无法确定,它们会在加载完成后立即执行。
  2. 阻塞:
    • defer<script defer>不会阻塞HTML文档的解析,它会在后台下载脚本,直到HTML解析完成后才执行。
    • async<script async>不会阻塞HTML文档的解析,它会在下载脚本的同时执行,所以可能在HTML解析完成前或后执行。
  3. 依赖关系:
    • defer:如果一个<script defer>依赖于另一个<script defer>,那么它们的执行顺序是有保证的。这使得可以在多个脚本之间建立依赖关系。
    • async<script async>没有明确的执行顺序保证,不适合用于建立脚本之间的依赖关系。
  4. 应用场景:
    • defer:通常用于需要确保按照顺序执行脚本的情况,或者有脚本依赖关系的情况。这适用于在DOMContentLoaded事件之前加载和执行脚本,以避免对页面渲染的阻塞。
    • async:适用于不需要考虑执行顺序和不依赖其他脚本的情况,或者对于性能不是关键因素的情况。这种情况下,可以并行加载和执行多个脚本,从而提高性能。

9. 构建DOM树的过程

前端DOM(文档对象模型)树的构建过程通常在浏览器解析 HTML 和 CSS 文件时进行。以下是前端DOM树的构建过程的基本步骤:

  1. 解析 HTML: 浏览器首先会解析 HTML 文件,将其分解为各种标记和元素。
  2. 构建 DOM 树: 浏览器使用解析的标记和元素构建 DOM 树。DOM 树是一个树状结构,表示文档的逻辑结构。它由节点构成,包括元素节点、文本节点、注释节点等。
  3. 样式计算: 浏览器解析 CSS 文件,将样式信息应用到 DOM 树中的元素上。这包括计算元素的大小、位置、颜色和字体等。
  4. 生成渲染树: 根据 DOM 树和样式信息,浏览器构建渲染树(Render Tree)。渲染树包括了需要渲染到屏幕上的节点,但它并不包括所有的 DOM 节点,比如隐藏的元素通常不包括在渲染树中。
  5. 布局计算: 浏览器进行布局计算,确定每个元素在屏幕上的确切位置和大小。这个过程被称为"reflow"或"layout"。
  6. 绘制: 浏览器根据渲染树和布局信息将页面绘制到屏幕上。这个过程称为"paint"或"rasterization"。
  7. 交互和事件处理: 一旦页面被渲染到屏幕上,用户可以与页面进行交互,例如点击链接、提交表单等。浏览器会监听这些事件并触发相应的事件处理程序。
  8. 动态更新: 除了初始加载过程,前端DOM 树还会在用户与页面互动、JavaScript 代码修改 DOM 以及服务器发送新内容时进行动态更新。浏览器会根据更改重新构建渲染树、重新计算布局和重新绘制页面。

总之,前端DOM 树的构建过程是一个复杂的流程,它使浏览器能够将 HTML 和 CSS 转化为用户在屏幕上看到的页面。这个过程是浏览器渲染引擎的核心功能之一。

CSS

1. Flex布局

  1. flex布局是CSS3新增的一种布局方式,通过display: flex创造出一个flex容器
  2. `一个容器默认两条轴:水平的主轴;与主轴垂直的交叉轴,(轴)
    • 可以使用flex-direction来指定主轴的方向`,【属性:row、column、row-reverse、column-reverse】
    • flex-direction的属性;row、column、row-reverse、column-reverse
  3. 排列方式
    • 使用justify-content指定元素在主轴上的排列方式,
    • align-items来指定元素在交叉轴上的排列方式,
    • 还可以使用flex-wrap来规定当一行排列不下时的换行方式(换行)

使用flex布局来实现两栏布局

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>flex实现两栏布局</title>
    <!-- 
        解释:
            - display: flex 将 .container 元素设置为 Flex 布局。
            - flex-basis 属性指定了每个子元素的基础大小。在这个例子中,左栏占据了容器的 30%,右栏占据了容器的 70%。
    -->
    <style>
        .container {
            display: flex;
        }
        .left-column {
            flex-basis: 30%;
            background-color: yellow;
        }
        .right-column {
            flex-basis: 70%;
            background-color: red;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="left-column">这是左栏布局</div>
        <div class="right-column">这是右栏布局</div>
    </div>
</body>
</html>

2. div如何居中

  1. 宽高固定的元素
  • 我们可以利用** margin:0 auto **来实现元素的水平居中。
  • 利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心; 然后再通过margin负值来调整元素的中心点到页面的中心。(元素的宽高已知) | 利用translate来调整元素的中心点到页面的中心点(元素的宽高未知)
.center-element {
  position: absolute;
  top: 50%;
  left: 50%;
  // 利用translate来调整元素的中心点到页面的中心点
  transform: translate(-50%, -50%);
  // 通过margin负值来调整元素的中心点到页面的中心
  margin-left: -50px; /* 假设元素宽度为 100px */
  margin-top: -50px; /* 假设元素高度为 100px */
}
  • 使用flex布局,通过align-items:centerjustify-content:center设置容器的垂直和水平方向上为居中对 齐,然后它的子元素也可以实现垂直和水平的居中(元素的宽高未知也可)。

3. 盒子模型宽度的计算

问题:请问div1的offsetWidth的宽度是多大?
      offsetWidth = (内容宽度 + 内边距 + 边框),无外边距(margin不算了)
      答案:100 + 10 * 2 + 1 * 2 = 122 

      补充:如果让offsetWidth等于100px,该如何做?
          - 在上述style中,添加一个box-sizing: border-box (此时: 总宽度width已经包括了padding和border的值)
          - 解释:因为上述内容就是width: 100px

4. CSS选择器及优先级

1、选择器
- id选择器(#myid)
- 类选择器(.myclass)
- 属性选择器(a[rel=“external”])
- 伪类选择器(a:hover, li:nth-child)
- 标签选择器(div, h1, p)
- 伪元素选择器(p::first-line)
- 相邻选择器(h1 + p)
- 子选择器(ul > li)
- 后代选择器(li a)
- 通配符选择器(*)
2、优先级
- !important
- 内联样式(1000) 在元素中直接通过style标签来设置样式
- ID选择器(0100)
- 类选择器/属性选择器/伪类选择器(0010)
- 标签选择器/伪元素选择器(0001)
- 关系选择器/通配符选择器(0000)
3、带 !important 标记的样式属性优先级最高;样式表的来源相同时:!important > 行内样式> ID选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性
4、伪类选择器和伪元素选择器
伪类选择器

  • 伪类选择器用于选择处于特定状态或特定位置的元素,这些状态或位置无法通过基本的选择器来表达。它们以冒号(:)开头,例如:hover、:active、:nth-child(n)等。伪类可以用于选择元素的特定状态或行为。
  • 常见的选择元素特定状态的伪类元素;
:hover:鼠标悬停在元素上时触发。
:active:元素被激活(例如被点击)时触发。
:focus:元素获得焦点时触发,通常用于表单元素。
:nth-child(n):选择父元素的第 n 个子元素。
:first-child:选择父元素的第一个子元素。

伪元素选择器(用于在特定元素的内容之前或之后插入样式化的内容)

  • 伪元素选择器用于在特定元素的内容之前或之后插入样式化的内容,以实现在文档中生成额外的元素。它们以双冒号(::)开头,例如::before、::after。
  • 常见的伪元素选择器
::before:在元素内容之前插入样式化内容。
::after:在元素内容之后插入样式化内容。
::first-line:选择元素内容的第一行。
::first-letter:选择元素内容的第一个字母。

5. margin纵向重叠问题

  • !!! 相邻元素的margin-top和margin-bottom会发生重叠
如下代码:AAA和BBB之间的距离是多少?
            - 相邻元素的margin-top和margin-bottom会发生重叠
            - 空白内容的<p></p>也会重叠
        答案:15px(中间内容会重叠)

6. margin负值问题

1、margin-top和margin-left负值,元素向上,向左移动
2、margin-right负值,右侧元素向左移动,自身不受影响
3、margin-bottom负值,下方元素上移,自身不受影响

7. BFC理解

1、block format context;(块级格式化上下文)
2、一块独立渲染区域;内部元素的渲染不会影响边界以外的元素

形成BFC的常见条件

  • position: absolute; position: fixed;
  • display: flex; display: inline-block;

BFC的常见应用

  • 清除浮动(在style标签中); 容器加上bfc, 即使浮动了,图片也不会撑开跑出去!!!
    .bfc {
        overflow:hidden
    }
    

BFC的实现原理

  • 通过 CSS 规范定义的规则和布局算法来控制元素的布局和排列。以下是一些与 BFC 相关的底层概念和实现原理:
  1. 块级元素布局: BFC 本质上是块级元素的一种布局规则,它定义了块级元素在页面上的排列和定位方式。这是通过 CSS 渲染引擎遵循 CSS 规范来实现的。
  2. 盒模型: CSS 中的所有元素都被看作是一个矩形盒子,包括块级元素。这些盒子在页面上根据各种属性(如宽度、高度、外边距、内边距等)进行排列。这是 CSS 渲染引擎的基本概念之一。
  3. 浮动和清除浮动: BFC 的一个重要应用是处理浮动元素。当元素浮动时,它们脱离了正常的文档流,而 BFC 可以用来清除浮动、防止父元素坍塌和控制浮动元素的位置。
  4. 外边距合并: BFC 内部的块级元素的外边距可能会发生外边距合并。这个特性是 CSS 规范中定义的,渲染引擎根据规范来处理外边距合并。
  5. 块级元素垂直排列: BFC 中的块级元素在垂直方向上一个接一个排列,不会重叠。这是通过 CSS 渲染引擎根据规范来实现的。
  • 总之,BFC 的底层实现原理涉及了 CSS 渲染引擎根据 CSS 规范来处理块级元素的布局、外边距、浮动和其他相关属性。虽然这些实现细节在不同的浏览器中可能有所不同,但它们都要遵循 CSS 规范中关于 BFC 的规则和定义。这些规则和定义是确保网页在不同浏览器中一致渲染的关键因素。

8. display:none 和 visibility:hidden的区别

  1. display:none; 隐藏后不占用文档流; visibility:hidden; 会占用文档流;
  2. visibility 具有继承性,给父元素设置 “visibility: hidden”,子元素也会继承该属性;
  3. display: none 会引起回流(重排)和重绘;visibility: hidden 会引起重绘。

9. 简述动画

  1. transform; 描述了元素的静态样式, 本身不会呈现动画效果,可以对元素进行旋转、缩放、移动(translate)等
  2. transition; 样式过度,从一种效果逐渐改变为另一种效果,它是一个合写属性
  3. animation; 动画, 使用@keyframes来描述每一帧的样式;
  • 具体代码示例;
/* 定义关键帧 */
@keyframes gradientAnimation {
  0% {
    background-color: red;
  }
  50% {
    background-color: yellow;
  }
  100% {
    background-color: green;
  }
}

/* 应用动画效果到元素 */
.element {
  width: 100px;
  height: 100px;
  animation: gradientAnimation 4s ease-in-out infinite;
}

10. line-height如何继承

  1. 父元素的line-height是具体数值,则子元素line-height继承该值
  2. 父元素的line-height是比例值,如’2’,则子元素line-height继承该比例
  3. 父元素的line-height是百分比,则子元素line-height继承的是父元素的font-size*百分比计算出来的值

11. 清除浮动的方法有什么

  • 清除浮动是为了解决浮动元素导致父元素无法自适应高度的问题。以下是一些清除浮动的常见方法:
  1. 使用空元素和clear属性:在浮动元素的后面插入一个空的<div>元素,并为该元素设置clear属性,通常使用clear: both;来清除浮动。这会创建一个空元素,将其放在浮动元素之后,以强制父元素适应浮动元素的高度。
  2. 使用伪元素::after:在浮动元素的父元素上应用伪元素::after,并使用content属性和display: table; clear: both;来清除浮动。这种方法避免了额外的HTML元素。
  3. 使用父元素的overflow属性:设置父元素的overflow属性为hiddenauto,或scroll。这会触发父元素创建包含浮动元素的块级格式化上下文,从而清除浮动。
  4. 使用display: flow-root:将父元素的display属性设置为flow-root。这是一种新的CSS属性,用于创建包含浮动元素的块格式上下文,以
  5. 使用Flex布局:将父元素的display属性设置为display: flex;display: inline-flex;,这可以清除浮动,同时提供了更强大的布局控制。

12. 移动端布局有哪些?

  • 响应式布局
    • 响应式设计是一种网页设计方法,通过使用弹性网格布局、弹性图像和媒体查询等技术,使网页能够适应不同设备和屏幕尺寸。
  • 移动端适配的几种方法
    • 流式布局;使用百分比布局,元素的宽度使用百分比表示,可以根据屏幕大小进行相对缩放;
    • 弹性布局;使用弹性盒子布局,可以方便地创建灵活的布局,适应不同尺寸的屏幕;
    • 媒体查询;使用CSS3中的媒体查询,根据设备的特性(屏幕宽度、设备类型)应用不同的样式;
@media only screen and (max-width: 600px) {
  /* 在屏幕宽度小于等于600px时应用的样式 */
  body {
    background-color: lightblue;
  }
}
- rem、em单位
- ViewPort单位(vw、vh)

13.vw,vh,rem移动端布局单位

  • vw (视窗宽度单位)
    • vw表示视窗宽度的百分比(例如: 如果你将一个元素的宽度设置为 10vw,它将占据视窗宽度的10%。)
  • vh(视窗高度单位)
    • vh表示视窗高度的百分比(和vw类似, 其也是根据视窗高度来调整元素的大小或位置)
  • rem (根元素字体大小单位)
    • rem是相对于根元素(通常是元素)的字体大小的单位
    • 这使得在整个页面中可以轻松控制元素的大小和间距
    • 如根元素大小为2px,子元素为2rem, 则子元素大小为 2 * 2px = 4px
  • em
    • em是相对于根元素(html元素)的字体大小
    • 继承性: rem 单位不受父元素字体大小的影响,始终相对于根元素的字体大小。
    • 应用场景: 常用于整体布局的调整,以及需要相对于根元素进行缩放的情况。
      总结
    • 总体而言,rem 更适合用于整体布局的调整,因为它不受父元素字体大小的影响,而 em 更适用于相对于元素自身字体大小进行调整的情况。在实际开发中,根据具体的需求和布局情况选择使用 rem 或 em。

14. 移动端:什么是Viewport?如何在移动端进行设置

  • Viewport是指浏览器用来显示网页的区域,移动设备上的Viewport通常比屏幕尺寸大,以适应网页的显示。
  • 可以通过 标签中的viewport设置来控制Viewport,例如:
<meta name="viewport" content="width=device-width, initial-scale=1.0">

meta元素

  • 是html文档中的一种元数据标签,用于提供关于文档的元信息;
  • 在移动端开发中,经常使用 标签来设置视口(Viewport)以确保网页在不同设备上有良好的显示效果。
  • 常用的 标签属性和值:
    viewport:
  • 该设置告诉浏览器使用设备的宽度(width=device-width),并初始化缩放级别为1.0。这是移动端开发中常用的设置,以确保网页自适应设备宽度。
    width:
  • 设置视口宽度等于设备宽度,确保网页在移动设备上不会被缩放。

initial-scale:

设置初始缩放级别为1.0,保持默认缩放水平,防止页面被自动缩放。

user-scalable:

禁止用户缩放网页,通常在一些移动应用中使用,以确保布局的稳定性。

minimum-scale 和 maximum-scale:

设置允许用户缩放的最小和最大缩放级别。

标签的作用:

  • 提供文档元信息: 标签可以提供有关文档的元信息,如字符集、作者、描述等。
  • 控制视口: 通过设置视口相关属性,可以控制移动设备上的视口大小和缩放行为,以确保网页在不同设备上有良好的显示效果。
  • 搜索引擎优化(SEO): 一些 标签用于提供搜索引擎有关网页的信息,如关键词、描述等。

JS

1. 数据类型

JS数据类型

  1. 五种基本数据类型,分别是 Number、String、Boolean、Null、Undefined
  2. ES6新增的: Symbol; 独一无二且不可变的数据类型(解决可能出现的全局变量冲突的问题)
  3. ES10新增的: BigInt; 其可以安全存储和操作超出了 Number 能够表示的安全整数范围(Number.MIN_VALUE, Number.MAX_VALUE)

引用类型
Object (对象)
Array (数组)
Function (函数)
Date (日期)
Map、Set、WeakMap、WeakSet (用于创建特殊类型的集合)
区别
存储方式
基础类型存储在栈内存中,直接存储变量的值。
引用类型存储在堆内存中,变量保存的是指向对象的引用地址。
传递方式
基础类型通过值传递
引用类型通过引用传递, 即复制变量的引用地址
比较方式:
基础类型使用值比较,两个相同的值相等。
引用类型使用引用比较,只有引用地址相同才相等。

1. 判断数据类型

typeof;

  • typeof操作符返回一个字符串, 表示未经计算的操作数的类型;
  • 其中null 和 [ ] 进行判断的时候, 返回的结果就是’object’;
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

instanceof

instanceof运算符:用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上;
总结:顺着原型链去找, 直到找到相同的原型对象, 返回true, 否则返回false;
object instanceof constructor
// 其中object为实例对象, constructor为构造函数
区别

typeof:会返回一个变量的基本类型,instanceof返回的是一个布尔值;
instanceof可以准确地判断复杂引用数据类型, 但是不能正确判断基础数据类型;
而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断;

注意:想要具体获取某个数据的类型; 可以使用Object.prototype.toString();

2. 作用域 & 作用域链

作用域

  • 概念; 指变量、函数和对象在代码中可访问的范围
  • 分类:
    • 全局作用域; 在整个代码中都可以访问的作用域.
    • 局部作用域; 在特定代码块内部定义的变量和函数的作用域.
    • 块级作用域; 在ES6中引入了块级作用域,用于定义在代码块(如: if语句、循环、函数等),这些变量只在块内部有效

作用域链

  • 概念; 指变量查找时的层级关系,即从当前作用域开始逐级向上查找,直到找到变量或到达全局作用域。如果在当前作用域和父级作用域中都找不到变量,则会产生一个 “未定义” 的错误。

作用域的特殊用途

  • 函数作为参数被传递;
  • 函数作为返回值被返回
var sex = '男';
function person() {
    var name = '张三';
    function student() {
        var age = 18;
        console.log(name); // 张三
        console.log(sex); // 男 
    }
    student();
    console.log(age); // Uncaught ReferenceError: age is not defined
}
person();

上述代码的工作

  • student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出“张三”
  • student内部输出sex时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出“男”
  • 在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到则报错

3. this的指向问题

分类; this的不同应用场景
关于this的指向问题: this取什么值,是在函数执行的时候决定的,而不是在函数定义的时候决定的

  1. 作为普通函数调用
function fn1() {
    console.log(this)
}

fn1() // window / global 

  1. 使用call、apply、bind
fn1.call({x : 100}) // { x: 100 }

const fn2 = fn1.bind({x: 200}) 

fn2() // { x: 200 }
  1. 作为对象方法调用
const zhangsan = {
    name:'张三', 
    sayHi() {
        // this即为当前对象 即zhangsan
        console.log('sayHi', this)
    }, 
    wait() {
        setTimeout(function() {
            /* 
                在setTimeout中使用普通函数作为参数时,
                该函数会在定时器触发后以全局作用域执行, 
                而不是在原来的上下文中执行; 
            */
            console.log('wait', this) // window 
            // console.log(this === Window);
        });
    }
}

zhangsan.sayHi()
zhangsan.wait()

const lisi = {
    name:'张三', 
    sayHi() {
        // this即为当前对象
        console.log(this)
    }, 
    wait() {
        setTimeout(() => {
            // this即为当前对象 => 箭头函数的取值是它的上级作用域的值
            // 还是当前对象;因为用了箭头函数
            console.log(this)
        });
    }
}
  1. 在class方法中调用
class People {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    sayHi() {
        console.log(this)
    }
}
// 使用new关键字来实例化一个类, 实例是类的具体实例,具有类定义的属性和方法; 
const zhangsan = new People('张三')
zhangsan.sayHi() // 张三这个对象
  1. 箭头函数(见上方)

  2. 习题,测试

let msg1 = {
    name:'msg1_name',
    print: function() {
        return () => console.log(this.name);
    }
}

let msg2 = { name:'msg2_name' }
msg1.print()()  // msg1_name
/* 
    1. 返回一个箭头函数
    2. 使用call方法将箭头函数绑定到msg2对象
    3. 箭头函数不会改变this的值
*/
msg1.print().call(msg2) // msg1_name
/* 
    1. 返回一个普通函数,但是该普通函数绑定在了msg2对象上
    2. 调用该普通函数,普通函数执行返回箭头函数,msg2_name
*/
msg1.print.call(msg2)() // msg2_name

注意点

  • 在绝大多数情况下, 函数的调用方式决定了this的值(运行时绑定)
  • this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象

4. call、apply、bind的区别

共同点

  • 三个方法都可以用于改变函数执行上下文对象(即: this的值)

区别

  1. call和apply用于立即执行函数,而bind返回一个新函数,允许稍后调用
  2. call和apply都接受明确的参数列表,而bind接受预设参数
  3. call接受参数列表,apply接受参数数组

预设参数的理解

function greet(name, greeting) {
  console.log(`${greeting}, ${name}!`);
}

const greetHello = greet.bind(null, 'Hello');
greetHello('Alice'); // 预设参数 'Hello',并传递额外参数 'Alice'
- 解释: 在这个示例中,bind 方法创建了一个新函数 greetHello,并预设了参数 'Hello'。当调用 greetHello('Alice') 时,它会将 'Hello' 作为预设参数,并接受额外的参数 'Alice'。

代码详细解释

/* 
    call
    1. call 方法用于调用一个函数,并指定函数执行时候的this值和参数列表
    2. 参数列表是逐个传入的
    3. 立即执行函数
*/
function sayHello(message) {
    console.log(message, this.name);
}

const person = { name: 'Alice' };
sayHello.call(person, 'Hello'); // 输出:Hello Alice


/* 
    apply
    1. apply方法也用于调用一个函数,类似于call,但它接受的是一个参数数组
    2. 参数数组中的元素会逐个传入函数作为参数
    3. 立即执行函数
*/

function sayHello(message) {
    console.log(message, this.name);
}

const person = { name: 'Bob' };
sayHello.apply(person, ['Hi']); // 输出:Hi Bob

/* 
    bind
    1. 不会立即执行, 而是返回一个新的函数, 新函数的this值被绑定到指定的值
    2. bind方法可以预设参数
*/
function sayHello(message) {
    console.log(message, this.name);
}

const person = { name: 'Charlie' };
const boundFunction = sayHello.bind(person, 'Hey');
boundFunction(); // 输出:Hey Charlie

5. ES6新特性

  1. let、const关键字
  2. 解构赋值;
// 举例: 对象的解构赋值
const person = { name: 'Alice', age: 30 };
const { name, age } = person;

console.log(name); // Alice
console.log(age);  // 30
  1. 模版字符串
const a = 10;
const b = 20;
const result = `The sum of ${a} and ${b} is ${a + b}.`;
console.log(result); // "The sum of 10 and 20 is 30."
  1. 简化对象写法; (当你在对象字面量中声明一个属性时,如果属性名和变量名相同,可以省略属性名的重复,只写变量名即可。)
const name = 'Alice';
const age = 30;

// 传统写法
const person = {
  name: name,
  age: age
};

// 简化的写法
const person = {
  name,
  age
};
  1. 箭头函数
  2. rest参数(使用剩余项的写法) + 扩展运算符(…)
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;

console.log(first); // 1
console.log(second); // 2
console.log(rest);   // [3, 4, 5]
  1. Symbol类型
  2. 迭代器 + 生成器
/* 
    代码说明
        - *的位置没有限制
        - 生成器函数返回的结果是迭代器对象,调用迭代器对象的 next方法可以得到yield语句后的值
        - yield相当于函数的暂停标记,也可以认为是函数的分隔符,每调用一次 next方法,执行一段代码
        - next方法可以传递实参,作为 yield语句的返回值
*/
function *gen() {
    yield '一只没有耳朵'
    yield '一只没有尾巴'
    return '真奇怪'
}
let iterator = gen()
// 通过done值来判断是否迭代结束
console.log(iterator.next()) // { value: '一只没有耳朵', done: false }
console.log(iterator.next()) // { value: '一只没有尾巴', done: false }
console.log(iterator.next()) // { value: '真奇怪', done: true }
  1. Promise (异步编程的新解决方案)
  • 语法上 Promise是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果。
    • Promise构造函数 : Promise (excutor) {}
    • Promise.prototype.then; (then方法的返回结果是 Promise 对象,对象状态由回调函数的执行结果决定)
    • Promise.prototype.catch
  1. Set集合
  • 概念: 类似于数组, 但成员的值都是唯一的,集合实现了iterator接口(可以使用『扩展运算符』和『 for…of…』进行遍历)
  • 属性的集合和方法
size 返回集合的元素个数
add 增加一个新元素,返回当前集合
delete 删除元素,返回 boolean 值
has 检测集合中是否包含某个元素,返回 boolean值
clear 清空集合,返回 undefined
  1. Map集合
  • 概念: 类似于对象, 也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键;集合实现了iterator接口(可以使用『扩展运算符』和『 for…of…』进行遍历)
  • Map的属性与方法
size 返回 Map的元素个数
set 增加一个新元素,返回当前 Map
get 返回键名对象的键值
has 检测 Map中是否包含某个元素,返回 boolean值
clear 清空集合,返回 undefined
  1. class类
  • 通过class关键字, 可以定义类
  • ES6中的class可以看作是一个语法糖; 新的 class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
  • class中的属性
class声明类
constructor定义构造函数初始化
extends继承父类
super调用父级构造方法
static定义静态方法和属性
父类方法可以重写
  • 类实现
// 父类
class Phone {
  // 构造方法
  constructor(brand, color, price) {
    this.brand = brand;
    this.color = color;
    this.price = price;
  }
  // 对象方法; 
  call() {
    console.log("我可以打电话");
  }
  // 当对某个属性进行获取时,调用get方法;
  // 当对某个属性进行修改时,调用set方法
  get price() {
    console.log("价格属性被读取了");
    return "5999";
  }
  set price(newValue) {
    this.price = newValue;
    console.log("价格属性被修改了");
  }
}

// 子类; 通过审extends来进行继承操作
class SmartPhone extends Phone {
  constructor(brand, color, price, screen, pixel) {
    // 父类有的直接继承; 
    super(brand, color, price);
    // 添加新的属性
    this.screen = screen;
    this.pixel = pixel;
  }
  // 子类方法
  photo() {
    console.log("我可以拍照");
  }
  // 方法重写
  call() {
    console.log("我可以视屏通话");
  }
  // 静态方法
  static run() {
    console.log("我可以运行程序");
  }
}

// 实例化对象; 通过new关键字来实现; 
const Nokia = new Phone("诺基亚", "白色", 230);
const iPhone6s = new SmartPhone("苹果", "黑色", 5999, "4.7inch", "500w");

// 调用子类方法
iPhone6s.photo();
  1. 数值扩展
Number.isFinite(10);   // true
Number.isFinite(NaN);  // false

Number.isNaN(NaN);     // true
Number.isNaN(10);      // false
  1. 对象扩展
  • 新增了一些Object对象的方法
    • Object.is 比较两个值是否严格相等,与『 ===』行为基本一致
    • Object.assign 对象的合并,将源对象的所有可枚举属性,复制到目标对象
    • proto、 setPrototypeOf、 setPrototypeOf可以直接设置对象的原型
  1. 模块化;
  • 是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来

  • 语法

    • 模块功能主要由两个命令构成:export和 import
    • export命令用于规定模块的对外接口
    • import命令用于输入其他模块提供的功能
    • export暴露方式: 统一暴露(暴露对象:export {})、分别暴露(分别使用export)、默认暴露(export default{})
    • import 导入方式:通用导入、结构赋值导入、针对默认暴露方式
  • 什么时候使用默认暴露?

    • 导出单个值或功能的时候;
// module.js
export default function myFunction() {
  // ...
}
// 在另一个文件中
import myFunction from './module.js';

6. 原型&原型链

  1. 原型;
  • 概念: 原型是一个对象,它作为另一个对象的基础,并从中继承属性和方法;
  • 特性: 每个对象都有一个原型,除了Object.prototype
  • 作用: 如果尝试访问对象上不存在的属性或方法, JS引擎会查询该对象的原型链来寻找该属性和方法.
  1. 原型链
  • 概念: 原型链是一种机制,用于在JS中实现继承;
  • 特性: 它是由每个对象的原型构成的链式结构,其中每个对象都继承其原型对象上定义的属性和方法;
  • 作用: 当访问一个对象的属性和方法时,JS引擎先会查找对象本身是否拥有该属性和方法,如果没有,则会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶部Object.prototype

7. 闭包(一个函数)

  • 概念
    • 有权访问另一个函数作用域当中变量的函数
  • 创建闭包
    • 在一个函数中创建另一个函数;创建的函数可以访问当前函数中的局部变量
  • 闭包用途
    • 在函数外部能够访问到函数内部的变量;通过闭包,在外面调用闭包函数,从而在外部访问到函数内部的变量,以此来创建私有变量
    • 在已经结束的函数上下文中的变量继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以变量对象不会被回收

缺点

  1. 内存消耗: 闭包可以访问其包含函数的变量,这意味着这些变量不会被垃圾回收,直到闭包不再被引用。如果您滥用闭包,尤其是在循环中创建闭包,可能会导致内存泄漏,因为每个闭包都会保留对其包含函数的引用。
  2. 性能开销: 由于闭包需要维护对外部变量的引用,它们可能比普通函数具有更高的性能开销。在某些情况下,频繁创建和销毁闭包可能导致性能下降。
  3. 难以理解和维护: 过度使用闭包可能导致代码难以理解和维护。闭包可以在不同作用域中引用变量,这可能使代码更加复杂,特别是在大型项目中。
  4. 变量共享和意外行为: 闭包可以访问外部函数的变量,这可能导致变量的意外共享和修改。如果不小心处理闭包中的变量,可能会引发 bug。
  5. 生命周期管理: 闭包的生命周期可能超出其包含函数的生命周期,这意味着您需要小心管理它们,以确保不会引发问题。
  6. 滥用闭包导致问题: 有时,开发人员可能滥用闭包,过度使用它们,导致代码复杂性增加、性能下降和维护问题。
    虽然闭包具有这些缺点,但它们在许多情况下仍然非常有用,特别是在处理回调函数、封装私有数据和创建模块化代码时。要避免闭包可能带来的问题,建议合理使用它们,尤其是小心处理内存管理和变量访问,以确保代码的正确性和性能。

8. 防抖

  • 原理
    • 在事件被触发n秒后执行回调,如果在这n秒内事件又被触发,则重新计时
  • 个人理解
    • 事件点击一次触发回调;多次点击,则会重新计时再触发
    • 防抖:回城,被打断就要重新来(重新计时)
  • 用途
    • 在一些请求事件上,避免用户多次点击向后台发送请求
  • 使用场景
    • 一般用在:连续的事件只需触发一次回调的场合
      • 1、搜索框输入;只需用户最后一次输入完,再发送请求
      • 2、用户名、手机号、邮箱输入验证
  • 手写防抖(demo)
/* 
    防抖:频繁触发,只会触发最后一次

    参数一:sum;当sum函数传入进来,会在定时器时间结束之后在调用,
    参数二:delay;决定返回时间
*/
function antiShake(Sum, delay) {
    // 设置timer的值为空,防止定时器没有被清空
    let timer = null
    // arguments是作为伪数组的形式存放参数,存放的是本省函数的参数
    let args = arguments
    // 返回闭包函数
    return function() {
        // 重复点击就会清除已经在计算的时间
        clearTimeout(timer)
        // 定时器的秒数
        timer = setTimeout(() => {
            // arg是存放当前函数的实参
            // this: 是window,Sum能接收到antiShake的实参
            Sum.apply(this, args)
        }, delay);
    }
}

9. 节流

  • 原理
    • 在规定的单位时间内,只能有一次触发事件的回调函数执行,如果在同一个时间内被触发多次,只会生效一次
  • 个人理解
    • 在规定时间内,多次触发,只会触发一次结果
    • 技能CD,CD没好,你用不了技能
  • 用途
    • 在scroll函数的事件监听上,通过事件节流来降低事件调用频率
  • 手写节流(demo)
/* 
    定时器方法
    参数
        - 参数一;fn 函数
        - 参数二:delay 时间
*/
function throttle(fn, timer) {
    // 重置定时器
    let timer = null
    // 返回闭包函数
    return function() {
        let args = arguments
        // 如果定时器为空
        if(!timer) {
            // 开启定时器
            timer = setTimeout(() => {
                // 执行函数
                fn.apply(this, args)
                // 函数执行完毕后重置定时器
                timer = null
            }, delay);
        }
    }
}

10. 浅拷贝 VS 深拷贝

概念

  • 浅拷贝和深拷贝是在处理复杂数据类型(对象/数组)时常用的两种拷贝方式

区别

  • 浅拷贝只复制引用,深拷贝递归复制对象和其属性。
  • 浅拷贝的新对象和原对象共享引用,深拷贝创建全新的对象。
  • 深拷贝需要递归遍历,性能较低

浅拷贝

  • 浅拷贝创建一个新的对象或数组,然后将原对象的属性值或数组元素的引用复制到新对象或数组中。
  • 新旧对象/数组共享同一组属性或元素, 如果修改了某个属性或元素,两者都会受到影响
  • 实现;
// 方式一
let a = { count: 1, deep: { count: 2 } };
console.log(a['count']); // []中使用字符串形式
let b = Object.assign({}, a);

// 方式二
let c = { ...a };

// 方式三
const originalArray = [1, 2, { value: 3 }];
const shallowCopiedArray = originalArray.slice();
shallowCopiedArray[2].value = 4;
console.log(originalArray);          // [1, 2, { value: 4 }]
console.log(shallowCopiedArray);     // [1, 2, { value: 4 }]

// 方式四
function shallowClone(obj) {
  const newObj = {};
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      newObj[prop] = obj[prop];
    }
  }
  return newObj;
}

深拷贝

  • 深拷贝创建一个新的对象或数组,然后递归复制原对象的所有属性值或数组元素,直到达到基本数据类型。
  • 新对象与原对象完全独立,修改新对象的属性或元素不会影响原对象。
  • 由于深拷贝需要递归遍历所有属性,因此可能会导致性能问题,尤其是对于嵌套结构复杂的对象。
  • 实现; 深拷贝可以使用一些库或自行编写递归函数来实现,如: JSON.parse(JSON.stringify(obj))、lodash.cloneDeep(obj)
const originalObject = { a: 1, b: { c: 2 } };
const deepCopiedObject = JSON.parse(JSON.stringify(originalObject));

deepCopiedObject.b.c = 3;

console.log(originalObject);          // { a: 1, b: { c: 2 } }
console.log(deepCopiedObject);        // { a: 1, b: { c: 3 } }

function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 如果是原始值或null,直接返回
  }

  if (Array.isArray(obj)) {
    return obj.map(item => deepCopy(item)); // 处理数组
  }

  const newObj = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key]); // 递归复制对象属性
    }
  }
  return newObj;
}

JSON.parse(JSON.stringify(obj)) 的缺陷:

不支持特殊对象和函数: JSON.stringify() 不支持一些特殊的对象类型,如 RegExp、Date、Map、Set,以及包含函数的对象。这些对象在进行深拷贝时可能会失去其原始类型或信息。

不支持循环引用: 如果对象包含循环引用,即对象属性之间相互引用,JSON.stringify() 无法处理,会导致堆栈溢出或错误。

性能问题: 对于大型对象或嵌套深的对象,JSON.stringify() 和 JSON.parse() 可能不是最高效的方法,因为它需要将对象序列化为 JSON 字符串,然后再解析为对象。

11. 事件循环

事件循环; 是一种机制,用于管理和协调代码的执行顺序,特别是在涉及异步操作的情况下.

JS代码运行的特点

  • JS是单线程运行的(特点)
  • 异步要基于回调来实现(基于回调)
  • event loop就是异步回调的实现原理

JS如何执行?

  • 从前到后,一行一行执行
  • 如果某一行执行报错;则停止下面代码的执行
  • 先把同步代码执行完,再执行异步!

执行过程(自己)
- 执行完毕以后清空调用栈(Call Stack)
- 执行setTimeout是浏览器APIS,
- 一旦同步代码执行完了;放到(回调队列)Callback Queue中

执行过程
- 同步代码,一行一行放在调用栈(Call Stack)执行;
- 遇到异步(定时、网络请求等), 会先记录下来,等待时机;
- 时机到了(异步任务完成),就移动到(事件队列)Callback Queue;
- 如Call Stack为空(即同步代码执行完)Event Loop开始工作(此时定时器那个函数可能还没结束)
- 轮循查找Callback Queue,如有则移动到Call Stack执行
- 然后继续轮询查找(永动机一样)

DOM事件和eventloop的关系(DOM事件也是基于eventloop来实现的)

  • JS是单线程的
  • 异步(setTimeout、ajax(什么时候网络请求返回了)等)使用回调,基于event loop
  • DOM事件(什么时候用户点击了)也使用回调,基于event loop

宏任务(Macrotasks)和微任务(Microtasks)是用于管理异步代码执行的机制,它们用于控制JavaScript中事件循环的执行顺序。以下是它们的一些示例:

宏任务(Macrotasks)

  1. 定时器回调:通过setTimeoutsetIntervalsetImmediate(在Node.js中)等设置的异步回调。
  2. I/O操作:例如读取文件、发送HTTP请求等。
  3. 用户交互事件:如鼠标单击、键盘输入等。
  4. DOM操作:对DOM元素的增删改查等操作。

微任务(Microtasks)

  1. Promise回调:thencatch方法的回调。
  2. async/awaitawait表达式的执行。
  3. process.nextTick(在Node.js中)。
  4. MutationObserver:DOM变化观察者的回调。

在事件循环中,微任务的执行优先级高于宏任务,这意味着微任务通常会在宏任务之前执行。具体的事件循环顺序通常如下:

  1. 执行当前宏任务队列中的一个宏任务。
  2. 执行所有微任务队列中的微任务。
  3. 渲染页面。
  4. 回到步骤1,继续执行下一个宏任务。

这个循环一直持续下去,直到宏任务队列和微任务队列都为空。

12. Promise详解

Promise概念

  • ES6引入的Promise是一种用于处理异步操作的机制, 它提供了一种更结构化的方式来处理异步代码, 特别是解决了回调地狱问题
    • 回调地狱: 指在处理多个嵌套的回调函数时,代码嵌套层级过深,导致代码难以理解、维护和扩展的情况。
      then和catch改变状态(只要是正常返回的话,都是返回resolved)
  • then正常返回resolved,里面有报错则返回rejected
    • resolved触发后续then回调
  • catch正常返回resolved,里面有报错则返回rejected
    • rejected触发后续catch回调

Promise总结

  • 三种状态,状态的表现和变化
    • pending;resolved;rejected
    • pending -> resolved; pending -> rejected
    • 变化不可逆
  • then和catch对状态的影响(重要)
  • then和catch的链式调用(常考)

知识点

  • 初始化Promise时,传入的函数会立刻被执行
  • setTimeout虽然里面的时间设置为0;但是它也是一个宏任务;需要放到后面再执行

给定一大串代码;问你执行顺序的话

  1. 同步代码执行完毕(event loop - call stack清空)
  2. 执行微任务
  3. (尝试触发DOM渲染)有的话
  4. 触发Event Loop,执行宏任务

Promise代码案例

// 演练1
// 实例化Promise对象,成功是resolve,失败是reject
const p = new Promise(function (resolve, reject) {
    setTimeout(() => {
        let data = "数据库中的数据"
        resolve(data)
    }, 1000);
})

// 指定回调; 其中回调函数直接通过.then来实现即可
p.then(
    function (value) {
        console.log(value);
    },
    function (reason) {
        console.log(reason);
    }
)

// 演练2; 
// 发送ajax请求; 有相应的接口地址
// 重点观察发送ajax请求的步骤;  
const p2 = new Promise((resolve, reject) => {
    // 1、创建对象
    const xhr = new XMLHttpRequest() // 浏览器自带的函数; 
    // 2、初始化
    xhr.open("GET", "http://api.apiopen.top/getJoke")
    // 3、发送
    xhr.send()
    // 4、绑定事件,处理响应结果
    xhr.onreadystatechange = function() {
        // 判断
        if(xhr.readyState === 4) {
            // 判断响应码是不是在200~300之间
            if(xhr.status >= 200 && xhr.status < 300) {
                // 表示成功
                resolve(xhr.response)
            }else {
                reject(xhr.status) // 调用状态码
            }
        }
     }
})

// 指定回调; 其中回调函数直接通过.then来实现即可
p2.then(
    function (value) {
        console.log(value);
    },
    function (reason) {
        console.log(reason);
    }
)

Promise 习题

  1. 习题1;
Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

// 结果
// 1
// 3
  1. 习题2
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).then(() => {
    console.log(3)
})
// 结果
// 1
// 2
// 3

// 第一行它肯定会去执行Promise.resolve() 
// 但是里面报错,返回的就是rejected状态的Promise
  1. 习题3
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise 还是会调用.then 
    console.log(2)
}).catch(() => {
    console.log(3)
})

// 结果
// 1
// 2
  1. 习题4
// 演练3
new Promise((resolve, reject) => {
    resolve(1)
    console.log(`2`);
}).then(r => {
    console.log(`1`);
})

// 输出 
2
1
  1. 习题5: 实现一个Promise在3秒后执行
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // 成功的回调
    resolve("Promise resolved after 3 seconds");
  }, 3000);
});

myPromise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });

Promise.all()

/* 
    Promise.all() 的用法
    - Promise.all 是一个 JavaScript 函数,
    - 它接受一个包含多个 Promise 实例的数组作为参数,
    - 并返回一个新的 Promise 对象。
    - 该 Promise 对象在所有的 Promise 实例都已经成功执行后才会被解决,
    - 如果其中任何一个 Promise 实例被拒绝(rejected),
    - 则整个 Promise 将被拒绝。
*/
// 语法
Promise.all(iterable); // 其中iterable是一个可迭代对象,如数组

/* 
    例如,假设你有两个异步函数 promise1() 和 promise2(),它们分别返回 Promise 实例。
    你可以使用 Promise.all 如下组合这两个 Promise 实例:
*/
Promise.all([promise1(), promise2()])
  .then(([result1, result2]) => {
    console.log(`Result 1: ${result1}`);
    console.log(`Result 1: ${result2}`);
  })
  .catch((error) => {
    console.log(error);
  });

13. async / await

async & await

  • Promise then catch 链式调用,但也是基于回调函数(基于回调函数)
  • async/await 是同步语法,彻底消失回调函数(async/await是同步语法,彻底消失回调函数)

async & await 和 Promise的关系

  • async/await是消灭异步回调的终极武器(进行同步)
  • 但是和Promise并不互斥,反而相辅相成
    • 执行async函数,返回的是Promise对象(执行async函数,返回的是Promise对象)
    • await相当于Promise的then
    • try…catch 可以捕获异常,代替了Promise的catch

异步的本质

  • async/await是消灭异步回调的终极武器
  • JS还是单线程;还的是有异步,还得是基于event loop
  • async/await只是一颗语法糖;但这颗糖真香

底层的执行原理

  • 是的,async/await 的底层实现利用了生成器(Generators)。当你在一个 async 函数内使用 await 关键字时,JavaScript 引擎会在后台使用生成器来实现异步操作的挂起和继续执行。
function asyncGenerator(generatorFunction) {
  return function () {
    const generator = generatorFunction.apply(this, arguments);

    function handle(result) {
      // { done, value } 对象表示生成器的状态
      if (result.done) return Promise.resolve(result.value);

      // 将 Promise 对象解决后,继续生成器的执行
      return Promise.resolve(result.value).then(
        function (res) {
          return handle(generator.next(res));
        },
        function (err) {
          return handle(generator.throw(err));
        }
      );
    }

    try {
      return handle(generator.next());
    } catch (error) {
      return Promise.reject(error);
    }
  };
}
// 一个简单的生成器函数
function* myGenerator() {
  const result1 = yield someAsyncOperation1();
  const result2 = yield someAsyncOperation2(result1);
  return result2;
}

// 使用 asyncGenerator 包装生成器函数
const myAsyncFunction = asyncGenerator(myGenerator);

// 调用 async 函数
myAsyncFunction().then(result => {
  console.log(result);
});

在这个例子中,asyncGenerator 函数接受一个生成器函数作为参数,并返回一个新的函数。这个新的函数在执行时负责管理生成器的状态,并根据生成器的状态调用 Promise.resolve 来实现异步操作的处理。这种机制允许异步操作的挂起和继续执行,从而达到 async/await 的效果。

需要注意的是,这只是一个简化的例子,实际的实现可能涉及更多复杂的细节和优化。生成器在实现异步编程时确实起到了关键的作用。

执行顺序习题1

async function async1() {
    console.log('async start') // 2
    await async2() 
    /* 
        await的后面,都可以看作是callback里的内容,即异步
        类似:event loop; setTimeout(cb1)
        异步:
            - setTimeout(function(){})
            - Promise.resolve().then() 
    */
    console.log('async1 end') // 5
}

async function async2() {
    console.log('async2') // 3
}

console.log('script start') // 1
async1()
console.log('script end') // 4
// 同步执行完;(eventLoop)

async和await的内容

async function fn1() {
    return 100 // 相当于return Promise.resolve(100)
}

const res1 = fn1() // 执行async
console.log('res1', res1) // res1 Promise { 100 } 是一个Promise对象

res1.then( data => {
    console.log('data', data) // 100
})

!(async function() {
    const p1 = Promise.resolve(300)
    const data1 = await p1 // await相当于 Promise.then 
    console.log('data1', data1) 
})()

!(async function() {
    const data1 = await 400 // await Promise.resolve(400)
    console.log('data1', data1) 
})()

14. 箭头函数和普通函数的区别

  1. 语法
  2. this的处理
  • 在箭头函数内部,this 的值是继承自包含它的函数(或全局作用域)的。箭头函数没有自己的 this,它会捕获外部函数的 this 值。
  • 在普通函数内部,this 的值取决于函数的调用方式。在全局作用域内,this 指向全局对象(如 window)。而在对象方法中,this 指向调用该方法的对象。
  1. arguments
  • 箭头函数没有自己的arguments对象,无法直接访问传递给函数的参数
  • 普通函数有自己的 arguments 对象,可以用来访问传递给函数的参数。
  1. 构造函数
  • 箭头函数不能用作构造函数,不能使用 new 关键字来实例化对象。
  • 普通函数可以用作构造函数,可以使用 new 关键字来创建对象实例。
  1. 返回值
  • 箭头函数可以在简单情况下隐式返回一个表达式的值。例如,x => x * 2 会直接返回结果。
  • 普通函数需要使用 return 语句显式指定返回值。
  1. 原型
  • 箭头函数没有自己的prototype属性,因此不能用来定义原型方法
  • 普通函数可以通过其 prototype 属性定义原型方法,从而在创建对象时共享方法。

15. Proxy和Reflect的使用

  1. Proxy
  • 概念; Proxy是ES6中的一个新特性,在Vue3中用于监听数据的变化并触发响应式更新;它可以代替Vue2中的使用Object.defineProperty()实现数据的响应式。
  • 区别于Object.defineProperty()
    • Proxy可以监听整个对象及其嵌套属性的变化,不需要逐个定义每个属性
    • 支持更多的拦截器函数,比如set、get、delete、apply等
    • Vue3中的reactive函数会返回一个proxy对象;对数据的修改都会被Vue3所捕获并进行响应式更新
  1. Reflect
  • 用于执行与代理对象相关的默认行为
  • Reflect方法与Proxy拦截器一一对应, Reflect.get() 对应 handler.get()
  • 使用Reflect方法获取目标对象的属性

16. JS比较两个值的方法

  1. 严格相等比较(===) , 比较值和类型是否都相等.
  2. 松散相等比较(==), 进行类型转换后比较.
  3. 比较运算符; 使用 >、<、>=、<= 操作符来比较数字或字符串
  4. Object.is(); 类似于严格相等比较, 但处理了一些特殊情况, 例如: NaN和 -0
  5. 数组和对象的比较,使用=== 和 == 操作符会比较它们的引用,而不是内容

17. 匿名函数和普通函数有什么区别?

  1. 命名
    • 普通函数有一个名称,通过这个名称可以在代码中引用和调用它。
    • 匿名函数没有名称,它们通常作为表达式内的一部分或作为回调函数传递给其他函数。
  2. 定义方式
// 普通函数
function add(a, b) {
  return a + b;
}
// 匿名函数
// 匿名函数(无函数名)
const add = function(a, b) {
  return a + b;
};
// 箭头函数(匿名)
const add = (a, b) => a + b;
  1. 使用情况
    • 普通函数通常用于代码中多次调用的情况
    • 匿名函数通常用于一次性的、不需要命名的场景,例如作为回调函数传递给其他函数,或者用于创建立即执行的函数。
document.getElementById('myButton').addEventListener('click', function() {
  alert('Button clicked!');
});

18. ES6中数组新增了哪些扩展

  1. 扩展运算符的应用
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
  1. 构造函数新增的方法
  • Array.from()
  • Array.of()
  1. 实例对象新增的方法
  • copyWithin(); 将指定位置的成员复制到其他位置(会覆盖原有成员), 然后返回当前数组
  • find(), findIndex()
  • fill()
  • entries(),keys(),values()
  • includes() // 返回true、false
  • flat()、flatMap()

19. ES6中对象新增了哪些扩展

  1. 属性的简写
  2. 属性名表达式; 将表达式放在括号内; [lastWord]: ‘world’
  3. super关键字; 指向当前的原型对象
  4. 扩展运算符的应用
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
  1. 属性的遍历
  • for…in
  • Object.keys(obj)
  • Object.getOwnPropertyNames(obj)
  • Object.getOwnPropertySymbols(obj)
  • Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
  1. 对象新增的方法
  • Object.is(); 严格判断两个值是否相等 ; 不同之处只有两个值
+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
  • Object.assign(); 用于对象的合并
  • Object.getOwnPropertyDescriptors(); 返回指定对象所有自身属性的描述对象
    Object.setPrototypeOf(),Object.getPrototypeOf(); 用来设置一个对象的原型对象, 以及, 读取一个对象的原型对象
    Object.keys(),Object.values(),Object.entries(); 键值对的数组
    Object.fromEntries(); 用于将一个键值对数组转为对象
Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

20. ES6函数新增了哪些扩展?

  • 参数
    • ES6允许为函数的参数设置默认值
    • 函数的形参是默认声明的, 不能使用let 或 const 再次声明
    • 参数默认值可以与解构赋值的默认值结合起来使用
function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
  • 属性
    • length属性
    • name属性
  • 作用域
    • 一旦设置了参数的默认值, 函数进行声明初始化时, 参数会形成一个单独的作用域, 等到初始化结束, 这个作用域就会消失.
let x = 1;

function f(y = x) { 
  // 等同于 let y = x  
  let x = 2; 
  console.log(y);
}

f() // 1
// y=x会形成一个单独作用域,x没有被定义,所以指向全局变量x
  • 箭头函数
    • 如果箭头函数的代码块部分多于一条语句, 就要使用大括号将它们括起来, 并且使用return 语句返回
var sum = (num1, num2) => { return num1 + num2; }
- 如果返回对象, 需要加括号将对象包裹
let getTempItem = id => ({ id: id, name: "Temp" });

箭头函数的注意点

  • 函数体内的this对象, 就是定义时所在的对象, 而不是使用时所在的对象
  • 不可以当作构造函数, (即不可以使用new命令)
  • 不可以使用arguments对象, 该对象在函数体内不存在(可以使用rest参数代替)
  • 不可以使用yield命令, 因此箭头函数不能用做Generator函数

21. Set和Map

Set是一种叫做集合的数据结构, Map是一种叫做字典的数据结构

  • 集合; 由一堆无序的、相关联的, 且不重复的元素组成的集合
  • 字典; 是一些元素的集合, 每个元素有一个称为key的域, 不同元素的key各不相同

区别; 集合、字典都可以存储不重复的值; 集合是以[值, 值]的形式存储元素, 字典是以[键, 值]的形式存储

22. 怎么理解ES6中的 Generator的? 使用场景

Generator函数是ES6提供的一种异步编程解决方案

  • 执行Generator函数会返回一个遍历器对象, 可以依次遍历Generator函数内部的每一个状态
  • 形式上, Generator函数是一个普通函数, 两个特征
    • function关键字与函数名之间有一个星号
    • 函数体内部使用yield表达式, 定义不同的内部状态
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
hw.next()
// { value: 'hello', done: false }

解释: 通过next方法才会遍历到下一个内部状态, next()函数会返回一个对象, 其中的value表示对应状态值, done用来判断是否存在下个状态

将Promise、Generator、async/await 进行比较

  1. promise和async/await是专门用于处理异步操作的
  2. Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口…)
  3. Promise编写代码相比于Generator、async更为复杂化, 且可读性差
  4. async实质是Generator的语法糖,相当于会自动执行Generator函数
  5. async使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案

23. 怎么理解ES6的Proxy? 使用场景?

  • Proxy为构造函数, 用来生成Proxy实例
var proxy = new Proxy(target, handler)
// target; 表示所要拦截的目标对象
// handler; 通常以函数作为属性的对象, 各属性中的函数分别定义了在执行各种操作时代理p的行为
handler; 即为 {}

handler解析
get(target,propKey,receiver):拦截对象属性的读取
set(target,propKey,value,receiver):拦截对象属性的设置
has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
ownKeys(target):拦截Object.keys(proxy)、for…in等循环,返回一个数组
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作

Reflect

  • 若需要在Proxy内部调用对象的默认行为, 建议使用Reflect,(ES6新增)
    • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
    • 修改某些Object方法的返回结果,让其变得更合理
    • 让Object操作都变成函数行为

24. 字符串的常用方法

1. 操作方法

  • 增; concat()
  • 删; slice()、substr()、substring()
  • 改; 不是改变原字符串, 而是创建字符串的一个副本, 再进行操作 trim()、toLowerCase()
  • 查; charAt()、indexOf()、includes()

2. 转换方法

  • 把字符串按照指定的分割符, 拆分成数组中的每一项
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]

3. 模板匹配方法 (针对正则表达式)

  • match() ; 接收一个参数, 可以是一个正则表达式字符串
  • search() ; 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,找到则返回匹配索引,否则返回 -1
  • replace() ; 接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)

25. JS继承的方式有哪些

在JavaScript中,有多种方式可以实现继承,允许一个对象或类继承另一个对象或类的属性和方法。以下是一些常见的继承方式:

  1. 原型链继承(Prototype Inheritance):
    • 这是JavaScript中最基本的继承方式,通过将子类的原型对象指向父类的实例来实现继承。子类继承了父类的属性和方法。
    • 但原型链继承存在问题,如果子类修改了继承的属性,会影响到所有子类的实例。
  2. 构造函数继承(Constructor Inheritance):
    • 这种继承方式通过在子类构造函数中调用父类构造函数来实现继承。子类获得了父类构造函数中的属性。
    • 但构造函数继承无法继承父类原型上的方法。
  3. 组合继承(Combination Inheritance):
    • 组合继承结合了原型链继承和构造函数继承的优点。它同时继承了父类的属性和方法。
    • 但它会调用两次父类构造函数,一次是在原型链继承时,一次是在构造函数继承时,可能会导致性能问题。
  4. 原型式继承(Prototype-based Inheritance):
    • 这种继承方式使用一个现有对象作为新对象的原型,从而创建一个继承自该对象的新对象。
    • 原型式继承在不需要定义类的情况下创建对象,但也存在共享属性的问题。
  5. 寄生式继承(Parasitic Inheritance):
    • 寄生式继承是在原型式继承的基础上添加了一些额外的属性或方法,通常用于在已有对象基础上创建新对象。
    • 这种方式通常用于增强已有对象,但也容易导致不清晰的继承关系。
  6. ES6类继承
    • ES6引入了class关键字,可以更方便地实现面向对象的继承。通过extends关键字,子类可以继承父类的属性和方法。
  7. Object.create()继承
    • 使用Object.create()方法可以创建一个新对象,将现有对象作为新对象的原型。这种方式实现了原型链继承。

26. 获取DOM的方式有哪些

在JavaScript中,你可以使用不同的方法来获取DOM元素。以下是一些常见的方法:

  1. 通过元素的ID获取
    • 使用document.getElementById(id)可以根据元素的ID获取DOM元素。
    const element = document.getElementById("myElement");
    
  2. 通过选择器获取
    • 使用document.querySelector(selector)可以根据CSS选择器获取第一个匹配的元素。
    const element = document.querySelector(".myClass");
    
  3. 通过选择器获取多个元素
    • 使用document.querySelectorAll(selector)可以根据CSS选择器获取所有匹配的元素,返回一个NodeList。
    const elements = document.querySelectorAll(".myClass");
    
  4. 通过标签名获取
    • 使用document.getElementsByTagName(tagName)可以根据标签名获取元素,返回一个HTMLCollection。
    const elements = document.getElementsByTagName("div");
    
  5. 通过类名获取
    • 使用document.getElementsByClassName(className)可以根据类名获取元素,返回一个HTMLCollection。
    const elements = document.getElementsByClassName("myClass");
    
  6. 通过属性获取
    • 使用document.getElementsByName(name)可以根据元素的name属性获取元素,返回一个NodeList。
    const elements = document.getElementsByName("myName");
    
  7. 通过父元素的子元素获取
    • 使用parentNode.querySelector(selector)可以在父元素的范围内根据选择器获取子元素。
    const parentElement = document.getElementById("parentElement");
    const childElement = parentElement.querySelector(".myClass");
    
  8. 通过事件目标获取
    • 在事件处理程序中,你可以使用event.target来获取触发事件的DOM元素。
    element.addEventListener("click", function(event) {
      const clickedElement = event.target;
    });
    

这些是获取DOM元素的常见方法,你可以根据具体的需求和DOM结构选择适当的方法。请注意,获取的DOM元素可以用于操作和修改DOM,例如更改样式、内容、属性等。

27. 基本数据类型与布尔值的隐式转换

真值

  • 非空字符串(例如:“hello”)
  • 非零数字(例如:1、-1)
  • 对象(包括数组、函数、对象字面量)
  • true
    假值
  • 空字符串(例如:“”)
  • 数值0(包括-0)
  • null
  • undefined
  • false
  • NaN
    隐式布尔转换、逻辑运算符、比较操作符

[] == ![]在js中结果是什么?
在JavaScript中,[] == ![] 的比较结果是 true。这可能看起来有点令人惊讶,但这是因为涉及到类型转换和操作符优先级的特殊情况。

  1. [] 是一个空数组,它被视为"truthy"值(非常值),因为它存在。
  2. ![] 使用逻辑非操作符! 对空数组进行取反,会将其转换为false,因为逻辑非操作符会将"truthy"值转换为false,而"falsy"值(如nullundefined0、空字符串)转换为true
  3. 然后,[] == false 进行比较。在这里,JavaScript进行了类型转换,将[]转换为false,因为在比较时会尝试将操作数转换为相同的类型。这是因为==是松散相等操作符,它会尝试将操作数转换为相同的类型,然后进行比较。
    所以,最终的比较是 false == false,这是一个显而易见的相等比较,结果是 true
    为了避免混淆和提高代码的可读性,通常建议使用严格相等比较操作符 ===,因为它不会进行类型转换,只有在类型和值都相同时才返回 true。例如,[] === ![] 将返回 false,因为它们的类型不同。

28. cjs和es6 module区别

CommonJS (CJS) 和 ES6 模块 (ESM) 是两种不同的模块系统,它们有一些重要的区别:

  1. 语法差异:
    • CommonJS 使用require语句来导入模块,以及module.exports来导出模块。
    • ES6 模块使用import语句来导入模块,以及export语句来导出模块。
  2. 动态 vs. 静态:
    • CommonJS 模块是动态加载的,这意味着在运行时可以根据需要导入模块。
    • ES6 模块是静态的,它们在编译时确定导入的模块,这使得在静态分析工具和工具链中更容易优化。
  3. 顶层变量:
    • CommonJS 模块在模块内部创建了一个顶层作用域,模块内的变量不会污染全局作用域。
    • ES6 模块不会创建顶层作用域,它们只导出模块的具体内容,不会自动创建全局变量。
  4. 循环依赖:
    • CommonJS 允许循环依赖,但可能会导致问题,因为模块在运行时加载。
    • ES6 模块在编译时解决循环依赖,因此在循环依赖时更容易处理。
  5. 导入默认和命名导出:
    • ES6 模块支持默认导出和命名导出。一个模块可以有一个默认导出和多个命名导出。
    • CommonJS 模块通常只支持导出一个值或对象,通常使用module.exports来导出。
  6. 异步加载:
    • ES6 模块原生支持异步加载,通过import()函数,允许在运行时按需加载模块。
    • CommonJS 模块通常需要使用额外的工具或库来实现异步加载。
  7. 静态分析:
    • 由于ES6 模块是静态的,它们在静态分析时更容易优化和检测错误。
    • CommonJS 模块的动态性使得在编译时难以静态分析和优化。
      通常情况下,如果你使用的是现代JavaScript环境(如浏览器或支持ES6模块的Node.js版本),建议使用ES6模块,因为它们具有更好的性能和工具支持。然而,如果你正在维护旧的Node.js应用程序或与CommonJS模块集成,你可能需要继续使用CommonJS模块。

TS

项目中使用ts的优点

  1. 声明式编程:TypeScript是一种静态类型语言可以在编译时检查类型错误,避免在运行时出现类型错误,提高开发效率。
    • 动态类型语言
      • 动态类型语言指的是在运行时才确定变量的数据类型(运行时才确定变量的数据类型)
  2. 易于维护:TypeScript支持类型定义和接口的定义,代码可读性更强,更易于维护和更新。
  3. 对面向对象编程的支持:TypeScript支持面向对象编程,包括类、继承、抽象类、接口等,方便开发者进行面向对象的编程。
  4. 兼容性好:TypeScript可以编译成JavaScript,可以在任何支持JavaScript的环境中运行,具有很好的兼容性。
  5. 综上所述,使用TypeScript可以提高开发效率、代码可读性和可维护性,减少错误和提高代码质量,同时也有很好的兼容性。因此,在开发大型项目时,使用TypeScript可以提高开发效率和代码质量,减少维护成本。

ts中的联合类型和交叉类型

  1. 它们可以用来组合不同的类型, 以满足特定的需求;
  2. 联合类型;
  • 概念; 联合类型表示一个变量可以是多个类型之一, 使用 | 符号进行连接操作;
  • 例如;
let value: string | number
value = 'hello'
value = 100
  1. 交叉类型;
  • 概念; 交叉类型表示一个变量是多个类型的组合类型, 使用& 符号进行连接;
  • 例如;
interface A {
    name: string;
    age: number;
}

interface B {
    gender: string;
}
// 类型C是A和B类型的交叉类型,它拥有A和B类型的所有属性和方法;
type C = A & B;
let person: C = {
    name: 'Alice',
    age: 20,
    gender: 'female'
};

总结: 联合类型和交叉类型都是TS中非常有用的高级类型,可以用来组合不同的类型,以满足特定的需求

TS的范型interface

  1. 在TS中, 范型可以应用于接口,允许我们定义具有通用类型的接口
  2. 语法
interface GenericInterface<T> {
  property: T;
  method(arg: T): T;
}
  1. 使用范型接口并指定具体的类型
const myInterface: GenericInterface<number> = {
    property: 18, 
    method: (arg) => arg + 1
}
console.log(myInterface.property); // 输出: 42
console.log(myInterface.method(10)); // 输出: 11

interface和type的区别

  1. 定义方式不同;
interface Person {
  name: string;
  age: number;
}

type Person = {
  name: string;
  age: number;
};
  1. 语法的不同;
  • interface可以被扩展和实现, 而type不可以
interface Animal {
    name: string;
    eat(): void;
}

interface Dog extends Animal {
    bark(): void;
}

type Animal = {
    name: string;
    eat(): void;
};

// Error: Type aliases cannot be extended.
type Dog = Animal & {
    bark(): void;
};
  • interface可以声明多次并自动合并, 而type不可以
interface Person {
    name: string;
}

interface Person {
    age: number;
}

const person: Person = {
    name: 'Alice',
    age: 20
};

type Person = {
    name: string;
};

// Error: Duplicate identifier 'Person'.
type Person = {
    age: number;
};

const person: Person = {
    name: 'Alice',
    age: 20
};
  1. 适用场景不同
  • interface适合用于描述一个对象或类的结构, 或者用于定义函数的参数或返回值的类型;
  • type适合用于定义一些复杂的类型,如联合类型、交叉类型、函数类型等;

TS如何声明变量

在 TypeScript 中,可以使用不同的方式声明类型,确保代码具有更强的类型检查和更好的代码提示。

  1. 基本类型声明: 使用关键字声明基本类型,如 stringnumberbooleannullundefined 等。
let str: string = 'Hello';
let num: number = 42;
let bool: boolean = true;
let n: null = null;
let u: undefined = undefined;
  1. 数组类型声明: 使用 Array<type>type[] 表示数组。
let arr: number[] = [1, 2, 3];
let arr2: Array<string> = ['a', 'b', 'c'];
  1. 对象类型声明: 使用接口(interface)或类型别名(type alias)来声明对象类型。
interface Person {
  name: string;
  age: number;
}

let user: Person = {
  name: 'Alice',
  age: 25
};
  1. 函数类型声明: 声明函数参数类型和返回值类型。
function add(a: number, b: number): number {
  return a + b;
}
  1. 联合类型和交叉类型: 使用 | 表示联合类型(可以为多个类型中的一个)和 & 表示交叉类型(组合多个类型)。
type Dog = { kind: 'dog'; woof: () => void };
type Bird = { kind: 'bird'; chirp: () => void };

type Pet = Dog | Bird; // 联合类型
type DogBird = Dog & Bird; // 交叉类型
  1. 泛型(Generics): 使用泛型使得类型可以在使用时指定。
function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>('Hello');

Vue组件

1. Vue2和Vue3的区别

  1. Vue2和Vue3双向数据绑定原理发生了改变;

    • vue2的双向数据绑定是利用了Object.defineproperty() 对数据进行劫持 结合发布订阅模式来实现的。vue3中使用了es6的proxyAPI对数据进行处理
      • 发布与订阅模式是一种组件之间通信的设计模式;该模式允许一个组件充当发布者并向其他组件(订阅者)发送消息,而不需要明确地知道这些订阅者的存在。
      • 任何组件都可以使用$emit方法来触发自定义事件,并使用$on来侦听该事件;
    • 使用proxy API 优势有:
      • defineProperty()只能监听某个属性,不能对全对象进行监听;
      • proxyAPI可以监听数组,不用再去单独的对数组做特异性操作,vue3可以检测到数组内部数据的变化;
      • defineProperty()只能监听属性的改变和属性的获取
  2. Vue3支持碎片(Fragments)

    • 可以拥有多个根节点
  3. Composition API

    • vue2使用选项类型api,对比vue3组合式api
    • 旧的选项型api在代码里分割了不同的属性:data,computed,methods等;新的组合式api能让我们使用方法来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁
  4. 建立数据data

    • vue2将数据放入data中;放入进去就已经是响应式数据了;
    • vue3需要使用一个新的setup方法,此方法在组件初始化构造的时候触发,使用三个步骤来建立响应式数据:1、从vue引入reactive()方法来声明数据为响应式数据 2、使用setup()方法来返回我们的响应式数据 3、从而在template中获取这些响应式数据
  5. 生命周期(生命周期不同)

      vue2     ------------------     vue3
    beforeCreate                 ->   setup()
    Created                      ->   setup()
    beforeMount                  ->   onBeforeMount
    mounted                      ->   onMounted
    beforeUpdate                 ->   onBeforeUpdate
    updated                      ->   onUpdated
    beforeDestroyed              ->   onBeforeUnmount
    destroyed                    ->   onUnmounted
    activated                    ->   onActivated
    deactivated                  ->   onDeactivated
    
  6. 父子传参不同,setup函数特性(相关特性)

    • setup() 函数接收两个参数:props、context(attrs、slots、emit)
    • setup函数是处于生命周期beforeCreated和created俩个钩子函数之前
    • 执行setup时,组件实例尚未被创建(在setup()内部,this不会是该活跃实例得引用,即不指向vue实例,Vue为了避免我们错误得使用,直接将setup函数中得this修改成了undefined)
    • 与模板一起使用时,需要返回一个对象
    • 因为setup函数中,props是响应式的,当传入新的prop时,它将会被更新,所以不能使用es6解构,因为它会消除prop得响应性,如需解构prop,可以通过使用setup函数中得toRefs来完成此操作
    • 父传子,用props,子传父用事件 Emitting Events。在vue2中,会调用this.$emit然后传入事件名和对象;在vue3中得setup()中得第二个参数content对象中就有emit,那么我们只要在setup()接收第二个参数中使用分解对象法取出emit就可以在setup方法中随意使用了(通过context调用属性方法即可)
    • 在setup()内使用响应式数据时,需要通过 .value 获取(从setup() 中返回得对象上得property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加.value。)
    • setup函数只能是同步的不能是异步的

2. Vue3中的hooks介绍与用法

  • hooks直译为“钩子”
  • 在vue中,hooks的定义更加模糊;总结:在vue3组合式API中被定义为:以“use”开头的,一系列提供组件复用,状态管理等开发功能的方法

3. MVVM模型

  • Model代表数据模型(数据模型)
  • View代表看到的页面(视图)
  • ViewModel; VM(视图模型)是连接模型和视图之间的桥梁
    数据会绑定到VM层并自动将数据渲染到页面中,视图变化(页面数据变化)会通知VM层更新数据。

4. Vue的生命周期

  1. 生命周期: 用来在组件不同阶段执行特定的操作,从而实现对组件的精细控制
  2. Vue2的生命周期
- `创建前后`(beforeCreate、created)
	- beforeCreate; 在实例初始化之后,在事件监听和事件配置之前触发
	- created; 在实例创建完成之后触发,此时可以访问data、method等属性; 但这个时候组件还没有被挂载到页面中去,所以访问不到$el属性; 可以进行一些页面初始化工作,如:通过Ajax请求数据来对页面进行初始化
- `挂载前后`(beforeMount、mounted)
	- beforeMount; 在组件被挂载到页面之前被触发; 在beforeMount之前,会找到对应的template,并编译成render函数
	- mounted; 在组件挂载到页面之后触发, 此时可以通过DOM API 获取页面中的元素
- `更新前后`(beforeUpdate、updated)
	- beforeUpdate; 在响应式数据更新时触发, 发生在虚拟DOM重新渲染和打补丁之前,这个时候我们可能会对被移除的元素做一些操作,如移除事件监听器
	- updated; 虚拟DOM重新渲染和打补丁之后调用
- `销毁前后`(beforeDestroy、destroyed)
	- beforeDestroy; 用于清理工作的钩子,在实例销毁之前调用,(我们可以销毁定时器、解绑全局事件)
	- destroyed; Vue3执行完这个钩子以后Dom节点会被完全清除,而在Vue2中执行完这个钩子以后这个节点还在只是响应式数据什么的没有了
- 以及一些特殊场景的生命周期
    - activated;keep-alive组件激活时调用
    - deactivated;keep-alive组件停用时调用
  1. Vue3的生命周期
    区别于Vue2的生命周期
  • setup函数代替了beforeCreate、created
  • 使用Hooks函数的形式, 如mounted改为onMounted
- setup函数代替了beforeCreate、created
- 挂载前后;onBeforeMount、onMounted
- 更新前后;onBeforeUpdate、onUpdated
- 卸载前后;onBeforeUnmount、onUnmounted
- 新增的钩子
	- onRenderTracked; 用于追踪组件渲染时的依赖追踪情况
	- onRenderTriggered; 用于追踪组件渲染时的依赖触发情况

Ajax应该出现在哪个生命周期呢

  • created; 因为在created钩子中, 数据已经是可以用了;
  • mounted; 因为在mounted中DOM也渲染完了,还可以在AJAX请求结束以后操作DOM;

5. Vue.$nextTick

  • 概念; 在下次DOM更新循环结束之后执行延迟回调, 在修改数据之后立即使用这个方法,获取更新后的DOM
  • 使用场景
    • 想要修改数据后立刻得到更新后的DOM结构,可以使用 Vue.$nextTick
    • 在created生命周期中进行DOM操作
      • created生命周期钩子是在实例被创建之后,但是在模板渲染之前触发的, 此时Vue已经创建了虚拟DOM,并且正在准备将其渲染到页面中
      • 如果您在created生命周期中进行的DOM操作依赖于Vue更新视图后的结果,那么最好使用$nextTick 确保在DOM操作之前等待Vue完成视图的更新。这是因为Vue的更新是异步的,所以在created中立即进行DOM操作可能会导致操作的DOM元素还没有被渲染到页面上。
  • 微任务 or 宏任务
    • Vue2一开始是宏任务后面是微任务,交替变化
    • Vue3确定了就是微任务

Vue.$nextTick()的底层实现原理是什么?
$nextTick()主要关注的是DOM更新的时机, 其底层实现原理是基于浏览器的事件循环机制, 在下一个事件循环周期中执行传递的回调函数,以确保在DOM更新队列中的任务都已经完成。
实现的基本原理

DOM更新队列; Vue.js内部维护一个DOM更新队列,用于存储需要在下一次事件循环中更新到DOM的任务。
触发状态改变, 当Vue组件的数据发生变化(状态改变)时,Vue会将需要更新的任务添加到DOM更新队列中。
事件循环; 当Vue组件的数据发生变化(状态改变)时,Vue会将需要更新的任务添加到DOM更新队列中。
n e x t T i c k 的使用:当你调用 V u e . nextTick的使用:当你调用Vue. nextTick的使用:当你调用Vue.nextTick(callback)时,它实际上是在Vue内部的事件循环周期中注册了一个微任务(Microtask)或宏任务(Macrotask),具体取决于浏览器的实现。这个微任务或宏任务会在当前事件循环周期结束后执行,确保在DOM更新队列中的任务都已经完成。
回调执行; 一旦事件循环中的当前任务完成,Vue会执行你传递给$nextTick的回调函数,这意味着此时DOM已经更新完成。

6. Vue实例挂载过程发生了什么

  1. 初始化阶段; 创建Vue实例
  2. 模板编译阶段; 将模板编译成渲染函数
  3. 挂载阶段; 将Vue实例挂载到DOM元素中
  4. 渲染阶段; 将虚拟DOM 转化成 真实DOM
  5. 更新阶段; 当响应式数据发生变化时,Vue 会重新生成虚拟 DOM 树,并通过 diff 算法比较新旧虚拟 DOM 树的差异,并将差异更新到真实 DOM 中
  6. 销毁阶段;生命周期钩子函数 beforeDestroy、destroyed,并执行一些必要的清理工作,如解绑事件、清除定时器等。

7. Vue的响应式原理

  • Vue2的响应式原理;
    • Vue 2 中的数据响应式会根据数据类型做不同的处理
      • 如果是对象,则通过Object.defineProperty(obj,key,descriptor)拦截对象属性访问,当数据被修改和查询时,感知并作出反应
      • 如果是数组,则通过覆盖数组原型方法,扩展它的7个变更方法(push、pop、shift、unshift、sort、reverse、splice)
    • 缺点
      • 新增或删除对象属性无法拦截
      • 需要通过Vue.set及delete这类API才能生效
/* 
    1、模拟Vue2的响应式原理
        - 通过在浏览器中进行模拟
*/
let person = {
  name: "张三",
  age: 18,
};
// 通过另一个对象来
let p = {};
// 可以用循环的方式来对所有的属性进行代理
/* 
  1. 对象
  2. 属性
  3. 相应的配置
*/
Object.defineProperty(p, "name", {
  configurable: true,
  // 有人读取name时调用
  get() {
    console.log("有人读取了name属性");
    return person.name;
  },
  // 有人修改name时调用
  set(value) {
    console.log("有人修改了name属性");
    person.name = value;
  },
});

Object.defineProperty(p, "age", {
  get() {
    //有人读取age时调用
    return person.age;
  },
  set(value) {
    //有人修改age时调用
    console.log("有人修改了age属性,我发现了,我要去更新界面!");
    person.age = value;
  },
});
  • Vue3的响应式原理
    • Vue 3 中利用ES6的Proxy机制代理需要响应化的数据。可以同时支持对象和数组,动态属性增、删都可以拦截,新增数据结构均支持
    • p对象已经成为代理了,在浏览器控制台中,对其进行相关操作即可
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue3响应式原理</title>
</head>
<body>
    <script>
        let person = {
            name:'hk', 
            age: 18
        }
        const p = new Proxy(person, {
            // 有人读取p的某个属性时调用
            get(target, propName) {
                console.log(`有人读取了p身上的${propName}属性`);
                // 读取了直接返回即可
                return Reflect.get(target, propName)
            }, 
            set(target, propName, value) {
                console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`);
                Reflect.set(target, propName, value)
            },
            // 有人删除p中的某个属性时使用
            deleteProperty(target, propName) {
                console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`);
                return Reflect.deleteProperty(target, propName);
            }
        })
    </script>
</body>
</html>

8. 虚拟DOM

  • 概念;它是JS框架或库中一种优化技术,以避免频繁更新实际DOM
  • 使用;实际使用中,虚拟DOM通常是由Vue和React等框架生成的,在每次数据发生变化时,框架会通过对比新旧虚拟DOM,找到需要更新的地方,并将更新的部分渲染到页面上,从而实现页面的动态更新
  • 好处;性能提升,直接操作DOM的代价是比较昂贵的,频繁的操作DOM容易引起页面的重绘和回流。如果通过抽象VNode进行中间处理,可以有效减少直接操作DOM次数,从而减少页面的重绘和回流

9. Diff算法

  • 概念;diff算法是一种对比算法, 通过对比旧的虚拟DOM和新的虚拟DOM,得出是哪个虚拟节点发生了改变,找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点,实现精准地更新真实DOM,提高效率。(主要用于比较两个DOM树的差异,以便减少渲染次数,提高页面性能)
  • 差异比较算法
    • 先对新旧DOM树进行标记
    • 标记出新增、删除、移动位置、属性变化等差异
    • 将这些差异进行合并,最终只需要对发生变化的部分进行更新
  • Diff算法为什么要同层比较?
    • 同层级比较,会逐行对比两个文本文件,找到不同之处,这种比较方式可以有效地找出两个文本之间的差异,速度比较快

10. key的作用

  1. Vue中Key的作用?
  • key中的作用主要是:为了更加高效的更新虚拟DOM
  • Vue判断两个节点是否相同,主要判断两者的key
    • 如果不设置key,它的值就是undefined;则可能永远认为这是两个不相同的节点,只能去做更新操作,造成大量的DOM更新操作
  1. 虚拟DOM中key的作用?
  • key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】;随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:
    • 旧虚拟DOM与新虚拟DOM有相同的key,
      • 若虚拟DOM中的内容没变,直接使用之前的真实DOM
      • 若虚拟DOM中的内容变了,生成新的真实DOM,随后替换页面之前的真实DOM
    • 旧虚拟DOM未找到与新虚拟DOM相同的key
      • 直接创建出新的真实DOM,随后渲染到页面中

11. 为什么组件中的data是一个函数,而不是一个对象?

  • Vue组件是可以复用的,如果data是一个普通的对象,那么在多个组件实例中,它们都会共享同一个data对象,导致数据混乱
  • 如果将data定义为一个函数,每个组件实例都会调用该函数生成一个独立的data对象,确保数据不会相互干扰

12. 组件间的通信方式

- 父子组件通信
    -  => 子;通过props
    -  => 父;通过$emit触发事件 ; 即自定义事件的形式;
    - 通过父链/子链也可以通信($parent/$children)
        - 使用$parent和$children可以访问到组件实例的父组件和子组件
    - ref也可以访问组件实例
    -  =>/孙;provide/inject
    - $attrs/$listeners
- 兄弟组件通信
    - 全局事件总线$eventBus;
    - Vuex;
- 跨层级组件间通信
    - 全局事件总线eventBus
    - Vuex
    - provide/inject

13. v-show和v-if的区别

  • 控制手段不同;
    • v-show是通过给元素添加 css 属性display: none,但元素仍然存在
    • v-if控制元素显示或隐藏是将元素整个添加或删除。
  • 性能消耗不同
    • v-if有更高的切换消耗
    • v-show有更高的初始渲染消耗
  • 使用场景
    • 如果需要非常频繁地切换,则使用v-show较好,如:手风琴菜单,tab 页签等;
    • 如果在运行时条件很少改变,则使用v-if较好,如:用户登录之后,根据权限不同来显示不同的内容。

14. computed和watch的区别。

  • computed计算属性,依赖其他属性计算值,内部任一依赖项的变化都会重新执行该函数,计算属性有缓存,多次重复使用计算属性时会从缓存中获取返回值,计算属性必须要有return关键词
  • watch侦听到某一数据的变化从而触发函数。当数据为对象类型时,对象中的属性值变化时需要使用深度侦听deep属性,也可在页面第一次加载时使用立即侦听immdiate属性。
  • 使用场景
    • 计算属性一般用在模板渲染中,某个值是依赖其它响应对象甚至是计算属性而来;
    • 监听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

15. v-if和v-for为什么不建议放在一起使用

  • Vue2中
    • v-for的优先级比v-if高,这意味着v-if将分别重复运行于每一个v-for循环中。如果要遍历的数组很大,而真正要展示的数据很少时,将造成很大的性能浪费。
  • Vue3中
    • Vue 3 中,则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量可能还不存在,会导致异常。
  • 通常有两种情况需要导致这么做
    • 为了过滤列表中的项目,比如:v-for = “user in users” v-if = “user.active”。这种情况,可以定义一个计算属性,让其返回过滤后的列表即可。
    • 为了避免渲染本该被隐藏的列表,比如v-for = “user in users” v-if = “showUsersFlag”。这种情况,可以将v-if移至容器元素上或在外面包一层template即可。

16. Vue2中的set方法

  • set是Vue2中的一个全局API
  • 手动添加响应式数据,解决数据变化视图未更新问题。
  • 当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,会发现页面并没有更新。这是因为Object.defineProperty()的限制,监听不到数据变化,可通过this.$set(数组或对象,数组下标或对象的属性名,更新后的值)解决

17. keep-alive是什么

  • 概念;
    • keep-alive 可以将其包裹的动态组件缓存起来,以避免因多次渲染导致的性能问题。
    • 在需要缓存的组件外面包裹该标签,从而使得组件在切换时不会被销毁
  • 作用
    • 实现组件缓存,保持组件的状态,避免反复渲染导致的性能问题
  • 工作原理
    • Vue.js 内部将 DOM 节点,抽象成了一个个的 VNode 节点,keep-alive组件的缓存也是基于 VNode 节点的。它将满足条件的组件在 cache 对象中缓存起来,重新渲染的时候再将 VNode 节点从 cache 对象中取出并渲染。
  • 设置了keep-alive缓存的组件,会多出两个生命周期钩子:activated、deactivated。
    • 首次进入组件时:beforeCreate --> created --> beforeMount --> mounted --> activated --> beforeUpdate --> updated --> deactivated
    • 再次进入组件时:activated --> beforeUpdate --> updated --> deactivated

组件内部使用了一个缓存对象来存储已渲染的组件,这个缓存对象是在内存中
组件内部使用了一个缓存对象来存储已渲染的组件,这个缓存对象是在内存中。

18. Mixin

  • 概念; mixin(混入), 它提供了一种非常灵活的方式,来实现Vue 组件中的可复用功能
  • 使用场景;
    • 不同组件中经常会用到一些相同或相似的代码,这些代码的功能相对独立。可以通过mixin 将相同或相似的代码提出来
  • 缺点
    • 变量来源不明确
    • 多 mixin 可能会造成命名冲突(解决方式:Vue 3的组合API)
    • mixin 和组件出现多对多的关系,使项目复杂度变高。

19. 插槽

  • slot插槽,一般在组件内部使用,封装组件时,在组件内部不确定该位置以何种形式的元素展示时,可以通过slot占据这个位置,该位置的元素需要父组件以内容形式传递过来,
  • slot插槽分类
- **默认插槽**:子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构作为后备内容,当父组件在使用的时候,可以直接在子组件的标签内写入内容,该部分内容将插入子组件的<slot>标签位置。如果父组件使用的时候没有往插槽传入内容,后备内容就会显示在页面。
- **具名插槽**:子组件用name属性来表示插槽的名字,没有指定name的插槽,会有隐含的名称叫做 default。父组件中在使用时在默认插槽的基础上通过v-slot指令指定元素需要放在哪个插槽中,v-slot值为子组件插槽name属性值。使用v-slot指令指定元素放在哪个插槽中,必须配合<template>元素,且一个<template>元素只能对应一个预留的插槽,即不能多个<template> 元素都使用v-slot指令指定相同的插槽。v-slot的简写是#,例如v-slot:header可以简写为#header。
- **作用域插槽**:子组件在<slot>标签上绑定props数据,**以将子组件数据传给父组件使用**。父组件获取插槽绑定 props 数据的方法:
      - scope="接收的变量名"<template scope="接收的变量名">
      - slot-scope="接收的变量名"<template slot-scope="接收的变量名">
      - v-slot:插槽名="接收的变量名"<template v-slot:插槽名="接收的变量名">

20. Vue中的修饰符有哪些

  • 概念; 在Vue中,修饰符处理了许多DOM事件的细节
  • Vue修饰符分类
- 表单修饰符
    - lazy 填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步。
    - number:自动将用户输入值转化为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值。
    - trim:自动过滤用户输入的首尾空格,而中间的空格不会被过滤。
- 事件修饰符
    - stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法。
    - prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法。
    - self 只当在 event.target 是当前元素自身时触发处理函数。
    - once 绑定了事件以后只能触发一次,第二次就不会触发。
    - capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理。
    - passive 告诉浏览器你不想阻止事件的默认行为。
    - native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件。

21. 对SPA的理解

  • SPA;即单页面应用, 通过动态重写当前页面来与用户交互,这种方式避免了页面之间切换时打断用户体验
  • SPA的优缺点
    • 优点
      • 用户体验好,快,内容的改变不需要重新加载整个页面
      • 良好的前后端分离,分工更明确
    • 缺点
      • 首次渲染速度相对较慢

22. 双向绑定

  • 概念; Vue 中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图的变化能改变该值
  • 使用; 通常在表单项上使用v-model; 还可以在自定义组件上使用,表示某个值的输入和输出控制
  • 原理; v-model是一个指令,实际上是value属性的绑定及input事件的监听,事件回调函数中会做相应变量的更新操作

23. 子组件是否可以直接改变父组件的数据

  • 所有的prop都遵循着单项绑定原则,props因父组件的更新而变化,自然地将新状态向下流往子组件,而不会逆向传递(这避免了子组件意外修改父组件的状态的情况)
  • 实际开发中,通常有两个场景导致需要修改prop
    • prop被用于传入初始值,而子组件想在之后将其作为一个局部数据属性。这种情况下,最好是新定义一个局部数据属性,从props获取初始值即可。
    • 需要对传入的prop值做进一步转换。最好是基于该prop值定义一个计算属性。
  • 实践中,如果确实要更改父组件属性,应emit一个事件让父组件变更

24. Vue Router中的常用路由模式和原理

  • hash模式

    • location.hash的值就是url中 # 后面的东西;它的特点在于:hash虽然出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面;
    • 特点:兼容性好,不美观
  • history模式

    • 利用pushState()和replaceState()方法。这两个方法应用于浏览器的历史记录栈,在当前已有的back、forward、go 的基础上,他们提供了对历史记录进行修改的功能。
    • 特点:美观,但是刷新会出现 404 需要后端进行配置;兼容性不好
  • 两个方法共同点:当调用他们修改浏览器历史记录栈后,虽然当前url改变了,但浏览器不会刷新页面,这就为单页面应用前端路由“更新视图但不重新请求页面”提供了基础

25. 动态路由

  • 概念;
    • 动态路由:允许在路由路径中添加参数,以便根据这些参数显示不同的页面内容
  • 应用场景
    • 动态路由通常用于根据用户输入或应用程序状态动态生成页面的情况
  • 使用
    • 在Vue中,可以使用: 来定义动态路由参数,例如:我们想要创建一个动态路由来匹配包含用户ID的路径
  {
    path:'/user/:id', 
    component: User
  }
  - 解释:在上面的代码中,:id 表示动态路由参数,当用户访问 /user/123 时,User组件将被渲染,并且可以通过this.$route.params.id来访问路由参数
  - 在动态路由中可以有多个参数,它们可以按任意顺序出现,并且还可以与普通路由参数结合使用
 {
    path: '/posts/:postId/comments/:commentId', 
    component: Comments
  }
  - 解释:在上面代码中:postId和:commentId都是动态路由参数,当用户访问/posts/123/comments/456时,Comments组件将被渲染,并且可以通过this.$route.params.postId和this.$route.params.commentId来访问路由参数

26. Vuex的理解

  • 概念;
    • Vuex是Vue中专用的状态管理库,它以全局方式集中管理应用的状态。
  • 解决的问题
    • Vuex 主要解决的问题是多组件之间状态共享
      • 利用各种通信方式,虽然也能够实现状态共享,但是往往需要在多个组件之间保持状态的一致性,这样会使得代码逻辑变复杂;
      • Vuex 通过把组件的共享状态抽取出来,以全局单例模式管理,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向流动,使代码变得更具结构化且易于维护
  • 什么时候使用?
    • 如果我们打算开发大型单页应用或应用里有大量全局的状态需要维护,可以使用vuex
  • 用法
    - Vuex 将全局状态放入state对象中,它本身是一颗状态树,组件中使用store实例的state访问这些状态;然后用配套的mutation方法修改这些状态,并且只能用mutation修改状态,在组件中调用commit方法提交mutation如果应用中有异步操作或复杂逻辑组合,需要编写action,执行结束如果有状态修改仍需提交mutation,组件中通过dispatch派发action。最后是模块化,通过modules选项组织拆分出去的各个子模块,在访问状态(state)时需注意添加子模块的名称,如果子模块有设置namespace,那么提交mutation和派发action时还需要额外的命名空间前缀。

27. 页面刷新后Vuex状态丢失怎么解决

  • Vuex只是在内存中保存状态,刷新后就会丢失,如果要持久化就需要保存起来
    • localStorage就很合适,提交mutation的时候同时存入localStorage,在store中把值取出来作为state的初始值即可
    • 也可以通过后台进行存储,前端通过接口来进行调用

28. 关于Vue SSR

  • SSR即服务端渲染(Server Side Render),就是将 Vue 在客户端把标签渲染成 html 的工作放在服务端完成,然后再把 html 直接返回给客户端。(标签渲染放在服务端完成)
  • 优点
    • 有更好的SEO(搜索引擎优化)
    • 首屏加载速度快
  • 缺点
    • 开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求。

29. Vue的性能优化方案

  • 路由懒加载; 访问时才异步加载
  • 图片懒加载;
  • keep-alive缓存页面。避免重复创建组件实例,且能保留缓存组件状态。(组件需要长时间切换时)
  • v-for遍历避免同时使用v-if。实际上在 Vue 3 中已经是一个错误用法了。
  • v-once; 不再变化的数据使用v-once,
  • 事件销毁,组件销毁后把全局变量和定时器销毁。(把监听事件销毁等)
  • 第三方插件按需引入
  • 比较复杂的组件,进行拆分操作

30. Echarts的使用

1. 下载ECharts;
    - 从 ECharts 官网下载最新版本的 ECharts。
    - echarts.min.js
2. 导入ECharts;
    - 将 ECharts 的 JavaScript 文件引入 HTML 页面中:
    ```
    <script src="echarts.min.js"></script>
    ```
3. 创建容器元素
    -HTML 页面中创建一个容器元素,用于放置图表:
    ```
    <div id="myChart" style="width: 600px; height: 400px;"></div>
    ```
4. 初始化图表对象
    - 在 JavaScript 中,使用 **echarts.init** 方法创建一个 ECharts 实例,并给它指定容器元素:
    ```
    var myChart = echarts.init(document.getElementById('myChart'));
    ```
5. 配置图表
    - 通过 **setOption**方法配置图表参数,包括图表类型、数据、样式等:
    ```
    myChart.setOption({
        title: {
            text: 'ECharts 入门示例'
        },
        tooltip: {},
        xAxis: {
            data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
        },
        yAxis: {},
        series: [{
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20, 5]
        }]
    });
    ```
6. 渲染图标
    - 调用 myChart 对象的 render 方法渲染图表:
    ```
    myChart.render()
    ```

31. Vue3中获取响应式数据为什么要.value

  • ref
    • 在Vue3中,响应式数据通过’ref’和’reactive’函数进行创建;使用ref函数创建的响应式数据,其值被包装在一个对象中,并且通过 .value来访问其值;
import { ref } from 'vue';

const count = ref(0); // 创建一个响应式的计数器

console.log(count.value); // 输出 0
count.value++; // 修改计数器的值
console.log(count.value); // 输出 1
- 在代码中,通过 ref(0) 创建了一个初始值为 0 的响应式数据 count,可以通过 count.value 来访问其值。在修改计数器的值时,也需要使用 count.value 来修改其值。
  • reactive
    • 使用reactive函数创建的响应式数据,则不需要使用.value来访问其值
import { reactive } from 'vue';

const state = reactive({ count: 0 }); // 创建一个响应式的状态对象

console.log(state.count); // 输出 0
state.count++; //    修改状态对象的值
console.log(state.count); // 输出 1

32. 父组件怎么拿到子组件的方法

  • 可以通过在子组件中使用ref来获取对子组件实例的引用,并将其传递给父组件
  • 示例
this.childRef.current.myMethod()

33. 跨域问题

  • 概念; “跨域问题”是浏览器的同源策略(协议、域名、端口号)所引起的,为了保障网页的安全,浏览器限制了不同源之间的相互访问。
  • 解决跨域问题的方法
  1. JSONP;
JSONP是一种利用`<script>`标签的跨域方式。通过在页面上动态添加一个`<script>`标签,并指定`src`属性为要获取数据的URL,服务器返回的数据需要以函数调用的形式返回,例如:(服务器返回的数据要以函数调用的形式返回)

function processData(data) {
  console.log(data);
}

const script = document.createElement('script');
script.src = 'http://example.com/data?callback=processData';
document.body.appendChild(script);
  1. CORS
  • CORS是一种通过设置HTTP响应头来解决跨域问题的方法。如果服务器支持CORS,可以在响应头中添加Access-Control-Allow-Origin字段来指定允许访问的源。例如:
Access-Control-Allow-Origin: http://example.com
  1. 代理服务器
**使用代理服务器是一种比较常用的跨域解决方案,可以在同一域名下设置一个代理服务器**,将需要访问的跨域资源请求转发到该代理服务器,由代理服务器向目标服务器请求数据,然后将响应返回给浏览器。例如:
浏览器 -> 代理服务器 -> 目标服务器
  1. WebSocket
使用WebSocket进行跨域通信可以避免跨域问题,因为WebSocket是一种全双工通信协议,它不受同源策略限制。例如:
const socket = new WebSocket('ws://example.com');
// 使用socket来进行监听操作; 
socket.addEventListener('message', function(event) {
  console.log(event.data);
});

34. Pinia

  1. 响应式系统;Pinia 是为 Vue 3 设计的,它利用了 Vue 3 的 Composition API 和 Proxy 响应式系统。
  2. 类型安全;Pinia 提供了更好的类型安全支持,因为它是使用 TypeScript 编写的,并且能够与 TypeScript 的类型系统良好集成。
  3. 模块化; Pinia 引入了 “Store” 概念,每个 Store 都是独立的,可以更容易地组织和管理状态。这使得代码更加模块化和可维护。
  4. 生态系统更加适合Vue3

35. $route和$router的区别

  1. $route是路由信息对象, 包括path、params、hash、query、name等路由信息参数
  2. $router是路由实例对象, 包括了路由的跳转方法, 钩子函数等;

36. 开发中常用的几种Content-type

  • application/x-www-form-urlencoded;
浏览器原生的form表单,如果不设置enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据
这种方式提交的数据放在body里面,数据按照
key1 = val1 & key2 = val2的方式进行编码
key和val都进行了URL转码
  • multipart/form-data; 常见的POST提交方式, 通常表单上传文件时使用这种方式
  • application/json; 告诉服务器消息主体是序列化后的JSON字符串
  • text/xml; 主要用来提交XML格式的数据;

37. delete和Vue.

Vue3

1. Vue3中watch和watchEffect有什么区别吗

区别

  • API类型
    • watch是一个函数, 它接受要监视的数据和一个回调函数, 并且可以选择传递一些选项
    • watchEffect是一个函数,接受一个回调函数作为唯一参数
  • 触发时机
    • watch是基于依赖的, 它只会在监视的数据发生变化时才触发回调函数(具有惰性lazy, 第一次页面展示的时候不会执行)
      • (可以设置immediate:true时可以变为非惰性, 页面首次加载就会执行)
    • watchEffect在每次组件渲染时都会立即执行回调函数, 并自动追踪依赖
      • (立即执行,没有惰性, 页面的首次加载就会执行)
  • 用途
    • watch更适合那些需要在特定数据变化时执行代码的情况, 例如验证输入字段
    • watchEffect更适合那些需要在组件渲染时执行的代码, 例如自动更新UI

2. Vue3的性能提升主要通过哪几个方面来体现的

  • 编译阶段 + 源码体积(Tree shanking) + 响应式系统
    • Vue2: 在Vue2中,每个组件实例都对应一个watcher实例, 它会在组件渲染的过程中把用到的数据properety记录为依赖, 当依赖发生变化,触发setter, 则会通知watcher, 从而使得关联的组件重新渲染.
      在这里插入图片描述
    • 如上图所示, 但是组件内部的结构可能只有一个动态节点, 剩余一堆都是静态节点, 所以这一块很多的diff和遍历操作其实都是不需要的, 会造成很严重的性能浪费
<template>
    <div id="content">
        <p class="text">静态文本</p>
        <p class="text">静态文本</p>
        <p class="text">{{ message }}</p>
        <p class="text">静态文本</p>
        ...
        <p class="text">静态文本</p>
    </div>
</template>

// 静态枚举类型
export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}
  • Vue3在编译阶段进行了优化
    • diff算法优化
    • 静态提升
    • 事件监听缓存
    • SSR优化

静态算法优化

  • Vue3在diff算法中相比Vue2增加了静态标记(其会在发生变化的地方添加一个flag标记, 下次发生变化的时候直接找该地方进行比较)
    在这里插入图片描述
    静态提升
  • Vue3对不参与更新的元素, 会做静态提升, 只会被创建一次, 在渲染时直接复用
  • 这样就免去了重复的创建节点, 大型应用会受益于这个改动, 免去了重复的创建操作, 优化了内存

事件监听绑定

  • 默认情况下, 绑定事件行为会被视为动态绑定, 所以每次都会追踪它的变化
  • 但是开启事件监听器缓存后, 没有了静态标记, 也就是说下次diff算法的时候可以直接使用;

SSR优化

  • 当静态内容大到一定量级的时候, 会用createStaticVnode方法在客户端去生成一个static node, 这些静态node会被直接innerHtml, 这样一来就不需要创建对象了, 直接渲染;

3. Vue3中为什么要用Proxy替代defineProperty API

defineProperty API的缺点

  1. 检测不到对象属性的添加和删除
  2. 数组API方法无法监听到
  3. 需要对每个属性都进行遍历监听, 如果嵌套对象,则需要深度监听, 造成性能问题.

Proxy

  • Proxy的监听是针对一个对象的, 这个对象的所有操作会进入监听操作, 这就完全可以代理所有属性了
  • 而且Proxy可以直接监听到数组的变化
  • Proxy有多达13种拦截方法, 正是因为defineProperty的缺陷,导致Vue2在实现响应式过程中需要实现其他方法的辅助

4. Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?

Vue2

  • 代码可读性随着组件变大而变差
  • 每一种代码复用的方式, 都存在缺点
  • TypeScript支持有限

Vue3中的Composition Api 是根据逻辑功能来组织的

  • 其将一个功能所定义的所有API会放在一起, (更加的高内聚、低耦合); 即使项目很大, 功能很多, 我们都能快速的定位到这个功能的所有API
  • 将某个逻辑关注点相关的代码全都放在一个函数里, 这样当修改一个功能时, 就不再需要在文件里跳来跳去

逻辑复用

  • 在Vue2中, 我们是通过mixin来复用相同的逻辑
    • 命名冲突
    • 数据来源不清晰
  • 使用Vue3中, 可以编写多个hooks,也不会出现命名冲突的问题, 而且整个数据来源更加清晰了.

总结

  1. 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  2. 因为Composition API几乎是函数,会有更好的类型推断。
  3. Composition API对 tree-shaking 友好,代码也更容易压缩
  4. Composition API中见不到this的使用,减少了this指向不明的情况
  5. 如果是小型组件,可以继续使用Options API,也是十分友好的

5. setup()函数

  • 在Vue 3中,使用setup()函数代替了Vue 2中的created和beforeCreate等声明周期钩子函数。setup()函数接收两个参数,props和context,用于初始化组件。通过setup()函数可以直接访问和操作传递给组件的props,实现了更便捷的props访问。
  • 这两个参数用于在函数式组件中访问组件的属性和上下文信息。
    • props参数; props 是一个包含了组件接收的属性值的对象。它允许您从父组件传递数据给子组件。这些属性可以是静态的或动态的,具体取决于父组件如何传递它们。
    • context 参数: context 是一个包含了一些有用的上下文信息的对象。这包括一些实用函数和属性,如 context.attrs 和 context.slots。您可以使用这些上下文信息来访问组件的属性、插槽内容以及一些其他信息。
    • context 对象还包括了 context.emit 方法,用于触发自定义事件,以便与父组件进行通信。

6. defineProps() 函数:

  • 用于定义组件的props。在Vue 3中,可以使用defineProps()函数来明确声明组件的props,取代Vue 2中的props选项。这样可以更直观地定义和描述组件需要接收的props,包括类型、默认值等。

7. defineEmits() 函数:

  • 用于定义组件可触发的事件。在Vue 3中,可以使用defineEmits()函数明确声明组件可以触发的事件,并指定事件名称和参数。这样可以增加代码的可读性和组件的可维护性。

7. defineExpose() 函数

  • defineExpose 是 Vue 3 Composition API 中的一个函数,它用于将一些在组件内部的属性或方法暴露给组件的父组件或组件的使用者。它的主要作用是定义一个清晰的 API 接口,以便其他组件可以访问和使用组件内部的一些信息或功能。

8. inject() 和 provide() 函数:

  • 用于跨级组件之间的数据传递。Vue 3引入了inject()和provide()函数,允许在父组件中使用provide()提供数据,然后在子孙组件中使用inject()来注入和访问这些数据。这种方式可以避免一层层通过props传递数据,使得组件间的数据传递更加便捷和灵活。

9. ref和reactive的区别

在Vue 3中,refreactive 都用于创建响应式数据,但它们有一些重要的区别,主要是关于用法和用例:

  1. ref
    • ref 是用于包装基本数据类型(如数字、字符串、布尔值等)的响应式对象。它将基本数据类型包装在一个对象中,使其成为响应式。
    • ref 创建的对象具有 .value 属性,您需要通过 .value 来访问或修改包装的值。
    • ref 适用于需要包装单个值的情况,通常用于基本数据类型的响应式变量。
  2. reactive
    • reactive 用于创建包含多个属性的响应式对象。它接受一个普通对象作为参数,并将整个对象转换为响应式。
    • reactive 创建的对象的属性进行访问和修改时,不需要额外的 .value,而是可以直接访问属性。
    • reactive 适用于需要管理多个属性的情况,通常用于复杂的对象或组件状态。
      总的来说,ref 用于包装单个基本数据类型,而 reactive 用于创建包含多个属性的响应式对象。选择使用哪种方式取决于您的具体需求和数据结构。通常,简单的变量可以使用 ref,而包含多个属性的数据结构可以使用 reactive

React

react和vue的diff算法的区别

React和Vue是两个流行的前端JavaScript框架,它们都使用了虚拟DOM和差异算法(Diff算法)来高效地更新用户界面,但它们的具体实现有一些区别:

  1. React的Diff算法
    • React使用的是经典的虚拟DOM和差异算法。当组件的状态发生变化时,React会生成新的虚拟DOM树,然后与之前的虚拟DOM树进行比较,找出它们之间的差异。
    • React的Diff算法可以分为两个阶段:
      • Reconciliation(协调)阶段:在这个阶段,React会比较新旧虚拟DOM树的结构,以确定哪些部分需要更新,哪些部分需要重新渲染。React会尽量保持原有组件的状态以最小化DOM的变更。
      • Commit(提交)阶段:在这个阶段,React会应用变更到实际的DOM树上,更新用户界面。
  2. Vue的Diff算法
    • Vue使用的是一种基于双向绑定和依赖追踪的响应式系统。当组件的数据发生变化时,Vue会触发依赖追踪,确定需要更新的部分,然后更新视图。
    • Vue的Diff算法与React的差异比较算法不同,它不需要生成虚拟DOM树,而是通过直接追踪依赖来更新DOM。
      总的来说,两者的差异算法实现方式不同,但目标是相同的,即在组件状态变化时,高效地更新用户界面,以提供更快的性能

React Hooks是什么

React Hooks是React 16.8版本引入的一项重要特性,它允许你在无需编写类组件的情况下,在函数式组件中使用React的状态和生命周期特性。Hooks的引入改变了React组件的编写方式,使代码更加简洁、可维护和易于理解。React提供了一些内置的Hooks,以便开发人员更轻松地管理组件状态和副作用。

以下是一些常用的React内置Hooks:

  1. useStateuseState Hook 允许函数式组件保存和更新本地状态(state)。
const [count, setCount] = useState(0);
  1. useEffectuseEffect Hook 用于处理副作用,如数据获取、订阅、DOM操作等。
useEffect(() => {
  // 在组件渲染后执行副作用代码
}, [dependencies]);
  1. useContextuseContext Hook 允许组件访问React上下文(context)中的值。
const value = useContext(MyContext);
  1. useReduceruseReducer Hook 可以用于管理复杂的本地状态逻辑,它是替代useState的一种方式。
const [state, dispatch] = useReducer(reducer, initialArg, init);
  1. useRefuseRef Hook 创建可变的引用对象,通常用于访问DOM元素或保存组件之间的数据。
const myRef = useRef(initialValue);
  1. 自定义Hooks:开发人员可以编写自定义Hooks,将可复用的逻辑封装到函数中,以便在多个组件中共享。
function useCustomHook() {
  // 自定义逻辑
  return someValue;
}

React Hooks的引入使得函数式组件能够拥有与类组件相似的功能,如状态管理和副作用处理,同时保持了函数组件的简洁性和可读性。它还有助于减少组件之间的耦合,更容易进行单元测试。由于React持续发展,Hooks也不断完善,成为了React生态系统的一部分。

数据库

1. 什么是事务?事务的四大特性是什么

事务

  • 事务是一组数据库操作(读取、写入)的执行单元,它被视为一个独立的、不可分割的工作单元。事务的目的是确保数据库的一致性和完整性。事务具有以下四大特性
    • 原子性;要求事务是不可分割的工作单元,要么全部执行成功,要么全部失败
    • 一致性; 一致性要求事务在执行前和执行后都必须保持数据库的一致性。这意味着事务在执行后,数据库从一个一致的状态转移到另一个一致的状态。
    • 隔离性;隔离性要求事务在执行过程中应该被隔离,即一个事务的执行不应该对其他事务产生影响。
    • 持久性;持久性要求一旦事务提交成功,其结果应该在数据库中永久保存,即使系统崩溃或重启。这确保了数据不会因系统故障而丢失。

操作系统

1. 什么是进程,什么是线程

  • 进程
    • 进程是计算机中正在执行的程序的一个实例。每个进程都有自己的内存空间,因此进程之间是相互独立的。
    • 进程可以包含多个线程,这些线程可以协同工作以完成进程的任务。
  • 线程
    • 线程是进程内的执行单元,它共享进程的内存空间和资源(多个线程可以在同一个进程中访问和修改相同的数据。)
    • 线程可以协同工作以完成进程的各种任务,每个线程可以独立执行不同的操作。
  • 关键区别
    • 进程之间是相互独立的,而线程是共享同一进程的资源
    • 由于进程之间的隔离性,如果一个进程崩溃,不会影响其他进程。但线程共享相同的内存,一个线程的错误可能会影响整个进程的稳定性。
      (美团到家问了)

2. 进程间通信方式

  1. 管道
    • 无名管道:通常用于父子进程之间的通信。它是一种单向通信方式,数据只能从一个进程流向另一个。
    • 有名管道(FIFO):允许不相关的进程通过文件系统进行通信。它允许多个进程在同一时刻读取和写入数据。
  2. 消息队列
  3. 共享内存
  4. 信号
  5. 套接字

3. 什么是死锁,造成死锁的条件,如何解决

  • 死锁
    • 指的是一组线程或进程彼此等待对方释放资源,从而导致所有线程或进程都无法继续执行的情况
    • 死锁通常发生在多个线程或进程试图同时获取多个互斥资源(某个时刻只能被一个线程、进程访问的资源)时。
  • 死锁的必要条件
    • 互斥; 至少有一个资源必须处于互斥状态,即同一时刻只能由一个线程或进程访问。
    • 占有和等待;线程或进程必须持有至少一个资源,并且在等待获取其他线程或进程持有的资源时不释放已经持有的资源。
    • 不可剥夺;资源不能被线程或进程强行剥夺,只能在持有资源的线程或进程主动释放后才能让其他线程或进程获取。
    • 循环等待;存在一个等待资源的环路,即线程或进程A等待线程或进程B持有的资源,B等待C持有的资源,C等待A持有的资源。
  • 解决死锁问题
    • 避免死锁;(银行家算法、资源分配图)
    • 检测和恢复:实现死锁检测机制,定期检查系统是否处于死锁状态。如果检测到死锁,可以采取措施来中断一个或多个线程,以解除死锁。
    • 避免资源独占
    • 资源剥夺; 必要时剥夺某些线程或进程的资源,以打破死锁。
      (美团到家问了)

4. 虚拟内存

  • 虚拟内存
    • 操作系统中的一种内存管理技术,它允许程序访问似乎比物理内存更大的内存空间
    • 虚拟内存通过将部分数据和代码存储到磁盘上,而不是全部加载到物理内存中,来提供这种扩展内存的效果
    • 计算机可以在物理内存不足时继续运行程序,尽管需要从磁盘上频繁地读取和写入数据。
  • 虚拟内存的主要目的
    • 扩展内存容量:虚拟内存允许程序访问比物理内存更大的内存空间,这对于运行大型应用程序和处理大型数据集非常有用。程序可以认为它们具有足够大的内存,而无需担心物理内存的限制。
    • 内存隔离:虚拟内存将每个进程的内存空间隔离开来,使得一个进程无法直接访问其他进程的内存。这提高了系统的稳定性和安全性,因为一个进程的错误不会影响其他进程的内存。
    • 内存管理:虚拟内存允许操作系统动态地将数据和代码从磁盘加载到物理内存中,以及在需要时将不再使用的数据写回磁盘。这样可以优化内存的使用,确保最常用的数据位于物理内存中,同时将不常用的数据保存在磁盘上,以节省物理内存。
    • 内存保护:虚拟内存可以通过设置页面权限(例如只读、读写、执行等)来保护内存区域,以防止非法访问和恶意软件攻击。
  • 实现
    • 虚拟内存的实现通常使用分页(Page)或分段(Segmentation)的技术,操作系统会将程序的内存空间划分为小块(页面或段),并将这些小块映射到物理内存或磁盘上。当程序访问一个尚未加载到物理内存的页面时,操作系统会将其从磁盘加载到内存中。如果物理内存不足,操作系统会根据页面替换策略选择哪些页面写回磁盘,以便为新的页面腾出空间。
      (美团到家问了)

5. 垃圾回收有几种机制

垃圾回收(Garbage Collection)是一种自动管理计算机内存的机制,它有多种不同的实现和算法。以下是一些常见的垃圾回收机制:

  1. 引用计数(Reference Counting):这是一种最简单的垃圾回收机制。它通过跟踪每个对象的引用数来确定何时释放不再被引用的对象。当引用数降为零时,对象会被回收。然而,引用计数不能解决循环引用问题。
  2. 标记-清除(Mark and Sweep):标记-清除是一种更复杂的垃圾回收机制,它通过两个阶段来执行。首先,在标记阶段,它标记所有可达对象。然后,在清除阶段,它清除不可达对象。这解决了引用计数的循环引用问题。
  3. 分代垃圾回收(Generational Garbage Collection):这种机制将内存分为几个代,通常是新生代和老年代。新创建的对象首先分配到新生代,如果它们存活下来,将被晋升到老年代。因为大部分对象很快就变成垃圾,所以针对新生代的垃圾回收会更频繁,而老年代的垃圾回收则更稀少。
  4. 引用计数加标记(Reference Counting with Mark-Sweep):这种机制是引用计数和标记-清除的结合,通过引用计数来快速识别不再被引用的对象,然后使用标记-清除来处理循环引用和其他问题。
  5. 复制式垃圾回收(Copying Garbage Collection):在这种机制中,内存被划分为两块,一块用于分配新对象,另一块用于垃圾回收。当新对象被分配时,它们被放入一个块中,当块满时,所有可达对象将被复制到另一个块中,然后原块被清空。这种方式适用于短暂的对象生命周期。
  6. 引用计数加追踪(Reference Counting with Tracing):这是一种结合了引用计数和标记-清除的机制。引用计数用于快速识别不再被引用的对象,标记-清除用于处理循环引用和复杂的情况。
    不同的编程语言和运行环境可能采用不同的垃圾回收机制,选择机制的依据通常是语言特性、性能需求和内存管理的复杂性。每种机制都有其优点和缺点,以满足不同场景的需求。

计算机网络

1. http状态码

HTTP;

  • 用于在Web浏览器和服务器之间传递内容的协议;
  • 在HTTP通信过程中,服务器会通过HTTP状态码来向客户端传达请求的处理结果;
    • 每个状态码都传达了特定的意义,以便客户端和服务器之间更好地沟通

常见的HTTP状态码

  1. 1xx Informational(服务器收到请求状态码)

    • 100 Continue: 表示服务器已经收到请求的初始部分,客户端应该继续发送其余的请求。
  2. 2xx Success(成功状态码)

    • 200 OK: 请求已成功,服务器正常处理并返回请求的内容。
  3. 3xx Redirection(重定向状态码)

    • 301 Moved Permanently: 请求的资源已永久移动到新的URL。(永久重定向)
    • 302临时重新定向;
  4. 4xx Client Errors(客户端错误状态码)

    • 404 Not Found: 请求的资源不存在。
  5. 5xx Server Errors(服务器错误状态码)

    • 500 Internal Server Error: 服务器内部错误,无法完成请求。

HTTP状态码

  • HTTP状态码是服务器向客户端返回的一个三位数的状态标识,用以表示客户端发起的请求的处理结果。状态码以数字形式给出,并且每个状态码都有特定的含义。
  • HTTP状态码中,403 Forbidden 表示服务器理解客户端的请求,但拒绝执行该请求。这通常发生在服务器理解了请求,但认为客户端没有权限访问请求的资源。主要原因可能包括:
  • 权限问题: 用户没有足够的权限访问资源。这可能是因为缺乏认证、身份验证失败或者是访问权限限制。
  • IP限制: 服务器基于IP地址对访问进行了限制,禁止特定IP访问。
  • 访问限制: 服务器可能因为其他原因拒绝了访问,比如恶意请求、非法访问尝试等。

当服务器返回403 Forbidden 状态码时,通常会伴随一些额外的信息,用以说明拒绝访问的原因。对于开发者和用户来说,了解这些信息能够更好地理解为什么服务器拒绝了请求,以及可能的解决方案。
在开发过程中,处理403状态码通常需要检查请求的权限、认证信息,或者与服务器管理员联系以获取更多关于禁止访问的资源信息。

2. socket是什么?websocket呢?和http发送请求有什么区别吗?

Socket:

  • Socket是一种在计算机网络中实现进程间通信的机制,它提供了一种流式的、双向的通信方式。在网络编程中,Socket被用于建立客户端和服务器之间的通信连接。Socket允许数据在这两个端之间进行双向传输,实现实时的数据交换。Socket编程通常包括服务器端和客户端,服务器端监听特定的端口,而客户端则尝试连接到服务器的地址和端口。

WebSocket:

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议,它允许服务器主动向客户端推送数据,而不需要客户端明确地请求。WebSocket通常用于实现实时性要求较高的应用,例如在线聊天、实时游戏等。WebSocket的优势在于它相对于传统的HTTP请求来说,减少了通信的延迟,同时减轻了服务器和网络的负担。

区别和与HTTP请求的比较:

  1. 持久连接:
    • HTTP是基于请求-响应的协议,每次请求都需要建立新的连接。相比之下,WebSocket在建立连接后保持打开状态,使得在单个连接上可以进行双向通信,减少了连接建立的开销。
  2. 实时性:
    • WebSocket通常用于需要实时性的应用,因为它允许服务器主动向客户端推送数据。HTTP则是被动的,只有在客户端发起请求时,服务器才会响应。
  3. 头部开销:
    • HTTP协议的每个请求和响应都包含一定量的头部信息,这会增加通信的开销。WebSocket的头部相对较小,因为它的主要目的是在建立连接后提供轻量级的通信。
  4. 应用场景:
    • HTTP通常用于传统的网页浏览和数据请求,而WebSocket更适用于实时性要求较高的应用,如在线聊天、实时通知、实时游戏等。
      总体而言,WebSocket和HTTP都有各自适用的场景,选择哪种取决于应用的需求。WebSocket适用于需要实时性和双向通信的场景,而HTTP则更适用于请求-响应模式的场景。

HTTP协议本身是基于请求-响应模型的,这意味着通信是单向的,客户端发送请求,服务器端返回响应。每个HTTP事务都是由客户端发起的请求和服务器端的响应组成。

3. 解释下三次握手和四次挥手过程

TCP(传输控制协议)是计算机网络中常用的一种协议,

  • 三次握手(建立连接)
    • 客户端向服务器发送连接请求(SYN): 客户端首先发送一个特殊的TCP包,称为SYN(同步)包,其中包含一个随机生成的序列号(SYN序列号)。这个包告诉服务器客户端希望建立连接。
    • 服务器确认连接请求(SYN-ACK): 服务器接收到客户端的SYN包后,会回复一个包含自己的SYN序列号的包,同时也会确认客户端的SYN包。这个包被称为SYN-ACK包。
    • 客户端确认连接(ACK):最后,客户端收到服务器的SYN-ACK包后,会发送一个带有服务器SYN序列号的ACK(确认)包,表示连接已经建立。此时,双方都认为连接已经成功建立,可以开始进行数据传输。
  • 四次挥手(终止连接)
    • 客户端发起关闭连接(FIN): 当客户端希望关闭连接时,它会发送一个FIN包,表示不再发送数据,但仍然愿意接收数据。
    • 服务器确认关闭请求(ACK): 服务器接收到客户端的FIN包后,会发送一个ACK包,确认收到关闭请求。
    • 服务器关闭连接(FIN): 一段时间后,服务器可能也会希望关闭连接,于是发送一个FIN包给客户端,表示服务器不再发送数据。
    • 客户端确认关闭请求(ACK): 客户端接收到服务器的FIN包后,发送一个ACK包,确认收到关闭请求。此时连接被完全终止。

注意

  • 四次挥手是因为TCP是全双工(通信两端可以同时发送和接收数据)的协议,双方都可以独立地关闭自己的发送通道。因此,每个端点都需要发送一个FIN来表示它不再发送数据。最后的ACK是用来确认对方的FIN包的接收。
  • 这个四次挥手过程确保了数据的可靠传输和连接的优雅关闭,同时避免了数据丢失和不完整传输。这些步骤也是TCP连接管理的关键部分。

4. HTTP的依次改进

HTTP/2.0 相对于 HTTP/1.1 的改进:

  • 二进制协议:HTTP/2 使用了二进制协议,将数据以二进制形式传输,而不是文本形式,这降低了通信的开销,提高了效率。
  • 多路复用: HTTP/2 支持多路复用,多个请求和响应可以在同一个连接上同时传输,减少了串行请求的问题。
  • 头部压缩: HTTP/2 使用 HPACK 压缩算法来压缩请求和响应的头部信息,减小了头部的大小,从而减少了带宽占用。
  • 流量控制和优先级: HTTP/2 提供了更好的流量控制和请求优先级管理机制,允许客户端和服务器更精细地控制数据流。

HTTP/3.0 相对于 HTTP/2.0 的改进:

  • 传输协议; HTTP/3 使用QUIC作为底层传输协议,取代了HTTP/2中的TCP。QUIC具有更低的连接建立时延迟和更好的拥塞控制。
  • 保留了多路复用和头部压缩; 但是在QUIC的基础上进一步提高了性能
  • 减少连接数,进一步减少延迟和提高效率
  • 更好的可靠性,考虑了更好的网络可靠性,适用于高延迟、高丢包率的网络环境。

5. tcp和udp的区别?

区别

  • 连接性
    • Tcp是面向连接的协议; 在建立通信前, 它需要在两端建立一个连接, 然后才能进行数据传输.
    • Udp是无连接协议; 它不需要在发送数据前建立连接, 因此更加轻量级.
  • 可靠性
    • Tcp提供可靠性传输, 它使用确认和重传机制来确保数据的可靠交付, 即使在网络发生故障或数据包丢失时也能恢复;
    • Udp不提供可靠性传输, 它没有确认或重传机制, 因此发送的数据包可能会在传输过程中丢失或乱序, 不提供数据完整性保障
  • 数据顺序
    • TCP保证数据按照发送顺序到达接收端, 因为它使用了序号和确认来维护数据包的顺序
    • UDP不保证数据包的顺序, 因此接收端可能会按照不同的顺序接收到数据包
  • 流量控制
    • TCP具有流量控制机制,它使用滑动窗口机制来控制数据的发送速率
    • UDP没有内置的流量控制机制, 因此可能导致网络拥塞或丢失数据包
  • 适用场景
    • TCP适用于需要可靠性和顺序传输的应用, 比如: 网页浏览、电子邮件、文件传输等
    • UDP适用于对传输速度要求较高, 可以容忍一些数据丢失的应用, 如: 实时音频和视频通话等

数据结构

1. 什么是哈希表?(HashTable)

  • 哈希表(散列表):用于存储键值对,并支持高效的数据检索操作(一种高效的数据结构)
  • 哈希表通过将键映射到一个固定大小的数组来实现快速的数据查找,它基于哈希函数将键转换成数组的索引,然后将值存储在该索引位置

2. 什么是堆?

在计算机科学中,“堆”(Heap)可以指不同的概念,具体取决于上下文。以下是两种主要的堆结构概念:

  1. 内存堆(Memory Heap):
    • 内存堆是计算机内存的一部分,用于动态分配内存空间。在程序运行时,如果需要存储变量或对象,但在编写代码时无法确定其大小,就可以通过内存堆来进行动态分配。
    • 动态分配的内存通常在堆上分配,并在程序员明确释放之前一直存在。这种分配和释放的管理由程序员负责,因此需要小心防止内存泄漏。
  2. 优先队列堆(Priority Queue Heap):
    • 优先队列堆是一种数据结构,通常用于实现优先队列。在这个堆结构中,每个元素都有一个关联的优先级,具有最高优先级的元素将始终位于堆的顶部。
    • 这种堆通常是一个二叉堆或其它变种,具有高效的插入和删除最大/最小元素的操作。堆排序算法也是建立在这种堆结构的基础上。
      总体而言,在计算机科学中,堆结构通常是指内存堆或优先队列堆。在具体的上下文中,要根据问题的要求来确定是哪种类型的堆结构。

Git

1. 和分支相关的Git命令有哪些?

Git 中与分支相关的命令有很多,以下是一些常用的分支相关命令:

  1. 创建分支:
    • git branch <branch_name>: 创建一个新的分支。
    • git checkout -b <branch_name>: 创建一个新的分支并立即切换到该分支。
    • git switch -c <branch_name>: 创建一个新的分支并立即切换到该分支(Git 2.23+ 版本支持)。
  2. 查看分支:
    • git branch: 列出所有本地分支,当前分支前会有一个星号标记。
    • git branch -r: 列出所有远程分支。
    • git branch -a: 列出所有本地和远程分支。
  3. 切换分支:
    • git checkout <branch_name>: 切换到指定分支。
    • git switch <branch_name>: 切换到指定分支(Git 2.23+ 版本支持)。
  4. 创建并切换分支:
    • git checkout -b <branch_name>: 创建并切换到新的分支。
    • git switch -c <branch_name>: 创建并切换到新的分支(Git 2.23+ 版本支持)。
  5. 删除分支:
    • git branch -d <branch_name>: 删除指定分支(只能删除已经合并到当前分支的分支)。
    • git branch -D <branch_name>: 强制删除指定分支,不管它是否被合并。
  6. 合并分支:
    • git merge <branch_name>: 将指定分支合并到当前分支。
  7. 查看合并基础:
    • git merge-base <branch1> <branch2>: 查找两个分支的最近的共同祖先提交,通常在解决合并冲突时会用到。
  8. 查看分支历史:
    • git log --graph --oneline --all: 以图形化的方式查看所有分支的提交历史。
  9. 远程分支:
    • git push origin <branch_name>: 推送本地分支到远程仓库。
    • git pull origin <branch_name>: 拉取远程分支到本地。
    • git branch -dr <remote>/<branch_name>: 删除本地已经不存在于远程仓库的远程分支引用。

2. 项目想要上线去要用到哪些Git命令

当你准备将项目上线时,通常会使用一系列 Git 命令来确保代码的正确性、版本管理的一致性,并将最终的代码部署到生产环境。以下是上线前和上线时可能会用到的一些 Git 命令:

  1. 检查代码状态:
    • git status: 查看工作区和暂存区的状态,确保没有未提交的更改。
  2. 拉取最新代码:
    • git pull origin <branch_name>: 拉取远程仓库的最新代码。
  3. 解决冲突:
    • 如果在拉取最新代码时发生冲突,需要解决冲突并提交修改。
  4. 切换到发布分支:
    • git checkout <release_branch>: 切换到用于发布的分支。
  5. 合并开发分支:
    • git merge <development_branch>: 将开发分支合并到发布分支。
  6. 版本标签(可选):
    • git tag -a v1.0 -m "Release version 1.0": 创建一个版本标签,以便将来可以轻松地回到这个特定的发布版本。
  7. 构建项目:
    • 运行构建脚本或者编译项目,确保生成的文件和依赖项是最新的。
  8. 测试代码:
    • 在本地或者使用持续集成工具进行测试,确保代码在生产环境中运行正常。
  9. 提交更改:
    • git add .git commit -m "Prepare for release": 将构建后的文件和任何必要的更改提交到发布分支。
  10. 推送到远程仓库:
    • git push origin <release_branch>: 将发布分支推送到远程仓库。
  11. 部署到生产环境:
    • 将构建后的文件和相关配置文件部署到生产服务器。
  12. 监控和回滚(可选):
    • 监控生产环境,确保一切正常。如果发现问题,可以回滚到之前的版本。
    • git revert <commit_hash>: 创建一个新的提交,撤销指定的提交。
  13. 版本文档更新(可选):
    • 更新版本文档,记录发布的版本号、变更内容等信息。
  14. 清理工作区(可选):
    • git clean -dfX: 清理工作区中未被版本控制的文件和目录,确保只有必要的文件在工作目录中。
  15. 通知团队:
    • 通知团队关于新版本的发布,提供必要的文档和信息。
      这只是一个一般性的上线流程,具体的步骤可能会因项目结构、团队流程等因素而有所不同。在上线前,建议仔细检查所有的配置文件、依赖项和数据库迁移等内容,以确保上线过程的顺利进行。

Webpack

1. webpack中的Loader和Plugin的区别是什么?

  • Loader主要用于处理模块的加载和转换,而Plugin用于执行各种构建过程的任务。
    • Loader常用于处理各种资源文件,例如将ES6代码转换为ES5代码,将Sass/LESS转换为CSS
    • Plugin用于执行各种任务,如代码拆分、压缩、生成HTML文件等、

2. 对webpack的理解,loader和plugin的区别

  • Webpack
    • 一个用于构建现代Web应用程序的开源JavaScript模块打包工具。主要作用:将多个模块(包括JavaScript、CSS、图片等)打包成一个或多个静态资源文件,以便在浏览器中加载和运行。
  • loader和plugin的区别
    • Loader主要用于处理模块的加载和转换,而Plugin用于执行各种构建过程的任务
      • Loader常用于处理各种资源文件,例如将ES6代码转换为ES5代码,将Sass/LESS转换为CSS
      • Plugin用于执行各种任务,如代码拆分、压缩、生成HTML文件等

3. webpack

Webpack是一款现代的JavaScript模块打包工具,它的主要功能是将项目中的各种模块(JavaScript、CSS、图片等)打包成一个或多个捆绑文件,以便在浏览器中加载。Webpack的操作流程包括以下关键步骤:

  1. 入口点(Entry Point): 首先,Webpack需要确定应用程序的入口点。入口点是Webpack开始构建过程的地方,通常是一个JavaScript文件。Webpack会从这个入口文件开始,然后分析依赖关系,递归地查找和处理其他模块。
  2. 模块解析(Module Resolution): Webpack会分析入口文件及其依赖的模块,然后根据配置文件中的规则来解析模块的路径。这包括处理不同文件类型(如JavaScript、CSS、图片等)以及查找模块所在的位置。
  3. 加载器(Loaders): 当Webpack识别到不同类型的模块时,它会使用加载器来转换这些模块。加载器是一些特定的转换器,可以将不同格式的文件转换为JavaScript模块。例如,通过Babel加载器,你可以将ES6代码转换为ES5。
  4. 插件(Plugins): Webpack允许使用插件来执行各种构建任务。插件是自定义操作的工具,可以用于压缩、优化、提取公共代码、生成HTML文件等。插件通过配置文件中的配置来使用。
  5. 生成输出(Output Generation): 一旦Webpack处理完所有的模块,加载器和插件,它将生成一个或多个输出文件。这些输出文件包括捆绑后的JavaScript文件、CSS文件、图像文件等。
  6. 模块拆分(Module Splitting): Webpack还支持将代码拆分成多个捆绑文件,以提高应用程序的性能和加载速度。这通常涉及到代码拆分、按需加载(懒加载)和共享代码。
  7. 缓存和版本控制: Webpack会为生成的文件添加哈希值或版本号,以确保浏览器能够正确缓存文件。这有助于避免浏览器缓存旧文件的问题。
  8. 开发服务器: 在开发环境中,Webpack通常与开发服务器一起使用,以便在文件更改时实时重新构建并自动刷新浏览器,提供更好的开发体验。
  9. 代码优化和压缩: 在生产环境中,Webpack通常会进行代码优化和压缩,以减小文件大小,提高性能。这通常包括压缩JavaScript、CSS和图片文件。
  10. 输出部署: 最后,生成的文件通常需要部署到Web服务器或CDN上,以供访问。
  • Webpack的操作流程涉及许多配置选项和插件,以适应不同的项目需求。配置文件是控制Webpack构建过程的关键,允许你自定义Webpack的行为和输出
    loader和plugin的区别
    • Loader主要用于处理模块的加载和转换,而Plugin用于执行各种构建过程的任务
      • Loader常用于处理各种资源文件,例如将ES6代码转换为ES5代码,将Sass/LESS转换为CSS
      • Plugin用于执行各种任务,如代码拆分、压缩、生成HTML文件等

4. 说下webpack是如何压缩代码的?

  1. 配置压缩插件; UglifyJS
  2. 使用代码分割
  3. 启用Tree Shaking(一种用于删除未引用代码的技术, 通常与Webpack一起使用)
  4. 执行Webpack构建、生成压缩后的文件
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  // ...其他配置
  optimization: {
    minimizer: [new UglifyJsPlugin()], 
    splitChunks: {
      chunks: 'all'
    }, 
    usedExports: true
  }
};

webpack有哪些配置选项

 configureWebpack: {
    output: {  // 输出文件;
      filename: `js/[name]${Timestamp}.js`,
      chunkFilename: `js/[name]${Timestamp}.js`
    },
    // 插件配置
    plugins: [
      new CompressionWebpackPlugin({
        algorithm: 'gzip',
        test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
        threshold: 10240,
        minRatio: 0.8
      })
    ], 
    // optimization是用于配制代码优化和分割的选项
    optimization: {
      minimizer: [new UglifyJsPlugin()], 
      splitChunks: {
        chunks: 'all'
      }, 
      usedExports: true
    }
  },

如果想要根据具体的文件来进行切割

  • 使用splitChunks来进行切割
optimization: {
  splitChunks: {
    chunks: 'async', // 只对异步加载的模块进行分割
    minSize: 20000, // 模块的最小大小才会被分割
    maxSize: 0, // 无最大限制
  },
},
  • 如果你想仅对特定的模块进行分割,可以使用函数来筛选这些模块
optimization: {
  splitChunks: {
    chunks: (chunk) => {
      // 仅对指定的模块进行分割
      return chunk.name === 'my-special-module';
    },
  },
},

5. Webpack, Vite的配置,有什么区别?

Webpack 和 Vite 都是前端构建工具,用于构建现代Web应用程序,但它们在设计和执行方式上存在一些重要区别:

  1. 打包方式:
    • Webpack: Webpack 是一种强大的静态模块打包工具,它通过配置文件定义构建过程,支持在开发和生产环境中使用。Webpack 会将所有项目文件打包成一个或多个捆绑文件。
    • Vite: Vite 是一种基于ES Modules的构建工具,它在开发中使用原生ES模块,不进行传统的打包,而是采用现代的构建方式,充分利用浏览器的特性,只在需要时编译和提供模块。
  2. 启动速度:
    • Webpack: Webpack 在启动时需要对整个项目进行完整的构建,这可能需要一些时间,尤其是在大型项目中。
    • Vite: Vite 在开发模式下具有极快的启动速度,因为它采用了按需编译,只编译你实际使用的代码。
  3. 热更新:
    • Webpack: Webpack 提供了热模块替换(HMR),但需要配置和插件的支持,有时候可能需要一些额外的设置。
    • Vite: Vite内置了热更新,而不需要额外的配置,这使得开发过程中的模块更新更加快速。
  4. ES模块支持:
    • Webpack: Webpack也支持ES Modules,但通常需要使用Babel等工具来处理模块,或使用CommonJS模块作为默认模块系统。
    • Vite: Vite天生支持ES Modules,这使得在开发中使用原生ES模块更加简单。
  5. 插件系统:
    • Webpack: Webpack有一个庞大的生态系统,支持各种插件和加载器,可以处理各种不同的资源和任务。
    • Vite: Vite相对较新,其插件生态系统不如Webpack丰富,但不断增长,而且它的配置更为简单。
  6. 生产构建:
    • Webpack: Webpack可用于生产构建,能够优化、压缩和拆分代码以提高性能。
    • Vite: Vite通常用于开发,但也可以用于生产构建,但构建输出通常更轻量,不同于Webpack的输出。

6. tree shanking的具体作用以及底层是如何实现的知道吗

  • Tree Shaking的主要作用是去除那些在项目中没有被引用的模块、变量、函数等,从而减小最终的构建文件大小。
  • 实现方式; 基于静态代码分析来实现的
    • 当你使用ES6模块化语法导入模块时,编译工具(如Webpack、Rollup等)可以在构建过程中分析模块之间的依赖关系,以确定哪些代码是被引用的,哪些代码是未引用的。然后,未引用的代码将被从最终的构建输出中删除。
      • 静态分析; 编译工具会构建一个依赖图, 记录模块之间的依赖关系;
      • 标记引用; 当某个模块被另一个模块引用时,编译工具会标记这个模块为被引用状态。
      • 标记非引用
      • 删除未引用代码

7. promise.race和promise.any的区别

Promise.racePromise.any 都是用于处理多个 Promise 对象的方法,它们有一些区别:

  1. 解决方式
    • Promise.race:它返回一个 Promise,只要传递给它的多个 Promise 中有一个变为 resolved(成功状态)或 rejected(失败状态),它就会立即返回并采用第一个完成的 Promise 的状态和值。
    • Promise.any:它也返回一个 Promise,但它会等待所有传递给它的 Promise 全部变为 rejected,然后才会返回一个 rejected Promise。只有当所有传递的 Promise 都失败时,它才会失败,否则它会采用第一个成功的 Promise 的状态和值。
  2. 应用场景
    • Promise.race:通常用于竞速解决方案,其中你希望得到最快完成的结果。如果你有多个异步操作,但只关心最快完成的结果,可以使用 Promise.race
    • Promise.any:通常用于一种情况,即你希望等待多个异步操作完成,只要有一个成功就可以,但不必等到所有操作都失败。这对于执行多个相似的操作,但只需要一个成功的情况很有用。
  3. 兼容性
    • Promise.race 是标准 ES6 Promise 方法,具有广泛的浏览器和Node.js支持。
    • Promise.any 是ES2021(ES12)的新增特性,可能在某些环境中不被完全支持。在使用它时,要确保你的运行环境支持该方法,或考虑使用polyfill或其他解决方案。
  4. 错误处理
    • Promise.race 不会提供有关失败的 Promise 的信息,因为它只采用第一个完成的 Promise 的状态。
    • Promise.any 会等待所有 Promise 完成,以确定是否所有 Promise 都失败。如果所有 Promise 都失败,Promise.any 会返回一个带有所有失败原因的数组。
      总之,Promise.race 用于追求最快完成的结果,而 Promise.any 用于等待多个异步操作的任何一个成功。选择其中一个取决于你的具体需求。

Company

得物

1. 后台系统的登录流程

  1. 用户输入信息;搜
  2. 前端验证;
  3. 发送请求; (前端将用户输入的用户名和密码通过HTTP请求(POST)发送到后台服务器)
  4. 后台验证;
  5. 生成身份标识;
  6. 令牌返回; (后台将生成的令牌作为响应返回给前端)
  7. 前端处理; (前端接收到令牌后,通常会将令牌存储在浏览器的本地存储(LocalStorage或SessionStorage)中,以便在用户继续操作时使用。)
  8. 权限控制; (前端会将令牌包含在请求的头部或请求参数中发送给后台。后台会解析令牌,根据其中的信息判断用户的权限,从而控制用户能够访问的功能和数据。)
  9. 会话保持和过期处理; (令牌通常会有一个过期时间,前端需要定期检查令牌的有效性,并在即将过期时重新获取新的令牌,以保持用户的登录状态。)

2. 如果用户清除浏览器缓存,删除了token,如何依然保持登录状态

  1. 使用持久化Cookie; 你可以在用户登录成功时,将Token存储为一个持久化 Cookie。持久化 Cookie 在浏览器关闭后仍然保留。
  2. 使用本地存储和记住我选项; 除了Token外,你可以在用户登录时,给用户提供一个“记住我”选项。如果用户勾选了这个选项,在登录成功后,将Token存储到本地存储(LocalStorage或SessionStorage)中,并设置一个较长的过期时间。
  3. 使用Refresh Token: Refresh Token 是一种与Access Token(用于实际请求的令牌)配套使用的令牌。它通常拥有更长的过期时间,当Access Token过期时,可以使用Refresh Token来获取新的Access Token。

3. 怎么定期刷新token

  1. 颁发Token和Refresh Token: 在用户登录成功时,除了颁发一个短期有效的Access Token,还颁发一个长期有效的Refresh Token。
  2. 存储Refresh Token: 将Refresh Token 存储在安全的方式中,可以是HttpOnly的Cookie、Secure HttpOnly的Cookie(适用于HTTPS连接)或其他安全的存储机制。
  3. 使用Access Token: 用户在每次请求需要权限的资源时,都会在请求的Header或参数中携带Access Token。
  4. 检查Token有效性: 服务端会验证Access Token的有效性和权限。如果Access Token还有效,服务端处理请求。如果Access Token过期,进入下一步。
  5. 使用Refresh Token获取新Token: 当Access Token过期时,前端会向服务端发送一个特殊的请求,该请求携带Refresh Token。服务端验证Refresh Token,如果合法,颁发一个新的Access Token。
  6. 返回新Token给前端: 服务端将新的Access Token发送给前端作为响应。
  7. 前端更新Token: 前端收到新的Access Token后,更新本地存储的Token。

4. echarts呈现多个图表的时候,怎么优化?

  1. 数据分析与合并; (避免重复计算和多次请求数据)
  2. 延迟加载; 如果页面有多个图表,不要一次性全部加载.可以根据用户滚动或切换页面等事件,延迟加载未显示的图表,减少初始加载时间。
  3. 图表分组; 将多个相关的图表分组,按需加载;
  4. 使用Resize事件; 当浏览器窗口大小发生变化时,图表的大小可能需要调整;
  5. 数据缓存; 如果图表数据不频繁变化,可以考虑将数据缓存起来,避免重复请求;
  6. 图表刷新: 不是每次数据更新都需要重新渲染整个图表。ECharts提供了setOption方法来更新图表配置,避免重复渲染和重新初始化。
  7. 图表样式复用: 如果多个图表有相似的样式和配置,可以创建一个基础配置对象,然后在每个图表中根据需要进行修改。
  8. 分时加载; 如果数据量较大,可以采用分时加载的方式,先渲染部分数据,然后逐步加载剩余数据;

5. 项目交付的时候,怎么写文档、开发指南等

  1. 项目概述
  2. 安装和配置(所需的依赖项、环境配置、数据库设置)
  3. 文件结构
  4. 技术栈
  5. 功能说明
  6. 组件库
  7. 开发指南
  8. 部署指南
  9. 数据接口
    10.代码规范

6. 如果你刚接手一个项目,给你了一个开发指南,你最看中的是什么?

  1. 项目概述和目标; (开发指南中的项目概述能够让我快速了解项目的主要目标、用途和预期成果。)
  2. 技术栈和依赖项; (了解项目使用的技术栈、框架和库,以及依赖项的版本信息,可以让我快速适应项目的技术栈。)
  3. 代码规范和风格;
  4. API文档与接口信息;

7. 现在有1w行表格,不断滚动页面,向下加载?你怎么实现?

  1. 分页查询; 确保后台服务器支持分页查询,这意味着你可以通过发送请求来获取数据的特定页和每页的行数
  2. 前端页面设计;设计前端页面,包括表格和滚动容器,(确保滚动容器具有固定的高度,以便在滚动时只显示一部分数据。)
  3. 防抖、节流、分页、懒加载、虚拟滚动技术

虚拟滚动技术

  • 是一种用于处理大型列表或表格的前端性能优化技术; 主要目标是提高页面加载速度和减少内存消耗,特别是当需要展示大量数据时(仅渲染可见区域的数据)
    • 视口管理; ** 页面上的列表或表格被包装在一个具有固定高度的可视区域内。**
    • 懒加载;只有可见区域的数据会被渲染到DOM中
    • 动态高度;虚拟滚动技术通常能够处理不同高度的列表项,因此每个列表项的高度可以根据内容的大小而动态调整。
    • 性能优化;虚拟滚动技术通常使用一些性能优化策略,如对象池来重用DOM元素,从而减少DOM操作的开销。

美团到家 – 事业群

1. 浏览器输入url的整个过程

  1. URL解析, (协议、域名、端口号; 路径+查询参数等)
  2. DNS解析,(将域名转换为IP地址)
  3. 建立TCP连接,(通常是一个三次握手的过程,确保客户端和服务器之间的通信通道已经建立)
  4. 发起HTTP请求,(浏览器发送HTTP请求到服务器,这个请求包含了用户所请求的资源信息(请求头+请求体))
  5. 服务器处理请求,(根据URL, 服务器可能执行一些动态的操作,如查询数据库或生成页面内容,然后将响应返回给客户端)
  6. 接收和渲染响应,(浏览器接收到服务器的HTTP请求,开始渲染:执行js脚本+应用css样式)
  7. 呈现页面
  8. 连接关闭、清理资源

2. TCP

OSI模型: 物理层、数据链路层、网络层、传输层、会话层,表示层,应用层

  1. TCP在OSI模型中的传输层
  2. TCP是一种可靠的、面向连接的协议,它确保数据在发送和接收之间的可靠传递,以及按顺序传递,而不会丢失或损坏

3. http缓存是什么

HTTP缓存是一种用于减少网络传输和提高Web性能的技术。它允许浏览器、代理服务器和内容分发网络(CDN)在首次请求后,将资源的副本保存在本地,以便将来的请求可以直接从本地获取,而不必重新请求相同的资源。这有助于降低网络延迟,减少带宽消耗,提高网页加载速度,并减轻Web服务器的负担。

HTTP缓存可以分为两种主要类型:浏览器缓存和代理服务器/CDN缓存。

  1. 浏览器缓存:当用户访问网站时,浏览器可以缓存资源,例如HTML文件、CSS、JavaScript、图像等。浏览器会根据响应中的缓存控制头(例如Cache-ControlExpires)来确定资源的缓存策略,以及何时需要重新验证或重新加载资源。常见的浏览器缓存策略包括:

    • 强缓存:浏览器检查本地缓存,如果资源仍然有效(未过期),则直接使用本地缓存,不发送请求到服务器。
    • 协商缓存:如果资源过期或没有本地缓存,浏览器会向服务器发起条件请求,以验证资源是否仍然有效。服务器可以返回状态码304(未修改)或新的资源,以便浏览器更新本地缓存。
  2. 代理服务器/CDN缓存:代理服务器和CDN可以缓存Web资源,并在代理请求之前检查缓存。这有助于减少服务器负载,并提供更快的响应时间。代理服务器和CDN通常使用与浏览器缓存类似的缓存策略。

HTTP缓存可以通过多种方式实现,包括使用HTTP响应头(例如Cache-ControlExpiresETagLast-Modified等),以及在服务器端设置缓存策略。通过正确配置缓存策略,开发人员可以控制哪些资源可以缓存,以及缓存的有效期和更新方式。

使用HTTP缓存是Web性能优化的重要一环,它可以减少页面加载时间和网络带宽消耗,提高用户体验。

Cookie如何设置过期时间

  • 可以通过设置Cookie的expires属性来指定Cookie的过期时间. expires属性是一个表示Cookie过期日期的字符串,遵循特定的日期和时间格式。
    • 通过将expires属性添加到Cookie字符串中, 然后使用document.cookie属性设置Cookie. 浏览器将在指定的过期时间后自动删除Cookie。

3. https的s多在哪里

  1. 多在Secure (安全),其表明在HTTP(超文本协议)之上添加了一层安全性,以保护数据的传输和通信安全。
    在这里插入图片描述
  2. 安全性的体现:① 数据加密;(HTTPS 使用加密算法(通常是TLS或SSL)来加密在客户端和服务器之间传输的数据。)②身份验证(可以通过数字证书来验证服务器的身份)③ 数据完整性(使用哈希函数等技术来确保传输的数据在传输过程中不会被篡改或修改)

4. https怎么实现的,为什么需要https

实现

  • 加密通信
  • 数字证书
  • 数据完整性

为什么需要

  • 保护用户隐私
  • 防止数据篡改
  • 信任和身份验证

4. https的加密过程是什么

HTTPS(Hypertext Transfer Protocol Secure)是一种通过计算机网络进行安全通信的协议,它在HTTP的基础上通过使用TLS或SSL协议来加密数据传输。以下是HTTPS的加密过程:

  1. 客户端发起连接请求: 用户在浏览器中输入一个以"https://"开头的URL,表示要与服务器建立安全连接。
  2. 服务器端配置SSL/TLS: 服务器需要有SSL/TLS证书来支持加密连接。证书包含了用于加密和解密数据的公钥。
  3. 服务器发送证书: 服务器在连接建立时会将其SSL/TLS证书发送给客户端。
  4. 客户端验证证书: 客户端收到服务器的证书后,会验证证书的有效性。这包括检查证书的签名是否有效、证书是否过期,以及证书是否由受信任的证书颁发机构(CA)签发。
  5. 密钥协商: 如果证书验证成功,客户端生成一个用于对称加密的随机密钥(session key)。然后,客户端使用服务器的公钥加密这个随机密钥,然后将其发送回服务器。
  6. 服务器解密随机密钥: 服务器使用其私钥(只有服务器知道的)解密客户端发送的随机密钥。
  7. 建立安全连接: 客户端和服务器现在都有了相同的随机密钥,可以用于对称加密和解密通信。双方之间的通信现在是安全的,因为数据在传输过程中使用这个共享的密钥进行加密和解密。
  8. 安全数据传输: 客户端和服务器之间的所有数据现在都是通过使用这个共享密钥进行加密和解密的,从而保证了通信的机密性和完整性。
    通过这个加密过程,HTTPS提供了对数据传输的保护,防止中间人攻击和窃听,从而确保了用户与网站之间的安全通信。

SSL/TLS是什么?
SSL(Secure Sockets Layer)和TLS(Transport Layer Security)都是用于保护网络通信安全的协议,它们主要用于加密和保护在计算机网络上传输的数据。以下是它们的简要介绍:

  1. SSL(Secure Sockets Layer):
    • SSL是早期用于加密通信的协议,最初由网景公司(Netscape)在1995年推出。
    • SSL的目标是在客户端和服务器之间建立安全的通信通道,以防止中间人攻击和窃听。
    • 不同版本的SSL包括SSL 1.0、SSL 2.0、SSL 3.0。由于SSL 3.0存在安全漏洞,目前已经被废弃。
  2. TLS(Transport Layer Security):
    • TLS是SSL的继任者,由IETF(Internet Engineering Task Force)进行标准化。TLS 1.0于1999年发布。
    • TLS的设计目标是解决SSL存在的一些安全漏洞,并提供更强大的加密算法和协议。
    • TLS 1.0之后的版本包括TLS 1.1、TLS 1.2、TLS 1.3。每个版本都引入了新的安全特性和加密算法,同时弃用了不安全的部分。
  3. 关系和转变:
    • TLS实际上是SSL的升级版本,TLS 1.0被视为SSL 3.1。虽然术语不同,但TLS仍然继承了SSL的基本工作原理。
    • 由于SSL存在安全问题,后续版本的协议更倾向于称之为TLS。TLS逐渐替代了SSL,并成为当前安全通信的标准。
      总的来说,SSL和TLS都是用于安全通信的协议,而TLS是SSL的现代化和更安全的版本。在实际应用中,术语TLS更为常见,因为SSL已经过时且存在安全问题。

4. 对称加密和非对称加密分别是什么,他们之间有什么区别

对称加密和非对称加密是两种不同的加密方式,它们在加密和解密过程中使用的密钥类型和算法有很大的区别。

  1. 对称加密(Symmetric Encryption)
    • 密钥类型: 对称加密使用相同的密钥进行加密和解密。这意味着发送方和接收方必须共享相同的密钥。
    • 算法: 常见的对称加密算法有DES、3DES、AES等。这些算法在加密和解密时使用相同的密钥。
    • 性能: 对称加密通常比非对称加密更快,因为它使用的密钥较短,算法较简单。
   加密: ciphertext = Encrypt(plaintext, symmetric_key)
   解密: plaintext = Decrypt(ciphertext, symmetric_key)
  1. 非对称加密(Asymmetric Encryption)
    • 密钥类型: 非对称加密使用一对密钥,分别是公钥和私钥。信息被公钥加密后,只能用对应的私钥解密,反之亦然。公钥可以公开分享,而私钥必须保密。
    • 算法: 常见的非对称加密算法有RSA、ECC等。这些算法使用一对相关的密钥进行加密和解密。
    • 安全性: 非对称加密相对于对称加密更安全,因为即使公钥被泄露,也不会影响密文的安全性。
    • 性能: 非对称加密的计算成本较高,通常用于安全传输密钥而不是直接加密大量数据。
   加密: ciphertext = Encrypt(plaintext, public_key)
   解密: plaintext = Decrypt(ciphertext, private_key)

总体而言,对称加密适合在性能要求较高的场景下加密大量数据,而非对称加密适合在安全传输密钥和对安全性要求较高的场景下使用。通常的实践是使用对称加密来加密实际数据,而使用非对称加密来安全地传输对称密钥。这种结合使用的方式被称为混合加密。

对称加密的过程
对称加密是一种加密方式,其中同一个密钥用于加密和解密数据。下面是对称加密的基本过程:

  1. 密钥生成: 在通信双方之间,首先需要生成一个共享的密钥。这个密钥是保密的,因为任何知道了这个密钥的人都可以使用它来解密加密的信息。
  2. 加密过程:
    • 明文输入: 发送方有一些需要传输的明文数据,这是未经加密的原始数据。
    • 密钥应用: 使用共享的密钥,发送方通过加密算法对明文进行加密。加密算法采用相同的密钥来执行加密和解密操作。
    • 生成密文: 加密算法生成密文,这是加密后的数据。
      公式表示为:Ciphertext = Encrypt(Plaintext, SecretKey)
  3. 传输密文: 密文通过不安全的通信渠道传输给接收方。即使在传输过程中被截获,密文也不容易被解读,因为只有知道密钥的人才能解密。
  4. 解密过程:
    • 接收密文: 接收方收到密文。
    • 密钥应用: 接收方使用相同的密钥和解密算法对密文进行解密,恢复原始的明文数据。
    • 生成明文: 解密算法生成明文,这是原始数据。
      公式表示为:Plaintext = Decrypt(Ciphertext, SecretKey)
      需要注意的是,对称加密的关键在于保持密钥的安全性。如果密钥被未经授权的人获得,那么整个加密系统的安全性将受到威胁。因此,在实际应用中,安全地分发和管理密钥是非常重要的。

5. http2.0了解过吗。

  1. HTTP/2旨在改善Web性能,降低延迟,减少页面加载时间,并提高网络效率。
  2. 关键特点和要点:① 多路复用(允许多个请求和响应同时在单个TCP连接上传输。这意味着不再需要等待一个请求的响应才能发送下一个请求,从而加速了页面加载速度。)、② 二进制协议(HTTP/2将HTTP报文从文本格式转换为二进制格式,这使得数据的解析更加高效。)

6. 页面渲染的时候如果卡顿了该从哪方面分析原因。

  1. 网络问题; 过多的资源请求
  2. JS问题; 大型或低效的JS代码可能会导致页面卡顿
  3. DOM操作;频繁的DOM操作
  4. CSS问题;复杂的CSS选择器和规则,外部引入的css文件过大
  5. 大型图像和视频的引入
  6. 第三方脚本和插件的性能

7. js代码会影响浏览器页面渲染

  1. 阻塞渲染;当浏览器解析HTML文档时,遇到JavaScript代码(通常位于

8. 引入js代码如何实现异步?

  1. 使用回调函数,(您可以将一个函数传递给另一个函数,以便在某个操作完成时调用该函数。)
  2. 使用Promise,(Promise是一种更现代的异步编程方法,它提供了更好的可读性和错误处理机制。Promise可以表示一个异步操作的成功或失败,并且可以链式调用。)
function fetchData(url) {
  return new Promise(function (resolve, reject) {
    // 模拟异步操作,例如Ajax请求
    setTimeout(function () {
      const data = "这是异步数据";
      resolve(data);
    }, 1000);
  });
}

fetchData("https://example.com/api/data")
  .then(function (data) {
    console.log("处理数据:" + data);
  })
  .catch(function (error) {
    console.error("发生错误:" + error);
  });
  1. 使用async/await(async/await是一种基于Promise的语法糖,它提供了更简单的方式来处理异步操作。async函数返回一个Promise,而await可以在异步操作完成时暂停函数执行。)

9. js和css代码会引发卡顿吗?

  • JS
    • 大量计算
    • 阻塞渲染(如果JavaScript代码在DOM元素渲染之前运行,可能会阻塞页面的渲染,导致页面卡顿。)
    • 大型DOM操作
    • 内存泄露(未正确释放不再使用的JS对象和事件处理程序)
  • CSS
    • 复杂的CSS选择器
    • CSS动画和过渡

10. 如何优化css代码?

  1. 合并和压缩css文件
  2. 使用恰当的选择器
  3. 避免不必要的通用选择器
  4. 减少重绘和重排
  5. 使用CSS预处理器(Less),来提高代码的可维护性
  6. 按需加载样式

11. 如何减少重排和重绘?

  1. 使用transform和opacity属性:(transform属性和opacity属性不会触发重排,因此它们是创建动画和过渡效果的良好选择。使用这些属性来执行元素的平移、旋转、缩放和淡入淡出等动画效果。)
  2. 使用position: absolute和fixed:(当一个元素的位置使用position: absolute或position: fixed时,它会脱离文档流,不会影响其他元素的布局,因此不会触发重排。这对于创建浮动菜单、弹出框等元素非常有用。)
  3. 使用CSS动画库:(使用现成的CSS动画库(如Animate.css)可以简化动画创建过程,同时优化了动画的性能。)

12. 手写:先写ES6类的继承,再用ES5实现。

使用ES6类继承

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

class Student extends Person {
  constructor(name, age, grade) {
    super(name, age);
    this.grade = grade;
  }

  study() {
    console.log(`${this.name} is studying in grade ${this.grade}.`);
  }
}

const student = new Student("Alice", 20, 10);
student.sayHello(); // 输出:Hello, my name is Alice and I am 20 years old.
student.study();   // 输出:Alice is studying in grade 10.

使用ES5继承

// 基类构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 基类原型方法
Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
};

// 子类构造函数
function Student(name, age, grade) {
  Person.call(this, name, age); // 调用父类构造函数
  this.grade = grade;
}

// 使用Object.create来设置原型链,实现继承
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 修复构造函数指向

// 子类原型方法
Student.prototype.study = function() {
  console.log(this.name + " is studying in grade " + this.grade + ".");
};

var student = new Student("Bob", 18, 12);
student.sayHello(); // 输出:Hello, my name is Bob and I am 18 years old.
student.study();   // 输出:Bob is studying in grade 12.

super函数的作用是什么?

  • 是JS中的一个关键字,用于在子类中调用父类的构造函数和方法
class Parent {
  sayHello() {
    console.log("Hello from Parent");
  }
}

class Child extends Parent {
  sayHello() {
    super.sayHello(); // 调用父类的同名方法
    console.log("Hello from Child");
  }
}

Object.create是干什么的?

  • 其是JS中的一个方法,用于创建一个新对象,并以指定对象作为新对象的原型
const person = {
  name: "John",
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}.`);
  }
};

// 创建一个继承自 person 的新对象
const john = Object.create(person);

// 新对象继承了原型对象的属性和方法
john.sayHello(); // 输出:Hello, my name is John.

13. webpack中的Loader和Plugin的区别是什么?

  • Loader主要用于处理模块的加载和转换,而Plugin用于执行各种构建过程的任务。
    • Loader常用于处理各种资源文件,例如将ES6代码转换为ES5代码,将Sass/LESS转换为CSS
    • Plugin用于执行各种任务,如代码拆分、压缩、生成HTML文件等

14. Vue中计算属性中我写了a+b,如果页面渲染没有涉及这一部分,还会计算吗?

  • Vue中的计算属性会基于其依赖的响应式数据进行计算,并且只有在其依赖的数据发生变化时才会重新计算。
  • 如果计算属性的依赖数据没有发生变化,那么它不会重新计算,也不会触发页面重新渲染。
  • 所以,如果您在计算属性中写了 a + b,但页面渲染没有涉及这一计算属性,或者页面渲染之后 a 和 b 的值没有发生变化,那么该计算属性不会被重新计算。

15. 计算属性原理是什么?执行顺序呢?

原理

  • 计算属性原理是基于响应式数据系统的,它允许您定义一个属性,该属性的值是依赖于其他响应式数据的计算结果。
  • 计算属性具有缓存机制,只有当其依赖的响应式数据发生变化时,计算属性才会重新计算。

执行顺序

  1. 当组件初始化时,计算属性会首次计算其值,并缓存结果
  2. 如果计算属性的依赖数据(响应式数据)发生变化,Vue会侦听到这些变化,并触发计算属性的重新计算。
  3. 重新计算后,计算属性的新值将被缓存,以备后续使用
  4. 计算属性的值只有在其依赖的响应式数据发生变化时才会重新计算,否则将返回缓存的值。

16. 回答场景问题,用户在搜索框中输入内容可以显示类似内容,如何避免用户频繁多次搜索导致多次访问请求?

  1. 防抖
  2. 节流
  3. 搜索建议(使用搜索建议功能,当用户输入时,立即显示一些可能的搜索结果,而不必等到完整的搜索词输入完成。)
  4. 限制请求频率(在后端服务器端实施请求频率限制,例如每秒最多允许几次请求,以减轻服务器负载。
  5. 本地缓存技术(使用本地缓存技术,将之前的搜索结果存储在客户端,当用户再次搜索相同的词汇时,可以先检查本地缓存是否有相关结果,避免不必要的请求。)

17. 回答场景问题,在用户点击内容发送请求后,如何实现因为等待时间过长阻止该请求的发送。

  1. 设置请求超时时间;(在发出请求时,设置一个合理的超时时间,即允许服务器在特定时间内响应请求。如果在超时时间内未收到响应,可以取消请求或采取其他操作。)
  2. 前端限制请求频率;(在前端代码中,限制用户发起请求的频率,确保用户不会频繁点击发送请求按钮。您可以通过在点击按钮后禁用按钮,直到请求完成,以防止多次点击。
  3. 后端限制请求频率(在服务器端实施请求频率限制,例如限制每个用户在一定时间内的请求次数。这可以通过IP地址、用户令牌或其他标识符来实现。)
  4. 用户提示

18. 常见请求头共有哪些?

  1. Host(指定目标服务器的主机名和端口号)
Host: www.example.com
  1. User-Agent(包含发送请求的用户代理(通常是浏览器)的信息。服务器可以使用它来识别客户端。)
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
  1. Cookie(包含来自服务器的Cookie信息,用于在客户端和服务器之间维护会话状态。)
Cookie: session_id=abc123; user_id=12345
  1. Content-Type(用于指定请求主体的媒体类型和字符集,通常在发送POST请求时使用。)
Content-Type: application/json; charset=utf-8

19. 数组去重的方法?

在JavaScript中,有多种方法可以对数组进行去重,以下是一些常见的方法:

  1. 使用Set
  • ES6引入了Set数据结构,它可以自动去重数组中的重复元素。您只需将数组转换为Set,然后将其转回数组即可。
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [...new Set(array)];
  1. 使用filter()方法
  • 使用filter()方法来创建一个新数组,只包含原数组中第一次出现的每个元素。
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.filter((item, index, self) => self.indexOf(item) === index);
  1. 使用reduce()方法
  • 使用reduce()方法遍历数组,并将每个元素添加到结果数组中,但只添加第一次出现的元素。
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.reduce((accumulator, currentValue) => {
  if (!accumulator.includes(currentValue)) {
    accumulator.push(currentValue);
  }
  return accumulator;
}, []);
  1. 使用for…of循环
  • 使用for...of循环遍历数组,并将每个元素添加到结果数组中,但只添加第一次出现的元素。
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [];
for (const item of array) {
  if (!uniqueArray.includes(item)) {
    uniqueArray.push(item);
  }
}
  1. 使用Map
  • 使用Map数据结构来去重数组。
const array = [1, 2, 2, 3, 4, 4, 5];
const map = new Map();
const uniqueArray = [];
for (const item of array) {
  if (!map.has(item)) {
    map.set(item, true);
    uniqueArray.push(item);
  }
}

20. 构造函数 this 指向

  1. 在构造函数中的** this 关键字指向新创建的对象**, 当您使用new关键字来调用构造函数时,JS会自动创建一个新的空对象,并将该对象分配给构造函数内部的 this。
function Person(name) {
  this.name = name;
}

const person1 = new Person("Alice");
console.log(person1.name); // 输出 "Alice"

21. 如何实现三角形

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .triangle {
            width: 0;
            height: 0;
            border-left: 50px solid transparent;
            /* 左边 */
            border-right: 50px solid transparent;
            /* 右边 */
            border-bottom: 100px solid red;
            /* 底边,可以设置为任何颜色 */
        }
    </style>
</head>

<body>
    <div class="triangle"></div>
</body>

</html>

22. 手写:判断回文子串

  • 通过比较字符串的正向和反向版本来判断:
function isPalindrome(str) {
  // 移除字符串中的非字母数字字符并将其转换为小写
  // str = str.toLowerCase().replace(/[^a-zA-Z0-9]/g, '');

  // 反转字符串
  const reversedStr = str.split('').reverse().join('');

  // 检查正向和反向字符串是否相同
  return str === reversedStr;
}

// 示例用法
console.log(isPalindrome("A man, a plan, a canal, Panama")); // true
console.log(isPalindrome("race car")); // true
console.log(isPalindrome("hello world")); // false

顺丰

1. 对闭包和执行上下文的理解

执行上下文

  • 执行上下文是JS代码执行时的环境,它包含了变量、函数和其他数据的上下文信息。
  • 每个执行上下文都有两个主要组成部分
    • 变量环境,包含了变量、函数声明以及形参等信息,这些信息可以在当前上下文中访问。
    • 词法环境,与作用域相关,包含了变量环境,并且还可以包含对外部词法环境的引用,形成了作用域链。
  • 执行上下文可以形成一个栈,称为执行上下文栈

闭包

  1. 闭包

卓望数码

1. flex布局的所有属性和功能

  • flex布局是CSS3新增的一种布局方式,通过displa:flex来创造一个flex容器
  • flex-direction: 用来定义主轴的方向,可选值有row(水平方向),row-reverse,column(垂直方向)、column-reverse
  • flex-wrap: 用来定义是否允许子元素换行,可选值nowrap(默认,不换行)、wrap、wrap-reverse
  • flex-flow: 是flex-direction和flex-wrap的简写属性,可同时设置主轴方向和换行属性
  • justify-content:用于定义主轴上子元素的对齐方式,可选值flex-start(默认)、flex-end、center、space-between、space-around、space-evenly。
  • align-items: 定义交叉轴上子元素的对齐方式,可选值有stretch(默认,占满整个交叉轴)、flex-start、flex-end、center、baseline。

2. Promise的所有API接口,以及.all和.allsettled的区别

  • 概念
    • Promise是JS中的一个对象,用于处理异步操作。其表示一个异步操作的最终完成或失败,并提供一种处理操作结果的方式,而不会阻塞主线程。
  • 三个状态
    • pending(等待状态):初始状态,表示操作尚未完成或失败
    • resolved(已完成):表示操作成功完成。
    • rejected(已拒绝):表示操作失败。
  • 两个回调方法
    • Promise提供了.then()方法,用于注册回调函数,以处理操作成功(resolved)或失败(rejected)的情况。它还提供了.catch()方法,用于捕获和处理任何可能的错误。
  • 常见的API接口
    • Promise.resolve(value): 创建一个已成功(resolved)的Promise,可选择传递一个值作为成功结果
    • Promise.reject(reason): 创建一个已拒绝(rejected)的Promise,可选择传递一个原因(通常是一个错误对象)作为失败原因。
    • Promise.all(iterable):接收一个可迭代对象(通常是包含多个Promise的数组)作为参数,返回一个新的Promise。其在所有Promise都成功时才会成功(成功时,返回一个包含所有成功结果的数组。)。如果任何一个Promise失败,就会立刻失败。
    • Promise.allSettled(iterable): 与Promise.all()类似,接收一个可迭代对象作为参数,但不管Promise成功还是失败,它都会等待所有Promise都已经 settled(不再处于pending状态)才会返回。返回一个包含所有Promise的结果(无论成功或失败)的数组,每个结果都包含status属性来表示状态。
  • .all和.allsettled的区别
    • Promise.all()会在任何一个Promise失败时立即失败,而Promise.allSettled()会等待所有Promise都 settled 后返回结果,不管它们成功或失败
    • Promise.all()只有在所有Promise都成功时才会成功,而Promise.allSettled()不关心Promise的成功或失败,只要它们都 settled 了就会返回结果。
// 模拟两个异步操作,分别返回一个Promise
function asyncOperation1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Async Operation 1 is done");
    }, 2000); // 模拟异步操作需要2秒完成
  });
}

function asyncOperation2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Async Operation 2 is done");
    }, 1500); // 模拟异步操作需要1.5秒完成
  });
}

// 使用Promise.all()同时发起这两个异步操作
Promise.all([asyncOperation1(), asyncOperation2()])
  .then((results) => {
    // 当所有操作都成功完成时,results包含所有操作的结果
    console.log("All operations are completed:");
    console.log(results[0]); // 结果来自asyncOperation1
    console.log(results[1]); // 结果来自asyncOperation2
  })
  .catch((error) => {
    // 如果任何一个操作失败,这里会捕获到错误
    console.error("An error occurred:", error);
  });

3. v-if和v-show的区别,实现原理

  • 区别
    • 控制手段不同;
      • v-show是通过给元素添加 css 属性display: none,但元素仍然存在DOM当中
      • v-if控制元素显示或隐藏是将元素整个添加或删除。
  • 性能消耗不同
    • v-if有更高的切换消耗
    • v-show有更高的初始渲染消耗
  • 使用场景
    • 如果需要非常频繁地切换,则使用v-show较好,如:手风琴菜单,tab 页签等;
    • 如果在运行时条件很少改变,则使用v-if较好,如:用户登录之后,根据权限不同来显示不同的内容。
  • 实现原理
    • v-if; 当条件为真时,Vue.js会创建一个新的元素实例,调用其生命周期钩子(如created、mounted等),然后将该实例插入到DOM中。当条件为假时,Vue.js会销毁该元素实例,调用其销毁生命周期钩子(如beforeDestroy、destroyed等),并从DOM中移除该实例。
    • v-show; 无论条件如何,元素始终存在于DOM中。当条件为真时,Vue.js会将元素的display属性设置为适当的值(通常是block),从而使元素可见;当条件为假时,Vue.js会将元素的display属性设置为none,从而隐藏;

4. 对webpack的理解,loader和plugin的区别

  • Webpack
    • 一个用于构建现代Web应用程序的开源JavaScript模块打包工具。主要作用:将多个模块(包括JavaScript、CSS、图片等)打包成一个或多个静态资源文件,以便在浏览器中加载和运行。
  • loader和plugin的区别
    • Loader主要用于处理模块的加载和转换,而Plugin用于执行各种构建过程的任务
      • Loader常用于处理各种资源文件,例如将ES6代码转换为ES5代码,将Sass/LESS转换为CSS
      • Plugin用于执行各种任务,如代码拆分、压缩、生成HTML文件等

5. 对模块化的理解

  • 是指将程序拆分成为独立的、可重用的模块或组件,这些模块可以单独开发、测试和维护,然后通过组合它们来构建复杂的应用程序。

  • 语法

    • 模块功能主要由两个命令构成:export和 import
    • export命令用于规定模块的对外接口
    • import命令用于输入其他模块提供的功能
    • export暴露方式: 统一暴露(暴露对象:export {})、分别暴露(分别使用export)、默认暴露(export default{})
    • import 导入方式:通用导入、结构赋值导入、针对默认暴露方式
  • 什么时候使用默认暴露?

    • 导出单个值或功能的时候;
// module.js
export default function myFunction() {
  // ...
}
// 在另一个文件中
import myFunction from './module.js';

6. 前端做过哪些性能优化

  • 压缩和缩小资源(压缩JavaScript、CSS和HTML文件,以减小文件大小。)
  • 减少HTTP请求数量(合并多个CSS和JavaScript文件,减少HTTP请求。)
  • 使用CDN(使用CDN来分发静态资源,减少加载时间和减轻服务器负载。)
  • 使用浏览器缓存(设置适当的HTTP缓存头,以便于浏览器可以缓存资源,介绍重复加载)
  • 延迟加载(延迟加载非关键资源,如图片、广告和一部分JavaScript,以减少首次页面加载时间。)
  • 使用响应式设计(使用响应式Web设计来适应不同设备和屏幕尺寸,以减少不必要的资源加载和渲染。)
  • 异步加载JS(使用async和defer属性来异步加载脚本,以不阻塞页面渲染。)
  • 减少重绘和重排
  • 代码分割(使用代码分割(Code Splitting)将应用程序拆分成更小的代码块,以实现按需加载。)

7. 前端接收到一个新任务后的开发流程

  1. 需求分析; 包括功能、设计要求、用户体验等方面。
  2. 项目设置;选择开发工具、配置开发环境
  3. 创建项目结构;创建项目所需的目录结构,
  4. 编写代码;根据任务需求,编写HTML、CSS和JavaScript代码。
  5. 调试和测试;在本地开发环境中测试代码,确保功能正常运行。
  6. 性能优化;进行前端性能优化,包括文件压缩、资源缓存、代码拆分、延迟加载等。确保页面加载速度快且性能良好。
  7. 兼容性测试;在不同的浏览器和设备上进行兼容性测试,确保网站在各种环境下正常工作。
  8. 部署和上线;将项目部署到生产环境。这可能涉及上传文件到服务器、配置服务器环境、域名解析等操作。
  9. 监测和维护;监测上线后的网站性能和稳定性,解决任何潜在的问题。
  10. 文档编写;
  11. 任务交付和反馈;
  12. 迭代和改进:

8. XSS(跨脚本攻击)

  • 是一种常见的Web安全漏洞,攻击者通过在Web应用程序中插入恶意脚本,将其传递给其他用户执行的漏洞。
  • XSS攻击通常发生在Web应用程序未充分验证和过滤用户输入数据的情况下,导致攻击者能够注入恶意脚本并在受害用户的浏览器上执行。

9. JS中的基础类型和引用类型,它们的区别是什么?如何判断这些数据类型?

  • 基础类型
    • Number
    • String
    • Boolean
    • Undefined
    • Null
    • Symbol
    • BigInt
  • 引用类型
    • Object (对象)
    • Array (数组)
    • Function (函数)
    • Date (日期)
    • Map、Set、WeakMap、WeakSet (用于创建特殊类型的集合)
  • 区别
    • 存储方式
      • 基础类型存储在栈内存中,直接存储变量的值。
      • 引用类型存储在堆内存中,变量保存的是指向对象的引用地址。
    • 传递方式
      • 基础类型通过值传递
      • 饮用类型通过引用传递, 即复制变量的引用地址
    • 比较方式:
      • 基础类型使用值比较,两个相同的值相等。
      • 引用类型使用引用比较,只有引用地址相同才相等。
  • 如何判断数据类型?
    • 使用typeof操作符可以判断一个值的数据类型. (注意:typeof对于数组和null的判断不太准确,它们都会返回"object"。)
      • “number”:表示数字类型。
      • “string”:表示字符串类型。
      • “boolean”:表示布尔类型。
      • “undefined”:表示未定义类型。
      • “object”:表示对象类型(包括引用类型和null)。
      • “function”:表示函数类型。
    • instanceof操作符 (用于检查一个对象是否属于指定的构造函数。它可以用来判断引用类型。)
    var arr = [];
    arr instanceof Array; // true,arr是一个数组
    
    • Object.prototype.toString.call()方法:可以获取值的内部[[Class]] 属性,然后将其与相应的类型字符串进行比较(这种方式数组和null也能够正确识别.
    Object.prototype.toString.call(42); // "[object Number]"
    Object.prototype.toString.call("Hello"); // "[object String]"
    Object.prototype.toString.call(true); // "[object Boolean]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(function() {}); // "[object Function]"
    
    • Array.isArray()方法; 对于数组类型的判断, 这是一个专门用于判断数组的方法

10. vue有哪些生命周期

11. 讲述一下父组件和子组件的生命周期执行顺序

  • 父组件的生命周期
    • beforeCreate; 在实例初始化之后,
    • created:在实例创建完成后立即调用; 可以访问数据,但模版和虚拟DOM尚未创建
    • beforeMount: 在挂载开始之前调用,DOM还未生成
    • mounted: 在挂载完成后调用,DOM已经生成,可以访问DOM元素
    • beforeUpdate: 在数据更新之前调用, 发生在虚拟DOM重新渲染之前。
    • updated:在数据更新后调用,发生在虚拟DOM重新渲染之后。
    • beforeDestroy: 在实例销毁之前调用,可以进行一些清理工作
    • destroyed; 在实例销毁后调用,用于完成清理和解绑定操作。
      子组件的生命周期类似于父组件,但是在调用时机上略有不同
  • 子组件的生命周期会在父组件的相应生命周期之后被调用。例如,子组件的created钩子会在父组件的created钩子之后执行,mounted会在父组件的mounted之后执行,以此类推。

总结: 父组件和子组件的生命周期钩子函数按照创建、挂载、更新、销毁的顺序依次执行。父组件的生命周期在子组件之前执行.

12. 数据请求一般放在哪些生命周期钩子里,这些钩子之间有什么区别

  • 数据请求通常放在created(实例创建完成后立即调用)和mounted(在挂载完成后调用)
    • created
      • 钩子在组件实例被创建后立即调用。在这个时候,组件实例已经被初始化,但模板和虚拟DOM尚未被创建和挂载到页面上。
      • 通常,在created钩子中进行数据请求是一种常见做法,因为在这个阶段可以访问到组件的数据和方法,但还没有进行DOM操作。
      • 这个阶段适合用于初始化组件的数据、进行一次性的数据获取、设置计时器、订阅事件等操作。
    • mounted
      • mounted钩子在组件挂载到页面后调用. 在这个时候,组件的模板和虚拟DOM都已经被渲染到页面上,可以访问和操作DOM元素。
      • 在这个时候,组件的模板和虚拟DOM都已经被渲染到页面上,可以访问和操作DOM元素。
      • 通常,在mounted钩子中进行数据请求也是常见的,因为此时可以确保页面已经可见,用户能够看到加载中的状态,而且可以在数据请求完成后立即更新UI。

总结

  • 如果你的数据请求不依赖于DOM元素或是需要在组件的数据初始化阶段就获取数据,可以将请求放在created钩子中。
  • 如果你的数据请求依赖于DOM元素或需要在页面加载后才执行,可以将请求放在mounted钩子中。

13. vue-router的路由守卫有哪些应用

  • 路由守卫; 允许你在导航到不同路有时执行一些自定义逻辑, 这些守卫可以用于各种用途, 包括身份验证、路由授权、页面跳转前的验证等
  • 应用场景
    • 全局前置守卫; beforeEach
      • 应用场景:身份验证,全局路由拦截。
      • 使用场景:在用户访问每个路由之前,可以进行身份验证,检查用户是否已登录,如果未登录则重定向到登录页。
    • 全局后置守卫 afterEach:
      • 应用场景:日志记录、页面统计等全局操作。
      • 使用场景:在用户访问每个路由之后执行一些全局操作,如日志记录、页面统计等。
    • 路由独享守卫
      • 应用场景:路由特定的拦截逻辑。
      • 使用场景:在路由配置中可以为特定路由设置beforeEnter守卫,用于路由级别的身份验证或其他验证逻辑。
    • 路由重定向
      • 应用场景; 将某个路由重定向到另一个路由
      • 使用场景:可以使用路由配置的redirect字段来配置路由重定向,将用户重定向到指定路由,例如登录后自动跳转到仪表盘。

在前端路由中,有两种常见的导航方法:router.pushrouter.replace,它们之间有一些区别:

  1. router.push:

    • 使用 router.push 方法导航到一个新路由时,会将新的路由地址添加到浏览器的历史堆栈中。
    • 这意味着你可以通过浏览器的后退按钮返回到前一个路由,以前的路由会保留在历史记录中。
    • 适用于常规页面切换,用户可以通过浏览器的后退按钮返回到之前的页面。
  2. router.replace:

    • 使用 router.replace 方法导航到一个新路由时,不会将新的路由地址添加到历史堆栈中。
    • 这意味着前一个路由会被直接替换掉,用户无法通过浏览器的后退按钮返回到前一个路由。
    • 适用于不希望前一个路由保留在历史记录中的情况,例如登录成功后重定向到主页,用户不应该回退到登录页面。
      总的来说,router.pushrouter.replace 主要区别在于如何处理浏览器的历史记录。router.push 会将新路由添加到历史记录中,允许用户后退,而 router.replace 直接替换当前路由,不会保留前一个路由在历史记录中。你可以根据具体需求选择合适的方法来进行路由导航。

14. 用户鉴权有那些方案

  1. 基于角色的访问控制(Role-Based Access Control,RBAC):将用户分配给不同的角色, 每个角色有不同的权限. 用户的访问控制基于其所属的角色.
  2. 基于资源的访问控制, 这种模型将权限关联到资源, 用户可以有不同资源的访问权限.
  3. JSON Web Tokens(JWT); JWT 是一种用于身份验证和鉴权的令牌格式。它包含用户信息和签名,通常用于跨域或跨服务的鉴权。

15. 前端有哪些数据存储方案?

  • cookie,localstorage和sessionstorage以及html5新增的前端数据库支持
  • 它们的区别(见上方)

16. Cookie如何设置过期时间

  • 可以通过设置Cookie的expires属性来指定Cookie的过期时间. expires属性是一个表示Cookie过期日期的字符串,遵循特定的日期和时间格式。
    • 通过将expires属性添加到Cookie字符串中, 然后使用document.cookie属性设置Cookie. 浏览器将在指定的过期时间后自动删除Cookie。

17. Localstorage如何设置过期时间?

  • Localstorage是一个持久性存储在浏览器中的Web Storage API, 不提供内置的过期时间功能. 但是你可以通过自己的逻辑来实现
// 设置localStorage数据,并指定过期时间(单位:毫秒)
function setLocalStorageWithExpiration(key, value, expirationInMilliseconds) {
  const now = new Date().getTime();
  const item = {
    value: value,
    expiration: now + expirationInMilliseconds,
  };
  localStorage.setItem(key, JSON.stringify(item));
}

// 获取localStorage数据,如果数据已过期则返回null
function getLocalStorageWithExpiration(key) {
  const item = JSON.parse(localStorage.getItem(key));
  if (!item || new Date().getTime() > item.expiration) {
    localStorage.removeItem(key);
    return null; // 数据已过期或不存在
  }
  return item.value;
}

解释: 将数据存储在localStorage中, 并为每个数据项添加一个过期时间. 当你尝试获取数据时, 会检查数据是否存在以及是否过期. 如果数据过期或不存在,将返回null

18. 后端一次性返回大量数据,如何确保前端渲染性能?

  1. 分页加载; 将数据分为多个页面或分块, 并在需要时仅加载当前页或分块的数据;
  2. 延迟加载; 仅在数据首次显示或用户滚动到特定位置时加载可见部分的数据; 使用虚拟列表技术,只渲染可见的数据项.
  3. 前端数据缓存; 缓存已加载的数据,以减少对后端的重复请求
  4. 懒加载模块; 使用懒加载技术将模块按需加载,而不是一次性加载所有模块;
    • 使用动态import()语法(ES6中的动态导入)来实现懒加载, 这允许你在需要时异步加载模块
    • 在代码中使用mport()语法,并将其返回的Promise对象用于加载模块
// 假设有一个模块名为 "myModule"

// 使用动态导入加载模块
const loadMyModule = () => import("./myModule.js");

// 在需要时调用
loadMyModule().then(module => {
  // 使用模块中的函数或变量
  module.myFunction();
});

19. 怎么去理解HTML,CSS,JS,交互的话是怎么实现的呢?

  • HTML(超文本标记语言); 是一种创建网页结构和内容的标记语言.
  • CSS(层叠样式表); 用于描述HTML文档的外观和样式
  • JS; 是一种用于添加交互性和动态行为的编程语言

交互是怎么实现的

  • 通过HTML和CSS,可以创建静态网页,但要实现交互性,通常需要JavaScript的帮助。
  • JavaScript可以通过以下方式与HTML和CSS交互:
    • 事件处理; (鼠标点击、滚动)
    • DOM操作; (JavaScript可以访问和修改HTML文档的DOM)
    • 样式操作; (JavaScript可以通过修改元素的CSS样式来改变其外观和布局。)
    • 异步通信;
    • JavaScript可以使用AJAX或Fetch等技术与服务器进行异步通信,获取或发送数据,以更新页面内容。

20. Diff算法需要用到几个指针,时间复杂度是多少 (本人)

  • 指针个数
    • Diff算法通常需要使用两个指针来比较新旧虚拟DOM树的节点, 这两个指针通常称为新虚拟DOM指针旧虚拟DOM指针
  • 时间复杂度
    • 时间复杂度大致在O(n)到O(n^3)之间,其中n是树的节点数量; 通常情况下,不会达到最坏的情况的时间复杂度.

21. for…of 可以遍历原生的对象吗?

  • for…of 循环通常用于遍历可迭代对象(数组、字符串、Map、Set等), 原生的对象通常是不可迭代的,因此不可以使用其遍历
  • 如果希望遍历原生对象的属性,通常会使用for…in来进行遍历操作
const myObject = {
  a: 1,
  b: 2,
  c: 3
};

for (const key in myObject) {
  console.log(key, myObject[key]);
}

扩展

  • 如果你想要在原生对象上使用 for…of 循环,你需要确保该对象实现了可迭代协议(Iterable Protocol)。这通常需要定义一个迭代器对象(Iterator),其中包含一个 Symbol.iterator 方法,该方法返回一个包含 next 方法的对象。
const myIterableObject = {
  data: [1, 2, 3, 4],
  [Symbol.iterator]: function() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (const item of myIterableObject) {
  console.log(item);
}

22. 手撕; TS; 如果某个对象里面有姓名是string类型,年龄是number类型,想要确保姓名和年龄是肯定要的,但是还可以任意添加别的属性, 使用TS应该如何书写?

可以使用接口(interface)来描述对象的形状,并且使用索引签名来表示可以添加任意其他属性;

  • 特别注意的是索引签名的写法;
interface Person {
  name: string;
  age: number;
  [key: string]: any; // 索引签名,允许添加任意其他属性
}

// 使用示例
const person1: Person = {
  name: "Alice",
  age: 30,
  city: "New York", // 可以添加其他属性
};

const person2: Person = {
  name: "Bob",
  age: 25,
  country: "Canada", // 可以添加其他属性
  job: "Engineer",
};

23. 两个async怎么同时执行

可以使用Promise.all方法或者await关键字来进行操作

  1. 使用Promise.all()方法
async function asyncFunction1() {
    await someAsyncOperation() // 第一个async函数的相关逻辑
    console.log('async function 1 Done');
}

async function asyncFunction2() {
    await someAsyncOperation()
    console.log('async function 2 Done');
}

// 使用Promise.all来同时执行两个async函数
Promise.all([asyncFunction1(), asyncFunction2()]).then(() => {
    console.log('Both Async Function completed');
}) 
  1. 使用await关键字
async function asyncFunction1() {
  // 这里是第一个 async 函数的逻辑
  await someAsyncOperation();
  console.log('Async Function 1 Done');
}

async function asyncFunction2() {
  // 这里是第二个 async 函数的逻辑
  await someAsyncOperation();
  console.log('Async Function 2 Done');
}

// 使用 await 关键字来依次执行两个 async 函数
async function executeAsyncFunctions() {
  await asyncFunction1();
  await asyncFunction2();
  console.log('Both Async Functions Completed');
}

executeAsyncFunctions();

24. 数组有哪些方法,哪些是O(n),O(logn),O(1);

  • O(1)复杂度的数组方法;
    • push(): 在数组末尾添加一个元素。
    • pop(): 移除数组末尾的元素。
    • shift(): 移除数组的第一个元素。
    • unshift(): 在数组的开头添加一个元素。
    • concat(): 合并数组。
    • length: 获取数组的长度。
    • join(): 将数组的元素连接成一个字符串。
    • indexOf(): 查找元素在数组中的索引位置。
    • lastIndexOf(): 查找元素在数组中的最后一个索引位置。
  • O(n)复杂度的数组方法
    • forEach(): 对数组中的每个元素执行一个函数。
    • map(): 创建一个新数组,其中包含对原始数组的每个元素应用函数后的结果。
    • filter(): 创建一个新数组,其中包含满足特定条件的原始数组元素。
    • reduce(): 对数组中的元素执行一个归约操作。
    • reduceRight(): 从数组的右侧开始执行归约操作。
    • some(): 检查数组中是否有满足特定条件的元素。
    • every(): 检查数组中的所有元素是否都满足特定条件。
    • find(): 查找满足特定条件的第一个元素。
    • findIndex(): 查找满足特定条件的第一个元素的索引。
  • O(log n)复杂度的数组方法
    • 没有直接提供; 要执行O(log n)操作, 你通常需要使用二分搜索等算法, 使用某种特定的结构,
    • 如果需要O(log n)复杂度的操作,可以考虑使用Set、Map、堆(Heap)等数据结构

25. sort方法底层是如何实现的,用的什么算法

底层是通过快速排序算法来进行排序的, 快速排序是一种高效的排序算法, 平均时间复杂度为O(nlogn); 最坏情况会达到O(n*n)

  • 快速排序的思想: 通过选取一个基准元素,将数组分为两部分,一部分包含小于基准的元素,另一部分包含大于基准的元素,然后递归地对这两部分进行排序,最终得到一个有序数组。

底层实现的原理

  • 选择一个基准元素(通常是数组的第一个元素或随机选择)。
  • 将数组分为两个子数组,一个包含小于基准的元素,另一个包含大于基准的元素。
  • 对这两个子数组递归地应用相同的排序算法。
  • 将排序后的子数组合并在一起,以获得最终的排序数组。

sort方法会原地排序数组, 因此会直接修改原始数组

26. 快速排序和冒泡排序的区别?

  • 性能差异
    • 快速排序通常比冒泡排序更快。快速排序的平均时间复杂度为O(n log n),而冒泡排序的平均时间复杂度为O(n^2)。因此,在大多数情况下,快速排序要快得多。
    • 冒泡排序的性能相对较差,特别是对于大型数据集,因为它需要多次迭代和元素交换,导致时间复杂度高。
  • 算法思想
    • 快速排序是一种分治算法,它通过选择一个基准元素,将数组分为两部分,并对这两部分进行递归排序。
    • 冒泡排序是一种比较排序算法,它通过多次遍历数组,比较相邻的元素并进行交换,将较大的元素逐渐“冒泡”到数组的末尾,直到整个数组排序完成。
// 快速排序
function quickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }

    const pivot = arr[0]; // 选择一个基准元素
    const left = [];
    const right = [];

    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]); // 比基准小的放在左边
        } else {
            right.push(arr[i]); // 比基准大的放在右边
        }
    }

    return [...quickSort(left), pivot, ...quickSort(right)]; // 递归排序左右两侧并合并结果
}

// 示例用法
const unsortedArray = [4, 2, 7, 1, 9, 5, 3];
const sortedArray = quickSort(unsortedArray);
console.log(sortedArray); // 输出 [1, 2, 3, 4, 5, 7, 9]

27. 如果给一个很大的数组,里面是字符串,我要搜索以“abc”为前缀的字符串有哪些,用什么方法

  • 可以使用前缀树的数据结构来高效的完成这个任务, 其用于存储一组字符串,并支持高效的前缀搜索,
    • 构建 Trie:将所有字符串插入到 Trie 中。
    • 搜索前缀:“abc”:从 Trie 根节点开始,沿着每个字符的路径移动,直到找到前缀的末尾或无法继续移动。
    • 收集结果:在找到前缀的末尾时,可以沿着 Trie 的子树收集所有以该前缀为前缀的字符串。

28. 如果在代码里添加一个script外链,比如外链前输出123,外链后输出456,456会输出吗,如果输出什么时候才输出(假设外链加载要10s)

外部脚本的加载不会阻塞HTML是页面的解析和其他脚本的执行

  • 那么在外链加载完毕之前(在你所说的10秒内),"123"会被输出到页面。然后,一旦外链脚本加载完成并执行,“456” 也会被输出。

29. ES6有哪些新特性,

30. 箭头函数能够改变this的指向吗?

  • 箭头函数不会改变 this 的指向,而是继承了外部函数的 this。
    • 在箭头函数内部, this的值是在定义函数时外部作用域中的this的值, 而不是在调用函数时的 this;
// 普通函数
function regularFunction() {
  console.log("Regular Function: this is", this);
}

const obj = { value: 42 };

// 在对象上调用普通函数
obj.regularMethod = regularFunction;
obj.regularMethod(); // 输出 "Regular Function: this is { value: 42 }"

// 箭头函数
const arrowFunction = () => {
  console.log("Arrow Function: this is", this);
};

// 在对象上调用箭头函数
obj.arrowMethod = arrowFunction;
obj.arrowMethod(); // 输出 "Arrow Function: this is Window" (全局对象)

31. for of方法能遍历对象吗,不能,那怎样才能让他可以,symbol定义迭代器

  • for…of方法一般用于迭代可迭代对象(数组、字符串、Map、Set等), 但是不能直接用于普通对象,如果你想在普通对象上使用for…of循环,需要使用到Symbol迭代器
  • 如何自定义一个Symbol迭代器,示例
// 定义一个普通对象
const myObject = {
  property1: 'value1',
  property2: 'value2',
};

// 使用 Symbol.iterator 创建自定义迭代器
myObject[Symbol.iterator] = function () {
  const keys = Object.keys(this);
  let index = 0;

  return {
    next: () => {
      if (index < keys.length) {
        const key = keys[index++];
        return { value: this[key], done: false };
      } else {
        return { done: true };
      }
    },
  };
};

// 使用 for...of 循环迭代对象的值
for (const value of myObject) {
  console.log(value);
}

32. flex布局的align-items、align-self、align-content有什么区别?

  • align-items; 其是用来控制整个容器内的子项在交叉轴上的对齐方式; (设置的属性值可以同align-items)
  • align-self; 用来控制单个子项在交叉轴的对齐方式
  • align-content; 用于控制多行子项在交叉轴上的整体排列方式,只有一行子项时,其没有效果[多了space-between、space-around(两边有空白间隔)]

33. flex省略属性是省略了哪三个,初始值是多少,basis的0和auto的区别

  • flex-grow: 0; 表示弹性盒子不会在剩余空间中增长
  • flex-shrink: 1; 表示弹性盒子会在空间不足时缩小, 按比例缩小;
  • flex-basis: auto; 表示弹性盒子的基础尺寸将根据其内容大小自动计算,通常情况下会根据内容自动撑开
    • 弹性盒子的基础尺寸被设置为 0,意味着它不会根据内容自动撑开,而是会完全依赖于 flex-grow 和 flex-shrink 来分配可用空间。
    • 值为auto则表示; 弹性盒子的基础尺寸将根据其内容大小自动计算,这是默认值。

flex: 1 1 0%中的0%表示什么意义

  • flex-grow: 1表示子元素可以平均分配额外的可用空间。
  • flex-shrink: 1表示子元素可以等比例缩小,以适应容器的较小空间。
  • flex-basis: 0%表示子元素的基础大小被设置为0%,将根据flex-grow来自动分配大小

flex:1 表示什么意思

  • flex: 1 表示子元素将以相同的比例伸展并缩小,且它们的基础大小被设置为0%,以便在弹性容器中均匀分配可用空间,这是一种常见的设置,用于创建具有平均伸缩特性的弹性盒子子元素。

34. dns解析的步骤了解吗

  1. 浏览器缓存:首先Web浏览器会根据自身的缓存, 如果已经解析过,浏览器会直接使用缓存的 IP 地址,而不需要进行新的 DNS 查询。
  2. 操作系统是否有缓存; 操作系统也会保存最近解析的域名及其对应的 IP 地址。
  3. 本地主机文件; windows上的hosts文件等
  4. DNS递归查询
    • 根域名服务器查询:本地 DNS 服务器首先向根域名服务器发送查询请求,以获取顶级域的域名服务器的 IP 地址。
    • 顶级域名服务器查询:根域名服务器返回 TLD 域名服务器的 IP 地址后,本地 DNS 服务器会向顶级域名服务器发送查询请求,以获取二级域名服务器的 IP 地址。
    • 二级域名服务器查询:顶级域名服务器返回二级域名服务器的 IP 地址后,本地 DNS 服务器会向二级域名服务器发送查询请求,以获取目标域名的 IP 地址。
  5. 目标域名服务器响应:最终,目标域名服务器收到本地 DNS 服务器的查询请求后,会返回与域名相关联的 IP 地址。
  6. 本地DNS缓存更新;
  7. 操作系统缓存更新
  8. 浏览器缓存更新

35. 如果http发送多个请求会建立多次tcp连接吗,如果设置了cdn会再次tcp连接吗

  • HTTP/1.1中每个HTTP请求通常会建立单独的TCP连接
  • HTTP/2允许多个请求共享单个TCP连接(多路复用)
  • CDN(内容分发网络) 通常会使用已建立的TCP连接来传输内容, 以减少与源服务器的连接次数

36. http如果只两次握手会造成什么

  • Http协议本身是不负责进行握手的, 而是建立在TCP协议之上的
  • TCP协议使用三次握手来建立连接
  • 如果只进行两次握手,可能导致
    • 连接不稳定; TCP的三次握手用于确保客户端和服务器都已准备好进行数据传输。如果只进行两次握手,连接可能会不稳定,因为某一方可能并没有完全准备好。
    • 旧连接的残留数据; (没有通过第三次握手来关闭连接, 可能会导致旧连接的残留数据在网络中流动,这可能会对后续的连接产生干扰。)
    • 连接资源泄漏; (不进行第三次握手来正常关闭连接,会导致服务器上的连接资源泄漏)

37. vue的响应式原理

  • Vue2的响应式原理;

    • Vue 2 中的数据响应式会根据数据类型做不同的处理;(数据劫持)
      • 如果是对象,则通过Object.defineProperty(obj,key,descriptor)拦截对象属性访问,当数据被修改和查询时,感知并作出反应
      • 如果是数组,则通过覆盖数组原型方法,扩展它的7个变更方法(push、pop、shift、unshift、sort、reverse、splice)
    • 缺点
      • 新增或删除对象属性无法拦截
      • 需要通过Vue.set及delete这类API才能生效
  • Vue 3 中利用ES6的Proxy机制代理需要响应化的数据。可以同时支持对象和数组,动态属性增、删都可以拦截,新增数据结构均支持

  let person = {
            name:'hk', 
            age: 18
        }
const p = new Proxy(person, {
    // 有人读取p的某个属性时调用
    get(target, propName) {
        console.log(`有人读取了p身上的${propName}属性`);
        // 读取了直接返回即可
        return Reflect.get(target, propName)
    }, 
    set(target, propName, value) {
        console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`);
        Reflect.set(target, propName, value)
    },
    // 有人删除p中的某个属性时使用
    deleteProperty(target, propName) {
        console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`);
        return Reflect.deleteProperty(target, propName);
    }
})

38. vuex用过吗,介绍一下

  • 概念;
    • Vuex是Vue中专用的状态管理库,它以全局方式集中管理应用的状态。
  • 解决的问题
    • Vuex 主要解决的问题是多组件之间状态共享
      • 利用各种通信方式,虽然也能够实现状态共享,但是往往需要在多个组件之间保持状态的一致性,这样会使得代码逻辑变复杂;
      • Vuex 通过把组件的共享状态抽取出来,以全局单例模式管理,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向流动,使代码变得更具结构化且易于维护
  • 什么时候使用?
    • 如果我们打算开发大型单页应用或应用里有大量全局的状态需要维护,可以使用vuex
  • 用法
    - Vuex 将全局状态放入state对象中,它本身是一颗状态树,组件中使用store实例的state访问这些状态;然后用配套的mutation方法修改这些状态,并且只能用mutation修改状态,在组件中调用commit方法提交mutation如果应用中有异步操作或复杂逻辑组合,需要编写action,执行结束如果有状态修改仍需提交mutation,组件中通过dispatch派发action。最后是模块化,通过modules选项组织拆分出去的各个子模块,在访问状态(state)时需注意添加子模块的名称,如果子模块有设置namespace,那么提交mutation和派发action时还需要额外的命名空间前缀。

39. 在vuex中, 如果在mutations里写了异步方法能使用吗,还是会报异常

  • 在 Vuex 的 mutations 中不建议直接写异步方法,因为 mutations 应该是用来同步地修改 state 的。如果在 mutations 中尝试直接执行异步操作,Vue.js 会在开发模式下发出警告,因为这会破坏 Vuex 的响应式数据流。
  • 如果需要在 Vuex 中执行异步操作,应该使用 actions。Actions 允许你在异步操作完成后提交 mutations 来修改 state。这是 Vuex 的标准做法。

40. 组件间如何传值

  1. Props(属性传递); 使用props可以在父组件中向子组件传递数据。在子组件中,可以通过props来接收传递的数据。
  2. 自定义事件; 子组件可以通过触发自定义事件来向父组件传递数据. 父组件通过@符,或者v-on来在子组件上绑定自定义事件, 子组件中通过this.$emit() 方法来触发自定义事件向父组件传递数据
  3. Vuex;
  4. Provide/Inject; (祖先组件向后代组件中传递数据)
  5. 全局事件总线

41. eventbus是如何实现的

  • 是一种基于Vue实例的自定义来实现的, 通常用于跨组件传递事件或数据;
    • EventBus的实现通常涉及创建一个全局的Vue实例,充当事件中心,其他组件可以通过这个实例来订阅事件和触发事件。
    • 在main.js中创建好了eventBus以后, 在每个使用这个组件中都需要进行引入
      • 发送数据的组件通过eventbus.$emit(‘message’, ‘’)来进行触发
      • 接收数据的组件通过eventbus.$on(‘message’, (message) => {}) 来进行接收操作

42. 如果要一个组件改变了,多个组件就输出值怎么实现;如果是多个组件改变了一个组件才输出值怎么实现

可以使用全局事件总线和vuex来实现 — 使用Vuex是最方便的!

  • 如果要实现多个组件改变后,一个组件输出值,只需在多个组件中触发一个共享的事件或 mutation 即可,其他组件监听该事件或 mutation 并更新状态或视图。

43. 订阅者模式和观察者模式有什么区别

  • 两种设计模式思路是一样的,举个生活例子:
    • 观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是“公司”和“员工”是一个整体
    • 发布-订阅模式:某公司要给其他人发各种快递,因为“公司”和“其他人”是独立的,其唯一的桥梁是“快递”,所以这件事适合交给第三方快递公司解决
      上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务,因此使用何种模式需要考虑什么情况两者是需要耦合的

区别

  • 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
  • 在发布订阅模式中,组件是松散耦合的, 正好和观察者模式相反;
  • 观察者模式大多数是同步的, 发布-订阅模式大多数是异步的

44. webpack

Webpack是一款现代的JavaScript模块打包工具,它的主要功能是将项目中的各种模块(JavaScript、CSS、图片等)打包成一个或多个捆绑文件,以便在浏览器中加载。Webpack的操作流程包括以下关键步骤:

  1. 入口点(Entry Point): 首先,Webpack需要确定应用程序的入口点。入口点是Webpack开始构建过程的地方,通常是一个JavaScript文件。Webpack会从这个入口文件开始,然后分析依赖关系,递归地查找和处理其他模块。
  2. 模块解析(Module Resolution): Webpack会分析入口文件及其依赖的模块,然后根据配置文件中的规则来解析模块的路径。这包括处理不同文件类型(如JavaScript、CSS、图片等)以及查找模块所在的位置。
  3. 加载器(Loaders): 当Webpack识别到不同类型的模块时,它会使用加载器来转换这些模块。加载器是一些特定的转换器,可以将不同格式的文件转换为JavaScript模块。例如,通过Babel加载器,你可以将ES6代码转换为ES5。
  4. 插件(Plugins): Webpack允许使用插件来执行各种构建任务。插件是自定义操作的工具,可以用于压缩、优化、提取公共代码、生成HTML文件等。插件通过配置文件中的配置来使用。
  5. 生成输出(Output Generation): 一旦Webpack处理完所有的模块,加载器和插件,它将生成一个或多个输出文件。这些输出文件包括捆绑后的JavaScript文件、CSS文件、图像文件等。
  6. 模块拆分(Module Splitting): Webpack还支持将代码拆分成多个捆绑文件,以提高应用程序的性能和加载速度。这通常涉及到代码拆分、按需加载(懒加载)和共享代码。
  7. 缓存和版本控制: Webpack会为生成的文件添加哈希值或版本号,以确保浏览器能够正确缓存文件。这有助于避免浏览器缓存旧文件的问题。
  8. 开发服务器: 在开发环境中,Webpack通常与开发服务器一起使用,以便在文件更改时实时重新构建并自动刷新浏览器,提供更好的开发体验。
  9. 代码优化和压缩: 在生产环境中,Webpack通常会进行代码优化和压缩,以减小文件大小,提高性能。这通常包括压缩JavaScript、CSS和图片文件。
  10. 输出部署: 最后,生成的文件通常需要部署到Web服务器或CDN上,以供访问。
  • Webpack的操作流程涉及许多配置选项和插件,以适应不同的项目需求。配置文件是控制Webpack构建过程的关键,允许你自定义Webpack的行为和输出
    loader和plugin的区别
    • Loader主要用于处理模块的加载和转换,而Plugin用于执行各种构建过程的任务
      • Loader常用于处理各种资源文件,例如将ES6代码转换为ES5代码,将Sass/LESS转换为CSS
      • Plugin用于执行各种任务,如代码拆分、压缩、生成HTML文件等

44. 说下webpack是如何压缩代码的?

  1. 配置压缩插件; UglifyJS
  2. 使用代码分割
  3. 启用Tree Shaking(一种用于删除未引用代码的技术, 通常与Webpack一起使用)
  4. 执行Webpack构建、生成压缩后的文件
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  // ...其他配置
  optimization: {
    minimizer: [new UglifyJsPlugin()], 
    splitChunks: {
      chunks: 'all'
    }, 
    usedExports: true
  }
};

webpack有哪些配置选项

 configureWebpack: {
    output: {  // 输出文件;
      filename: `js/[name]${Timestamp}.js`,
      chunkFilename: `js/[name]${Timestamp}.js`
    },
    // 插件配置
    plugins: [
      new CompressionWebpackPlugin({
        algorithm: 'gzip',
        test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
        threshold: 10240,
        minRatio: 0.8
      })
    ], 
    // optimization是用于配制代码优化和分割的选项
    optimization: {
      minimizer: [new UglifyJsPlugin()], 
      splitChunks: {
        chunks: 'all'
      }, 
      usedExports: true
    }
  },

如果想要根据具体的文件来进行切割

  • 使用splitChunks来进行切割
optimization: {
  splitChunks: {
    chunks: 'async', // 只对异步加载的模块进行分割
    minSize: 20000, // 模块的最小大小才会被分割
    maxSize: 0, // 无最大限制
  },
},
  • 如果你想仅对特定的模块进行分割,可以使用函数来筛选这些模块
optimization: {
  splitChunks: {
    chunks: (chunk) => {
      // 仅对指定的模块进行分割
      return chunk.name === 'my-special-module';
    },
  },
},

44. Webpack, Vite的配置,有什么区别?

Webpack 和 Vite 都是前端构建工具,用于构建现代Web应用程序,但它们在设计和执行方式上存在一些重要区别:

  1. 打包方式:
    • Webpack: Webpack 是一种强大的静态模块打包工具,它通过配置文件定义构建过程,支持在开发和生产环境中使用。Webpack 会将所有项目文件打包成一个或多个捆绑文件。
    • Vite: Vite 是一种基于ES Modules的构建工具,它在开发中使用原生ES模块,不进行传统的打包,而是采用现代的构建方式,充分利用浏览器的特性,只在需要时编译和提供模块。
  2. 启动速度:
    • Webpack: Webpack 在启动时需要对整个项目进行完整的构建,这可能需要一些时间,尤其是在大型项目中。
    • Vite: Vite 在开发模式下具有极快的启动速度,因为它采用了按需编译,只编译你实际使用的代码。
  3. 热更新:
    • Webpack: Webpack 提供了热模块替换(HMR),但需要配置和插件的支持,有时候可能需要一些额外的设置。
    • Vite: Vite内置了热更新,而不需要额外的配置,这使得开发过程中的模块更新更加快速。
  4. ES模块支持:
    • Webpack: Webpack也支持ES Modules,但通常需要使用Babel等工具来处理模块,或使用CommonJS模块作为默认模块系统。
    • Vite: Vite天生支持ES Modules,这使得在开发中使用原生ES模块更加简单。
  5. 插件系统:
    • Webpack: Webpack有一个庞大的生态系统,支持各种插件和加载器,可以处理各种不同的资源和任务。
    • Vite: Vite相对较新,其插件生态系统不如Webpack丰富,但不断增长,而且它的配置更为简单。
  6. 生产构建:
    • Webpack: Webpack可用于生产构建,能够优化、压缩和拆分代码以提高性能。
    • Vite: Vite通常用于开发,但也可以用于生产构建,但构建输出通常更轻量,不同于Webpack的输出。

45. tree shanking的具体作用以及底层是如何实现的知道吗

  • Tree Shaking的主要作用是去除那些在项目中没有被引用的模块、变量、函数等,从而减小最终的构建文件大小。
  • 实现方式; 基于静态代码分析来实现的
    • 当你使用ES6模块化语法导入模块时,编译工具(如Webpack、Rollup等)可以在构建过程中分析模块之间的依赖关系,以确定哪些代码是被引用的,哪些代码是未引用的。然后,未引用的代码将被从最终的构建输出中删除。
      • 静态分析; 编译工具会构建一个依赖图, 记录模块之间的依赖关系;
      • 标记引用; 当某个模块被另一个模块引用时,编译工具会标记这个模块为被引用状态。
      • 标记非引用
      • 删除未引用代码

45. promise.race和promise.any的区别

Promise.racePromise.any 都是用于处理多个 Promise 对象的方法,它们有一些区别:

  1. 解决方式
    • Promise.race:它返回一个 Promise,只要传递给它的多个 Promise 中有一个变为 resolved(成功状态)或 rejected(失败状态),它就会立即返回并采用第一个完成的 Promise 的状态和值。
    • Promise.any:它也返回一个 Promise,但它会等待所有传递给它的 Promise 全部变为 rejected,然后才会返回一个 rejected Promise。只有当所有传递的 Promise 都失败时,它才会失败,否则它会采用第一个成功的 Promise 的状态和值。
  2. 应用场景
    • Promise.race:通常用于竞速解决方案,其中你希望得到最快完成的结果。如果你有多个异步操作,但只关心最快完成的结果,可以使用 Promise.race
    • Promise.any:通常用于一种情况,即你希望等待多个异步操作完成,只要有一个成功就可以,但不必等到所有操作都失败。这对于执行多个相似的操作,但只需要一个成功的情况很有用。
  3. 兼容性
    • Promise.race 是标准 ES6 Promise 方法,具有广泛的浏览器和Node.js支持。
    • Promise.any 是ES2021(ES12)的新增特性,可能在某些环境中不被完全支持。在使用它时,要确保你的运行环境支持该方法,或考虑使用polyfill或其他解决方案。
  4. 错误处理
    • Promise.race 不会提供有关失败的 Promise 的信息,因为它只采用第一个完成的 Promise 的状态。
    • Promise.any 会等待所有 Promise 完成,以确定是否所有 Promise 都失败。如果所有 Promise 都失败,Promise.any 会返回一个带有所有失败原因的数组。
      总之,Promise.race 用于追求最快完成的结果,而 Promise.any 用于等待多个异步操作的任何一个成功。选择其中一个取决于你的具体需求。

46. promise和async/await的区别是什么,链式调用是如何实现的

区别

  1. 语法:Promise 使用 .then() 和 .catch() 来处理异步操作,而 async/await 使用 await 和 try/catch。
  2. 错误处理:Promise 需要在每个 .then() 链中使用 .catch() 来处理错误,而 async/await 使用 try/catch 在同一地方集中处理错误,更容易维护。
  3. 可读性:async/await 通常更易读,尤其对于串行的异步操作,因为代码看起来更像同步代码。
  4. 使用场景:async/await 更适合于较新的 JavaScript 环境,而 Promise 在更广泛的环境中都可以使用。

链式调用是如何实现的

  • 这是通过在每个 .then() 或 await 后返回一个新的 Promise 对象来实现的。这个新的 Promise 对象会在上一个操作完成后解决,并传递给下一个操作。这种方式允许异步操作按照顺序执行,使得代码更易于阅读和理解。

47. promise的allsettled方法了解吗?

  • Promise.all(iterable):接收一个可迭代对象(通常是包含多个Promise的数组)作为参数,返回一个新的Promise。其在所有Promise都成功时才会成功(成功时,返回一个包含所有成功结果的数组。)。如果任何一个Promise失败,就会立刻失败。
  • Promise.allSettled(iterable): 与Promise.all()类似,接收一个可迭代对象作为参数,但不管Promise成功还是失败,它都会等待所有Promise都已经 settled(不再处于pending状态)才会返回。返回一个包含所有Promise的结果(无论成功或失败)的数组,每个结果都包含status属性来表示状态。
  • .all和.allsettled的区别
    • Promise.all()会在任何一个Promise失败时立即失败,而Promise.allSettled()会等待所有Promise都 settled 后返回结果,不管它们成功或失败
    • Promise.all()只有在所有Promise都成功时才会成功,而Promise.allSettled()不关心Promise的成功或失败,只要它们都 settled 了就会返回结果。

48. var、let、const的区别?

1. 变量提升

  • var声明的变量存在变量提升, 值为undefined
  • let和const不存在变量提升, 声明的变量一定要在声明后使用, 否则报错
    2. 重复声明
  • var允许重复声明变量
  • let 和 const 在同一作用域不允许重复声明变量
    3. 块级作用域
  • var 不存在块级作用域
  • let 和 const 存在块级作用域
    4. 修改声明的变量
  • var 和 let 可以修改声明的变量
  • const 声明一个只读的常量

49. Array.from()和Array.of()的区别

  • Array.from(); 将两类对象转为真正的数组, (类似数组的对象; 可遍历的对象), 还可以接受第二个参数, 对每个元素进行处理
  • Array.of(); 用于将一组值转换为数组, 没有参数的时候, 返回一个空数组, 参数只有一个的时候, 实际上指定的是数组的长度

50. set和WeakSet以及map和WeakMap

Set和WeakSet的区别;

  • WeakSet中成员只能是引用类型, 而不能是其他类型的值;
  • 且WeakSet中没有遍历操作的API
  • 没有Size属性
  • WeakSet里面的引用只要在外部消失,它在 WeakSet里面的引用就会自动消失

Map和WeakMap的区别;

  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
  • WeakMap没有遍历操作的API,
  • WeakMap没有clear清空方法
  • WeakMap的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

51. 什么是插槽?

  • slot插槽,一般在组件内部使用,封装组件时,在组件内部不确定该位置以何种形式的元素展示时,可以通过slot占据这个位置,该位置的元素需要父组件以内容形式传递过来,

  • slot插槽分类

  • 默认插槽:子组件用标签来确定渲染的位置,标签里面可以放DOM结构作为后备内容,当父组件在使用的时候,可以直接在子组件的标签内写入内容,该部分内容将插入子组件的标签位置。如果父组件使用的时候没有往插槽传入内容,后备内容就会显示在页面。

  • 具名插槽:子组件用name属性来表示插槽的名字,没有指定name的插槽,会有隐含的名称叫做 default。父组件中在使用时在默认插槽的基础上通过v-slot指令指定元素需要放在哪个插槽中,v-slot值为子组件插槽name属性值。使用v-slot指令指定元素放在哪个插槽中,必须配合元素,且一个元素只能对应一个预留的插槽,即不能多个 元素都使用v-slot指令指定相同的插槽。v-slot的简写是#,例如v-slot:header可以简写为#header。

  • 作用域插槽:子组件在标签上绑定props数据,以将子组件数据传给父组件使用。父组件获取插槽绑定 props 数据的方法:
    - scope=“接收的变量名”:
    - slot-scope=“接收的变量名”:
    - v-slot:插槽名=“接收的变量名”:<template v-slot:插槽名=“接收的变量名”>

52. Pinia

  • 响应式系统;Pinia 是为 Vue 3 设计的,它利用了 Vue 3 的 Composition API 和 Proxy 响应式系统。
  • 类型安全;Pinia 提供了更好的类型安全支持,因为它是使用 TypeScript 编写的,并且能够与 TypeScript 的类型系统良好集成。
  • 模块化; Pinia 引入了 “Store” 概念,每个 Store 都是独立的,可以更容易地组织和管理状态。这使得代码更加模块化和可维护。
  • 生态系统更加适合Vue3

53. 如何获取后端的数据? axios库是什么? 跨域要怎么实现?

  • 获取后端数据通常涉及前端的HTTP请求来向后端服务器请求数据
  • Axios是一个流行的JS库, 用于发起HTTP请求
  • 使用axios库来获取后端数据的步骤
    • 安装Axios库
    • 导入axios库
    • 发起HTTP请求; 使用Axios方法来发起HTTP请求, 例如使用axios.get()来用于GET请求, axios.post() 用于POST请求等
axios.get('后端API的URL')
  .then(response => {
    // 处理后端响应数据
    console.log(response.data);
  })
  .catch(error => {
    // 处理请求错误
    console.error(error);
  });

Axios是什么?

  • Axios是一个基于Promise的HTTP客户端库, 用于在浏览器和Node.js环境中发送HTTP请求,
  • Axios支持各种HTTP请求方法, 拦截请求和响应, 以及处理Promise

解决跨域问题的方法

  1. 后端设置CORS(跨域资源共享)
  2. 代理服务器
  3. JSONP
  4. CORS代理

54. 如果想要将本地存储就是localstorage、sessionstorage发送到后端呢?如何获取以及发送?

先获取本地存储中的数据, 然后通过Axios或其他HTTP库来将数据发送到后端

import axios from 'axios';

const dataToSend = localStorage.getItem("myData"); // 获取本地存储数据

// 创建一个包含要发送的数据的对象
const requestData = {
  data: dataToSend
};

// 使用Axios发送POST请求到后端
axios.post('后端API的URL', requestData)
  .then(response => {
    // 处理后端响应
    console.log(response.data);
  })
  .catch(error => {
    // 处理请求错误
    console.error(error);
  });

55. 如何用一个div实现一个圆圈⭕?如何触发事件后让这个⭕弹出来,覆盖在其他元素上面

考点主要是操作DOM元素

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #circle {
            width: 100px;
            height: 100px;
            background-color: red;
            border-radius: 50%;
            position: absolute; 
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            display: none;  
        }
    </style>
</head>
<body>
    <div id="circle" class="hidden"></div>
    <button id="showCircle">显示圆圈</button>

    <script>
        const circle = document.getElementById('circle')
        const showCircleButton = document.getElementById('showCircle')
        showCircleButton.addEventListener('click', () => {
            circle.style.display = 'block'
        })
    </script>
</body>
</html>

56. 事件冒泡是怎样的?他的好处是什么?

事件冒泡

  • 是一种事件传播机制, 它描述了事件从触发元素开始, 然后逐级向上(冒泡)传播到DOM树的根节点.
  • 这意味着当一个元素触发了某个事件时,该事件将在父元素、祖父元素等所有祖先元素上触发,直到达到DOM树的根节点或事件被取消冒泡(stopPropagation)为止。
  • 最大的好处
    • 事件委托; 通过事件冒泡, 你可以将事件处理程序附加到祖先元素, 而不是每个子元素上, 这样可以减少内存消耗并提高性能.

57. 一个前端项目应该怎么实现(同之前的开发流程)

  1. 需求分析; 包括功能、设计要求、用户体验等方面。
  2. 项目设置;选择开发工具、配置开发环境
  3. 创建项目结构;创建项目所需的目录结构,
  4. 编写代码;根据任务需求,编写HTML、CSS和JavaScript代码。
  5. 调试和测试;在本地开发环境中测试代码,确保功能正常运行。
  6. 性能优化;进行前端性能优化,包括文件压缩、资源缓存、代码拆分、延迟加载等。确保页面加载速度快且性能良好。
  7. 兼容性测试;在不同的浏览器和设备上进行兼容性测试,确保网站在各种环境下正常工作。
  8. 部署和上线;将项目部署到生产环境。这可能涉及上传文件到服务器、配置服务器环境、域名解析等操作。
  9. 监测和维护;监测上线后的网站性能和稳定性,解决任何潜在的问题。
  10. 文档编写;
  11. 任务交付和反馈;
  12. 迭代和改进:

58. 移动端适配(vw,vh,rem),你使用过哪个

移动端适配是确保网页在不同移动设备上具有一致的显示和用户体验

  • vw (视窗宽度单位)
    • vw表示视窗宽度的百分比(例如: 如果你将一个元素的宽度设置为 10vw,它将占据视窗宽度的10%。)
  • vh (视窗高度单位)
    • vh表示视窗高度的百分比(和vw类似, 其也是根据视窗高度来调整元素的大小或位置)
  • rem (根元素字体大小单位)
    • rem是相对于根元素(通常是元素)的字体大小的单位
    • 这使得在整个页面中可以轻松控制元素的大小和间距
    • 如根元素大小为2px, 自元素为2rem, 则子元素大小为 2 * 2px = 4px

59. typeof 与 instanceof的区别

typeof

  • typeof操作符返回一个字符串, 表示未经计算的操作数的类型
  • 其中null和[]进行判断的时候, 返回的结果就是’object’
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

instanceof

  • instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
object instanceof constructor
// 其中object为实例对象, constructor为构造函数

总结就是: 顺着原型链去找, 直到找到相同的原型对象, 返回true, 否则返回false

区别

  1. typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
  2. instanceof可以准确地判断复杂引用数据类型, 但是不能正确判断基础数据类型
  3. 而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

想要具体获取某个数据的类型

  • 可以使用Object.prototype.toString()

60. 场景题: 现有100个组件以及另外一个A组件,都注册在同一个页面中;

现在要求100个组件激活完成且最后一个激活完成的时候,A组件打印出123;
1、假设触发mounted就认为是激活
2、无法确定组件的激活顺序
3、无法确定各个组件的层级关系
4、假设A组件已经激活

实现方案一: 使用自定义事件和$emit方法来实现这个功能,

  • 首先,在A组件中创建一个方法,用于接收激活事件并检查是否所有组件都已激活。如果是,则打印出123。
<template>
  <!-- A组件的模板 -->
  <div>
    <!-- 内容 -->
  </div>
</template>

<script>
export default {
  mounted() {
    // 假设A组件已经激活
    this.$emit('component-activated');
  },
  methods: {
    checkActivation() {
      // 检查是否所有组件都已激活
      // 这里可以根据您的实际情况来编写检查逻辑
      // 例如,可以在组件激活时触发一个事件,并在这里进行监听
      // 如果100个组件都已激活,则打印123
      if (/* 所有组件都已激活的条件 */) {
        console.log(123);
      }
    }
  }
};
</script>
  • 在每个组件中, 当组件激活时触发一个自定义事件, 并在A组件中监听这些事件
<template>
  <!-- 组件的模板 -->
  <div>
    <!-- 内容 -->
  </div>
</template>

<script>
export default {
  mounted() {
    // 当组件激活时触发自定义事件
    this.$emit('component-activated');
  }
};
</script>
  • 在A组件中,监听所有组件的激活事件,当所有组件都激活时,调用checkActivation方法。
<script>
import AComponent from './AComponent.vue'; // 导入A组件
import Component1 from './Component1.vue'; // 导入其他组件
import Component2 from './Component2.vue';
// 导入其他99个组件

export default {
  components: {
    AComponent,
    Component1,
    Component2,
    // 导入其他99个组件
  },
  mounted() {
    // 监听所有组件的激活事件
    this.$on('component-activated', this.checkActivation);
  },
  methods: {
    checkActivation() {
      // 在这里检查是否所有组件都已激活,如果是,则打印123
      // 您需要实现检查逻辑,这可能涉及到一个计数器或其他方法来跟踪组件的激活状态
      if (/* 所有组件都已激活的条件 */) {
        console.log(123);
      }
    }
  }
};
</script>

实现方案二: 使用Vuex:您可以使用Vuex来管理组件的激活状态。创建一个存储状态的Vuex模块,然后在每个组件中触发一个mutation,将组件标记为已激活。当所有组件都已激活时,触发一个action,A组件监听该action并打印出123。

// 在Vuex中创建一个模块用于管理组件的激活状态
// store/modules/activation.js

const state = {
  componentsActivated: 0,
};

const mutations = {
  incrementActivation(state) {
    state.componentsActivated++;
  },
};

const actions = {
  checkActivation({ commit, state }) {
    if (state.componentsActivated === 100) {
      console.log(123);
    }
  },
};

export default {
  state,
  mutations,
  actions,
};

实现方案三: 使用事件总线:您可以创建一个事件总线实例来进行事件通信。每个组件在激活时触发一个自定义事件,然后在A组件中监听这些事件。当事件触发的次数达到100时,A组件打印出123。

<script>
export default {
  data() {
    return {
      activatedComponents: 0,
    };
  },
  mounted() {
    // 在A组件的mounted钩子函数中监听自定义事件
    this.$on('component-activated', this.handleComponentActivated);
  },
  methods: {
    handleComponentActivated() {
      // 当其他组件触发 'component-activated' 事件时,该方法会被调用
      // 在这里可以递增激活的组件数量
      this.activatedComponents++;

      // 检查是否所有组件都已激活
      if (this.activatedComponents === 100) {
        // 所有组件都已激活,可以执行打印123的操作
        console.log(123);
      }
    },
  },
};
</script>

61. 讲讲你知道的设计模式

设计模式是在软件工程中常见的解决特定问题的模板化解决方案。

  1. 单例模式 (Singleton Pattern)
    单例模式确保一个类只有一个实例,并提供了一个全局访问点。这在需要共享资源的情况下非常有用,如配置管理、数据库连接池等。
const Singleton = (function () {
  let instance;
  
  function createInstance() {
    // 私有构造函数
    function SingletonObject() {
      this.property1 = 'example';
    }
  
    return new SingletonObject();
  }
  
  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,只有一个实例
  1. 工厂模式 (Factory Pattern)
    工厂模式是一种创建模式,它将对象的创建过程封装起来,以便于管理和扩展。它包括简单工厂、工厂方法和抽象工厂等不同的变体。
class Product {
  constructor(name) {
    this.name = name;
  }
}

class ProductFactory {
  createProduct(name) {
    return new Product(name);
  }
}

const factory = new ProductFactory();
const product1 = factory.createProduct('Product 1');
const product2 = factory.createProduct('Product 2');
  1. 观察者模式 (Observer Pattern)
    观察者模式定义了一种一对多的依赖关系,允许多个观察者对象同时监听和响应某个主题对象的状态变化。常用于事件处理系统和GUI开发中。
class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  notifyObservers(message) {
    this.observers.forEach(observer => observer.update(message));
  }
}

class Observer {
  update(message) {
    console.log(`Received message: ${message}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers('Hello, observers!');
  1. 策略模式 (Strategy Pattern)
    策略模式定义了一系列算法,将它们封装成独立的策略对象,并使它们可以相互替换。这允许算法的选择和切换更加灵活。
class PaymentStrategy {
  pay(amount) {
    throw new Error('This method should be overridden by concrete strategies');
  }
}

class CreditCardPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid $${amount} using credit card.`);
  }
}

class PayPalPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid $${amount} using PayPal.`);
  }
}

const orderAmount = 100;
const creditCardPayment = new CreditCardPayment();
const payPalPayment = new PayPalPayment();

creditCardPayment.pay(orderAmount);
payPalPayment.pay(orderAmount);
  1. 装饰者模式 (Decorator Pattern)
    装饰者模式允许您通过将对象包装在装饰器类中来动态地为对象添加新的行为。这对于扩展现有类的功能非常有用,而不需要修改其代码。
class Coffee {
  cost() {
    return 5;
  }
}

class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost() + 2;
  }
}

class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost() + 1;
  }
}

const simpleCoffee = new Coffee();
const coffeeWithMilk = new MilkDecorator(simpleCoffee);
const coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk);

console.log(coffeeWithMilkAndSugar.cost()); // 8
  1. 适配器模式 (Adapter Pattern)
    适配器模式用于将一个接口转换成另一个接口,以便于不兼容的接口之间的协作。这通常用于集成外部库或系统。

  2. 模板方法模式 (Template Method Pattern)
    模板方法模式定义了一个算法的框架,将某些步骤的实现留给子类。它允许在不改变算法结构的情况下重新定义某些步骤。

  3. 建造者模式 (Builder Pattern)
    建造者模式用于创建复杂对象,它将对象的构建过程分解成多个步骤,以便于更灵活地构建对象。

  4. 命令模式 (Command Pattern)
    命令模式将请求封装成对象,使得可以参数化客户端对象,队列请求或记录请求,以及支持撤销操作。

  5. 状态模式 (State Pattern)
    状态模式允许对象在内部状态改变时改变其行为。这可以使得对象在不同状态下有不同的行为,而无需使用大量的条件语句。

62. import.meta.glob是什么语法?

import.meta.glob 是 JavaScript 中用于在 ES Modules(模块)中进行动态导入和处理文件的一种语法。它通常与 import 语句一起使用,用于获取匹配特定模式的模块文件的信息。这个语法通常在浏览器环境或Node.js中的模块中使用。

import.meta 是一个包含有关模块本身信息的特殊对象,其中包括 url 属性,该属性表示当前模块的 URL 地址。import.meta.glob 是一个用于处理模块文件的方法,它接受一个匹配文件模式的参数,并返回一个对象,该对象包含匹配模式的所有模块的信息。

说明了如何在 JavaScript 模块中使用 import.meta.glob

const files = import.meta.glob('./myFiles/*.js');

for (const path in files) {
  if (Object.hasOwnProperty.call(files, path)) {
    const module = files[path];
    module().then((moduleExports) => {
      // 使用模块导出
      console.log(moduleExports);
    });
  }
}

63. 说一说ajax,能自己封装一个http请求库么

  • Ajax(Asynchronous JavaScript and XML)是一种用于从Web浏览器向服务器发送HTTP请求并接收响应的技术。它通常使用XMLHttpRequest对象或现代浏览器提供的Fetch API来实现。
  • 实现
// HTTP请求库
function httpRequest(url, options) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(options.method || 'GET', url);

    // 设置请求头
    if (options.headers) {
      for (let header in options.headers) {
        xhr.setRequestHeader(header, options.headers[header]);
      }
    }

    // 处理响应
    xhr.onload = function () {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.statusText));
      }
    };

    // 处理网络错误
    xhr.onerror = function () {
      reject(new Error('Network Error'));
    };

    // 发送请求
    xhr.send(options.body || null);
  });
}

// 示例用法
const url = 'https://jsonplaceholder.typicode.com/posts/1';
httpRequest(url, { method: 'GET' })
  .then((response) => {
    console.log('成功:', response);
  })
  .catch((error) => {
    console.error('失败:', error);
  });

64. ajax, fetch, axios怎么解决兼容性问题

在处理Ajax请求时,可以使用不同的技术和工具,如原生的XMLHttpRequest,Fetch API和Axios。每种技术都有其自己的兼容性特点,下面是如何解决兼容性问题的一些方法:

  1. XMLHttpRequest
    • 原生的XMLHttpRequest在几乎所有现代浏览器中都有广泛的支持。兼容性通常不是问题。但需要注意的是,XMLHttpRequest的使用方式相对复杂,因此可能需要更多的代码来处理各种情况。
  2. Fetch API
    • Fetch API 是现代浏览器支持度很好的标准。然而,它在一些旧版本的浏览器中不受支持,如Internet Explorer。要解决这个问题,可以使用 “whatwg-fetch” 或 “isomorphic-fetch” 等polyfill来提供Fetch API的支持。这些polyfills可以在旧浏览器中模拟Fetch API的行为。
  3. Axios
    • Axios 是一个基于Promise的HTTP客户端,广泛用于处理Ajax请求。它不依赖于浏览器的原生特性,因此在现代浏览器和旧版本的浏览器中都可以使用。Axios自身包含了解决兼容性问题的逻辑,因此通常无需担心。
      具体来说,解决兼容性问题的方法包括:
  • Polyfills:对于Fetch API,可以使用polyfills来提供对旧浏览器的支持。Polyfills是JavaScript代码片段,通过模拟现代特性来实现兼容性。例如,“whatwg-fetch” 是一个Fetch API的polyfill。
  • 检测浏览器支持:在使用某个特性之前,可以检测浏览器是否支持它,如果不支持,可以采取备用措施。例如,你可以检查是否存在Fetch API,如果不存在,就使用XMLHttpRequest或使用polyfill。
    以下是一个使用polyfill来支持Fetch API的示例:
// 导入Fetch polyfill
import 'whatwg-fetch';

// 使用Fetch API
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('请求失败', error);
  });

要注意,Polyfills不仅可以用于Fetch API,还可以用于其他新特性和API的支持,以确保你的应用程序在各种浏览器中都能正常运行。

HR面试

1. 围绕实习和项目和我讨论应届生必备的素质

必备的素质

  • 技术能力; 实习和项目中所使用的技术栈和工具, 确保具备必要的技术基础
  • 问题解决能力; 分析、创新和解决问题的能力
  • 团队合作和沟通; 强调你的团队合作、沟通和协调能力。
  • 自主学习和自我驱动力; 提供你主动学习新知识和技能的例子,以展示你的自我驱动力。
  • 项目管理和组织能力; 说明你如何规划、执行和跟踪项目,以确保项目按计划完成。
  • 快速学习能力; 提供你适应新环境和快速学习新技能的例子。
  • 职业目标和愿景; 展示你对职业发展的清晰思考,并与公司的价值观相符。

2. 自己的优点和缺点

优点

  • 学习能力较强; 学习新知识和技能很有热情,容易适应不同领域和工具。
  • 团队合作; 我擅长与不同背景和个性的人合作。(擅长与不同背景和个性的人合作)
  • 沟通能力; 能够清晰地表达自己的想法和观点。
  • 自我驱动能力较强; 对工作和目标有很高的动力.

缺点

  • 过于自我要求; 有时,我会对自己有过高的期望,导致工作压力增加。
  • 细节精力不足; 有时会在关注大局时忽略细节, 正在努力改善这一点, 确保工作的全面性
  • 时间管理困难: 有时,我会在时间管理方面遇到困难,导致任务安排不合理。我正在学习更好地规划我的工作日程。
  • 公共演讲紧张: 虽然我擅长与他人交流,但在公共演讲时,我有时会感到紧张。我正在通过实践来克服这种不安全感。

3. 未来的规划

  • 职业短期目标;
    • 想要进一步学习后端开发, 让自己的技术变得更加的全面; 可以学到更多的技术
  • 职业中期目标;
    • 工作年限比较长的话,可能会向项目主管这个方向来进一步努力;
  • 适应性; 强调你的愿意适应和调整规划,以应对不断变化的职业和行业需求。

4. 给出三个你适合这个岗位的理由

  • 技术能力与经验; 我拥有比较丰富的前端开发经验, 在多个项目中都有使用过Vue.js框架, 在实习的时候也参与了一个与该岗位相关的大型Web应用开发项目
  • 团队合作和沟通能力较强; 比如在实习的时候, 在项目遇到bug的时候,我会和我师傅沟通问题, 比如某个bug, 究竟前端这边改会更合适还是后端那边更改会更合适呢? 有效的沟通可以确保项目能够按时的交付;
  • 问题解决能力较强; 面对问题我可以里面给出自己的建议, 并去执行它;
  • 每天坚持学习能力; 每天会坚持学习;

5. 大学期间比较有成就感的事

  1. 对待每次考试都会很认真, 所以相应的也获得了多次奖学金
  2. 参与院系里面的篮球比赛, 比了3次迎新杯,团队作战的全场跑感觉真的挺好的
  3. 参加了爱心社社团; 1. 组织防艾日活动 2. 多次参与志愿者活动(尤其是运动会) 3. 多次爱心捐款
  4. 努力考研上岸

6. 未来3-5年的规划

  1. 明确自己的职业目标; 个人还是想要走技术开发路线, 让自己的技术更加全面一点
  2. 定期评估自己; 了解自己的进展和不足之处, 反思并调整计划, 以确保自己能够朝着这个方向前进
  3. 还有就是保证自己的身体, 每周要抽出时间来锻炼, 保证身心健康和生活平衡

7. 除了技术,你想从我们公司得到什么?

  1. 学习机会和职业发展的机会; 因为我了解到卓望平台比较大, 所以希望能够在这个环境中帮助我不断成长
  2. 比较有挑战性的工作; 希望能从这个公司得到一些很有挑战性的项目和任务, 不断提升自己的能力
  3. 相应的机会; 比如说如果工作年限够长的话, 能够独立负责一个项目的部署、开发、与上线等;

8. 为什么选择我们公司?

  • 首先我了解到的卓望它是从事支持通信及互联网的软件开发, 以及相关系统集成的技术开发与服务技术的公司,
  • (卓望公司)历史比较久, 2000年的时候就已经成立了, 是中国移动的控股子公司, 平台和机会会比较多
  • 最重要的是: 我应聘的这个岗位和我的熟悉的技术是非常匹配的,所用的技术栈也是一模一样;
    • 因为前面的面试中也和两位技术面试官聊到用的是Vue技术栈
    • 然后之前的两次技术面试其实面试官对于基础技术都有一定的深挖, 它们的技术水平都是很高的, 所以想进来学到更多的东西;

9. 曾经做过最成功的一件事

  • 可以围绕自己的实习相关项目来说; 比如说当时遇到的困难; 以及自己的整个项目上线了;

    • 花了很多的心力, 做了什么样的创新, 取得了什么样的突破;
    • 项目的成功点 VS 公司和项目的点; 扣上公司的需求;
  • 我觉得最成功的一件事是暑期实习项目的成功上线; 因为当时实习的公司是一家创业公司, 当时开发成员只有我和一个带我的师傅, 因为人员比较少, 所以整个项目的开发任务都落在我们头上, 期间有遇到很多的技术问题, 我们都会在一起讨论, 尝试各种方案去解决这些问题, 最终项目也是在预定的时间3个月成功上线了第一版.

10. 你的缺点是什么?

  1. 不要和应聘的岗位特别冲突;
  • 过于自我要求; 有时,我会对自己有过高的期望,导致工作压力增加。
  • 细节精力不足; 有时会在关注大局时忽略细节, 正在努力改善这一点, 确保工作的全面性
  • 时间管理困难: 有时,我会在时间管理方面遇到困难,导致任务安排不合理。我正在学习更好地规划我的工作日程。
  • 公共演讲紧张: 虽然我擅长与他人交流,但在公共演讲时,我有时会感到紧张。我正在通过实践来克服这种不安全感。

11. 你还有什么问题要问我吗?

  • 加入贵公司以后, 对我们这种毕业生有什么样的培训, 有什么样的规划, 或者说在我未来的职业生涯中有怎样的晋升途径呢?
  • 就是面试有几轮, 因为之前看那个群里说有三次面试, 然后我之前已经面试过两次了, 加上今天的面试是第三次, 后面还有吗
  • 假如流程比较顺利的话, 可以提前实习吗?

杭州银行

1. JavaScript有哪些数据类型?

  • 见上方

2. cookie和session的区别?

  • 存储位置
    • Cookie; Cookie是在客户端中存储的小型文本文件;
    • Session; Session数据通常存储在服务器上;
  • 数据类型
    • Cookie; Cookie只能存储字符串数据,如会话令牌用户身份验证信息
    • Session; Session会话可以存储更复杂的数据结构,如对象、数组和其他数据类型
  • 安全性
    • Cookie; 存储在客户端,可以被用户查看和编辑(不应将敏感信息存储在Cookie中)
    • Session; 会话存储在服务器上,对于客户端来说是不可见的,因此更安全,可以用于存储敏感信息
  • 生命周期
    • Cookie; 可以设置Cookie的过期时间,使其在浏览器关闭后继续存在; (持久性Cookie),或者在会话结束后被删除(会话Cookie)。
    • Session; 会话数据通常在用户关闭浏览器之后自动删除
  • 跨页面通信
    • Cookie: Cookie 可以在不同页面之间共享数据,因为它们存储在客户端。
    • Session: 会话数据通常在服务器上存储,可以在同一会话中的不同页面之间共享,但通常不适用于不同会话之间的数据共享。
  • 资源消耗
    • Cookie; 由于Cookie存储在客户端,对服务器的资源开销较小
    • Session; 会话数据存储在服务器上,可能会对服务器的资源产生一定的负担,特别是在大量并发用户的情况下。

3. 浏览器是如何对 HTML5 的离线储存资源进行管理和加载?

通过离线Web应用程序功能, 允许开发者将Web应用程序的资源,如(HTML、CSS、JS、图像等)缓存到客户端的本地存储中,以便没有网络连接时仍能访问应用程序. 使用两个重要的API来实现

  • Application Cache
  • LocalStorage

步骤

  1. 创建应用程序缓存清单
  2. 浏览器下载并缓存资源
  3. 资源的离线访问
  4. 定期更新缓存
  5. 手动更新缓存
  6. 清楚应用程序缓存

4. linux如何查找文件内的某个内容?

  1. grep命令;
  • grep是一个用于在文本文件中搜索指定文本模式的强大工具; 可以通过以下命令来查找文件内的某个内容:
grep "要查找的内容" 文件名
  • 例如,要在一个名为example.txt的文件中查找包含字符串"关键词"的行,可以执行以下命令:
grep "关键词" example.txt

5. linux有哪些命令?

  • 文件和目录管理命令:

    • ls:列出目录中的文件和子目录。
    • pwd:显示当前工作目录的路径。
    • cd:改变当前工作目录。
    • touch:创建空文件或更新文件的时间戳。
    • mkdir:创建新目录。
    • rm:删除文件或目录。
    • cp:复制文件或目录。
    • mv:移动或重命名文件或目录。
    • find:在文件系统中搜索文件和目录。
    • grep:在文件中搜索文本模式。
    • cat:显示文件内容。
  • 用户和权限管理命令

    • who:显示当前登录用户列表。
    • whoami:显示当前用户名。
    • passwd:更改用户密码。
    • su:切换用户。
    • sudo:以超级用户权限执行命令。
    • chmod:修改文件或目录的权限。
  • 网络管理命令

    • ifconfig: 显示和配置网络接口
    • ping: 测试网络连接
    • ssh: 远程登录到其他计算机
  • 压缩和解压命令

    • zip和unzip: 用于创建和提取.zip文件

6. 数据库中有很多重复的数据, 需要使用什么关键字来进行筛选

  • SELECT DISTINCT: 如果你想要从表中选择唯一的值,可以使用 SELECT DISTINCT 关键字。例如,如果你有一个包含城市名称的表,并希望获取所有不同的城市名称,可以这样做:
SELECT DISTINCT city FROM cities;
  • GROUP BY 和HAVING: 使用GROUP BY子句将数据按特定列进行分组, 并使用HAVING子句来筛选符合条件的分组;
// 如果你想找到出现超过一次的城市名称:
SELECT city, COUNT(*) 
FROM cities
GROUP BY city
HAVING COUNT(*) > 1;
  • 唯一约束和主键; 如果你希望确保表中没有重复的数据,可以在适当的列上添加唯一约束(UNIQUE)或主键约束(PRIMARY KEY)。这将强制确保该列中的值都是唯一的。

京东

1. 实现一个深拷贝

/* 
    使用js来实现一个深拷贝,可以编写一个递归函数
    来复制对象的所有属性,包括嵌套对象
*/
function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj
    }
    // 根据对象的类型创建一个新的空对象或数组
    const copy = Array.isArray(obj) ? [] : {}
    // 遍历对象的属性,并递归进行深拷贝
    for (const key in obj) {
        // 用于检查一个对象是否拥有指定的属性; 
        if (Object.hasOwnProperty.call(obj, key)) {
            copy[key] = deepCopy(obj[key])
        }
    }
    return copy
}

// 示例用法:
const originalObject = {
    name: 'John',
    age: 30,
    address: {
        street: '123 Main St',
        city: 'Anytown'
    }
};

const copiedObject = deepCopy(originalObject);

console.log(originalObject); // 原始对象
console.log(copiedObject);   // 深拷贝后的对象

2. 创建一个单链表, 判断其是否成环

快慢指针的思想

class ListNode {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

function hasCycle(head) {
  if (!head || !head.next) {
    return false; // 没有节点或只有一个节点,肯定不成环
  }

  let slow = head;
  let fast = head;

  while (fast !== null && fast.next !== null) {
    slow = slow.next;       // 慢指针移动一步
    fast = fast.next.next;  // 快指针移动两步

    // 如果存在环,快指针最终会追上慢指针
    if (slow === fast) {
      return true;
    }
  }

  return false; // 没有找到环,链表不成环
}

// 示例用法:
const node1 = new ListNode(1);
const node2 = new ListNode(2);
const node3 = new ListNode(3);

node1.next = node2;
node2.next = node3;
node3.next = node2; // 创建一个环

console.log(hasCycle(node1)); // 输出 true,因为链表成环

多抓鱼

1. 判断输入的字符是不是Good Word

1. 假设字符串中出现最多的字母出现的次数是maxn,最少的字母出现的次数是minn
2.若maxn除以minn的结果是一个大于1的整数,则是 Good Word
3. 若是Good Word则返回true,否则返回false
4. 字符串中只包含英文字母
例如: 输入"duozhuayu"     输出"true"

代码实现

function isGoodWord(word) {
  // 创建一个对象来存储每个字母的出现次数
  const letterCount = {};

  // 遍历字符串,统计每个字母的出现次数
  for (let i = 0; i < word.length; i++) {
    const letter = word[i].toLowerCase(); // 将字母转换为小写以区分大小写
    if (/[a-z]/.test(letter)) {
      // 仅处理英文字母
      if (letterCount[letter]) {
        letterCount[letter]++;
      } else {
        letterCount[letter] = 1;
      }
    }
  }

  // 找到最多和最少的字母出现次数
  let maxCount = 0;
  let minCount = Infinity;

  for (const letter in letterCount) {
    const count = letterCount[letter];
    if (count > maxCount) {
      maxCount = count;
    }
    if (count < minCount) {
      minCount = count;
    }
  }

  // 判断是否是 Good Word
  if (maxCount % minCount === 0 && maxCount / minCount > 1) {
    return true;
  } else {
    return false;
  }
}

// 测试函数
console.log(isGoodWord("duozhuayu")); // 输出 true
console.log(isGoodWord("hello"));     // 输出 false

2. 将两个升序的单链表合并为一个整体升序的单链表

例如: 输入{1, 3, 5}, {2, 4}  输出{1, 2, 3, 4, 5}

代码实现

class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}
function mergeSortedLists(l1, l2) {
  const dummyHead = new ListNode(0); // 创建一个虚拟头节点
  let current = dummyHead; // 初始化一个指针用于构建合并后的链表
  while (l1 !== null && l2 !== null) {
    if (l1.val < l2.val) {
      current.next = l1; // 连接 l1 到合并链表
      l1 = l1.next; // 移动 l1 指针
    } else {
      current.next = l2; // 连接 l2 到合并链表
      l2 = l2.next; // 移动 l2 指针
    }
    current = current.next; // 移动合并链表的指针
  }

  // 如果其中一个链表已经处理完,直接连接剩余的链表
  if (l1 !== null) {
    current.next = l1;
  } else {
    current.next = l2;
  }
  return dummyHead.next; // 返回合并后的链表的头节点
}

3. 解决压缩字符串的规则问题

1. 如果字母x连续出现n次, 则表示为(a)n
2. 可以表示嵌套,比如((a)2(b)2)2表示的是aabbaabb的压缩后结果
3. 只出现一次的字母不进行压缩,a的压缩后的结果仍然为a
输入为: 一个字符串的压缩结果,请输出压缩前的字符串
例如输入:"(a)2(b)2(c)2"  输出"aabbcc"
代码实现
function decompressString(compressed) {
  let index = 0; // 当前处理字符的索引

  function helper() {
    let result = ''; // 用于存储解压后的字符串

    while (index < compressed.length) {
      const char = compressed[index];
      index++;

      if (char === '(') {
        // 遇到左括号,递归处理括号内的内容
        const subResult = helper();
        const repeatCount = parseInt(compressed[index]);
        index++; // 跳过数字
        result += subResult.repeat(repeatCount);
      } else if (char === ')') {
        // 遇到右括号,返回当前括号内的结果
        return result;
      } else {
        // 遇到普通字符,直接添加到结果中
        result += char;
      }
    }
    return result;
  }
  return helper();
}

同有科技(2023年10月7日)

1. 图片懒加载的底层实现原理是什么?

  • 总结: 图片懒加载的底层实现原理是通过监听滚动事件, 计算图片位置, 根据需要加载可见区域内的图片, 并缓存已加载的图片,以提高页面加载性能和用户体验.
  • 核心思想: 将图片的加载延迟到用户需要查看它们的时候
    • 初始加载; 在网页初始加载时,只加载首屏可见区域的图片,而不是所有图片。
    • 监听滚动事件;
    • 判断图片位置; (当滚动事件触发时, 代码会计算每张图片相对于视口的位置)
    • 加载可见图片; 如果计算出某张图片进入了可见区域,就触发加载该图片的操作。
    • 缓存加载过的图片;(一旦图片加载过, 通常会将它们缓存起来)
    • 滚动事件性能优化; (使用防抖和节流以限制事件处理函数的执行频率)

2. Vue.$nextTick()的底层实现原理是什么?

$nextTick()主要关注的是DOM更新的时机, 其底层实现原理是基于浏览器的事件循环机制, 在下一个事件循环周期中执行传递的回调函数,以确保在DOM更新队列中的任务都已经完成。
实现的基本原理

  • DOM更新队列; Vue.js内部维护一个DOM更新队列,用于存储需要在下一次事件循环中更新到DOM的任务。
  • 触发状态改变, 当Vue组件的数据发生变化(状态改变)时,Vue会将需要更新的任务添加到DOM更新队列中。
  • 事件循环; 当Vue组件的数据发生变化(状态改变)时,Vue会将需要更新的任务添加到DOM更新队列中。
  • $nextTick的使用:当你调用Vue.$nextTick(callback)时,它实际上是在Vue内部的事件循环周期中注册了一个微任务(Microtask)或宏任务(Macrotask),具体取决于浏览器的实现。这个微任务或宏任务会在当前事件循环周期结束后执行,确保在DOM更新队列中的任务都已经完成。
  • 回调执行; 一旦事件循环中的当前任务完成,Vue会执行你传递给$nextTick的回调函数,这意味着此时DOM已经更新完成。

3. TS中的pick()函数的使用

在TS中, 如果你想从一个对象中选取指定的属性, 可以使用Pick类型来实现这个功能

  • Pick 类型允许你从一个对象类型中选取部分属性,并返回一个新的类型,只包含选取的属性。
type MyType = {
  name: string;
  age: number;
  address: string;
};

type MyPickedType = Pick<MyType, "name" | "age">;

const myObject: MyPickedType = {
  name: "John",
  age: 30,
};

console.log(myObject); // { name: 'John', age: 30 }

酷家乐

1. React-router这一块你有没有遇到就是在跳转的时候,有没有白屏这种情况,尤其是路由切换的时候,是怎么处理的

白屏问题在路由切换时可能出现,这通常是因为路由切换时需要加载新的组件或数据,而加载时间较长可能会导致较大的延迟;

  1. 代码分割; 通过Webpack或React.lazy和Suspense等React特性,将应用拆分为更小的代码块。这样在路由切换时,只需加载当前页面所需的代码,而不是一次性加载整个应用。
  2. 预加载(Preloading):使用Webpack或React.lazy的React.Suspense的fallback属性,可以提前加载将要显示的组件,避免在切换路由时因加载慢而造成白屏。
  3. 资源优化:确保你的代码和资源进行了优化,例如图片压缩、代码精简、使用CDN等,以加快加载速度。
  4. Loading状态:在路由切换时显示加载动画或状态,让用户知道应用正在加载内容。
  5. 浏览器缓存:利用浏览器缓存机制,合理设置缓存策略,减少重复加载资源。

2. 假设你某一个单页面,比较大,我iport的时间比较旧的话,这种场景,如何让他变得体验好一点

当一个单页面应用(SPA)很大且导入时间较长时,为了提升用户体验,有一些策略可以考虑:

  1. 代码分割(Code Splitting):利用Webpack或者动态导入(dynamic imports)等技术,将应用拆分成更小的模块。这样可以将用户首次访问需要的代码和资源减少到最小,延迟加载其他部分。
  2. 懒加载(Lazy Loading):延迟加载不是立即需要的组件、路由或资源。React.lazy和Suspense可以帮助实现懒加载,只在需要时加载相应的模块或页面。
  3. 预加载(Preloading):根据用户行为预加载即将访问的页面或资源。使用<link rel="preload">标签或者Webpack的prefetchpreload功能,提前加载可能需要的资源,优化未来的加载速度。
  4. 缓存策略:利用浏览器缓存和服务端缓存技术,合理设置缓存策略,减少重复加载相同资源的时间。
  5. 服务端渲染(Server-Side Rendering):考虑使用服务端渲染框架(如Next.js或Nuxt.js),它们可以加速首屏渲染,提高页面加载速度。
  6. 性能优化:对代码和资源进行优化,包括压缩、精简代码,使用图像压缩等技术,以减少加载时间。
  7. 加载状态提示:在加载时间较长的情况下,提供合适的加载状态提示,比如加载动画、占位符或加载进度条,让用户知道应用正在处理数据。

3. 讲讲你图片懒加载的策略,为什么用这个策略,有什么考虑吗

图片懒加载是一种优化网页性能的策略,它延迟加载页面上的图片,只有当图片即将进入用户视野时才加载,而不是在页面加载完成后立即加载所有图片。这样可以减少初始加载时间和带宽占用,并提升用户体验。

策略和考虑点包括:

  1. 降低初始加载时间:在页面加载时,只加载首屏或用户即将看到的图片,而延迟加载其他图片,从而加快初始加载速度。特别是对于较大数量或较大尺寸的图片,这个优化效果尤为明显。
  2. 节省带宽:延迟加载图片可以减少初始页面加载时的网络请求量,节省用户的带宽消耗。这对移动端用户尤其重要,因为他们可能面临网络速度较慢或者流量限制的情况。
  3. 提升用户体验:通过懒加载图片,可以更快地展示页面内容,让用户能够更快地开始浏览和与页面交互。这可以提升用户对页面响应速度的感知。
  4. 优化性能:懒加载可以减少不必要的资源请求,降低了页面的整体资源消耗,从而有助于提高页面的加载速度和性能表现。

懒加载的实现通常使用JavaScript来监测图片元素的位置和用户滚动行为。一般会将图片的真实src属性替换为一个占位符(如空白的base64图片),然后通过Intersection Observer API或监听scroll事件等方法,当图片进入用户视野时,再将真实的图片路径赋值给src属性,触发图片加载。

3. 在图片懒加载中,判断是否加载图片

  1. 视口范围: 判断图片是否在用户的视口(可见区域)内。如果图片在视口内,可以考虑开始加载;否则,暂不加载,直到用户将其滚动到可见范围内。
  2. 滚动事件监听: 监听滚动事件,检测图片是否进入视口范围。当用户滚动页面时,检查图片的位置是否在视口内,若在则触发图片加载。
  3. 计算元素位置: 使用 JavaScript 获取图片元素的位置信息(比如 getBoundingClientRect()),并与视口的位置信息进行比较,判断图片是否在可见范围内。
  4. 交叉观察器(Intersection Observer): 使用浏览器提供的 Intersection Observer API,它可以异步地观察目标元素(比如图片)与其父元素或根元素的交集情况,从而判断图片是否在视口范围内。
    基于以上方法中的一种或多种,可以判断图片是否在用户的视口内,从而决定是否加载图片。这种方式可以提高页面加载性能,因为仅当用户需要查看图片时才会加载,而不是一开始就加载所有图片。

4. 这样一个情况,有一个图片,这个图片是有一个高度的,我加载这个图片的一瞬间,会把我们图片内容给顶到下面去。比如:本来我想点一个按钮,但是一开始不知道哪里有一个图片懒加载,让我点按钮的一瞬间,图片加载出来了,把按钮顶到下面去了。这考虑国就是,图片加载会引起页面布局的变化你应该了解吗?有什么办法去解决吗?拿图片举例,有的方案会有一个纯色的底图,他会在懒加载出来之后,给你替换掉,有知道是怎么去实现的吗?

图片加载引起页面布局变化是常见的问题,特别是在图片高度未知或者未设置的情况下。这种情况可能导致内容的位移,给用户带来不良体验。解决这个问题的方法有几种:

  1. 设置固定尺寸:在加载图片之前,可以为图片元素设置固定的宽度和高度。这样即使图片加载时会占据相应的空间,避免了因加载图片而引起的布局变化。但是这种方法要求提前知道图片的尺寸,对于动态内容或响应式设计不太适用。
  2. 使用占位符:可以在图片加载之前,使用占位符(placeholder)来占据图片即将出现的空间。这个占位符可以是一个纯色的背景、图片尺寸相同但内容为空的元素,或者是一张模糊的预览图。这样可以在图片加载完成之前,保持页面布局的稳定性,避免内容抖动。
  3. 懒加载技术:使用懒加载技术,例如Intersection Observer API或者自定义JavaScript来控制图片加载时机。当图片即将进入用户视野时再开始加载,这样可以减少图片加载对页面布局的影响。
  4. 优化加载方式:如果可能的话,可以考虑将图片进行优化,减小图片尺寸、压缩图片大小,以降低加载时间,从而减少对布局的影响。
  5. 渐进式加载:使用渐进式加载技术,首先加载低分辨率或模糊的预览图,然后逐步加载高清图像。这样在加载过程中可以让用户感知到图片的加载,避免了突然改变页面布局的情况。

5. 有遇到过,有大量的图片在一个区域内展示,在加载完的时候,他就是一大串图片在这展示,但是在没加载完的时候,他们的节点就是会都挤在一起,懒加载会失效,因为这时候observe的判断,就会认为你的图片都在可加范围内,还是会导致大量的请求

这是一个常见的问题,特别是在大量图片需要加载,并且这些图片都在一个区域内展示时。当图片尚未加载完成时,它们的节点会根据默认尺寸或未加载图片的大小预留空间,导致布局错乱,并可能使得懒加载策略失效。

这里有几种解决方法:

  1. 虚拟滚动(Virtual Scrolling):只加载可见区域内的图片,而不是一次性加载所有图片。这可以通过一些库或组件(例如React Virtualized、react-window、vue-virtual-scroller)实现,它们会根据滚动位置动态加载图片,避免一次性加载大量图片。
  2. 图片占位符:在图片加载前使用占位符,确保在未加载完成时占据正确的空间。可以使用纯色占位符、模糊的预览图像或者图片尺寸相同但内容为空的元素来作为占位符,避免因图片加载引起的布局变化。
  3. 懒加载策略优化:如果使用Intersection Observer API进行懒加载,可以调整触发加载的条件。可以尝试延迟触发加载,确保只有当图片进入用户视野时才开始加载,而不是基于默认的可见性范围。
  4. 分批加载:将大量图片分批加载,逐步增加可见区域内的图片数量。这样可以减少一次性请求过多资源,避免页面因加载延迟而布局错乱。

6. 分页加载机制你能大概讲讲吗

分页加载机制是一种用于处理大量数据的常见策略,特别是在Web应用中展示数据时。它通过分批次获取数据并逐步呈现给用户,以提高页面加载速度和用户体验。

基本概念包括:

  1. 数据分页:将大量数据分割成较小的页面或数据块。每页通常包含固定数量的数据条目,例如每页显示 10、20 或更多条数据。
  2. 前端展示:在前端界面中,用户浏览数据时,首先展示第一页的数据。随着用户向下滚动或点击“下一页”等交互动作,逐渐加载和展示其他页的数据。
  3. 分页控制:通常有按钮或者滚动触发的方式,用户可以请求下一页或者上一页的数据。这些控制组件通常与后端API交互,请求相应页数的数据。
  4. 服务端支持:后端服务必须支持分页查询,接收前端传来的页码或偏移量,并返回相应页数的数据。通常通过查询参数(如pagelimit)来控制。
    实现分页加载的常见方式包括:
  • 传统分页:通过后端的API接口,传递页码和每页条目数量的参数来请求数据。前端接收到数据后,更新页面内容。
  • 滚动加载(Infinite Scroll):当用户滚动到页面底部时,自动加载下一页数据。这种方式可以给用户一种无限加载数据的体验,但需要注意合理控制加载触发时机,避免给用户过多的数据和不必要的滚动。
  • 触发式加载(Load More):在页面底部显示一个“加载更多”的按钮或其他触发方式,用户点击后加载下一页数据。

分页加载机制有助于减少初始加载时间,提高页面加载速度,并在用户交互时动态加载数据,提供更流畅的用户体验。

7. 优化方式还有那些

在前端开发中,有许多优化策略可以帮助提升性能、用户体验和代码质量。一些常见的优化方式包括:

  1. 代码分割(Code Splitting):将代码拆分成更小的模块,按需加载,以减少初始加载时间,提高页面性能。
  2. 懒加载(Lazy Loading):延迟加载不是立即需要的资源,例如图片、组件或数据,以减少页面加载时间。
  3. 缓存优化:利用浏览器缓存和HTTP缓存,合理设置缓存策略,减少重复请求,提高页面加载速度。
  4. 网络请求优化:合并请求、减少重定向、使用CDN等方式优化网络请求,减少网络延迟,加快页面加载速度。
  5. 图片优化:压缩图片、使用合适的图片格式、懒加载图片等方式减少图片大小和数量,提高页面加载速度。
  6. 性能监控和分析:使用工具监控页面性能,并进行分析,找出并解决性能瓶颈,优化页面加载速度和用户体验。
  7. 前端框架优化:针对所使用的前端框架,遵循最佳实践、优化渲染逻辑和组件性能,提高页面渲染速度。
  8. CSS和JS优化:减少CSS和JS文件大小、避免不必要的样式和脚本、压缩代码等方式来提高加载速度。
  9. 响应式设计优化:针对不同设备和屏幕尺寸优化页面布局和资源加载,提供更好的用户体验。
  10. 安全性优化:确保代码安全,防止XSS、CSRF等攻击,保障用户信息安全。
  11. 可访问性优化:遵循无障碍设计原则,使页面对所有用户包括残障人士都更易访问和使用。
  12. 持续优化:持续追踪性能指标,不断优化和改进页面,确保页面性能和用户体验保持在一个良好状态。
    这些优化策略可以根据具体项目和需求相互结合,综合使用,以提高网站性能、用户体验和代码质量。

8. 服务器渲染

服务器渲染是指在服务器端生成网页内容并将其发送到客户端的过程。这种技术可以提供更快的页面加载速度和更好的搜索引擎优化(SEO),因为在将内容发送到浏览器之前,服务器已经生成了完整的HTML文档。
在前端面试中,可能会问到服务器渲染的相关问题,这里列出一些可能的问题和相关的答案:

  1. 什么是服务器渲染(Server-Side Rendering,SSR)?
    • SSR是指在服务器上生成网页内容,并将已渲染好的HTML文档发送到客户端,与传统的客户端渲染(Client-Side Rendering)相对。
  2. 与客户端渲染相比,服务器渲染有哪些优势?
    • 更好的SEO:搜索引擎可以更轻松地索引页面内容。
    • 更快的首次加载时间:客户端接收到的是已经渲染好的HTML,而不是仅有JavaScript的框架,因此可以更快地显示内容。
    • 更好的性能:尤其是对于初始页面加载,因为不需要等待JavaScript的下载和执行。
  3. 服务器渲染的实现方式有哪些?
    • 模板引擎:例如使用EJS、Handlebars等在服务器端生成HTML。
    • 后端框架:像Next.js(基于React)、Nuxt.js(基于Vue)这样的框架提供了服务器渲染的支持。
    • 自定义服务器端渲染:编写自己的服务器端代码来处理渲染和路由逻辑。
  4. 有没有听说过SPA(Single Page Application)和SSR的混合应用?
    • 是的,这种混合应用被称为“同构应用”(Isomorphic/Universal Applications),它们在服务器端和客户端都执行渲染。通常在首次加载时使用服务器渲染,然后在之后的页面导航中使用客户端渲染。
  5. 服务器渲染的局限性是什么?
    • 服务器压力:每次请求都要在服务器上进行渲染,可能增加服务器负载。
    • 复杂性增加:实现服务器渲染可能需要更多的配置和考虑,因为涉及到服务器端的逻辑。
  6. 怎么选择何时使用服务器渲染或客户端渲染?
    • 如果对SEO和首次加载性能要求较高,可以选择服务器渲染。
    • 如果应用程序依赖于大量的客户端交互、实时数据更新等,客户端渲染可能更合适。
      这些问题可以帮助你对服务器渲染有一个基本的了解,但是要根据具体情况和实际经验来回答更加完整。

9. 移动端适配这里,分辨率的适配是怎么做的。有接触过媒体查询吗;物理像素与逻辑像素的关系

分辨率的适配方式:

  • 响应式设计(Responsive Design): 这种方式使用CSS媒体查询来根据设备的屏幕宽度和高度应用不同的样式。可以使用@media查询针对不同的屏幕尺寸和方向应用不同的CSS规则,使页面在不同分辨率下自适应。
  • Viewport设置: 设置标签中的viewport可以控制页面在移动设备上的显示效果。例如,可以让页面的宽度与设备的宽度相匹配,并且初始缩放为1。
  • 适配不同像素密度: 使用像素密度相关的CSS单位(如rem、em、vw、vh等),以及srcset属性来提供不同像素密度的图片版本。

媒体查询;可以通过max-width和min-width来处理;

物理像素与逻辑像素的关系:

  • 物理像素(Physical Pixels): 是显示设备上的实际物理像素点,它们组成了屏幕的实际显示内容。
  • 逻辑像素(CSS Pixels): 是网页或应用布局使用的抽象像素单位,与设备的物理像素不完全对应。逻辑像素通常是为了在不同分辨率和屏幕尺寸下提供一致的显示效果而引入的。
  • 设备像素比(Device Pixel Ratio,DPR)是物理像素与逻辑像素之间的比率。例如,一个设备像素比为2的设备,表示每个逻辑像素对应着2x2的物理像素网格。这种机制让高像素密度的屏幕在相同尺寸下显示更多的内容或更清晰的图像。

10. 浏览器渲染的过程,就是CSS与HTML是怎么结合的

浏览器渲染过程涉及将HTML、CSS和JavaScript转换为可视化的网页。CSS与HTML是如何结合的可以被概括为以下几个步骤:

  1. 构建DOM(Document Object Model): 浏览器接收到HTML文档后,会解析HTML代码并构建DOM树。DOM是网页的结构表示,它描述了文档的层次结构,即HTML元素之间的关系。
  2. 解析CSS: 浏览器会解析CSS文件,构建CSSOM(CSS Object Model)。CSSOM是用于描述文档样式和布局的抽象表示。浏览器将CSS规则与DOM树中的元素进行匹配,形成渲染树(Render Tree)。
  3. 生成渲染树: 渲染树是DOM树和CSSOM的结合,它包含了需要显示在页面上的所有元素和它们的样式信息。但它并不包括那些在渲染过程中不会显示的元素,比如<head><script>等。
  4. 布局计算(Layout): 浏览器根据渲染树中的元素信息(比如大小、位置等)进行布局计算。这个阶段确定了每个元素在页面中的确切位置。
  5. 绘制(Painting): 浏览器根据渲染树和布局信息开始将页面上的元素绘制到屏幕上。这个阶段包括了绘制元素的边框、背景颜色、文字等内容。
  6. 合成(Compositing): 在某些情况下,浏览器会对不同图层(层叠上下文)进行合成,这可以优化页面的渲染性能,尤其是在进行动画或滚动时。
    CSS与HTML的结合主要发生在构建渲染树的阶段,浏览器将DOM树和CSSOM树结合起来形成渲染树,这个树决定了页面上哪些内容将被渲染,并且以怎样的样式呈现出来。最终,浏览器根据渲染树进行布局、绘制和合成,将网页内容呈现给用户。

10. 浏览器事件代理机制

浏览器事件机制描述了当特定事件发生时,浏览器是如何处理和传递这些事件的。这个机制包含了事件的捕获阶段、目标阶段和冒泡阶段。

  1. 事件捕获阶段(Capture Phase):
    • 事件从文档的根节点(window 对象)开始传播到目标元素之前的阶段。
    • 事件按照从外向内的顺序经历父元素、祖先元素,直到达到目标元素之前。
  2. 目标阶段(Target Phase):
    • 事件达到目标元素。
    • 事件被触发在目标元素上,并在目标元素上执行相关的事件处理函数。
  3. 事件冒泡阶段(Bubbling Phase):
    • 事件从目标元素开始,向外传播至文档的根节点(window 对象)的阶段。
    • 事件按照从内向外的顺序经历目标元素的父元素、祖先元素,直到达到文档的根节点。
      在这个过程中,每个阶段都可以有对应的事件处理函数。事件捕获阶段和事件冒泡阶段可以利用事件委托的方式来进行事件处理。
      事件处理程序:
  • 可以通过 addEventListener 方法添加事件处理程序。
  • 事件处理程序可以通过 event 对象访问事件相关的信息,比如事件目标、触发元素等。
  • 可以使用 event.stopPropagation() 阻止事件的进一步传播。
  • 可以使用 event.preventDefault() 阻止事件的默认行为。

11. script标签的async与defer

<script> 标签的 asyncdefer 属性都是用来控制脚本加载和执行的行为,特别是在页面加载时对脚本的处理方式。它们对脚本的执行时机有不同的影响:

  1. async 属性:
    • 异步加载脚本。脚本的下载和执行不会阻止页面的解析和渲染。
    • 脚本的下载和页面的渲染是并行进行的。一旦脚本下载完毕,会立即执行。
    • 多个 async 脚本之间的执行顺序是不确定的,取决于下载完成的顺序。
<script src="example.js" async></script>
  1. defer 属性:
    • 延迟加载脚本。与 async 不同,defer 告诉浏览器立即下载脚本,但是延迟执行脚本直到文档解析完成。
    • 多个 defer 脚本会按照它们在文档中的顺序执行。
<script src="example.js" defer></script>

关于使用这两个属性的一些建议:

  • 如果脚本不依赖于页面加载过程中的其他元素,并且它不会影响页面的展示和行为,可以使用 async,特别是在需要尽快下载并执行脚本的情况下(如一些统计分析脚本)。
  • 如果脚本依赖于页面结构,或者需要在页面加载后立即执行,但又不想阻塞页面的解析和渲染,可以考虑使用 defer
  • 如果需要确保脚本按照顺序执行,并且在文档完全解析后执行,defer 更适合,因为它保证了脚本执行的顺序。
    总的来说,asyncdefer 都是用来优化脚本加载和执行的方式,但使用它们需要根据具体情况和脚本的依赖关系来决定,以达到更好的性能和用户体验。

12. 手写:一个数组,进行删除,然后我想现在有一个操作就是删除其中的一个元素,怎么去做?那我不想影响原始的数组,我想构建一个新的数组,这个怎么做?假设 我不关心数组的位置,我删掉以后,数组元素的位置他变不变也无所谓,有没有什么办法在o(1)的复杂度完成这个动作。就是我删除索引为3的数值,我不想遍历它,就让他的复杂度为o(1)。不关心顺序

  • 在普通的数组数据结构中,通常无法在常量时间内完成删除操作(O(1))。通常,删除元素涉及移动数组中被删除元素之后的所有元素,以便填补删除的位置。这个移动操作的复杂度是O(n),其中n是数组中元素的数量。
  • 但是,如果不关心元素的顺序,并且删除的位置无关紧要,你可以考虑使用一个特殊的技巧:将待删除元素和最后一个元素交换位置,然后将数组长度缩减1,这样就能在O(1)时间内完成删除操作。
let arr = [1, 2, 3, 4, 5];
const indexToDelete = 3; // 要删除的索引

if (indexToDelete !== arr.length - 1) {
  // 将待删除元素和最后一个元素交换位置
  [arr[indexToDelete], arr[arr.length - 1]] = [arr[arr.length - 1], arr[indexToDelete]];
}

arr.pop(); // 删除最后一个元素,时间复杂度为O(1)

// 现在 arr 变成 [1, 2, 3, 5]
  • 这个方法只在删除最后一个元素时达到了O(1)的时间复杂度,而其他情况下的时间复杂度是O(n)。虽然这种方式不会保持原始数组的顺序,但它在特定场景下可能是有效的。

14. TS的价值;泛型

  • 在 TypeScript 中,泛型(Generics)是一种参数化类型的机制,允许在定义函数、类、接口时使用类型变量。这种参数化能力使得这些数据类型能够在定义时不指定具体类型,而是在使用时动态地确定类型。
  • 泛型的主要目的是提高代码的复用性、灵活性和类型安全性。它允许开发者编写更通用、更灵活的函数、类、接口等,从而适用于多种类型而不失去类型检查和推断。
  • 泛型的用途:
  1. 函数中使用泛型: 可以创建可以处理多种类型的函数,增加了函数的灵活性。

    function identity<T>(arg: T): T {
      return arg;
    }
    let output = identity<string>("hello"); // output 类型为 string
    
  2. 类中使用泛型: 可以创建具有泛型类型成员的类,使其能够与多种类型协同工作。

    class Box<T> {
      value: T;
      constructor(value: T) {
        this.value = value;
      }
    }
    let numberBox = new Box<number>(10); // numberBox 的值为 number 类型
    
  3. 接口中使用泛型: 可以创建泛型接口,使其能够适应不同的数据类型。

    interface Pair<T, U> {
      first: T;
      second: U;
    }
    let pair: Pair<number, string> = { first: 1, second: "two" }; // pair 中存储的是数字和字符串的键值对
    

泛型让代码更具通用性和可维护性,通过将类型参数化,能够在不同场景下复用代码,并在编译时保持类型的安全性,避免一些潜在的类型错误。

15. 我有一个二维数组,他的值是几,高度就是几,求问这些块级的表面积。边界考虑

这个问题需要计算二维数组中每个非零元素的表面积,考虑其上、下、左、右四个方向的相邻元素情况。每个非零元素的表面积由其自身的高度以及周围相邻元素对其造成的遮挡部分构成。
以下是求解的一个基本思路:

  1. 遍历二维数组,针对每个非零元素,计算其上、下、左、右四个方向的表面积贡献。
  2. 对于每个非零元素,先加上自身的高度乘以4,表示未被遮挡时的表面积。
  3. 然后减去上、下、左、右四个方向上的被遮挡的表面积。被遮挡的表面积由两个相邻元素高度的较小值所决定。
    下面是一个 JavaScript 实现的例子:
function surfaceArea(grid) {
  const rows = grid.length;
  const cols = grid[0].length;
  let area = 0;

  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      const height = grid[i][j];

      if (height > 0) {
        area += height * 4 + 2; // 初始表面积,每个非零元素都有6个面积

        // 减去被遮挡的表面积
        if (i > 0) area -= Math.min(height, grid[i - 1][j]) * 2; // 上方遮挡
        if (j > 0) area -= Math.min(height, grid[i][j - 1]) * 2; // 左方遮挡
      }
    }
  }
  return area;
}
// 示例二维数组
const grid = [
  [1, 3, 4],
  [2, 2, 3],
  [1, 2, 4],
];
console.log(surfaceArea(grid)); // 输出表面积

16. 手写: 反转链表

class ListNode {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}
function reverseLinkedList(head) {
  let prev = null;
  let current = head;

  while (current !== null) {
    const nextTemp = current.next;
    current.next = prev;
    prev = current;
    current = nextTemp;
  }
  return prev;
}

17. 手写:数组乱序输出

洗牌算法; 对数组进行乱序处理,可以有效地随机打乱数组的顺序;

function shuffleArray(arr) {
  const array = [...arr]; // ...表示展开运算符

  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // 生成随机索引

    // 交换当前元素和随机位置元素
    [array[i], array[j]] = [array[j], array[i]];
  }

  return array;
}

// 示例用法
const originalArray = [1, 2, 3, 4, 5];
const shuffledArray = shuffleArray(originalArray);

console.log(shuffledArray); // 输出乱序后的数组

18. 手写: 书写一个二分查找算法

在这里插入代码片
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    // 如果找到目标值,则返回索引
    if (arr[mid] === target) {
      return mid;
    } else if (arr[mid] < target) {
      // 如果目标值大于中间值,则在右侧继续查找
      left = mid + 1;
    } else {
      // 如果目标值小于中间值,则在左侧继续查找
      right = mid - 1;
    }
  }

  // 如果未找到目标值,返回 -1
  return -1;
}

// 示例用法
const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15];
const targetValue = 7;

const result = binarySearch(sortedArray, targetValue);
console.log(result); // 输出查找到的索引位置,如果未找到则输出 -1

18. 手写:数组去重(对于Object类型的去重)

function uniqueObjects(arr) {
  const seen = {};
  const result = [];

  for (const obj of arr) {
    const stringified = JSON.stringify(obj);
    if (!seen[stringified]) {
      result.push(obj);
      seen[stringified] = true;
    }
  }

  return result;
}

// 示例对象数组
const objectsArray = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice' }, // 重复对象
  { id: 3, name: 'Alice' },
];

const uniqueArray = uniqueObjects(objectsArray);
console.log(uniqueArray);

在这个示例中,uniqueObjects 函数使用了一个 seen 对象来存储已经出现过的对象。它遍历传入的数组 arr,将每个对象转换成字符串(使用 JSON.stringify),并检查该字符串是否在 seen 对象中。如果没有出现过,则将该对象推入 result 数组,并在 seen 对象中记录该字符串。这样就实现了对对象数组的去重。

18. 手写: 给定两个方块坐标, 求两个方块相交面积

// 第一个方块,分别表示左上角和右下角的坐标
const square1 = {
  x1: 0,
  y1: 0,
  x2: 3,
  y2: 3,
};

// 第二个方块,同样表示左上角和右下角的坐标
const square2 = {
  x1: 1,
  y1: 1,
  x2: 4,
  y2: 4,
};
function calculateIntersectionArea(square1, square2) {
  const xOverlap = Math.max(0, Math.min(square1.x2, square2.x2) - Math.max(square1.x1, square2.x1));
  const yOverlap = Math.max(0, Math.min(square1.y2, square2.y2) - Math.max(square1.y1, square2.y1));
  return xOverlap * yOverlap;
}

const intersectionArea = calculateIntersectionArea(square1, square2);
console.log('Intersection Area:', intersectionArea);

解释: 上述代码使用了数学上的思路,分别计算了两个方块在水平和垂直方向上的重叠长度,然后相乘即可得到相交的面积。如果两个方块不相交,重叠长度会小于等于 0,此时相交面积为 0。

19. 手写:输出一个对象里面含有多少个k字符

function countK(obj, char) {
  let count = 0;

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 判断键中包含特定字符的数量
      count += (key.split(char).length - 1);
    }
  }

  return count;
}

// 示例用法
const testObject = {
  key1: 'kitten',
  key2: 'kangaroo',
  key3: 'monkey',
  key4: 'elephant',
};

const charToCount = 'k';
const result = countK(testObject, charToCount);
console.log(result); // 输出含有 'k' 字符的键的数量

function countKInKeys(obj) {
  let count = 0;

  for (let key in obj) {
    if (key.includes('k')) {
      count++;
    }
  }

  return count;
}

// 示例用法
const testObject = {
  key1: 'kitten',
  key2: 'apple',
  key3: 'monkey',
  key4: 'elephant',
};

const result = countKInKeys(testObject);
console.log(result); // 输出含有 'k' 字符的键的数量

19. 手写:翻转二叉树

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}
function invertTree(root) {
  if (!root) {
    return null;
  }
  // 交换当前节点的左右子树
  const temp = root.left;
  root.left = root.right;
  root.right = temp;

  // 递归处理左右子树
  invertTree(root.left);
  invertTree(root.right);

  return root;
}
// 示例:构建一个二叉树
const tree = new TreeNode(4);
tree.left = new TreeNode(2);
tree.right = new TreeNode(7);
tree.left.left = new TreeNode(1);
tree.left.right = new TreeNode(3);
tree.right.left = new TreeNode(6);
tree.right.right = new TreeNode(9);

console.log('原始二叉树:', JSON.stringify(tree, null, 2));
const invertedTree = invertTree(tree);
console.log('翻转后的二叉树:', JSON.stringify(invertedTree, null, 2));

19. 手写:翻转二叉树;不用递归

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

function invertTree(root) {
  if (!root) {
    return null;
  }

  const queue = [root];

  while (queue.length) {
    const currentNode = queue.shift();
    
    // 交换当前节点的左右子树
    const temp = currentNode.left;
    currentNode.left = currentNode.right;
    currentNode.right = temp;

    // 将当前节点的左右子节点加入队列中
    if (currentNode.left) {
      queue.push(currentNode.left);
    }
    if (currentNode.right) {
      queue.push(currentNode.right);
    }
  }

  return root;
}

// 示例:构建一个二叉树
const tree = new TreeNode(4);
tree.left = new TreeNode(2);
tree.right = new TreeNode(7);
tree.left.left = new TreeNode(1);
tree.left.right = new TreeNode(3);
tree.right.left = new TreeNode(6);
tree.right.right = new TreeNode(9);

console.log('原始二叉树:', JSON.stringify(tree, null, 2));
const invertedTree = invertTree(tree);
console.log('翻转后的二叉树:', JSON.stringify(invertedTree, null, 2));

19. 手写:二叉树的右视图

右视图指的是二叉树从右侧看所能看到的节点序列。要获取二叉树的右视图,可以使用广度优先搜索(BFS)的方式进行遍历,每层只取最右边的节点值。
以下是使用 JavaScript 手写获取二叉树右视图的示例:
```javascript
class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

function rightSideView(root) {
  if (!root) {
    return [];
  }

  const result = [];
  const queue = [root];

  while (queue.length) {
    const levelSize = queue.length;

    for (let i = 0; i < levelSize; i++) {
      const currentNode = queue.shift();

      // 如果是当前层的最后一个节点,将其值加入结果数组
      if (i === levelSize - 1) {
        result.push(currentNode.val);
      }

      // 将当前节点的左右子节点加入队列
      if (currentNode.left) {
        queue.push(currentNode.left);
      }
      if (currentNode.right) {
        queue.push(currentNode.right);
      }
    }
  }

  return result;
}

// 示例:构建一个二叉树
const tree = new TreeNode(1);
tree.left = new TreeNode(2);
tree.right = new TreeNode(3);
tree.left.right = new TreeNode(5);
tree.right.right = new TreeNode(4);

console.log('右视图节点值:', rightSideView(tree));

在这个例子中,rightSideView 函数使用了 BFS 的思想来遍历二叉树,每层只取最右边的节点值,并将其添加到结果数组中。最终返回的结果数组即为二叉树的右视图节点值序列。

19. 手写:输出数组第三大的数

function thirdMax(nums) {
  let first = Number.MIN_SAFE_INTEGER;
  let second = Number.MIN_SAFE_INTEGER;
  let third = Number.MIN_SAFE_INTEGER;
  let count = 0;

  for (let i = 0; i < nums.length; i++) {
    if (nums[i] > first) {
      third = second;
      second = first;
      first = nums[i];
      count++;
    } else if (nums[i] > second && nums[i] !== first) {
      third = second;
      second = nums[i];
      count++;
    } else if (nums[i] > third && nums[i] !== second && nums[i] !== first) {
      third = nums[i];
      count++;
    }
  }

  return count < 3 ? first : third;
}

// 示例
const array = [3, 2, 1, 5, 4, 6];
console.log('数组第三大的数是:', thirdMax(array)); // 输出第三大的数

function thirdMax(nums) {
  const uniqueNums = [...new Set(nums)];
  const sortedNums = uniqueNums.sort((a, b) => b - a);

  if (sortedNums.length < 3) {
    return sortedNums[0];
  }

  return sortedNums[2];
}

// 示例
const array = [3, 2, 1, 5, 4, 6];
console.log('数组第三大的数是:', thirdMax(array)); // 输出第三大的数

19. 使用js手写一个闭包

function outerFunction() {
  let outerVariable = 'I am from outer function';
  
  function innerFunction() {
    console.log(outerVariable);
  }
  
  return innerFunction; // 返回内部函数
}

// 创建闭包
const closure = outerFunction();

// 调用闭包,它仍然可以访问到 outerFunction 中的 outerVariable
closure(); // 输出: "I am from outer function"

19. 手写:发布订阅

  • 发布订阅模式(Publish-Subscribe Pattern)是一种消息范式,其中发布者(发布消息的对象)与订阅者(订阅消息的对象)之间存在解耦,允许多个订阅者订阅一个主题,并在主题发生变化时收到通知。
  • 以下是一个简单的 JavaScript 实现发布订阅模式的示例:
class PubSub {
  constructor() {
    this.events = {};
  }

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return () => {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    };
  }

  publish(event, data) {
    if (!this.events[event]) {
      return;
    }
    this.events[event].forEach(callback => {
      callback(data);
    });
  }
}

// 创建发布订阅对象
const pubsub = new PubSub();

// 订阅事件 "message"
const unsubscribe1 = pubsub.subscribe('message', data => {
  console.log('订阅者1收到消息:', data);
});

// 订阅事件 "message"
const unsubscribe2 = pubsub.subscribe('message', data => {
  console.log('订阅者2收到消息:', data);
});

// 发布事件 "message"
pubsub.publish('message', '这是一个消息');

// 取消订阅事件 "message" 中的第一个回调函数
unsubscribe1();

// 再次发布事件 "message"
pubsub.publish('message', '这是另一个消息');

在这个例子中,PubSub 类实现了发布订阅模式。使用 subscribe 方法订阅事件,使用 publish 方法发布事件,并使用返回的函数取消订阅。当发布者调用 publish 方法时,订阅该事件的所有回调函数都会被触发,并传递相应的数据。

19. 手写:原型链

原型链是 JavaScript 中实现继承的一种机制,通过原型链,对象可以访问其他对象的属性和方法。这里展示一个简单的原型链示例:

// 父类构造函数
function Animal(name) {
  this.name = name;
}

// 父类方法
Animal.prototype.getName = function() {
  return this.name;
};

// 子类构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数,类似于 super()
  this.breed = breed;
}

// 设置子类的原型为父类的实例
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor 属性指向

// 子类方法
Dog.prototype.getBreed = function() {
  return this.breed;
};

// 创建父类实例
const animal = new Animal('Generic Animal');
console.log(animal.getName()); // 输出: Generic Animal

// 创建子类实例
const dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.getName()); // 输出: Buddy
console.log(dog.getBreed()); // 输出: Golden Retriever

在这个例子中,Animal 是一个父类构造函数,拥有一个 getName 方法。Dog 是一个子类构造函数,使用 Object.create()Dog 的原型指向了 Animal 的原型,实现了继承。通过调用 Animal.call(this, name),在 Dog 的构造函数中可以调用父类的构造函数,类似于 super(),确保了继承的正确性。最后,Dog 类新增了自己的方法 getBreed

这样一来,dog 实例既可以调用 Animal 原型链上的 getName 方法,也可以调用 Dog 自己的 getBreed 方法。

19. 手写:使用js实现一个轮训方法,返回一个取消方法,能够强制中转轮询;当异步方法成功时,通过回调返回结果并且结束轮询;当异步方法失败时,隔一段时间进行重试,

  • 下面是一个使用 JavaScript 实现轮询的例子。在这个例子中,polling 函数用于轮询执行一个异步方法,成功时通过回调返回结果并结束轮询,失败时会在一定时间后进行重试。
function polling(asyncFunction, successCallback, errorCallback, interval, maxAttempts) {
  let attempts = 0;

  function poll() {
    asyncFunction()
      .then(result => {
        // 异步方法成功,调用回调返回结果并结束轮询
        successCallback(result);
      })
      .catch(error => {
        // 异步方法失败
        attempts++;

        if (attempts < maxAttempts) {
          // 还未达到最大尝试次数,延迟一定时间后进行重试
          setTimeout(poll, interval);
        } else {
          // 达到最大尝试次数,调用错误回调
          errorCallback(error);
        }
      });
  }

  // 初始执行轮询
  poll();

  // 返回取消方法
  return function cancel() {
    attempts = maxAttempts; // 中断轮询
  };
}

// 示例使用
const cancelPolling = polling(
  // 异步方法
  () => new Promise((resolve, reject) => {
    // 模拟异步操作
    const success = Math.random() < 0.8; // 模拟80%的成功概率
    setTimeout(() => {
      if (success) {
        resolve('Async operation succeeded');
      } else {
        reject(new Error('Async operation failed'));
      }
    }, 1000); // 模拟1秒的异步操作
  }),
  // 成功回调
  result => {
    console.log('Success:', result);
  },
  // 失败回调
  error => {
    console.error('Error:', error.message);
  },
  // 轮询间隔
  2000,
  // 最大尝试次数
  5
);

// 5秒后强制中断轮询
setTimeout(() => {
  cancelPolling();
}, 5000);

在这个例子中,polling 函数接受一个异步方法 asyncFunction、一个成功回调 successCallback、一个失败回调 errorCallback、轮询间隔 interval 和最大尝试次数 maxAttemptspolling 函数返回一个取消方法 cancel,可以用于中断轮询。通过调用这个取消方法,可以在任何时候停止轮询。

19. 手写:使用js计算一个任意对象里面‘k’字符的数量

function countCharacter(obj, char) {
  // 确保输入是对象
  if (typeof obj !== 'object' || obj === null) {
    throw new Error('Input must be an object');
  }

  // 将对象的所有值拼接成一个字符串
  const allValues = Object.values(obj).join('');

  // 使用正则表达式匹配字符,并返回匹配的数量
  const charCount = (allValues.match(new RegExp(char, 'g')) || []).length;

  return charCount;
}

// 示例使用
const myObject = {
  key1: 'Hello, world!',
  key2: 'This is a key with some k characters.',
  key3: 'Another key without the letter k.'
};

const result = countCharacter(myObject, 'k');
console.log('The character "k" appears', result, 'times in the object.');

// 方式2
function countCharacter(obj, char) {
  if (typeof obj !== 'object' || obj === null) {
    throw new Error('Input must be an object');
  }

  let charCount = 0;

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = obj[key];
      
      if (typeof value === 'string') {
        // 使用 for...of 循环遍历字符串中的字符
        for (const currentChar of value) {
          if (currentChar === char) {
            charCount++;
          }
        }
      }
    }
  }

  return charCount;
}

// 示例使用
const myObject = {
  key1: 'Hello, world!',
  key2: 'This is a key with some k characters.',
  key3: 'Another key without the letter k.'
};

const result = countCharacter(myObject, 'k');
console.log('The character "k" appears', result, 'times in the object.');
  1. 遍历方式:
    for…in 循环会遍历对象的所有可枚举属性,包括对象的自身属性和继承的属性。需要注意的是,for…in 也会遍历对象原型链上的属性。
    Object.keys() 方法返回一个包含对象自身可枚举属性的数组。它不会遍历对象原型链上的属性。
  2. 返回值:
    for…in 循环没有返回值,通过遍历对象的属性,你可以直接在循环体内执行操作。
    Object.keys() 返回一个数组,其中包含对象自身可枚举属性的键。
  3. 遍历顺序:
    for…in 循环的遍历顺序不保证按属性的添加顺序或其他顺序,因此在某些情况下可能不符合期望。
    Object.keys() 返回的数组的顺序是按照属性添加的顺序排列的。

19. 手写:两个重合的矩形,求覆盖面积

function calculateOverlap(rect1, rect2) {
  // 确定交集的左上角坐标(x、y)
  const x = Math.max(rect1.x, rect2.x);
  const y = Math.max(rect1.y, rect2.y);

  // 确定交集的右下角坐标(x、y)
  const overlapWidth = Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - x;
  const overlapHeight = Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - y;

  // 计算交集的面积
  const overlapArea = overlapWidth * overlapHeight;

  return overlapArea;
}

// 示例使用
const rect1 = { x: 0, y: 0, width: 5, height: 5 };
const rect2 = { x: 2, y: 2, width: 6, height: 6 };

const overlapArea = calculateOverlap(rect1, rect2);
console.log('Overlap Area:', overlapArea);

19. 使用js手写爬楼梯

function climbStairs(n) {
  if (n <= 2) {
    return n;
  }

  let prev = 1;
  let current = 2;

  for (let i = 3; i <= n; i++) {
    const next = prev + current;
    prev = current;
    current = next;
  }

  return current;
}

// 示例使用
const stairs = 5;
const ways = climbStairs(stairs);
console.log(`There are ${ways} ways to climb ${stairs} stairs.`);

19. 手写: 删除数组指定元素(逐渐优化至常数复杂度)。

function removeElement(arr, target) {
  let i = 0;
  let n = arr.length;

  while (i < n) {
    if (arr[i] === target) {
      // 将要删除的元素移到数组末尾
      arr[i] = arr[n - 1];
      n--;
    } else {
      i++;
    }
  }

  // 截取数组的长度
  arr.length = n;

  return arr;
}

// 示例使用
const myArray = [3, 1, 2, 3, 4, 3, 5];
const targetElement = 3;

removeElement(myArray, targetElement);
console.log(myArray); // [1, 2, 5]

19. 手写:若干个立方体叠放求表面积

假设有 n 个立方体,每个立方体的边长分别为 a1, a2, ..., an。求这些立方体叠放在一起时的总表面积。

表面积由三部分组成:

  1. 每个立方体的上表面积(a^2)。
  2. 相邻两个立方体之间的贴合表面积(2 * a1 * a2 + 2 * a2 * a3 + ... + 2 * an-1 * an)。
  3. 每个立方体的底表面积(a^2)。

以下是一个使用 JavaScript 手写计算若干个立方体叠放时的总表面积的函数:

function totalSurfaceArea(cubeSizes) {
  let surfaceArea = 0;

  // 遍历每个立方体
  for (let i = 0; i < cubeSizes.length; i++) {
    const currentCube = cubeSizes[i];

    // 上表面积
    const topSurface = currentCube * currentCube;

    // 底表面积
    const bottomSurface = currentCube * currentCube;

    // 贴合表面积(除了最后一个立方体)
    const adjacentSurface = i < cubeSizes.length - 1 ? 2 * currentCube * cubeSizes[i + 1] : 0;

    surfaceArea += topSurface + bottomSurface + adjacentSurface;
  }

  return surfaceArea;
}

// 示例使用
const cubeSizes = [2, 3, 4];
const result = totalSurfaceArea(cubeSizes);
console.log('Total Surface Area:', result);

在这个例子中,totalSurfaceArea 函数接受一个包含每个立方体边长的数组 cubeSizes。通过遍历数组,计算每个立方体的上表面积、底表面积以及与下一个立方体的贴合表面积,最后累加得到总表面积。

19. 手写:倒计时组件

下面是一个简单的使用 JavaScript 手写的倒计时组件的例子。在这个例子中,我们使用HTML和CSS创建了一个简单的界面,然后使用JavaScript实现了倒计时功能。

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Countdown Timer</title>
  <style>
    body {
      font-family: 'Arial', sans-serif;
      text-align: center;
      margin: 50px;
    }

    #countdown {
      font-size: 36px;
      font-weight: bold;
      color: #333;
    }
  </style>
</head>
<body>

<div id="countdown">00:00:00</div>

<script>
function countdownTimer(durationInSeconds) {
  const countdownElement = document.getElementById('countdown');
  let duration = durationInSeconds;

  function updateDisplay() {
    const hours = Math.floor(duration / 3600);
    const minutes = Math.floor((duration % 3600) / 60);
    const seconds = duration % 60;

    const formattedTime = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    
    countdownElement.textContent = formattedTime;
  }

  function updateTimer() {
    if (duration > 0) {
      duration--;
      updateDisplay();
    } else {
      clearInterval(timerInterval);
      countdownElement.textContent = 'Time's up!';
    }
  }

  // 初始化显示
  updateDisplay();

  // 设置定时器,每秒更新倒计时
  const timerInterval = setInterval(updateTimer, 1000);
}

// 示例使用:倒计时10分钟
countdownTimer(10 * 60);
</script>

</body>
</html>

在这个例子中,我们创建了一个HTML页面,包含一个用于显示倒计时的<div>元素。通过JavaScript,我们定义了countdownTimer函数,该函数接受一个表示倒计时总秒数的参数。在函数内部,我们使用setInterval设置了每秒更新倒计时的定时器。倒计时格式化和更新显示的逻辑被封装在updateDisplayupdateTimer函数中。

19. 手写:坐标轴自定义向量,给定点旋转碰撞

  • 以下是一个简单的 JavaScript 示例,演示了自定义向量、给定点的旋转,以及碰撞检测。在这个例子中,我们定义了 Vector 类表示二维向量,并使用旋转矩阵来旋转给定点。同时,我们也演示了一个简单的矩形碰撞检测。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vector Rotation and Collision</title>
  <style>
    canvas {
      border: 1px solid #ddd;
      margin-top: 20px;
    }
  </style>
</head>
<body>

<canvas id="myCanvas" width="400" height="400"></canvas>

<script>
class Vector {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  rotate(angle) {
    const cosAngle = Math.cos(angle);
    const sinAngle = Math.sin(angle);

    const newX = this.x * cosAngle - this.y * sinAngle;
    const newY = this.x * sinAngle + this.y * cosAngle;

    this.x = newX;
    this.y = newY;
  }
}

// 初始化点和矩形
const point = new Vector(100, 100);
const rectangle = { x: 150, y: 150, width: 100, height: 50 };

function draw() {
  const canvas = document.getElementById('myCanvas');
  const ctx = canvas.getContext('2d');

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 绘制点
  ctx.beginPath();
  ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI);
  ctx.fillStyle = 'red';
  ctx.fill();
  ctx.closePath();

  // 绘制矩形
  ctx.beginPath();
  ctx.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
  ctx.strokeStyle = 'blue';
  ctx.stroke();
  ctx.closePath();
}

// 旋转点并检测碰撞
function rotateAndCheckCollision() {
  point.rotate(Math.PI / 180); // 旋转1度
  draw();

  // 检测碰撞
  if (
    point.x >= rectangle.x &&
    point.x <= rectangle.x + rectangle.width &&
    point.y >= rectangle.y &&
    point.y <= rectangle.y + rectangle.height
  ) {
    console.log('Collision detected!');
  }
}

// 示例使用:每隔一秒旋转点并检测碰撞
setInterval(rotateAndCheckCollision, 1000);
</script>

</body>
</html>

在这个例子中,我们使用 Vector 类表示二维向量,定义了 rotate 方法来旋转向量。然后,我们在 canvas 中绘制了一个点和一个矩形,并通过定时器每隔一秒旋转点并检测是否与矩形发生碰撞。碰撞检测的条件是点是否在矩形内部。你可以根据需要调整碰撞检测的逻辑。

19. 手写一下深度优先排序(递归、迭代)

class TreeNode {
  constructor(value) {
    this.value = value;
    this.children = [];
  }

  addChild(childNode) {
    this.children.push(childNode);
  }
}

// 深度优先排序函数
function dfsSort(node, result = []) {
  if (!node) {
    return result;
  }

  result.push(node.value);

  for (const child of node.children) {
    dfsSort(child, result);
  }

  return result;
}

// 创建一个树结构
const root = new TreeNode(1);
const node2 = new TreeNode(2);
const node3 = new TreeNode(3);
const node4 = new TreeNode(4);
const node5 = new TreeNode(5);

root.addChild(node2);
root.addChild(node3);
node2.addChild(new TreeNode(6));
node3.addChild(node4);
node3.addChild(node5);

// 执行深度优先排序
const sortedResult = dfsSort(root);

// 输出结果
console.log(sortedResult);
  • 深度优先排序(Depth-First Search, DFS)是一种图遍历或树遍历的算法。其基本思想是从图或树的某个顶点(或树的根节点)出发,沿着当前分支尽可能深入,直到不能再继续深入为止,然后回溯到上一层,继续探索其他分支,直到遍历完整个图或树。
    • 选择起点: 选择一个起始顶点(或节点),开始遍历。
    • 访问节点: 访问当前节点,并标记为已访问,防止重复访问。
    • 探索下一层: 递归地对当前节点的未访问邻居节点进行深度优先遍历。
    • 回溯: 当无法继续深入时,回溯到上一层,继续探索未访问的节点。

递归的形式

// 无向图的邻接表表示
const graph = {
  1: [2, 3],
  2: [1, 4, 5],
  3: [1, 6],
  4: [2],
  5: [2],
  6: [3]
};

// 深度优先排序函数(递归形式)
function dfsSortRecursive(graph, startNode, visited = new Set(), result = []) {
  if (!graph[startNode] || visited.has(startNode)) {
    return result;
  }

  visited.add(startNode);
  result.push(startNode);

  for (const neighbor of graph[startNode]) {
    dfsSortRecursive(graph, neighbor, visited, result);
  }

  return result;
}

// 执行深度优先排序
const sortedResult = dfsSortRecursive(graph, 1);

// 输出结果
console.log(sortedResult);

迭代形式

// 无向图的邻接表表示
const graph = {
  1: [2, 3],
  2: [1, 4, 5],
  3: [1, 6],
  4: [2],
  5: [2],
  6: [3]
};

// 深度优先排序函数(迭代形式)
function dfsSortIterative(graph, startNode) {
  const visited = new Set();
  const result = [];
  const stack = [startNode];

  while (stack.length > 0) {
    const currentNode = stack.pop();

    if (!visited.has(currentNode)) {
      visited.add(currentNode);
      result.push(currentNode);

      const neighbors = graph[currentNode] || [];
      for (const neighbor of neighbors) {
        stack.push(neighbor);
      }
    }
  }

  return result;
}

// 执行深度优先排序
const sortedResult = dfsSortIterative(graph, 1);

// 输出结果
console.log(sortedResult);
  • 在这个例子中,我们使用一个显式的栈 stack 来模拟深度优先排序的递归过程。每次从栈中弹出一个节点,如果该节点未被访问过,则将其标记为已访问并加入结果数组,并将其邻居节点压入栈中。这样,通过迭代模拟了深度优先遍历的过程。

广度优先排序

// 无向图的邻接表表示
const graph = {
  1: [2, 3],
  2: [1, 4, 5],
  3: [1, 6],
  4: [2],
  5: [2],
  6: [3]
};

// 广度优先排序函数(迭代形式)
function bfsSortIterative(graph, startNode) {
  const visited = new Set();
  const result = [];
  const queue = [startNode];

  while (queue.length > 0) {
    const currentNode = queue.shift();

    if (!visited.has(currentNode)) {
      visited.add(currentNode);
      result.push(currentNode);

      const neighbors = graph[currentNode] || [];
      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          queue.push(neighbor);
        }
      }
    }
  }

  return result;
}

// 执行广度优先排序
const sortedResultIterative = bfsSortIterative(graph, 1);

// 输出结果
console.log(sortedResultIterative);

// 无向图的邻接表表示
const graph = {
  1: [2, 3],
  2: [1, 4, 5],
  3: [1, 6],
  4: [2],
  5: [2],
  6: [3]
};

// 广度优先排序函数(递归形式)
function bfsSortRecursive(graph, queue, visited = new Set(), result = []) {
  if (queue.length === 0) {
    return result;
  }

  const currentNode = queue.shift();

  if (!visited.has(currentNode)) {
    visited.add(currentNode);
    result.push(currentNode);

    const neighbors = graph[currentNode] || [];
    for (const neighbor of neighbors) {
      if (!visited.has(neighbor)) {
        queue.push(neighbor);
      }
    }
  }

  return bfsSortRecursive(graph, queue, visited, result);
}

// 执行广度优先排序
const sortedResultRecursive = bfsSortRecursive(graph, [1]);

// 输出结果
console.log(sortedResultRecursive);

19. 手写:书写new

new 操作符在 JavaScript 中用于创建一个对象实例。当你使用 new 来调用构造函数时,它会执行以下步骤:

  1. 创建一个新的空对象。
  2. 将这个新对象的 __proto__(内部属性,即原型链)指向构造函数的 prototype 属性。
  3. 将构造函数的 this 指向新创建的对象。
  4. 执行构造函数的代码,给新对象添加属性和方法。
  5. 如果构造函数返回了一个对象,则返回这个对象;否则,返回新创建的对象。
function myNew(constructor, ...args) {
  // 1. 创建一个新的空对象
  const obj = {};
  // 2. 将新对象的 __proto__ 指向构造函数的 prototype
  obj.__proto__ = constructor.prototype;
  // 3. 将构造函数的 this 指向新对象
  const result = constructor.apply(obj, args);
  // 4. 如果构造函数返回了一个对象,则返回该对象;否则,返回新创建的对象
  return result instanceof Object ? result : obj;
}
// 示例构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}
// 使用 myNew 创建对象实例
const person = myNew(Person, 'John', 25);
// 输出结果
console.log(person); // { name: 'John', age: 25 }
console.log(person instanceof Person); // true
  • 在这个例子中,myNew 函数模拟了 new 操作符的行为。它接收一个构造函数和参数,创建一个新对象并执行构造函数。这个实现是一个简化版本,实际中还有一些细节需要考虑,比如处理构造函数的返回值等。

19. 手写: 计算点到线的距离

  • 勾股定理
// 计算点到线段的距离
function distanceToLineSegment(px, py, x1, y1, x2, y2) {
  const APx = px - x1;
  const APy = py - y1;
  const ABx = x2 - x1;
  const ABy = y2 - y1;

  const dotProduct = APx * ABx + APy * ABy;
  const lengthAB = ABx * ABx + ABy * ABy;

  const t = Math.min(1, Math.max(0, dotProduct / lengthAB));

  const closestX = x1 + t * ABx;
  const closestY = y1 + t * ABy;

  const distanceAP = Math.sqrt((px - closestX) ** 2 + (py - closestY) ** 2);

  return distanceAP;
}

// 示例:计算点 (3, 4) 到线段 AB(1, 2) - CD(6, 8) 的距离
const px = 3, py = 4;
const x1 = 1, y1 = 2;
const x2 = 6, y2 = 8;

const distance = distanceToLineSegment(px, py, x1, y1, x2, y2);

console.log(distance); // 输出距离

19. 手写:排序算法

在这里插入图片描述
手写所有排序算法可能会比较冗长,这里我会为你提供几个常见的排序算法的实现示例。请注意,这些示例都是为了演示算法原理,实际项目中建议使用 JavaScript 内置的 Array.prototype.sort 方法,该方法使用快速排序算法实现,通常具有较好的性能。

冒泡排序(Bubble Sort)

function bubbleSort(arr) {
  const len = arr.length;

  for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换元素位置
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

// 示例
const arrayToSort = [5, 3, 7, 1, 9, 2];
const sortedArrayBubble = bubbleSort(arrayToSort.slice());
console.log(sortedArrayBubble);

选择排序(Selection Sort)

function selectionSort(arr) {
  const len = arr.length;

  for (let i = 0; i < len - 1; i++) {
    let minIndex = i;

    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    // 交换元素位置
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
  }
  return arr;
}

// 示例
const sortedArraySelection = selectionSort(arrayToSort.slice());
console.log(sortedArraySelection);

插入排序(Insertion Sort)

function insertionSort(arr) {
  const len = arr.length;

  for (let i = 1; i < len; i++) {
    let current = arr[i];
    let j = i - 1;

    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = current;
  }
  return arr;
}
// 示例
const sortedArrayInsertion = insertionSort(arrayToSort.slice());
console.log(sortedArrayInsertion);

快速排序(Quick Sort)

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const pivot = arr[0];
  const left = [];
  const right = [];

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return quickSort(left).concat(pivot, quickSort(right));
}

// 示例
const sortedArrayQuick = quickSort(arrayToSort.slice());
console.log(sortedArrayQuick);

19. 手写:我有一个二维数组,他的值是几,高度就是几,求问这些块级的表面积。边界考虑

function surfaceArea(grid) {
  const rows = grid.length;
  const cols = grid[0].length;

  let totalSurfaceArea = 0;

  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      const height = grid[i][j];

      if (height > 0) {
        // 上表面积
        totalSurfaceArea += 2;

        // 左侧表面积
        totalSurfaceArea += i > 0 ? Math.max(0, height - grid[i - 1][j]) : height;

        // 右侧表面积
        totalSurfaceArea += i < rows - 1 ? Math.max(0, height - grid[i + 1][j]) : height;

        // 上侧表面积
        totalSurfaceArea += j > 0 ? Math.max(0, height - grid[i][j - 1]) : height;

        // 下侧表面积
        totalSurfaceArea += j < cols - 1 ? Math.max(0, height - grid[i][j + 1]) : height;
      }
    }
  }

  return totalSurfaceArea;
}

// 示例
const grid = [
  [1, 3],
  [2, 2]
];

const result = surfaceArea(grid);
console.log(result); // 输出块级的表面积

统计一下真正的表面积

  • 整体思路是看看有多少个立方体,总表面积是立方体的数量 × 6,但是因为相邻的会互相盖住,统计一下被盖住的面,然后减去被盖住的面就行了;没有官方那么复杂;
var surfaceArea = function(grid) {
   if(grid == null || grid.length < 1 || grid[0].length < 1) return 0;
        //统计所有的立方体数量
        let blocks = 0;
        //统计有多少个面被其他面盖住,那么就在所有的立方体的表面积上减去被盖住的面数×2(因为盖住一个面需要另一个面来盖,所以会损失2个面);
        let cover = 0;
        for(let i = 0;i < grid.length;++i) {
            for(let j = 0; j < grid[0].length;++j) {
                blocks += grid[i][j];
               //这个是统计当前格子中因为堆叠而盖住了几个面
                cover += grid[i][j] > 1 ? grid[i][j] -1 : 0;
                if(i > 0) {
                    //看看上一行同一列盖住了多少个面
                    cover += Math.min(grid[i-1][j],grid[i][j]);
                }
                if(j > 0) {
                    //看看同一行前一列盖住了几个面
                    cover += Math.min(grid[i][j-1],grid[i][j]);
                }
            }
        }
        return blocks * 6 - cover * 2;
    }

20. 事件循环说一下,宏微任务有哪些

  • 事件循环(Event Loop)是 JavaScript 运行时处理异步操作的机制。它负责管理执行代码、处理事件、以及维护调用栈和消息队列。事件循环确保 JavaScript 单线程的特性,通过将异步任务委托给浏览器或者 Node.js 提供的其他线程来处理。
  • 在事件循环中,任务分为宏任务和微任务。它们的执行顺序和优先级略有不同。

宏任务(Macro Tasks):
宏任务是指由浏览器提供的任务,通常包括以下:

  • script整体代码
  • setTimeout
  • setInterval
  • I/O操作
  • UI 渲染更新
    这些任务会被放入宏任务队列中,等待执行。当执行栈为空时,事件循环会从宏任务队列中取出一个任务执行,执行完之后再去检查是否有微任务需要执行。

微任务(Micro Tasks):
微任务是在当前任务执行结束后立即执行的任务。微任务包括:

  • Promise.then/catch/finally
  • MutationObserver
  • process.nextTick(Node.js 环境)
    当一个宏任务执行完成,事件循环会检查是否有微任务队列需要执行,如果有,会一次性执行完所有微任务。微任务队列的执行优先级高于宏任务队列。

事件循环流程:

  1. 从宏任务队列中取出一个任务执行。
  2. 执行完宏任务后,立即执行所有微任务。
  3. 若有新的微任务加入,继续执行微任务,直到微任务队列为空。
  4. 检查是否需要触发 UI 渲染。
  5. 执行下一个宏任务。

21. promise,async,await是啥,有啥区别?

Promiseasyncawait 都是处理 JavaScript 中异步编程的工具,它们在不同时间点和场景下解决了处理异步操作的问题。以下是它们的主要特点和区别:

Promise:

  • Promise 是 ES6 引入的概念,用于处理异步操作。
  • 它代表了一个异步操作的最终结果(可以是成功或失败)。
  • 通过 Promise 可以更容易地管理和组织异步操作,避免了回调地狱。
  • 可以通过 then 方法注册异步操作的成功或失败回调,或者使用 catch 方法捕获错误。
  • Promise 的状态有三种:pending(进行中)、fulfilled(已成功)、rejected(已失败)。

async/await:

  • async/await 是 ES2017(ES8)引入的语法糖,用于更优雅地处理异步代码。
  • async 关键字用于声明一个函数是异步的,使函数返回一个 Promise。
  • await 用于暂停异步函数的执行,等待 Promise 解决,并返回解决的值。
  • async/await 更加直观和易读,使得异步代码看起来像同步代码,避免了回调嵌套和链式 .then 的复杂性。
  • await 只能在 async 函数内部使用,它等待一个 Promise 完成,可以等待 Promise 的解决或拒绝。

区别:

  • Promise 是一种基础的异步处理工具,通过链式调用 .then.catch 处理异步操作结果。
  • async/await 是在 Promise 基础上的语法糖,使异步代码更清晰、更易读,使用 async 声明函数为异步,用 await 等待异步操作结果。
  • async/await 更易于理解和维护,尤其是处理多个异步操作的场景,代码结构更加清晰。
    一般来说,async/await 是在 Promise 的基础上提供的更高级的异步编程方式,使得异步代码更容易理解和编写。

22. map,weakmap是啥,有啥区别

MapWeakMap 都是 ES6 中提供的用于存储键值对的集合,但它们有一些区别:

Map:

  • Map 是一种可以使用任意类型的键来存储值的数据结构。
  • 键可以是基本数据类型(字符串、数字等)或者对象引用。
  • Map 保存对键的引用,因此即使键是对象,只要有引用存在,键值对就不会被垃圾回收。
  • 可以使用 size 属性获取 Map 中键值对的数量。
  • Map 中的键是有序的,它们会按照插入顺序进行迭代。

WeakMap:

  • WeakMap 是一种特殊的 Map,键只能是对象。
  • 键必须是对象的弱引用,这意味着如果没有其他的引用指向该对象,则该对象会被垃圾回收器回收,这样也会导致 WeakMap 中对应的键值对被删除。
  • WeakMap 没有 size 属性,也没有提供迭代方法,因为键是弱引用,无法确保稳定性和顺序。
  • 由于键必须是对象的弱引用,因此 WeakMap 在一些场景下能够有效地防止内存泄漏。

区别

  1. 可迭代性和稳定性:
    • Map 是可迭代的,并且键值对是有序的,按照插入顺序进行迭代。
    • WeakMap 没有提供迭代方法,并且键是弱引用,不保证稳定性和顺序。
  2. 键类型:
    • Map 的键可以是任意类型,包括基本数据类型和对象引用。
    • WeakMap 的键只能是对象引用,并且是弱引用,不会阻止对象被垃圾回收。
  3. 内存管理:
    • Map 会持续保留键的引用,即使其它引用不存在,也不会被垃圾回收。
    • WeakMap 中的键是弱引用,键对象被垃圾回收后,对应的键值对也会被从 WeakMap 中删除。

23. Generator 函数,生成器

  • Generator 函数是 ES6 中引入的一种特殊类型的函数,它具有能够生成多次、暂停和恢复执行的能力。这种函数能够在需要时产生一系列的值,而不需要一次性生成所有值。
  • 基本特点:
  1. 使用 function* 关键字定义: Generator 函数通过 function* 关键字来定义,函数体内通常包含至少一个 yield 关键字。
function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}
  1. 生成迭代器: 调用 Generator 函数并不会立即执行,而是返回一个迭代器对象。迭代器对象可以通过 next() 方法逐步执行 Generator 函数内部的代码,每次调用 next() 都会执行到下一个 yield 表达式。
const iterator = myGenerator();
console.log(iterator.next()); // 输出 { value: 1, done: false }
console.log(iterator.next()); // 输出 { value: 2, done: false }
console.log(iterator.next()); // 输出 { value: 3, done: false }
console.log(iterator.next()); // 输出 { value: undefined, done: true }
  1. 暂停和恢复执行: Generator 函数可以通过 yield 关键字暂停函数的执行,并将值传递给迭代器。每次调用 next() 方法时,函数会从上一次暂停的地方恢复执行。
  2. 终止迭代: Generator 函数执行完所有的 yield 后会自动返回 undefined 并将 done 标志设为 true,表示迭代结束。

应用场景:

  • 异步编程: 可以简化异步操作的代码,比如使用 Generator 函数配合 yield 控制异步流程。
  • 数据流控制: 生成器能够生成一系列值,可以用于控制数据流。
  • 惰性求值: 通过生成器可以实现按需生成值,延迟计算。

24. 二叉树遍历说一下

  • 二叉树遍历是指按照一定的顺序访问二叉树中的节点,常见的遍历方式有三种:前序遍历、中序遍历和后序遍历。
  • 二叉树节点结构:
  • 二叉树节点通常包含一个值(value)、左子节点(left)和右子节点(right)。
class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}
    1. 前序遍历(Preorder Traversal):
      前序遍历顺序是:根节点 -> 左子树 -> 右子树。
function preorderTraversal(node) {
  if (node === null) return;
  console.log(node.value); // 先访问当前节点
  preorderTraversal(node.left); // 递归访问左子树
  preorderTraversal(node.right); // 递归访问右子树
}
    1. 中序遍历(Inorder Traversal):
      中序遍历顺序是:左子树 -> 根节点 -> 右子树。
function inorderTraversal(node) {
  if (node === null) return;
  inorderTraversal(node.left); // 递归访问左子树
  console.log(node.value); // 访问当前节点
  inorderTraversal(node.right); // 递归访问右子树
}
    1. 后序遍历(Postorder Traversal):
      后序遍历顺序是:左子树 -> 右子树 -> 根节点。
function postorderTraversal(node) {
  if (node === null) return;
  postorderTraversal(node.left); // 递归访问左子树
  postorderTraversal(node.right); // 递归访问右子树
  console.log(node.value); // 访问当前节点
}

25. 箭头函数和es5里面使用function写的函数有什么区别

  • 箭头函数与 ES5 中使用 function 关键字定义的函数在几个方面有一些区别:
  1. 语法: 箭头函数使用了更简洁的语法形式,通过 => 来定义函数。
  2. this 绑定:
  • 箭头函数没有自己的 this,它会捕获所在上下文的 this 值,指向的是函数创建时所在的词法作用域的 this
  • ES5 中的函数每次被调用时会重新绑定 this,它的 this 取决于函数被调用时的执行上下文。
  1. arguments 对象:
  • 箭头函数没有自己的 arguments 对象。箭头函数的 arguments 对象是继承自其父级函数的,即外部作用域的 arguments
  • ES5 中的函数有自己的 arguments 对象,用于访问函数的参数。
  1. new 关键字:
  • 箭头函数不能被用作构造函数,不能通过 new 关键字调用,因为它没有自己的 this 绑定。
  • ES5 中的函数可以被用作构造函数,并且通过 new 关键字调用,可以生成一个新的对象。
  1. 返回值:
  • 箭头函数在只有一条返回语句时,可以省略大括号和 return 关键字,自动返回表达式的值。
  • ES5 中的函数需要显式使用 return 来返回值。
    箭头函数更加简洁、清晰,尤其在处理 this 上有不同的行为。然而,它们并不适用于所有场景,因为缺少了一些传统函数的特性,比如没有自己的 thisarguments 对象。在需要特定 this 绑定或需要作为构造函数使用时,应该使用传统的函数声明。

26. 箭头函数不能用作构造函数,原因是什么?

箭头函数不能用作构造函数的主要原因在于它们没有自己的 this 绑定机制。

  1. 固定的 this: 箭头函数的 this 是在定义函数时确定的,它会捕获所在上下文的 this 值,指向的是函数创建时所在的词法作用域的 this,而不是在实例化的过程中动态绑定。这导致无法在使用 new 关键字实例化时改变箭头函数内部的 this
  2. 缺少原型: 箭头函数没有 prototype 属性,也无法通过 new 关键字创建一个具有 prototype 属性的对象。构造函数通常有一个 prototype 属性,用于创建实例的原型链,而箭头函数没有这个属性。
    因此,由于箭头函数固定了其 this 绑定,并且缺少 prototype 属性,使用 new 关键字无法正确地创建一个实例对象,所以箭头函数不能被用作构造函数来创建实例对象。试图使用 new 调用一个箭头函数会导致抛出错误。

27. 手写:使用js,实现一个bind,bind里面又用了apply,再实现一个apply

当实现 apply 方法后,可以使用 apply 来实现 bind 方法。下面先实现一个简化的 apply 方法,然后利用这个 apply 方法实现 bind 方法。

  • 实现 apply 方法:
Function.prototype.myApply = function (context, argsArray) {
  context = context || window; // 如果没有传递 context,默认为全局对象(浏览器中为 window)
  const uniqueID = '00' + Math.random(); // 随机生成唯一的属性名以避免属性覆盖
  while (context.hasOwnProperty(uniqueID)) {
    uniqueID = '00' + Math.random();
  }

  context[uniqueID] = this; // 将函数作为 context 的属性
  const result = context[uniqueID](...argsArray); // 使用展开运算符调用函数

  delete context[uniqueID]; // 删除添加的属性
  return result;
};
  • 实现 bind 方法:
    利用 apply 方法来实现 bind 方法。
Function.prototype.myBind = function (context, ...args) {
  const func = this;

  return function (...innerArgs) {
    return func.myApply(context, args.concat(innerArgs));
  };
};

myBind 方法返回一个新函数,这个新函数在被调用时会将传递的 context 和参数与绑定函数本身的参数合并,并使用 myApply 方法调用原函数,达到了修改 this 指向并预设参数的效果。

28. 手写:instanceof在判断数据类型时,可以使用instanceof来实现,是通过判断函数的原型是否出现在该对象的原型链上实现的,实现一下instanceof

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。下面是一个简化版的 instanceof 实现:
function myInstanceof(obj, constructor) {
  let prototype = constructor.prototype;

  while (obj !== null) {
    if (obj.__proto__ === prototype) {
      return true;
    }
    obj = obj.__proto__;
  }
  return false;
}
  • 这个 myInstanceof 函数接收两个参数:obj 表示要检测的对象,constructor 是要检测的构造函数。
  • 该函数会一直沿着 obj 的原型链(通过 __proto__ 属性)向上查找,直到找到构造函数的 prototype 属性,如果在原型链中找到了该属性,则返回 true,表示对象是该构造函数的实例。否则返回 false
  • 需要注意的是,这是一个简化的实现。在实际应用中,可能需要更多的边界条件和错误处理。此外,JavaScript 还提供了原生的 instanceof 运算符,它是用于检测对象的原型链中是否存在构造函数的 prototype 属性,通常情况下已经能够满足大部分情况的需求。

29. 大概讲一下我们在浏览器中输入一个网址,到展现出一个页面中间具体做了哪些事情

URL解析, (协议、域名、端口号; 路径+查询参数等)
DNS解析,(将域名转换为IP地址)
建立TCP连接,(通常是一个三次握手的过程,确保客户端和服务器之间的通信通道已经建立)
发起HTTP请求,(浏览器发送HTTP请求到服务器,这个请求包含了用户所请求的资源信息(请求头+请求体))
服务器处理请求,(根据URL, 服务器可能执行一些动态的操作,如查询数据库或生成页面内容,然后将响应返回给客户端)
接收和渲染响应,(浏览器接收到服务器的HTTP请求,开始渲染:执行js脚本+应用css样式)
呈现页面
连接关闭、清理资源

可以优化的点
在页面加载过程中,前端可以通过多种方式进行优化,提高页面加载速度和用户体验:

  1. 减少 HTTP 请求:
  • 合并文件: 将多个 CSS 或 JavaScript 文件合并为一个文件,减少 HTTP 请求次数。
  • CSS Sprites: 将多个小图片合并成一张大图,减少图片加载次数。
  • 资源内联: 将小体积的 CSS 或 JavaScript 直接内联到 HTML 中,减少请求次数。
  1. 压缩资源:
  • 压缩文件: 使用压缩工具(例如:Gzip)压缩 HTML、CSS、JavaScript 等文件,减小文件体积。
  • 图片压缩: 使用合适的图片格式并压缩图片,减少图片文件大小。
  1. 缓存优化:
  • 缓存策略: 设置合适的缓存头(例如:Expires、Cache-Control),使得浏览器可以缓存页面资源。
  • 使用 CDN: 使用内容分发网络(CDN)加速资源加载,将静态资源分发到全球多个节点。
  1. 资源延迟加载:
  • 延迟加载: 将非关键资源进行延迟加载,例如图片懒加载、异步加载 JavaScript、按需加载组件等。
  1. 优化图片加载:
  • 响应式图片: 使用 <picture> 标签或 srcset 属性提供不同分辨率的图片,根据不同设备加载不同尺寸的图片。
  • 懒加载: 仅当图片进入可视区域时才加载图片。
  1. DOM 操作和渲染优化:
  • 减少重排和重绘: 尽量减少触发 DOM 的重排和重绘,合理使用 CSS 动画。
  • 异步操作: 将耗时的任务放入异步队列,避免阻塞主线程。
  1. 使用缓存技术:
  • 使用缓存技术: 使用本地存储(LocalStorage、SessionStorage)存储部分数据,减少重复请求。
  1. 代码优化:
  • 精简代码: 去除无用代码、优化 JavaScript 性能,减少不必要的计算。
  • 使用延迟加载脚本: 将不影响首屏渲染的 JavaScript 延迟加载。
  1. 预加载和预渲染:
  • 预加载和预渲染: 使用 <link rel="preload"> 预加载资源,使用 <link rel="prerender"> 预渲染页面。

30. 手写:使用js写一个二叉树非递归的深度遍历

function TreeNode(val) {
  this.val = val;
  this.left = this.right = null;
}

function iterativeDFS(root) {
  if (!root) return [];

  const stack = [];
  const result = [];
  stack.push(root);

  while (stack.length) {
    const node = stack.pop();
    result.push(node.val);

    if (node.right) {
      stack.push(node.right);
    }
    if (node.left) {
      stack.push(node.left);
    }
  }

  return result;
}

// 示例用法:
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

console.log(iterativeDFS(root)); // 输出 [1, 2, 4, 5, 3]

// 递归的
function TreeNode(val) {
  this.val = val;
  this.left = this.right = null;
}

function recursiveDFS(root) {
  const result = [];

  function traverse(node) {
    if (!node) return;

    result.push(node.val);
    traverse(node.left);
    traverse(node.right);
  }

  traverse(root);
  return result;
}

// 示例用法:
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

console.log(recursiveDFS(root)); // 输出 [1, 2, 4, 5, 3]

31. Vue中的组件通信方式有哪些

在 Vue 中,组件通信可以通过多种方式实现:

  1. Props / 自定义属性:
  • 父传子通信: 父组件可以通过在子组件上使用属性绑定传递数据。
  • 子传父通信: 子组件通过 $emit 触发事件,并将数据作为参数传递给父组件。
  1. 自定义事件:
  • **$emit / o n : ∗ ∗ 可以使用 ‘ on:** 可以使用 ` on可以使用emit在子组件中触发自定义事件,并在父组件中使用$on` 监听该事件。
  1. Event Bus / 事件总线:
  • 创建 EventBus: 可以创建一个全局事件总线实例,用于充当中央事件总线,在组件之间进行通信。
  • **$emit / o n : ∗ ∗ 组件通过 ‘ on:** 组件通过 ` on组件通过emit触发事件,其他组件通过$on` 监听该事件。
  1. Vuex / 状态管理:
  • 集中式状态管理: 使用 Vuex 管理共享状态,各组件可以通过派发(dispatch)和获取(getters)来修改和获取状态。
  1. $refs:
  • 通过 $refs 访问子组件: 在父组件中可以通过 ref 获取子组件的实例,从而直接调用子组件的方法或访问其属性。
  1. provide / inject:
  • 祖先向后代传递数据: 使用 provide 在祖先组件中提供数据,然后在后代组件中使用 inject 接收数据。
  1. $attrs / $listeners:
  • 特性继承和事件监听继承: 在子组件中使用 $attrs$listeners 访问父组件传递的特性和监听器。
  1. 插槽:
  • 作用域插槽: 通过使用插槽(slot)实现父组件向子组件传递内容,并可以在子组件中对内容进行处理。
  1. 全局事件:
  • **使用 $root / p a r e n t : ∗ ∗ 可以通过 ‘ parent:** 可以通过 ` parent可以通过root访问根实例或者$parent` 访问父组件实例,进行通信。

32. 使用vuex进行组件之间通信有什么优势

使用 Vuex 进行组件之间通信有以下优势:

  1. 集中式状态管理:
  • 单一数据源: Vuex 将应用的状态集中存储在一个全局的 store 中,使得状态的修改和管理变得更加可预测和容易。
  1. 共享状态:
  • 状态共享: 多个组件可以共享同一个状态,可以在不同组件之间实现数据的共享和传递,避免了 props 和 emit 的层层传递。
  1. 易于调试和追踪变化:
  • 时间旅行调试: Vuex 可以通过记录每次 mutation 的方式实现时间旅行式的调试,方便追踪状态的变化。
  • 严格的状态变更追踪: Vuex 中的状态是响应式的,任何对状态的修改都可以被追踪到。
  1. 统一管理:
  • 状态一致性: Vuex 可以帮助管理应用中的所有状态,让状态的修改变得可预测,方便维护。
  1. 提供插件和工具支持:
  • 丰富的插件生态: Vuex 提供了丰富的插件和工具支持,可以扩展其功能,如 devtools 插件可以辅助调试。
  1. 更好的组件解耦:
  • 组件解耦: 使用 Vuex 可以让组件之间的通信更加松耦合,组件只需关注自身的业务逻辑,不需要关心其他组件的状态和变化。
  1. 适用于中大型应用:
  • 大型应用管理: 对于中大型应用,特别是状态管理较为复杂的情况下,使用 Vuex 可以更好地管理状态。
  1. 方便的 API:
  • 统一的 API: Vuex 提供了一些方便的 API,如 mapStatemapGettersmapMutationsmapActions,使得组件更容易使用 store 中的状态和方法。

总体来说,Vuex 提供了一种集中式管理状态的机制,使得状态变更更加可控和可预测,适用于管理应用中复杂的状态逻辑和多个组件之间的数据通信。

33. 手写:找出一个整数数组中只出现一次的数字num,其他都出现了两次

可以通过使用异或运算来解决这个问题。异或运算(XOR)具有性质:两个相同数字的异或结果为0,任何数字与0的异或结果是其本身。

步骤如下:

  1. 遍历整数数组,对数组中的所有数字进行异或操作。
  2. 由于出现两次的数字异或后结果为0,最终结果就是只出现一次的数字。

以下是示例代码:

function findSingleNumber(nums) {
  let result = 0;
  // 对所有数字进行异或操作
  for (let num of nums) {
    result ^= num;
  }

  return result;
}
// 示例用法:
const arr = [4, 2, 4, 2, 5];
console.log(findSingleNumber(arr)); // 输出 5,因为只有 5 出现了一次

34. 手写:实现一个bind,bind里面又用了apply,再实现一个apply

当使用 bind 方法并在其内部使用 apply 时,我们可以通过闭包来实现。下面是一个用 JavaScript 实现的简化版 bind 方法,同时再次实现一个简化版的 apply 方法:
实现 apply 方法:

Function.prototype.myApply = function (context, argsArray) {
  context = context || window; // 如果没有传递 context,默认为全局对象(浏览器中为 window)
  const uniqueID = '00' + Math.random(); // 随机生成唯一的属性名以避免属性覆盖
  while (context.hasOwnProperty(uniqueID)) {
    uniqueID = '00' + Math.random();
  }

  context[uniqueID] = this; // 将函数作为 context 的属性
  const result = context[uniqueID](...argsArray); // 使用展开运算符调用函数

  delete context[uniqueID]; // 删除添加的属性
  return result;
};

实现 bind 方法(使用 apply):

Function.prototype.myBind = function (context, ...args) {
  const func = this;

  return function (...innerArgs) {
    return func.myApply(context, args.concat(innerArgs));
  };
};

这个 myBind 方法返回一个新函数,这个新函数在被调用时会将传递的 context 和参数与绑定函数本身的参数合并,并使用 myApply 方法调用原函数,达到了修改 this 指向并预设参数的效果。

35. 1 == ‘1’会怎么进行比较;1+‘1’ 结果是什么; 1+ true结果是什么

这些情况都涉及JavaScript中的类型转换和隐式类型转换。

  1. 1 == '1' 会进行比较,JavaScript中的松散相等(==)会尝试进行类型转换,它会将字符串 '1' 尝试转换为数字来进行比较。在这种情况下,它们会被转换为相同的类型(数字),然后进行比较,所以结果是 true
  2. 1 + '1' 中涉及到加法操作符和字符串拼接。当一个数字和一个字符串相加时,JavaScript会进行隐式类型转换,将数字转换为字符串,然后执行字符串拼接操作。因此,1 + '1' 的结果是一个字符串 '11',而不是数字相加的结果。
  3. 1 + true 中也涉及到类型转换。JavaScript中,布尔值true会被转换为数字1,然后进行加法操作。因此,1 + true 的结果是 2,因为 1 + 1 等于 2

36. 讲一下你对原型链的理解,使用new Object() 创建出的实例,它的原型对象指向什么,一个函数的原型对象指向什么?我们一般创建一个函数,可以直接调用apply、bind等方法是为什么

**原型链;**原型链是一种机制,用于在JS中实现继承,当访问一个对象的属性和方法时,JS引擎先会查找对象本身是否拥有该属性和方法,如果没有,则会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶部Object.prototype;

  • 使用 new Object() 创建的实例,其原型对象指向 Object.prototype。这意味着该实例可以访问 Object.prototype 上定义的属性和方法。
  • 对于一个函数,它的原型对象指向一个空对象,这个对象有一个 constructor 属性指向该函数本身。比如:
function MyFunction() {}
console.log(MyFunction.prototype); // 输出一个空对象 { constructor: MyFunction }
  • JavaScript 函数是由 Function 构造函数创建的实例,Function 构造函数自身也有一些方法,比如 apply、bind、call 等,因此所有的函数对象都继承了这些方法。当你创建一个函数时,它就会继承这些方法,因此可以直接使用这些方法来操作函数。

37. 作用域了解吗,那么闭包呢,一般如何销毁一个闭包;

  • 作用域(Scope)指的是在代码中定义变量的区域,它规定了这些变量在哪里可以被访问和操作。JavaScript 中的作用域分为全局作用域和局部作用域(函数作用域、块级作用域)。作用域链规定了变量的查找顺序,当试图访问一个变量时,JavaScript 引擎会沿着作用域链向上查找变量的值。
  • 闭包(Closure)是指一个函数能够记住并访问其词法作用域(定义时的作用域),即使这个函数在当前词法作用域之外执行。闭包使得函数可以访问定义时的作用域中的变量,即使函数在定义的作用域之外执行。
  • 通常情况下,JavaScript 中的闭包会在其词法作用域外仍然保持对其作用域的引用,导致作用域中的变量在函数执行后仍然存在,不会被销毁。因此,手动销毁闭包并不常见,而是依赖于垃圾回收机制。但是有一些方法可以解除对闭包的引用,从而帮助垃圾回收机制释放闭包占用的内存:
  1. 赋值为 null: 将包含闭包的变量赋值为 null 可以解除对闭包的引用,帮助垃圾回收机制识别可以回收的内存。
  2. 停止使用闭包: 如果不再需要使用闭包,确保不再引用闭包中的函数或变量,这样在没有引用指向闭包时,垃圾回收机制会最终回收这部分内存。
  • 一个常见的情况是在事件处理函数中创建闭包,当事件处理函数执行完毕后,如果不再需要这些闭包中的变量或函数,确保事件处理函数结束后不再引用这些闭包中的内容,可以帮助垃圾回收机制释放这些闭包占用的内存空间。

38. let、const的区别有什么,声明一个const类型的数组,这个数组可以修改吗;了解const的实现原理吗

letconst 是 ES6 引入的两种声明变量的方式,它们有一些重要的区别:

  1. 可变性:
    • let 声明的变量可以被重新赋值。
    • const 声明的变量被赋予一个固定的值,这个值不能被重新赋值。
  2. 作用域:
    • letconst 都具有块级作用域,只在声明它们的块(比如一个花括号 {} 内部)中有效。
  3. 变量提升:
    • letconst 声明的变量不会像 var 那样被“提升”到当前作用域的顶部。这意味着在声明之前访问 letconst 的变量会触发暂时性死区(Temporal Dead Zone,TDZ)。

对于使用 const 声明的数组来说,const 并不使数组变得不可变。它仅保证了变量指向的数组引用不会被重新赋值。换句话说,使用 const 声明的数组变量本身不能被重新赋值,但数组内的元素是可以被修改的。

示例:

const myArray = [1, 2, 3];
myArray[0] = 10; // 可以修改数组元素
console.log(myArray); // 输出: [10, 2, 3]

// 但是这样是不允许的,会导致 TypeError: Assignment to constant variable.
// myArray = [4, 5, 6];

关于 const 的实现原理,它的行为不同于简单的变量赋值。const 创建的是一个常量引用。当你使用 const 声明变量时,JavaScript 引擎会阻止对该变量重新赋值,但不会阻止修改该变量引用的对象的属性。实际上,const 创建的是一个不可变的绑定,而不是一个不可变的值。

  1. 作用域:
    var 存在函数作用域和全局作用域的概念,它在函数内声明的变量在函数外也可以访问,但没有块级作用域。
    let 和 const 引入了块级作用域,即变量在 {} 块内声明,只在该块作用域内有效。

  2. 变量提升:
    var 存在变量提升(hoisting),即在代码执行前会将变量声明提升到当前作用域的顶部,但赋值保留在原位置。
    let 和 const 也有变量提升,但在初始化前不可访问(存在暂时性死区 TDZ),不会产生未定义而是会抛出引用错误。

  3. 重复声明:
    使用 var 可以重复声明同名变量而不会报错。
    使用 let 或 const 在同一作用域内重复声明同名变量会引发语法错误。

  4. 赋值和修改:
    使用 let 声明的变量可以重新赋值。
    使用 const 声明的变量必须初始化,并且一旦赋值后就不能再修改,但对于对象或数组等引用类型,其内容是可以修改的。

39. js的一些模块化,你说说es6 module和CMD的区别是什么

ES6 模块和 CommonJS(CMD 是 SeaJS 实现的一种规范)在 JavaScript 模块化方面有几个主要区别:

  1. 语法不同:
    • ES6 模块使用 import 导入模块,export 导出模块。
    • CommonJS 使用 require() 导入模块,module.exportsexports 导出模块。
  2. 静态 vs 动态:
    • ES6 模块是静态的,模块的依赖关系在代码静态分析阶段就确定了,因此无法在运行时动态加载模块。
    • CommonJS 是动态的,require() 是同步调用,可以在代码的任何位置使用。
  3. 异步加载:
    • ES6 模块是同步加载的,模块在解析阶段就会被加载,这使得它在性能方面更具优势。
    • CommonJS 是同步或异步加载的,它允许在运行时动态加载模块,适用于服务器端和同步加载的情况。
  4. 变量绑定:
    • ES6 模块是基于 exportimport,模块中的变量不会被自动绑定到全局对象。
    • CommonJS 中,require() 加载的模块会把导出的内容赋值给一个变量,该变量可作为全局对象的属性访问。
  5. 循环依赖处理:
    • ES6 模块对循环依赖有较好的支持,会自动处理循环依赖问题。
    • CommonJS 在处理循环依赖时,模块一旦被加载,其结果就会被缓存,可能会导致某些循环依赖场景下出现 undefined 或其他问题。
      这些区别主要体现了 ES6 模块与 CommonJS 在设计和实现上的不同。ES6 模块是 JavaScript 官方提供的模块化解决方案,而 CommonJS 是 Node.js 使用的模块化规范,两者在语法、加载方式、异步性和循环依赖处理等方面有所差异。

40. 讲解一下浏览器的渲染流程

浏览器的渲染流程是将 HTML、CSS 和 JavaScript 转换为用户可以交互和可视化的网页的过程。下面是一个简化的浏览器渲染流程:

  1. 构建 DOM 树:
    • 浏览器接收到 HTML 文档后,开始解析 HTML 标记,构建 DOM(文档对象模型)树。DOM 树表示文档的层次结构,每个 HTML 标签都成为 DOM 树的一个节点。
  2. 构建 CSSOM 树:
    • 浏览器同时解析 CSS 样式表,构建 CSSOM(CSS 对象模型)树。CSSOM 树表示样式表的层次结构,它记录了每个节点的样式和样式规则。
  3. 合并 DOM 和 CSSOM 形成 Render 树:
    • 浏览器将 DOM 树和 CSSOM 树合并成一个 Render 树(渲染树)。Render 树只包含渲染网页所需的节点,如页面内容和其样式信息。
  4. 计算布局(Layout):
    • 渲染树中的每个节点都有其在视口中的几何位置。浏览器进行布局计算,确定每个节点在屏幕上的确切位置和大小。这个过程叫做“布局”或“回流”。
  5. 绘制(Painting):
    • 浏览器根据渲染树的布局信息开始绘制页面。这个阶段称为绘制,浏览器将每个节点转换为屏幕上的实际像素。
  6. 合成(Composite):
    • 浏览器将各个图层合成为页面的最终视图,将不同图层按正确顺序叠加形成最终的像素展示。这个过程称为合成。
      在这个流程中,JavaScript 可能会对渲染流程产生影响。当 JavaScript 修改 DOM 或样式时,可能会触发重新计算布局和绘制,这会影响页面性能。因此,优化 JavaScript 的操作以减少不必要的重绘和回流对页面性能是很重要的。

41. js文件会阻塞页面的渲染吗

  • JavaScript 文件的加载和执行可能会阻塞页面的渲染,这取决于 JavaScript 文件的加载方式和位置,以及浏览器的行为。
  • 当浏览器遇到 <script> 标签时,会停止解析 HTML 并立即下载并执行 JavaScript 文件。这可能会导致以下情况:
  1. 阻塞渲染:
    • 如果 JavaScript 文件位于 <head> 标签中,浏览器会先加载并执行 JavaScript,这可能会阻塞页面的渲染,直到 JavaScript 执行完成。
    • 这会导致页面显示延迟,因为浏览器必须等待 JavaScript 文件下载、解析和执行完成后才能继续渲染页面。
  2. 异步加载和延迟加载:
    • 使用 asyncdefer 属性可以使 JavaScript 异步加载或延迟执行,这可以减少对渲染的阻塞。
    • async 属性表示立即异步加载并执行脚本,不阻塞页面的渲染,但执行时会阻塞后续 HTML 的解析。
    • defer 属性表示延迟执行脚本,直到 HTML 解析完成后再执行,因此不会阻塞 HTML 解析和渲染。
      因此,要尽量减少 JavaScript 对页面渲染的影响,可以考虑以下几点:
  • 将 JavaScript 文件放在页面底部(</body> 前),或者使用 asyncdefer 属性来异步加载和延迟执行脚本,以减少对渲染的阻塞。
  • 减小 JavaScript 文件的大小,优化代码以提高加载和执行速度。
  • 合理使用代码拆分和懒加载技术,只在需要时加载和执行特定的 JavaScript 代码。

42. 生命周期:什么使用使用created,什么时候使用mounted

createdmounted 都是 Vue 组件生命周期钩子函数,它们在组件生命周期中扮演不同的角色。

  • created 钩子函数:
    • created 钩子函数在 Vue 实例被创建后立即调用,此时实例已完成初始化,但尚未挂载到 DOM 中,因此无法访问到 $el
    • 适合进行一些初始的数据操作和逻辑处理,如异步请求数据、处理数据、初始化事件等。
    • 在这个阶段,Vue 实例已经被创建,但还未挂载到 DOM,因此在这里进行的 DOM 操作是无效的。
      示例:
export default {
  created() {
    // 初始化数据、发起异步请求等
    this.fetchData();
  },
  methods: {
    fetchData() {
      // 例如,发起异步请求
      // axios.get('/api/data').then((response) => {
      //   this.data = response.data;
      // });
    },
  },
};
  • mounted 钩子函数:
    • mounted 钩子函数在 Vue 实例挂载到 DOM 后调用,此时 Vue 实例已经挂载到 DOM 中。
    • 可以进行 DOM 操作、访问 $el、操作页面中的元素,执行需要在 DOM 渲染后才能进行的操作。
    • 适合执行需要依赖于 DOM 的操作,如操作 DOM 元素、启动定时器、访问外部库等。
      示例:
export default {
  mounted() {
    // 操作 DOM 元素
    this.$refs.example.innerHTML = 'Mounted!';
    
    // 启动定时器等操作
    this.timer = setInterval(() => {
      // ...
    }, 1000);
  },
  destroyed() {
    // 在组件销毁前清除定时器等
    clearInterval(this.timer);
  },
};

总的来说,created 适合进行数据的初始化和简单的逻辑处理,mounted 则适合进行涉及到 DOM 操作和依赖于 DOM 的其他操作,如启动定时器、操作外部库等。

43. 父子组件在创建和更新的过程中,生命周期函数顺序是什么样的

在 Vue 中,父子组件的生命周期函数的调用顺序是有一定规律的,下面是父子组件创建和更新过程中常见的生命周期函数执行顺序:
父组件创建过程:

  1. beforeCreate:父组件的 beforeCreate 钩子首先被调用。
  2. created:父组件的 created 钩子被调用,此时实例已经完成了数据观测、属性和方法的运算,但尚未挂载到 DOM 中。

子组件创建过程:
3. beforeCreate:子组件的 beforeCreate 钩子首先被调用。
4. created:子组件的 created 钩子被调用,此时子组件实例也完成了数据观测、属性和方法的运算,同样尚未挂载到 DOM 中。
5. beforeMount:子组件的 beforeMount 钩子被调用,在挂载之前。

子组件挂载过程:
6. mounted:子组件的 mounted 钩子被调用,子组件实例被挂载到 DOM 中。

父组件和子组件的更新过程:

  1. 当父组件重新渲染时(数据发生变化等),子组件也会随之重新渲染。
  2. 子组件的更新过程与创建过程类似:
    • beforeUpdate:子组件的 beforeUpdate 钩子在更新之前被调用。
    • updated:子组件的 updated 钩子在更新完成后被调用。

销毁过程:

  1. 当父组件被销毁时,子组件也会被销毁。
  2. 销毁过程中,钩子函数的调用顺序为:
    • beforeDestroy:子组件的 beforeDestroy 钩子在销毁之前被调用。
    • destroyed:子组件的 destroyed 钩子在销毁完成后被调用。

总的来说,父组件和子组件的生命周期函数调用顺序遵循先创建、再挂载、更新和销毁的顺序。

44. 了解双向数据绑定吗,也就是v-model,它的实现原理是什么,具体怎么做的

  • 双向数据绑定是 Vue 中一个重要的特性,通常由 v-model 指令来实现。它可以将表单元素和 Vue 实例的数据进行双向绑定,当表单元素的值发生变化时,Vue 实例的数据也会跟着变化,反之亦然。
  • 实现双向数据绑定的关键在于通过数据劫持和事件监听来实现视图和数据的同步更新。
  1. 数据劫持:
    • Vue 使用了一个称为响应式系统的机制来实现双向数据绑定。它通过 Object.defineProperty 来劫持数据的 getter 和 setter,当数据发生变化时,能够通知相关依赖进行更新。
  2. 事件监听:
    • 对于表单元素,v-model 通过在元素上绑定 inputchange 事件来监听用户的输入。
    • 当表单元素的值发生变化时,触发相应的事件,将新的值发送给 Vue 实例中的数据属性。
    • 同时,当 Vue 实例中的数据属性发生变化时,触发 setter,更新了数据后,会通知相关的视图进行更新。
      示例:
<div id="app">
  <input v-model="message" />
  <p>{{ message }}</p>
</div>

<script>
const app = new Vue({
  el: '#app',
  data() {
    return {
      message: 'Hello Vue!',
    };
  },
});
</script>
  • 当用户在输入框中输入内容时,v-model 绑定了 input 事件监听,实时更新 Vue 实例中的 message 数据属性。同时,当 message 发生改变时,由于数据的双向绑定关系,{{ message }} 的内容也会实时更新。
  • 总的来说,Vue通过数据劫持和事件监听实现了双向数据绑定,确保了数据的变化能够及时地反映在视图上,以及用户在视图上的操作也能够同步更新到数据模型中。

45. 在vue里面对一个数组执行push、pop等操作,页面也会随着发生变化,是怎么做的

  • 在 Vue 中,当你对数组执行像 pushpopsplice 等会改变数组内容的方法时,Vue 能够监听到这些数组的变化,并在页面上及时更新视图。这是因为 Vue 在数组上进行了代理和劫持,以便可以捕获到对数组的修改,并触发视图的更新。
  • 具体来说,Vue 使用了 Object.definePropertyProxy 来监听数组的变化,它会拦截数组变化的方法,比如 pushpopsplice 等,然后在这些方法被调用时,触发相应的更新通知。

举个例子:

new Vue({
  data() {
    return {
      myArray: [1, 2, 3]
    };
  },
  mounted() {
    setTimeout(() => {
      // 在mounted后修改数组,触发更新
      this.myArray.push(4); // 页面会更新显示 [1, 2, 3, 4]
    }, 2000);
  }
});
  • 在这个例子中,当 mounted 生命周期执行后,2秒后执行 this.myArray.push(4),这个数组的变化会被 Vue 监听到,然后 Vue 会触发相应的视图更新,将新的数组内容渲染到页面上。
  • Vue 的响应式系统利用了 JavaScript 的特性,使得在 Vue 实例中的数据变化可以追踪到,并且自动同步到视图上,让开发者无需手动操作 DOM,就能实现数据驱动视图的更新。

46. 使用虚拟DOM有什么优点

使用虚拟 DOM 有几个重要的优点:

  1. 性能优化:
    • 减少直接操作真实 DOM 的次数。虚拟 DOM 作为一个轻量级的 JavaScript 对象树存在于内存中,它可以进行频繁的修改而不会导致昂贵的 DOM 操作,比如频繁的创建、移动和删除 DOM 节点。
    • 虚拟 DOM 可以对一系列的 DOM 操作进行批处理,通过比较新旧虚拟 DOM 树的差异(称为 Virtual DOM Diffing),只对实际改变的部分进行实际的 DOM 更新操作,从而提高性能。
  2. 跨平台应用:
    • 虚拟 DOM 的概念不限于浏览器端,它也可以应用在其他平台,比如服务器渲染(如Node.js)、原生应用(如React Native、Weex等),因为它是一个基于 JavaScript 对象的抽象层,可以被映射到不同平台的真实 UI 元素。
  3. 简化复杂度:
    • 虚拟 DOM 提供了一种简单且高效的方式来描述 UI 的状态,降低了直接操作真实 DOM 的复杂度。它使开发者能够专注于组件状态和应用的逻辑,而不是手动处理 DOM 更新的细节。
  4. 提高渲染性能:
    • 虚拟 DOM 可以进行一些优化,比如在进行实际 DOM 更新前,先在虚拟 DOM 中进行比对,找出最小的变更,再将变更更新到实际的 DOM 中,减少了实际 DOM 更新的次数,提高了渲染性能。

总的来说,虚拟 DOM 的存在大大提高了前端框架的性能和开发效率,它通过对 DOM 的抽象和优化,减少了直接操作真实 DOM 带来的性能问题,并简化了复杂度,使得前端开发更加高效、便捷。

47. 什么时候会进入等待状态

进入等待状态通常发生在多种情况下,尤其在涉及异步操作的场景下:

  1. 异步操作:
    • 当执行一个异步操作时,比如网络请求、定时器、Promise 的 resolvereject 等,当前线程会进入等待状态直到异步操作完成或者达到一定条件。
  2. 锁机制:
    • 在多线程或多进程环境下,某个线程或进程可能因为等待资源的释放或者等待其他线程的操作完成而进入等待状态,直到条件满足。
  3. 事件监听:
    • 在事件驱动的编程模型中,当一个程序监听某个事件并且没有立即触发时,程序会进入等待状态,直到事件发生并被监听器捕获。
  4. 条件等待:
    • 在同步编程中,有时候可能会使用条件变量或者某些条件来控制程序的执行,当条件不满足时,程序会进入等待状态。
  5. 资源竞争:
    • 当多个进程或线程竞争同一个资源时,可能会出现等待状态,比如多个线程竞争某个锁,只有一个线程能够成功获取锁,其他线程会进入等待状态。
      总的来说,等待状态通常发生在程序需要等待某个条件满足、某个事件发生、异步操作完成或者资源可用时。在这些情况下,程序会暂时挂起当前任务,进入等待状态,直到条件满足或者资源可用,然后再继续执行。

48. http请求有哪些类型,什么时候使用post,什么时候用put,get和post有什么区别,get请求限制的数据大小是多少

在 HTTP 协议中,常见的请求类型包括 GET、POST、PUT、DELETE 等,每种请求类型都有不同的作用和用途:

  1. GET:
    • 用于从服务器获取资源,通常用于数据的读取,对服务器的查询请求。
    • 使用 URL 参数传递数据,请求的参数会附加在 URL 后面,可以在浏览器地址栏直接看到。
    • 常用于获取数据,对数据量有限制。
  2. POST:
    • 用于向服务器提交数据,通常用于提交表单、上传文件等操作。
    • 将数据放在请求的消息体中,不会在 URL 上显示。
    • 用于提交数据,对数据量无限制,适用于较大的数据。
  3. PUT:
    • 用于向指定资源位置上传新的内容,也可以创建新的资源。
    • 与 POST 类似,但 PUT 被认为是幂等的(多次调用不会产生不同的影响)。
  4. DELETE:
    • 请求服务器删除指定的资源。
      主要区别:
  • 数据传输方式: GET 请求数据放在 URL 上,POST 请求数据放在消息体中。
  • 数据大小限制: GET 请求对 URL 长度有限制(具体大小取决于浏览器和服务器),POST 请求没有明确的大小限制。
  • 幂等性: GET 是幂等的(多次调用不会产生不同的影响),POST 不是幂等的(多次调用可能会产生不同的影响)。
  • 安全性: GET 请求的参数暴露在 URL 中,不适合传输敏感信息,而 POST 请求的数据在消息体中,相对安全。
    一般情况下,使用 GET 请求获取数据,使用 POST 请求提交数据。但具体应用场景还需根据需求和规范进行选择。

关于 GET 请求限制的数据大小,没有固定的限制,但通常浏览器和服务器都会对 URL 的长度有一定的限制,例如在实际开发中,常见的浏览器对 URL 长度有 2KB 到 8KB 的限制不等。不同浏览器和服务器对 URL 长度的限制可能有所不同,因此最好避免将过大的数据通过 GET 请求传输。

49. 如果想要一个get请求传一个对象可以怎么做; 要用get请求传一个url,里面有http://这些,怎么完整地传输

  • 当使用 GET 请求传递一个对象时,通常可以将对象的属性拼接成查询字符串并附加在 URL 后面。可以使用 encodeURIComponent 对参数进行编码,确保特殊字符正确处理。
  • 举例来说,如果有一个对象 params 需要传递:
const params = { key1: 'value1', key2: 'value2' };
const queryString = Object.keys(params)
  .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
  .join('&');

const url = `http://example.com/api?${queryString}`;
  • 这样生成的 url 就包含了传递对象 params 的查询字符串,可以用于 GET 请求。
  • 至于在 GET 请求中包含类似 http:// 这样的字符,最好使用 URL 编码进行处理。JavaScript 提供了 encodeURIencodeURIComponent 方法来处理这类字符。
  • 例如,假设要传递一个带有 http:// 的 URL,可以使用 encodeURIComponent 对整个 URL 进行编码,然后将编码后的 URL 放入 GET 请求的参数中。
    示例:
const originalUrl = 'http://example.com/path';
const encodedUrl = encodeURIComponent(originalUrl);
const urlWithEncodedParam = `http://example.com/api?url=${encodedUrl}`;

这样可以将带有 http:// 的 URL 作为参数传递给 GET 请求。在服务器端,需要对这个参数进行解码处理以获取原始的 URL。

50. 说一下状态码,401是什么意思,403呢;301和302呢,如果返回一个301一个新地址,发现这个新地址错了,怎么回滚

状态码是在 HTTP 协议中用于表示服务器响应状态的三位数字代码。这些状态码提供了有关请求状态、错误和重定向的信息。

  • 401 Unauthorized(未授权):
    • 表示请求需要进行身份验证,但用户没有提供有效的身份验证信息。需要提供有效的凭据才能访问。
  • 403 Forbidden(禁止访问):
    • 表示服务器理解请求,但拒绝执行请求。通常表示服务器知道用户身份,但不具备执行请求的权限。
  • 301 Moved Permanently(永久重定向):
    • 表示请求的资源已被永久移动到新位置(URL)。任何后续的请求应该使用新的 URL。
  • 302 Found(临时重定向):
    • 表示请求的资源临时被移动到新位置(URL)。之后的请求应该继续使用原始的 URL。
      如果返回了一个 301 状态码,并且发现新地址错误,需要回滚的话,可以通过以下方式:
  1. 修改重定向规则:
    • 在服务器端修改重定向的配置,将原始的 URL 恢复为正确的地址。这样可以确保之后的请求再次访问正确的地址。
  2. 重新发布正确的 URL:
    • 如果新地址是不正确的,可以发布一个新的 301 状态码,将原始的 URL 恢复为正确的地址。这样客户端收到新的重定向后会再次访问正确的地址。

在处理 HTTP 重定向时,服务器端和客户端都有相应的处理方式,可以根据具体情况选择最合适的回滚方法。但需要注意,HTTP 重定向可能会导致缓存等问题,确保在回滚后清除可能存在的缓存。

51. 一个二级域名下有iframe嵌套了一个三级域名,会发生跨域吗?为什么?如果这种情况下要去访问cookie,要怎么做

  • 当一个二级域名下的页面嵌套了一个三级域名的 iframe 时,这属于不同域的跨域情况。跨域是基于域名和协议的安全策略,同源策略要求页面内所有资源(包括 JavaScript、CSS、图片等)必须来自同一个域名、端口和协议,否则会被当作跨域请求。
  • 对于域名而言,主要是考虑了同一主域名下的不同子域名之间的安全性,所以二级域名和三级域名属于不同域,会发生跨域问题。
  • 针对跨域访问 cookie 的问题,一般情况下,跨域的情况下无法直接读取或设置对方域的 cookie,这是浏览器的安全策略。但可以通过设置 document.domain 或者使用一些其他的跨域通信技术来实现跨域访问 cookie。
  1. document.domain:
    • 如果两个页面的域名是同一父域下的不同子域(比如 a.example.comb.example.com),可以通过设置 document.domain 使它们的域名相同,从而实现跨域访问 cookie。
    • 例如,将二级域名和三级域名都设置为相同的主域名:document.domain = 'example.com';
  2. 其他跨域通信技术:
    • 可以使用像 JSONP、CORS(跨域资源共享)、postMessage 等跨域通信技术来实现跨域通信,间接地进行数据交互,但需要服务器端的支持和配置。

需要注意的是,这些方法都需要在目标页面和嵌套页面上都进行相应的设置或者协议支持。因为跨域访问 cookie 涉及到浏览器的安全策略,为了保护用户隐私和数据安全,通常不建议直接在不同域的页面间访问对方域的 cookie。

52. token和cookie的区别

Token 和 Cookie 是用于在客户端和服务器之间进行身份验证和状态管理的两种不同机制,它们有一些区别:

  1. 位置和存储方式:
    • Cookie: Cookie 是存储在客户端(浏览器)的小型文本文件,由服务器在 HTTP 响应头中通过 Set-Cookie 设置,并被浏览器保存,每次请求时都会自动发送到服务器。
    • Token: Token 是一种用于身份验证的令牌,通常作为 HTTP 请求头中的一部分发送到服务器,可以存储在客户端的 Local Storage、Session Storage 或内存中。
  2. 安全性:
    • Cookie: Cookie 可能存在一些安全问题,比如可能会被窃取(跨站脚本攻击、跨站请求伪造攻击等),虽然可以通过设置 Cookie 的安全属性来增加安全性(比如设置 HttpOnly、Secure 和 SameSite 属性)。
    • Token: Token 的安全性取决于实现方式和存储位置,如果使用不当可能会有安全风险,但一般来说,在 HTTPS 协议下,将 Token 存储在客户端的 Local Storage 或者内存中相对较安全。
  3. 用途和灵活性:
    • Cookie: 主要用于客户端和服务器之间进行状态管理,比如用户身份验证、跟踪用户会话等。
    • Token: 通常用于身份验证和授权,特别适用于分布式系统中的身份验证,比如 JSON Web Token (JWT),可以方便地传递用户身份信息和授权信息,适用于跨域、移动端等场景。
  4. 跨域支持:
    • Cookie: 受同源策略的限制,不同域名之间的 Cookie 通常不共享。
    • Token: Token 通常可以跨域使用,在前后端分离、跨域等场景中更为灵活。
      总的来说,Cookie 是存储在客户端的标准机制,用于跟踪会话和存储状态信息;而 Token 作为一种认证方式,更多用于身份验证和授权,可以在分布式系统中更灵活地实现跨域认证和授权。在实际应用中,两者的选择取决于具体的需求和场景。

52. 用户权限

  1. 什么是前端权限管理?

    • 回答: 前端权限管理是指在前端控制用户对页面和功能的访问权限,确保用户只能访问其被授权的部分,保护敏感信息和操作。
  2. 常见的前端权限管理的实现方式有哪些?

    • 角色-Based权限控制: 将用户划分为不同角色,每个角色拥有不同的权限。通过检查用户所属的角色来控制权限。
    • 基于路由的权限控制: 根据路由配置信息,对用户进行权限验证,确保用户只能访问其有权限的路由。
    • 条件渲染: 根据用户的权限信息在界面上进行条件性的显示或隐藏某些功能或组件。
    • API权限控制: 在前端调用后端API时,对用户的请求进行权限验证,确保用户只能执行其有权限的操作。
  3. 什么是 RBAC(Role-Based Access Control)?

    • 回答: RBAC 是一种基于角色的访问控制,通过将用户划分为不同的角色,并为每个角色分配特定的权限,来管理和控制用户对系统资源的访问。
  4. 如何处理前端权限管理中的路由权限?

    • 回答: 可以通过在路由配置中添加权限字段,或者在路由跳转前进行权限验证。在页面切换前,检查用户是否有权限访问目标路由,如果没有则进行拦截或跳转到无权限页面。
  5. 前端权限管理的安全性考虑有哪些?

    • 前端验证和后端验证的结合: 不仅要在前端进行权限控制,还需要在后端进行验证,以防止绕过前端验证。
    • 安全的存储和传输: 确保权限信息在存储和传输过程中使用安全的方式,例如使用 HTTPS 进行加密传输,避免存储敏感信息在客户端。
    • 防御性编程: 防范各种攻击,如跨站脚本(XSS)和跨站请求伪造(CSRF),以提高系统的安全性。

这些问题涉及到前端权限管理的基本概念、实现方式和安全性等方面,面试者可以通过这些问题展示对于前端权限管理的理解和实际经验。

53. GIT: 有没有使用git,如果上线的代码,发现有bug,怎么回退,有哪些命令可以实现,回答了reset 和revert,又问有什么区别

当使用 Git 进行代码管理时,如果上线后发现有 bug 需要回退代码,有几种常用的命令可以实现:

  1. Reset:
    • 使用 git reset 命令可以将 HEAD 指向指定的 commit,同时修改索引和工作目录,回退到之前的提交。
    • git reset 命令有几种模式,比如 --soft--mixed--hard,分别表示不同的重置方式(保留修改、保留修改但取消暂存、丢弃修改)。
  2. Revert:
    • 使用 git revert 命令可以创建一个新的提交,来撤销之前的提交,保留历史记录,不会修改过去的提交记录,而是创建一个新的提交来撤销之前的更改。
      这两个命令的区别在于对历史提交的影响方式不同:
  • Reset 会改变历史记录,它会移动分支的指向,并且可以完全删除某些提交的记录,因此在多人协作或者公共分支上使用时可能会带来问题。
  • Revert 则会创建一个新的提交来撤销之前的提交,这种方式不会改变历史记录,因此更适合在公共分支上使用,但会增加提交的历史记录。

Git 中的 git rebase 是一个用于合并分支的命令,它可以将一个分支的提交历史移动到另一个分支上,重新应用在目标分支上,使得提交历史更加整洁和线性。
相比于 git mergegit rebase 有一些不同之处:

  1. 历史清晰: git rebase 可以将一个分支上的提交历史整理成线性结构,避免出现合并提交,使得提交历史更清晰、更易于阅读。
  2. 变基: git rebase 实际上是对提交历史进行变基(rebase),将当前分支的提交挪动到目标分支的最后,而不是简单地将两个分支的历史合并在一起。
  3. 冲突处理: 当变基时,如果发生冲突,需要手动解决冲突,然后使用 git rebase --continue 命令继续变基操作。
    在使用 git rebase 时需要注意以下几点:
  • 避免在公共分支上进行 rebase: 对于公共分支(比如 master),尽量避免使用 git rebase,因为它会改变提交历史,可能影响其他人的工作。
  • 注意风险: 使用 git rebase 可能会造成提交历史的混乱,需要谨慎操作,避免对已经分享给他人的提交历史进行修改。
    总体来说,git rebase 可以帮助整理提交历史,使得提交更加清晰、有序,但在操作时需要注意潜在的风险和对他人的影响。

54. CSRF和XSS攻击分别是什么,怎么防止

CSRF(Cross-Site Request Forgery)和 XSS(Cross-Site Scripting)是常见的网络安全攻击方式:

  1. CSRF(跨站请求伪造):

    • CSRF 攻击是指攻击者利用用户在当前已登录的身份下,伪造用户的请求,完成一些非法操作。
    • 攻击者通过引诱用户访问恶意网站,利用网站中的恶意代码发送请求至目标网站,利用用户的身份发起恶意请求。

    防范措施:

    • 使用 CSRF Token:在用户登录时生成一个随机 Token,并将 Token 存储在用户的 Session 或者 Cookie 中,在每次提交请求时都要验证 Token 的有效性。
    • 同源检测:检测请求的来源是否与期望的来源一致。
  2. XSS(跨站脚本攻击):

    • XSS 攻击是指攻击者将恶意脚本代码注入到网页中,当用户访问包含恶意脚本的页面时,脚本会在用户的浏览器上执行。
    • 攻击者可以窃取用户信息、会话信息,甚至篡改页面内容。

    防范措施:

    • 输入过滤和转义:对用户输入进行过滤和转义,避免直接将用户输入的内容作为 HTML 或 JavaScript 插入页面中。
    • HTTP 头部设置:使用 Content Security Policy(CSP)等机制,设置 HTTP 头部,限制页面资源加载和执行。

为防止这些攻击,常见的做法包括:

  • 输入验证和过滤: 对用户输入进行严格的验证,过滤特殊字符和敏感内容。
  • 输出转义: 在将用户输入展示在页面上之前,对其进行适当的转义,确保它们不会被执行。
  • 使用安全的认证机制: 如 CSRF Token 来验证用户请求的合法性。
  • 合理设置 HTTP 头部: 使用 Content Security Policy(CSP)等机制,限制页面资源的加载和执行。

55. 浏览器对DOM元素触发的过程 (其实就是事件处理机制)

浏览器的事件处理机制涉及多个阶段,包括事件捕获、目标阶段和事件冒泡。这些阶段组成了所谓的事件流,描述了事件在 DOM 中传播和处理的顺序。

  1. 事件捕获阶段(Capture Phase):
    • 事件从最外层的祖先元素(window)开始,逐级向下传播至目标元素之前。
    • 浏览器会在捕获阶段检查是否存在对事件的处理程序。
  2. 目标阶段(Target Phase):
    • 事件达到目标元素(即事件绑定的元素)。
    • 浏览器检查是否存在目标元素上的事件处理程序。
  3. 事件冒泡阶段(Bubbling Phase):
    • 事件从目标元素开始,逐级向上冒泡至最外层的祖先元素(window)。
    • 浏览器在冒泡阶段检查是否存在对事件的处理程序。
      在这个过程中,如果在某个阶段找到了对应的事件处理函数(比如 click、mouseover 等),则会执行相应的处理函数。事件处理程序可以通过阻止事件的默认行为或停止事件传播来干预事件的执行。

常见的事件处理方式包括:

  • DOM0级事件处理: 直接给元素添加事件处理函数,比如 element.onclick = function() {}
  • DOM2级事件处理: 使用 addEventListenerremoveEventListener 方法进行事件处理,支持添加多个事件处理函数,更加灵活。

56. 判断对象中某个字符出现的次数

function countCharOccurrences(obj, char) {
  let count = 0;

  for (let key in obj) {
    if (typeof obj[key] === 'string') {
      const str = obj[key];
      for (let i = 0; i < str.length; i++) {
        if (str.charAt(i) === char) {
          count++;
        }
      }
    }
  }

  return count;
}

// 示例对象
const myObj = {
  prop1: 'Hello World',
  prop2: 'OpenAI is awesome!',
  prop3: 'Have a great day!'
};

// 统计字符 'a' 在对象中出现的次数
const charCount = countCharOccurrences(myObj, 'a');
console.log(`Character 'a' appears ${charCount} times in the object.`);

解释: 这个 countCharOccurrences 函数接受两个参数:obj 表示要检查的对象,char 表示要计数的字符。它会遍历对象的属性值,检查每个字符串中特定字符出现的次数,最后返回该字符在对象中出现的总次数。

57. 手写:寄生组合式继承

  • 寄生组合式继承是一种通过借用构造函数来继承属性,通过原型链来继承方法的方式。下面是使用 JavaScript 手写寄生组合式继承的示例:
// 父类
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
  return this.name;
};

// 子类
function Child(name, age) {
  Parent.call(this, name); // 继承父类属性
  this.age = age;
}

// 使用寄生方式继承父类原型链上的方法
function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // 创建一个临时构造函数的原型副本
  prototype.constructor = child; // 将构造函数指针指向子类
  child.prototype = prototype; // 将子类的原型指向临时原型对象
}

// 将子类的原型与父类的原型链连接起来
inheritPrototype(Child, Parent);

// 示例使用
const child1 = new Child('Alice', 5);
const child2 = new Child('Bob', 8);

child1.colors.push('black'); // 修改子类实例属性
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green']

console.log(child1.getName()); // 'Alice'
  • 这里的 Parent 是父类,Child 是子类。在 Child 构造函数中使用 Parent.call(this, name) 来继承父类的属性。然后,通过 inheritPrototype 函数,将子类的原型与父类的原型链连接起来,实现了原型链的继承。

58. 路由懒加载原理

  • 路由懒加载(Route-based code splitting)是一种优化网页加载性能的技术,它允许在需要时才加载特定路由对应的代码。懒加载能够帮助减少初始页面加载所需的资源大小,提高页面的加载速度。
  • 在前端框架(比如 Vue、React)中,路由懒加载通常是通过动态导入(Dynamic Import)来实现的。这意味着在路由被访问之前,相关的组件代码不会被提前加载,而是在需要时才会异步加载。
  • 举例来说,在使用 Vue Router 实现路由懒加载时,可以像这样使用动态导入:
const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
];
  • 在这个例子中,import('./views/Home.vue')import('./views/About.vue') 是动态导入的语法。当用户访问 /about 路由时,才会下载和加载 About.vue 组件的代码。同样的原理也适用于其他前端框架的路由懒加载实现。
  • 这种技术的优点是可以减少初始页面加载的资源体积,提高页面的响应速度,特别是对于较大型单页应用或者页面中的复杂组件场景。通过懒加载,只有在需要时才加载所需的代码,提高了应用的性能和用户体验。

59. 浏览器事件机制

浏览器事件机制描述了当特定事件发生时,浏览器是如何处理和传递这些事件的。这个机制包含了事件的捕获阶段、目标阶段和冒泡阶段。

  1. 事件捕获阶段(Capture Phase):
    • 事件从文档的根节点(window 对象)开始传播到目标元素之前的阶段。
    • 事件按照从外向内的顺序经历父元素、祖先元素,直到达到目标元素之前。
  2. 目标阶段(Target Phase):
    • 事件达到目标元素。
    • 事件被触发在目标元素上,并在目标元素上执行相关的事件处理函数。
  3. 事件冒泡阶段(Bubbling Phase):
    • 事件从目标元素开始,向外传播至文档的根节点(window 对象)的阶段。
    • 事件按照从内向外的顺序经历目标元素的父元素、祖先元素,直到达到文档的根节点。
      在这个过程中,每个阶段都可以有对应的事件处理函数。事件捕获阶段和事件冒泡阶段可以利用事件委托的方式来进行事件处理。
      事件处理程序:
  • 可以通过 addEventListener 方法添加事件处理程序。
  • 事件处理程序可以通过 event 对象访问事件相关的信息,比如事件目标、触发元素等。
  • 可以使用 event.stopPropagation() 阻止事件的进一步传播。
  • 可以使用 event.preventDefault() 阻止事件的默认行为。

60. 重排重绘

重排(Reflow)和重绘(Repaint)是浏览器渲染页面时发生的两个重要过程。

  1. 重排(Reflow):
    • 当页面中的部分或全部内容发生影响布局的变化时,浏览器会重新计算元素的几何属性(比如尺寸、位置)并重新构建渲染树,这个过程称为重排。
    • 重排可能是由于以下行为触发的:改变窗口大小、改变元素的尺寸、内容的变化、添加或删除可见的 DOM 元素、修改 CSS 样式等。
    • 重排会导致性能开销较大,因为浏览器需要重新计算布局,并且可能触发重绘。
  2. 重绘(Repaint):
    • 当元素的样式发生变化,但不影响其布局时,浏览器会重新绘制元素的可视部分,这个过程称为重绘。
    • 重绘发生在重排之后,当元素的样式变化不会影响布局时,仅仅改变了元素的视觉样式,不会引起重新布局。
      重排和重绘的关系:
  • 重排一定会导致重绘,因为元素的几何属性变化后,必须重新绘制这些元素。
  • 但是,仅进行重绘并不一定会触发重排,因为元素的外观变化并不一定会影响到布局。
    优化重排和重绘:
  • 使用 CSS3 动画和过渡来代替 JavaScript 操作样式,因为它们可以通过 GPU 加速,减少重排和重绘的影响。
  • 批量修改样式:可以通过修改元素的 class,或者将需要多次改变的样式放在一个样式操作中,避免多次触发重排和重绘。
  • 使用文档片段(Document Fragment)进行 DOM 操作,最后一次性添加到页面中,减少频繁的 DOM 操作。
    理解重排和重绘对于优化页面性能和用户体验非常重要,因为频繁的重排和重绘会影响页面的流畅度和加载速度。

61. 事件冒泡和事件委托是什么

事件冒泡(Event Bubbling)和事件委托(Event Delegation)都是与事件传播相关的概念,它们涉及到事件在 DOM 结构中传播和处理的方式。

  1. 事件冒泡(Event Bubbling):

    • 事件冒泡是指在触发某个特定事件后,事件会从触发元素开始沿着 DOM 层级向上传播,一直传播至文档根节点。
    • 比如,当你点击一个嵌套的元素时,除了在最内层的元素上触发事件外,事件还会沿着父元素、祖先元素一直向上传播,直到文档根节点。
  2. 事件委托(Event Delegation):

    • 事件委托是一种利用事件冒泡原理,在父元素上统一管理子元素事件处理的方式。
    • 通过将事件处理器绑定在父元素上,并利用事件冒泡,在父元素上捕获并处理子元素上的事件。
  • 在事件处理中,targetcurrentTarget 是用于获取事件触发的目标元素和当前绑定事件的元素的属性。它们的区别在于:
    • target 属性:表示事件最初发生的元素,即事件的实际目标。
    • currentTarget 属性:表示当前绑定事件处理函数的元素,即事件处理程序当前正在处理事件的元素。
  • 具体来说,假设有一个列表,你在 <ul> 元素上绑定了点击事件,而列表项 <li> 元素是这个 <ul> 的子元素。当你点击列表项时,事件会冒泡至 <ul> 元素上,触发绑定的点击事件。
    • event.target 会返回你实际点击的 <li> 元素,即事件最初发生的目标。
    • event.currentTarget 会返回绑定点击事件的 <ul> 元素,即当前正在处理事件的元素。
<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<script>
  document.getElementById('myList').addEventListener('click', function(event) {
    console.log(event.target.textContent); // 返回你实际点击的列表项内容
    console.log(event.currentTarget.id); // 返回当前绑定事件的元素 ID
  });
</script>

这种区别在事件委托时特别有用。通过在父元素上绑定事件,可以利用 event.target 来确定实际触发事件的子元素,进而执行相应的操作。同时,currentTarget 可以用来处理多个子元素共享同一个事件处理程序的情况。

62. 垃圾回收机制

垃圾回收是指计算机内存管理的过程,它负责自动释放不再被程序使用的内存空间,以便其他程序或系统可以重用这些空间。在 JavaScript 中,垃圾回收是自动进行的,由 JavaScript 引擎负责管理。
JavaScript中的垃圾回收主要依赖于两种策略:

  1. 标记清除(Mark and Sweep):
    • 这是 JavaScript 中最常用的垃圾回收算法之一。
    • 当变量进入执行环境(如函数中声明变量)时,将其标记为“进入环境”。
    • 当变量离开环境时,则将其标记为“离开环境”。
    • 垃圾回收器会在特定时刻标记所有未被标记的变量(即视为不再需要的变量),并清除其占用的内存空间,以便后续重用。
  2. 引用计数(Reference Counting):
    • 这是一种较早的垃圾回收方式,但在处理循环引用时存在问题,现代浏览器已较少使用。
    • 基于变量的引用计数,每当有一个变量引用该对象时,对象的引用计数加一,当变量不再引用该对象时,引用计数减一。
    • 当引用计数为零时,即意味着没有变量再引用该对象,垃圾回收器会回收该对象所占的内存空间。
      JavaScript的垃圾回收器会周期性地运行,并标记和清除不再使用的变量,释放其占用的内存空间。虽然垃圾回收是自动处理的,但开发者可以通过避免全局变量的滥用、及时释放不再使用的对象引用等方式,优化内存管理,减少内存泄漏的可能性。

63. 问了个场景题是一个请求在20秒内效应就接受,20秒后就结束请求怎么做

  • 这种场景可以使用 Promise.race 和 setTimeout 来处理。Promise.race 可以接收一个包含多个 Promise 的可迭代对象,返回一个 Promise,该 Promise 在可迭代对象中的任何一个 Promise 解决或拒绝时解决或拒绝。
  • 你可以创建一个 Promise 对象,包括一个完成请求的 Promise 和一个在 20 秒后自动拒绝的延迟 Promise,并将它们传递给 Promise.race。当其中一个 Promise 在 20 秒内解决时,就执行相关操作,否则,20 秒后自动拒绝。
// 模拟请求的函数
function doRequest() {
  return new Promise((resolve, reject) => {
    // 这里模拟请求的处理
    setTimeout(() => {
      resolve('Request completed successfully');
    }, 15 * 1000); // 请求完成时间在 15 秒内
  });
}

function makeRequestWithTimeout() {
  const requestPromise = doRequest(); // 请求的 Promise

  const timeoutPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('Request timed out');
    }, 20 * 1000); // 20 秒后超时
  });

  // 使用 Promise.race 进行处理
  return Promise.race([requestPromise, timeoutPromise])
    .then(result => {
      console.log(result); // 请求成功的结果
      // 进行请求成功后的操作
    })
    .catch(error => {
      console.log(error); // 请求超时的结果
      // 进行请求超时后的操作
    });
}

makeRequestWithTimeout();

这段代码中,doRequest 函数模拟了一个请求,延迟了 15 秒来模拟请求的处理时间。makeRequestWithTimeout 函数创建了一个超时 Promise,以及一个实际请求的 Promise,并使用 Promise.race 来执行这两个 Promise。如果请求在 20 秒内完成,就会执行成功处理程序;如果在 20 秒后才完成,就会执行超时处理程序。

64. Promise.race 和 Promise.any

Promise.race:它返回一个 Promise,只要传递给它的多个 Promise 中有一个变为 resolved(成功状态)或 rejected(失败状态),它就会立即返回并采用第一个完成的 Promise 的状态和值。
Promise.any:它也返回一个 Promise,但它会等待所有传递给它的 Promise 全部变为 rejected,然后才会返回一个 rejected Promise。只有当所有传递的 Promise 都失败时,它才会失败,否则它会采用第一个成功的 Promise 的状态和值。

65. 为什么cache-control的优先级更高 expires有什么弊端

Cache-ControlExpires 都是用于设置 HTTP 响应头,控制浏览器缓存行为的指令。但是它们在优先级和灵活性上有所不同。

  1. 优先级:
    • Cache-Control 的优先级更高,因为它是 HTTP/1.1 的标准,而 Expires 是 HTTP/1.0 的标准。当这两个响应头同时存在时,Cache-Control 会覆盖 Expires
    • Cache-Control 允许更多的控制,如 max-age 可以指定缓存的最大存储时间,no-cache 可以指示浏览器在使用缓存前必须与服务器确认等。
  2. 灵活性:
    • Cache-Control 提供了更多的灵活性,允许设置更多参数来控制缓存的行为。
    • Expires 只能设置一个具体的过期时间,无法灵活地控制缓存策略。由于它是一个绝对的日期/时间,可能会导致在客户端和服务端时间不同步时出现缓存失效问题。
      Expires 的弊端:
  • 依赖于客户端和服务器的时间设置,如果二者的时间不同步或者存在时区差异,可能导致缓存不一致或者失效问题。
  • 由于其是一个绝对时间,如果服务器的时间和客户端的时间不同步,可能导致缓存的过期时间不准确。
    总的来说,推荐使用 Cache-Control 来控制缓存,因为它提供了更多的灵活性和更精确的控制,避免了时间同步不准确的问题,同时具有更高的优先级。

66. 浏览器是单线程的吗

  • 是的,浏览器的 JavaScript 引擎是单线程的。JavaScript 的单线程执行意味着在同一时间内只能执行一个任务,即同一时间只能做一件事情。
    这种设计的原因包括:
  1. 简化设计: 单线程减少了许多复杂性,使得编程和调试更加简单。
  2. 避免竞态条件(Race Condition): 多线程可能会导致数据竞争和不可预测性,而单线程避免了这些问题。
  3. 安全性: JavaScript 是运行在浏览器中的,多线程可能会带来安全隐患,比如死锁等问题。
    尽管 JavaScript 引擎是单线程的,但是现代浏览器通过一些机制来实现并发处理,比如:
  • 事件循环(Event Loop): JavaScript 引擎通过事件循环机制实现异步编程,允许在执行堵塞的操作时,同时执行其他任务。
  • Web Workers: 允许在独立的线程中执行 JavaScript 代码,但这些线程不能直接操作 DOM,主要用于执行计算密集型任务。(Web Worker通过加载一个脚本文件,进而创建一个独立工作的线程,在主线程之外运行,worker线程运行结束之后会把结果返回给主线程,worker线程可以处理一些计算密集型的任务,这样主线程就会变得相对轻松,这并不是说JS具备了多线程的能力,而是浏览器作为宿主环境提供了一个JS多线程运行的环境。)
    虽然主线程是单线程的,但浏览器是多线程的,比如渲染引擎会使用多个线程来处理网络请求、解析 HTML/CSS、执行 JavaScript 等。这样可以实现并行处理,提高整体性能。

67. 面积计算

矩形: 表面积 = 2 * (长 × 宽 + 长 × 高 + 宽 × 高)
圆柱体: 表面积 = 2 * π * 半径 * (半径 + 高度)
三角形: 根据三角形类型,可以应用不同的公式,如海伦公式或基本的底乘高除以二等等。

68. JS内存管理,内存泄漏的识别方法有什么

在 JavaScript 中,内存管理是自动进行的,但仍然存在内存泄漏的风险。内存泄漏指的是程序中不再使用的内存仍然被占用,无法被释放,导致内存不断增加,最终影响程序性能。

一些常见的内存泄漏识别方法包括:

  1. 监控工具: 使用浏览器的开发者工具(比如 Chrome 的 Memory 或 Performance 面板)进行内存使用情况的监控。这些工具可以显示内存使用量、堆栈信息等,帮助定位内存泄漏的问题。
  2. Heap Profiler: 利用堆(Heap)分析器,检查程序的堆内存情况。这些工具可以显示对象的引用情况,帮助找到被意外引用的对象或者意外保留的引用。
  3. 手动检查: 仔细审查代码,尤其是长期运行的或者频繁执行的部分。注意查看未被释放的对象或变量,例如未关闭的事件监听器、定时器、未移除的全局变量等。
  4. 内存快照(Memory Snapshot): 获取程序运行期间的内存快照,对比不同时间点的快照可以发现内存增长和内存泄漏的趋势。
  5. 使用性能工具检测: 在代码中插入监视器或者日志,追踪对象的创建和销毁,以及其生命周期,从而找出那些被意外保留的对象。
  6. 代码审查和静态分析: 定期审查代码,进行静态分析,寻找可能存在的潜在内存泄漏问题。
    识别内存泄漏需要结合多种方法,并对程序的内存使用进行全面的监控和分析。注意,有些情况下,并非所有的内存增长都是内存泄漏,有时候也可能是程序正常运行所需的内存增长。因此,对内存使用情况的分析和评估需要结合业务逻辑和实际情况进行判断。

69. New 的过程, 静态方法怎么处理

  1. 创建一个空对象: 通过 new 关键字创建一个空对象。
  2. 设置对象的原型: 将新对象的原型指向构造函数的 prototype 属性。
  3. 执行构造函数: 将构造函数作为方法来执行,同时将新创建的对象作为 this 绑定到构造函数中。
  4. 返回新对象: 如果构造函数没有显式返回其他对象,则返回新创建的对象,否则返回构造函数显式返回的对象。

70. HTML解析是同步的吗?

  • 在浏览器环境中,HTML 解析是逐步进行的,并且解析过程是异步的。当浏览器接收到 HTML 文件时,它会逐行解析 HTML,并生成 DOM(文档对象模型)。这个过程是逐步完成的,不需要等待整个 HTML 文件完全下载完成。
  • 解析 HTML 的过程中,浏览器会遇到外部资源(例如,CSS 文件、JavaScript 文件、图片等),这些资源的加载和解析过程可能是异步的。例如,当浏览器遇到 <script> 标签时,它会暂停 HTML 解析,去下载和执行脚本。同样,当遇到外部样式表或图片时,浏览器也可能异步地进行资源加载。
  • 在一定程度上,HTML 的解析和渲染是异步的,因为浏览器不需要等待整个文档下载完成,而是在接收到部分内容时就开始解析和渲染。这有助于提高页面加载性能,用户可以更快地看到页面内容。

71. 同源策略是为了解决什么问题?

  • 同源策略(Same-Origin Policy)是浏览器的一种安全机制,用于防止恶意网站通过脚本等手段访问另一个网站的敏感信息。同源策略限制了一个网页中的文档或脚本只能与同源的资源进行交互,而无法直接访问来自不同源的资源。
  • 同源是指协议、域名、端口号都相同。如果两个 URL 的协议、域名、端口号有一个不同,就被认为是不同源。同源策略的主要目标是:
  1. 保护用户隐私: 防止恶意网站通过脚本等手段访问用户在其他网站上的敏感信息,例如 Cookie、本地存储等。
  2. 防止跨站请求伪造(CSRF): 同源策略防止恶意网站伪造请求,以用户身份在其他网站上执行某些操作,例如修改密码、发起转账等。
  3. 防止跨站脚本攻击(XSS): 同源策略限制了脚本只能访问同源的文档,减少了恶意脚本攻击的可能性。
  • 虽然同源策略是一个有效的安全机制,但在一些特殊情况下可能会造成开发的不便,因此浏览器提供了一些方式来支持跨域访问,如 JSONP、CORS 等。这些技术允许服务器声明是否允许其他域的请求,并且可以在一定程度上绕过同源策略的限制。

72. 事件代理,事件冒泡,为什么用?优点?

  • 事件代理(Event Delegation)是一种通过将事件处理程序添加到父元素而不是每个子元素来管理事件的方法。当事件冒泡到父元素时,通过检查事件的目标来判断是哪个子元素触发的事件,并执行相应的操作。这种技术的优点包括:
  1. 性能优化: 将事件处理程序添加到单个父元素而不是多个子元素可以减少内存占用和提高性能。特别是在有大量子元素的情况下,减少事件处理程序的数量对性能有显著的影响。
  2. 动态元素: 当页面中的元素是通过异步加载、动态生成或后续添加的,使用事件代理可以确保这些新元素上的事件也被捕获,而不需要额外的绑定操作。这对于处理动态内容非常有用。
  3. 代码简洁: 通过将事件处理程序添加到父元素,可以减少事件绑定的代码量,使代码更简洁、易读、易维护。
  4. 减少内存占用: 少量的事件处理程序通常比为每个元素都绑定事件处理程序要占用更少的内存,因为它们被存储在父元素上,而不是每个子元素上。

代码手写

1. DOM操作

// 获取元素 很多种类的写法, 通过id选择器来获取元素   
const element = document.querySelector('#myDiv')
// 添加类 add  通过classList来添加类; add 
element.classList.add('highlight')
// 删除类 remove 
element.classList.remove('highlight')
// element.textContent = "Hello hk"
// 修改元素HTML内容 innerHTML innerHTML可以添加相关元素
element.innerHTML = '<strong>Hello, hk!</strong>'

2. 书写一个防抖函数

function debounce(func, delay) {
    let timer = null
    let args = arguments
    return function() {
        clearTimeout(timer)
        timer = setTimeout(() => {
            func.apply(this, args)
        }, delay);
    }
}

3. 书写一个节流函数

function throttle(func, delay) {
    let isThrottled = false

    return function (...args) {
        // 没有节流,从此处就开启节流模式
        if (!isThrottled) {
            func.apply(this, args)
            isThrottled = true
            // 这样在指定的时间间隔内不会再次执行func函数。
            setTimeout(() => {
                isThrottled = false
            }, delay);
        }
    }
}

4. 模拟Vue的响应式原理

Vue2的响应式原理

let person = {
    name: '张三',
    age: 18
}
let p = {}
// 通过一个对象去操控另一个对象; 通过Object.defineProperty() 来进行相关的模拟操作
Object.defineProperty(p, 'name', {
    // 用于决定该属性是否可以被重新配置或删除
    configurable: true,
    // 有人读取name属性时
    get() {
        console.log('有人读取了name属性');
        return person.name
    },
    // 有人修改name属性时
    set(value) {
        console.log('有人修改name属性');
        person.name = value
    }
})

Vue3的响应式原理

let person = {
    name: '张三',
    age: 18
}
// 会返回一个新的对象;
const p = new Proxy(person, {
    // 有人读取了p中的某个属性时调用
    // 参数;目标对象 || 属性值
    get(target, propName) {
        console.log(`有人读取了p身上的${propName}属性`);
        return Reflect.get(target, propName)
    },
    // 有人修改了p的某个属性 || 给p追加某个属性时调用
    set(target, propName, value) {
        console.log(`有人修改了p身上的${propName}属性`);
        Reflect.set(target, propName, value)
    },
    // 有人删除p的某个属性时调用
    deleteProperty(target, propName) {
        console.log(`有人删除了p身上的${propName}属性`);
        return Reflect.deleteProperty(target, propName)
    }
})

5. 实现数组的扁平化

// 1、使用递归实现(使用递归来实现)
function flattenArray(arr) {
    let result = []
    // 遍历; 
    for (let item of arr) {
        // 判断
        if (Array.isArray(item)) {
            result = result.concat(flattenArray(item))
        } else {
            result.push(item)
        }
    }
    return result 
}
// const nestedArray = [1, [2, 3], [4, [5, 6]]];
// const flattenedArray = flattenArray(nestedArray);
// console.log(flattenedArray); // Output: [1, 2, 3, 4, 5, 6]

6. 实现字符串反转

/* 
    反转字符串 ~ 
*/
// 1、使用数组反转方法
function reverseString(str) {
    /* 
        split() - 字符串转换为数组
        join() - 数组转换为字符串
    */
    return str.split('').reverse().join('')
}

// 2、循环逐渐字符反转
function reverseStringTwo(str) {
    let reversed = ''
    for(let i = str.length - 1; i >= 0; i--) {
        reversed += str.charAt(i)
    }
    return reversed
}

7. 手写数组的forEach方法

// 将方法添加到原型prototype中可能影响整个代码库,少用
/* 
    此代码中,this表示调用该方法的数组对象
*/
Array.prototype.myForEach = function(callback) {
    for(let i = 0; i < this.length; i++) {
        callback(this[i], i, this)
    }
}
const numbers = [1, 2, 3, 4, 5]
numbers.myForEach((value, index, array) => {
    console.log(`Value: ${value}, Index: ${index}, Array:[${array}]`);
})

8. 手写快速排序

function quickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    // 其实是找到一个基准数;
    // const pivot = arr[Math.floor(arr.length / 2)];
    const pivot = arr[0]
    const left = [];
    const right = [];

    for (const element of arr) {
        if (element < pivot) {
            left.push(element);
        } else if (element > pivot) {
            right.push(element);
        }
    }
    // 使用递归的形式来处理; 
    // 因为每次最后拿到的就是数组的形式;可以进行递归处理
    return [...quickSort(left), pivot, ...quickSort(right)];
}

const unsortedArray = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
const sortedArray = quickSort(unsortedArray);
console.log(sortedArray);

9. 两个async同时执行, 使用Promise.all()

async function asyncFunction1() {
    await someAsyncOperation() // 第一个async函数的相关逻辑
    console.log('async function 1 Done');
}

async function asyncFunction2() {
    await someAsyncOperation()
    console.log('async function 2 Done');
}

// 使用Promise.all来同时执行两个async函数
Promise.all([asyncFunction1(), asyncFunction2()]).then(() => {
    console.log('Both Async Function completed');
}) 

10. 画一个圆圈

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #circle {
            width: 100px;
            height: 100px;
            background-color: red;
            border-radius: 50%;
            position: absolute; 
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            display: none;  
        }
    </style>
</head>
<body>
    <div id="circle" class="hidden"></div>
    <button id="showCircle">显示圆圈</button>

    <script>
        const circle = document.getElementById('circle')
        const showCircleButton = document.getElementById('showCircle')
        showCircleButton.addEventListener('click', () => {
            circle.style.display = 'block'
        })
    </script>
</body>
</html>

11. 垂直居中弹窗实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        .modal {
            display: none;
            position: fixed;
            background-color: rgba(0, 0, 0, 0.5);
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
        }

        .modal-content {
            background-color: white;
            padding: 20px;
            border-radius: 5px;
            text-align: center;
        }

        button {
            margin-top: 10px;
        }
    </style>
</head>

<body>
    <div class="modal">
        <div class="modal-content">
            <p>Hello, this is a center</p>
            <button id="closeModal">Close</button>
        </div>
    </div>
    <button id="openModal">Open modal</button>

    <script>
        const openModalButton = document.getElementById("openModal");
        const closeModalButton = document.getElementById("closeModal");
        const modal = document.querySelector(".modal");

        openModalButton.addEventListener("click", () => {
            modal.style.display = "block";
        });

        closeModalButton.addEventListener("click", () => {
            modal.style.display = "none";
        });

    </script>
</body>

</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值