Skip to content

八股文

约 45185 字大约 151 分钟

面试题

2025-05-27

一、 前端基础 (HTML / CSS)

1. CSS实现水平垂直居中 (手写,尽可能多写)

水平垂直居中是CSS布局中的经典问题,有多种实现方式:

  1. Flexbox (弹性盒子) - 最推荐

    • 父元素设置:
      .parent {
        display: flex;
        justify-content: center; /* 水平居中 */
        align-items: center;     /* 垂直居中 */
        height: 100vh; /* 或其他固定高度 */
      }
      .child {
        /* 子元素无需特殊设置 */
      }
    • 子元素自身居中 (针对单个子元素):
      .parent {
        display: flex;
        height: 100vh;
      }
      .child {
        margin: auto; /* 子元素设置 margin: auto; 即可水平垂直居中 */
      }
  2. Grid (网格布局) - 现代推荐

    • 父元素设置:
      .parent {
        display: grid;
        place-items: center; /* 同时实现水平垂直居中 */
        /* 或 justify-items: center; align-items: center; */
        height: 100vh;
      }
      .child {
        /* 子元素无需特殊设置 */
      }
  3. Absolute + Transform (绝对定位 + 变形) - 常用且兼容性好

    • 父元素设置: position: relative; (作为定位上下文)
    • 子元素设置:
      .parent {
        position: relative;
        height: 100vh;
      }
      .child {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%); /* 自身宽度高度的-50% */
      }
  4. Absolute + Margin Auto (绝对定位 + 外边距自动) - 适用于已知子元素宽高

    • 父元素设置: position: relative;
    • 子元素设置:
      .parent {
        position: relative;
        height: 100vh;
      }
      .child {
        position: absolute;
        width: 100px;  /* 必须有明确的宽高 */
        height: 100px;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        margin: auto; /* 利用margin:auto在绝对定位下实现居中 */
      }

2. CSS优先级以及权重

CSS 的优先级(Specificity)决定了当多个 CSS 规则应用于同一个元素时,哪个规则的样式会生效。权重是计算优先级的一种机制。

权重计算规则:

权重值通常表示为 (A, B, C, D) 四个数字的组合,从左到右优先级递减。

  • A:内联样式 (Inline Style)

    • 直接写在 HTML 元素的 style 属性中的样式。
    • 权重值:1, 0, 0, 0
    • 示例:<div style="color: red;"></div>
  • B:ID 选择器 (ID Selector)

    • 使用 # 符号的 ID 选择器。
    • 权重值:0, 1, 0, 0
    • 示例:#myElement { color: blue; }
  • C:类选择器、属性选择器、伪类选择器 (Class, Attribute, Pseudo-class Selectors)

    • 类选择器(.className

    • 属性选择器([attribute="value"]

    • 伪类选择器(:hover, :nth-child(), :focus 等)

    • 权重值:0, 0, 1, 0

      示例:.myClass { color: green; }, [type="text"] { ... }, :hover { ... }

  • D:元素选择器、伪元素选择器 (Element, Pseudo-element Selectors)

    • 元素选择器(div, p, a 等)
    • 伪元素选择器(::before, ::after, ::first-line 等)
    • 权重值:0, 0, 0, 1
    • 示例:p { color: orange; }, ::before { ... }
  • 通用选择器 *、组合器 + > ~、否定伪类 :not()

    • 这些选择器本身的权重值是 0, 0, 0, 0
    • !not() 伪类不增加自身的权重,但它内部的参数会参与权重计算。例如,:not (.class)的权重是0,0,1,0(由.class 提供)。

!important 规则:

  • !important 是一个特殊的声明,它会覆盖所有其他正常的优先级规则。
  • 优先级: 拥有 !important 的声明优先级最高,甚至高于内联样式。
  • 使用建议: 尽量避免使用 !important,因为它会破坏正常的层叠规则,使得调试和维护变得困难。通常只在以下情况考虑使用:
    • 覆盖第三方库的样式。
    • 在开发过程中临时调试。
    • 特殊情况下的用户样式(例如,用户代理样式表)。
  • 注意: 多个 !important 声明之间,仍然会比较其原始选择器的优先级。如果原始优先级相同,则后定义的生效。

优先级比较规则:

  1. 比较 A 值: A 值越大,优先级越高。
  2. A 值相同,比较 B 值: B 值越大,优先级越高。
  3. B 值相同,比较 C 值: C 值越大,优先级越高。
  4. C 值相同,比较 D 值: D 值越大,优先级越高。
  5. 所有值都相同: 后定义的规则会覆盖先定义的规则(即“就近原则”或“后来者居上”)

3. 清除浮动的方法

浮动(float)是 CSS 中一种常用的布局方式,但它会导致父元素高度塌陷(即父元素无法包裹浮动子元素),从而影响后续元素的布局。清除浮动就是解决父元素高度塌陷及其他浮动带来的布局问题。

浮动引起的问题:

  1. 父元素高度塌陷: 当父元素只包含浮动子元素时,父元素的高度会变为 0,无法包裹浮动子元素。
  2. 兄弟元素布局混乱: 浮动元素脱离了文档流,后续的非浮动兄弟元素可能会“跑到”浮动元素的下方或旁边,导致布局错乱。

清除浮动的方法:

  1. 使用 clear 属性 (在浮动元素之后添加一个块级元素)

    • 原理: clear 属性用于阻止元素紧挨着浮动元素。当一个元素设置 clear: both; 时,它会强制自身向下移动,直到其左侧和右侧都没有浮动元素。
    • 实现: 在所有浮动元素的后面添加一个空的 div 元素,并对其应用 clear: both; 样式。
      <div class="parent">
        <div class="float-left"></div>
        <div class="float-right"></div>
        <div class="clear-fix"></div> <!-- 清除浮动的空div -->
      </div>
      .float-left { float: left; width: 100px; height: 100px; background: red; }
      .float-right { float: right; width: 100px; height: 100px; background: blue; }
      .clear-fix { clear: both; height: 0; overflow: hidden; } /* 通常设置height:0和overflow:hidden避免显示 */
    • 缺点: 增加了无意义的 HTML 元素,不语义化,不推荐。
  2. 父元素触发 BFC (Block Formatting Context - 块级格式化上下文)

    • 原理: BFC 是一个独立的渲染区域,内部的元素不会影响到外部的元素,反之亦然。BFC 区域会包含其内部的所有浮动元素。
    • 触发 BFC 的常见方式:
      • overflow: hidden; (最常用)
      • display: flow-root; (现代且推荐,专门用于创建 BFC)
      • float: left;float: right; (父元素也浮动,不常用)
      • position: absolute;position: fixed; (父元素绝对定位,不常用)
      • display: inline-block; (不常用,会使父元素变成行内块)
      • display: table-cell;display: table-caption;
      • display: flex;display: grid; (Flex/Grid 容器本身就会创建一个 BFC)
    • 实现 (以 overflow: hidden; 为例):
      <div class="parent overflow-hidden">
        <div class="float-left"></div>
        <div class="float-right"></div>
      </div>
      .parent.overflow-hidden {
        overflow: hidden; /* 触发 BFC */
        background: #f0f0f0; /* 仅为演示效果 */
      }
      .float-left { float: left; width: 100px; height: 100px; background: red; }
      .float-right { float: right; width: 100px; height: 100px; background: blue; }
    • 缺点:
      • overflow: hidden; 可能会裁剪溢出内容。
      • floatposition: absolute/fixed 会改变元素本身的布局特性,不适合作为清除浮动的主要手段。
      • display: inline-block 会使父元素变成行内块,可能不是期望的布局。
      • display: flow-root; 是专门为解决这个问题而生的,但兼容性略差于 overflow: hidden; (IE 不支持)。
  3. 使用伪元素 ::after (clearfix hack) - 最推荐和常用!

    • 原理: 利用 CSS 伪元素在父元素的末尾添加一个看不见的块级元素,并对其应用 clear: both; 属性,从而达到清除浮动的目的,同时不增加额外的 HTML 标签。
      <div class="parent clearfix">
        <div class="float-left"></div>
        <div class="float-right"></div>
      </div>
      .parent.clearfix::after {
        content: ""; /* 必须有 content 属性 */
        display: block; /* 转换为块级元素 */
        clear: both; /* 清除左右浮动 */
        height: 0; /* 避免占据空间 */
        visibility: hidden; /* 隐藏伪元素 */
      }
      /* 兼容 IE6/7,触发 hasLayout */
      .parent.clearfix {
        *zoom: 1; /* For IE 6/7 */
      }
    • 优点:
      • 不增加额外的 HTML 标签,语义化。
      • 兼容性好,适用于绝大多数浏览器。
      • 是最常用的清除浮动方法。

4. position的属性

position 属性用于指定一个元素的定位方式。它有五个主要的值,每个值都定义了元素在文档流中的行为以及如何使用 top, right, bottom, left (偏移量) 属性。

  1. static (静态定位)

    • 默认值。 元素按照正常的文档流进行布局。
    • top, right, bottom, left, z-index 属性无效
    • 元素不会脱离文档流。
      div {
        position: static; /* 默认行为 */
      }
  2. relative (相对定位)

    • 元素仍然保持在正常的文档流中,其占据的空间不会改变。
    • 元素会相对于自身的正常位置进行偏移。
    • top, right, bottom, left 属性有效,用于指定偏移量。
    • 可以作为 absolute 定位元素的包含块(containing block)。
    • z-index 属性有效,可以创建堆叠上下文。
      .box {
        position: relative;
        top: 20px;  /* 相对于自身向下偏移20px */
        left: 30px; /* 相对于自身向右偏移30px */
      }
  3. absolute (绝对定位)

    • 元素会脱离正常的文档流,不再占据空间。
    • 元素会相对于其最近的已定位祖先元素(即 position 属性值为 relative, absolute, fixed, sticky 的祖先元素)进行定位。
    • 如果没有已定位的祖先元素,则相对于初始包含块(通常是 <html> 元素或 <body> 元素)进行定位。
    • top, right, bottom, left 属性有效
    • z-index 属性有效,可以创建堆叠上下文。
      .parent {
        position: relative; /* 作为子元素的定位参考 */
        width: 200px;
        height: 200px;
      }
      .child {
        position: absolute;
        top: 10px;
        left: 10px; /* 相对于 .parent 的左上角偏移 */
      }
  4. fixed (固定定位)

    • 元素会脱离正常的文档流,不再占据空间。
    • 元素会相对于浏览器视口 (viewport) 进行定位。这意味着即使页面滚动,元素也会固定在屏幕的某个位置。
    • top, right, bottom, left 属性有效
    • z-index 属性有效,可以创建堆叠上下文。
    • 常用于创建导航栏、回到顶部按钮等。
      .fixed-header {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        background: #333;
        color: white;
      }
  5. sticky (粘性定位)

    • sticky 定位是 relativefixed 两种定位的混合。
    • 元素在跨越特定阈值前是相对定位 (relative)。
    • 一旦达到阈值,它就会变为固定定位 (fixed),直到父元素超出视口。
    • 元素不会脱离正常的文档流,仍然占据空间。
    • top, right, bottom, left 属性有效,它们定义了元素变为固定定位的“粘性”阈值。
    • z-index 属性有效,可以创建堆叠上下文。
    • 常用于创建粘性导航栏、侧边栏标题等。
    • 注意: 必须指定至少一个 top, right, bottom, left 属性,sticky 定位才能生效。
      .sticky-header {
        position: sticky;
        top: 0; /* 当元素顶部距离视口顶部为0时,变为固定定位 */
        background: lightgray;
        padding: 10px;
      }

5. CSS盒子模型

CSS 盒子模型(Box Model)描述了网页中所有元素都被渲染为一个矩形的盒子。这个盒子由四个部分组成:内容 (Content)、内边距 (Padding)、边框 (Border) 和外边距 (Margin)。

组成部分:

  1. Content (内容区):

    • 盒子最核心的部分,显示文本、图片等实际内容。
    • 大小由 widthheight 属性控制。
  2. Padding (内边距):

    • 内容区与边框之间的空间。
    • 受背景颜色影响。
    • 使用 padding-top, padding-right, padding-bottom, padding-left 或简写 padding 控制。
  3. Border (边框):

    • 内边距与外边距之间的线条。
    • 使用 border-width, border-style, border-color 或简写 border 控制。
  4. Margin (外边距):

    • 边框以外,用于控制元素与其他元素之间的空间。
    • 透明,不受背景颜色影响。
    • 使用 margin-top, margin-right, margin-bottom, margin-left 或简写 margin 控制。
    • 外边距合并 (Margin Collapse): 在垂直方向上,相邻的两个块级元素的上下外边距有时会合并为一个外边距,其大小取两者中较大的那个。

两种盒模型:

  1. W3C 标准盒模型 (Content-box):

    • 默认的盒模型。
    • widthheight 属性只包含 内容区 (Content) 的大小。
    • 元素的总宽度 = width + padding-left + padding-right + border-left-width + border-right-width + margin-left + margin-right
    • 元素的总高度 = height + padding-top + padding-bottom + border-top-width + border-bottom-width + margin-top + margin-bottom
  2. IE 盒模型 (Border-box):

    • 在 IE 早期版本(IE6 以下,以及 IE6-IE9 混杂模式)中使用的盒模型。
    • widthheight 属性包含 内容区 (Content) + 内边距 (Padding) + 边框 (Border) 的大小。
    • 元素的总宽度 = width + margin-left + margin-right
    • 元素的总高度 = height + margin-top + margin-bottom

box-sizing 属性:

为了统一盒模型行为,CSS3 引入了 box-sizing 属性。

  • box-sizing: content-box;:强制使用标准盒模型。
  • box-sizing: border-box;:强制使用 IE 盒模型。
    • 推荐使用: 在现代开发中,通常推荐将所有元素的 box-sizing 设置为 border-box,因为它使得布局计算更加直观和方便。
    /* 全局设置 */
    html {
      box-sizing: border-box;
    }
    *, *::before, *::after {
      box-sizing: inherit; /* 继承html的box-sizing */
    }

6. BFC (块级格式化上下文)

BFC (Block Formatting Context) 块级格式化上下文是 Web 页面中一块独立的渲染区域,它规定了内部块级盒子的布局方式,并且这块区域的渲染不会影响到外部的元素,反之亦然。

BFC 的特性/规则:

  1. 内部盒子垂直排列: BFC 内部的块级盒子会一个接一个地垂直排列。
  2. BFC 区域不会与浮动元素重叠: BFC 区域会形成一个独立的区域,不会被浮动元素覆盖。
  3. BFC 可以包含浮动元素: BFC 会包裹其内部的所有浮动元素,解决了父元素高度塌陷的问题。
  4. BFC 不会发生外边距合并: 属于不同 BFC 的相邻盒子的外边距不会合并。

如何创建 BFC (触发 BFC):

当一个元素满足以下条件之一时,就会创建一个新的 BFC:

  1. 根元素 (html)
  2. float 属性不为 none (例如 float: left;float: right;)
  3. position 属性为 absolutefixed
  4. display 属性为 inline-block, table-cell, table-caption, flex, inline-flex, grid, inline-grid
  5. overflow 属性不为 visible (例如 overflow: hidden;, overflow: auto;, overflow: scroll;)
  6. display: flow-root; (专门用于创建 BFC 的现代属性,兼容性较新)

BFC 的应用场景:

  1. 解决父元素高度塌陷问题: 当父元素内部有浮动子元素时,父元素高度会塌陷。通过给父元素创建 BFC (如 overflow: hidden;),可以使其包含浮动子元素。
  2. 防止浮动元素覆盖: 当一个元素与浮动元素相邻时,如果不想让其被浮动元素覆盖,可以为其创建 BFC。
  3. 防止外边距合并: 当两个相邻的块级元素的外边距发生合并时,可以通过为其中一个或两个元素创建 BFC 来阻止合并。

7. flex和grid的区别

Flexbox(弹性盒子)和 Grid(网格布局)都是 CSS 中强大的布局模块,但它们的设计理念和应用场景有所不同。

Flexbox (弹性盒子)

  • 设计理念: 一维布局系统。它主要用于在单个方向(行或列)上对项目进行布局。
  • 布局方向: 沿着主轴(main axis)和交叉轴(cross axis)进行布局。你可以选择主轴是水平的还是垂直的。
  • 适用场景:
    • 组件内部的布局,如导航栏、按钮组、表单元素。
    • 当你需要控制一组项目在单个方向上的对齐、分布、顺序时。
    • 响应式设计中,项目在单个方向上的伸缩和换行。
  • 关键词: flex-direction, justify-content, align-items, flex-wrap, flex-grow, flex-shrink, flex-basis, order

Grid (网格布局)

  • 设计理念: 二维布局系统。它同时处理行和列的布局,允许你将页面内容划分为复杂的网格结构。
  • 布局方向: 同时在行(row)和列(column)两个维度上进行布局。
  • 适用场景:
    • 整个页面的宏观布局(如头部、侧边栏、主内容、底部)。
    • 需要精确控制项目在行和列交叉区域的位置和大小。
    • 复杂的、非线性的布局需求。
    • 媒体查询之外的响应式布局(通过 grid-template-areasauto-fit/auto-fill)。
  • 关键词: grid-template-columns, grid-template-rows, grid-gap, grid-column, grid-row, grid-area, justify-items, align-items, place-items

核心区别总结:

特性Flexbox (弹性盒子)Grid (网格布局)
维度一维 (行或列)二维 (行和列)
目的容器内项目在单个方向上的对齐、分布、顺序页面或组件的整体结构布局,定义行和列的网格
内容流从主轴方向开始,项目一个接一个排列项目可以放置在任意网格单元格中,不限于线性排列
控制主要通过容器属性控制项目在主轴和交叉轴上的行为主要通过容器属性定义网格轨道,并通过项目属性放置
适用组件内部、导航、简单列表等页面骨架、复杂布局、内容区域划分等

选择建议:

  • 如果只需要在一条线上(水平或垂直)排列内容,使用 Flexbox。
  • 如果需要同时在行和列两个方向上管理内容,创建复杂的页面布局,使用 Grid。

它们并非互斥,而是互补的。在实际开发中,经常会结合使用:例如,使用 Grid 布局定义页面的整体结构,然后在 Grid 内部的某个区域(如侧边栏或主内容区)再使用 Flexbox 来布局其内部的元素。

8. 行内元素和块级元素有什么区别

在 HTML 中,元素根据其在文档流中的表现形式可以分为行内元素 (Inline Elements) 和块级元素 (Block-level Elements)。

块级元素 (Block-level Elements):

  • 独占一行: 默认情况下,块级元素会独占一行,即使其内容宽度不足,也会占据父元素的整个宽度。
  • 可以设置宽高: 可以通过 CSS 的 widthheight 属性来设置其宽度和高度。
  • 可以设置内外边距: 可以设置 marginpadding,并且上下左右的 marginpadding 都有效。
  • 可以包含行内元素和块级元素: 大多数块级元素可以包含其他块级元素和行内元素。
  • 常见块级元素: <div>, <p>, <h1> - <h6>, <ul>, <ol>, <li>, <form>, <header>, <footer>, <section>, <article>, <aside>, <nav> 等。

行内元素 (Inline Elements):

  • 不独占一行: 默认情况下,行内元素不会独占一行,而是与其他行内元素在同一行上排列,直到一行排满为止。
  • 不能设置宽高: widthheight 属性对其无效。其宽度和高度由内容撑开。
  • 内外边距限制:
    • margin-topmargin-bottom 无效。
    • padding-toppadding-bottom 视觉上有效,但不会影响其他元素的布局,可能会覆盖相邻行元素。
    • margin-left, margin-right, padding-left, padding-right 有效。
  • 只能包含行内元素或文本: 一般只能包含数据或其他行内元素。
  • 常见行内元素: <span>, <a>, <em>, <strong>, <i>, <b>, <label>, <input>, <select>, <textarea>, <button>, <img> 等。

行内块元素 (Inline-block Elements):

inline-blockdisplay 属性的一个值,它结合了行内元素和块级元素的特性。

  • 不独占一行: 像行内元素一样,可以在同一行上排列。
  • 可以设置宽高: 像块级元素一样,可以设置 widthheight
  • 可以设置内外边距: marginpadding 的所有方向都有效。
  • 常见应用: 常用于需要在一行内排列但又需要控制其尺寸和边距的元素,如导航菜单项、图片列表等。

如何转换元素的显示类型:

可以使用 CSS 的 display 属性来改变元素的默认显示类型:

  • display: block;:将元素转换为块级元素。
  • display: inline;:将元素转换为行内元素。
  • display: inline-block;:将元素转换为行内块元素。
  • display: flex;:将元素转换为块级弹性容器。
  • display: grid;:将元素转换为块级网格容器。

9. 布局知道哪些?

前端布局是指如何组织和排列页面上的元素,使其在不同设备和屏幕尺寸下都能良好地显示。常见的布局方式包括:

  1. 流式布局 (Normal Flow / Document Flow):

    • 这是浏览器默认的布局方式。块级元素垂直排列,行内元素水平排列。
    • 元素按照它们在 HTML 中出现的顺序自上而下、从左到右进行排列。
    • 特点: 简单直观,但缺乏灵活性,难以实现复杂的非线性布局。
  2. 浮动布局 (Float Layout):

    • 通过 float 属性使元素脱离正常文档流,向左或向右浮动,直到遇到父元素边缘或另一个浮动元素。
    • 常用于实现文字环绕图片、多列布局(如早期网页的左右分栏)。
    • 特点: 简单,兼容性好,但会引起父元素高度塌陷等问题,需要清除浮动。
  3. 定位布局 (Positioning Layout):

    • 通过 position 属性(relative, absolute, fixed, sticky)来精确控制元素的位置。
    • absolutefixed 会使元素脱离文档流。
    • 特点: 适用于元素叠加、层级控制、固定位置元素(如导航栏、回到顶部按钮)。
  4. 表格布局 (Table Layout):

    • 使用 HTML 的 <table> 标签或 CSS 的 display: table; 相关属性来模拟表格结构进行布局。
    • 特点: 能够实现等高列、垂直居中等,但语义化差,不推荐用于非表格数据。
  5. 弹性盒子布局 (Flexbox Layout):

    • 通过 display: flex; 开启,是一个一维布局系统,用于在行或列方向上排列、对齐和分配空间。
    • 特点: 强大、灵活,适用于组件内部布局、导航栏、响应式列表等。
  6. 网格布局 (Grid Layout):

    • 通过 display: grid; 开启,是一个二维布局系统,可以同时在行和列方向上定义网格,并精确控制元素在网格中的位置。
    • 特点: 适用于整个页面的宏观布局、复杂的非线性布局。
  7. 多列布局 (Multi-column Layout):

    • 通过 column-count, column-width 等属性将内容分成多列,类似报纸排版。
    • 特点: 适用于长文本内容的展示。

响应式布局 (Responsive Layout):

以上布局方式都可以结合响应式设计技术来实现,以适应不同设备的屏幕尺寸:

  • 媒体查询 (Media Queries): 根据屏幕宽度、高度、方向等条件应用不同的 CSS 样式。
  • 弹性图片/视频 (Fluid Images/Videos): max-width: 100%; height: auto; 使媒体元素按比例缩放。
  • 视口单位 (Viewport Units): vw, vh, vmin, vmax 根据视口大小动态调整尺寸。
  • rem/em: 基于根元素或父元素字体大小的相对单位。

10. 如何实现等高布局?如何实现两边固定,中间自适应?如何实现一边固定宽度,另一边自适应?

1. 如何实现等高布局?

等高布局是指多列布局中所有列的高度都相同,即使它们的内容长度不同。

  • Flexbox (最推荐):

    <div class="container-flex">
      <div class="col">Column 1 content</div>
      <div class="col">Column 2 content <br> more content</div>
      <div class="col">Column 3</div>
    </div>
    .container-flex {
      display: flex; /* 开启Flexbox */
      align-items: stretch; /* 默认值,使子元素等高 */
    }
    .col {
      flex: 1; /* 每个列占据等量空间 */
      padding: 20px;
      border: 1px solid #ccc;
      margin: 5px;
    }
  • Grid (现代推荐):

    <div class="container-grid">
      <div class="col">Column 1 content</div>
      <div class="col">Column 2 content <br> more content</div>
      <div class="col">Column 3</div>
    </div>
    .container-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr); /* 定义三列等宽 */
      /* grid-auto-rows: minmax(100px, auto); /* 可选,定义最小行高 */
    }
    .col {
      padding: 20px;
      border: 1px solid #ccc;
      margin: 5px;
    }

2. 如何实现两边固定,中间自适应?

  • Flexbox (最推荐):

    <div class="container-flex">
      <div class="left">Left Fixed</div>
      <div class="middle">Middle Auto-width</div>
      <div class="right">Right Fixed</div>
    </div>
    .container-flex {
      display: flex;
      height: 200px; /* 示例高度 */
    }
    .left, .right {
      width: 200px; /* 固定宽度 */
      background-color: lightblue;
      flex-shrink: 0; /* 防止收缩 */
    }
    .middle {
      flex: 1; /* 占据剩余所有空间 */
      background-color: lightgreen;
    }
  • Grid (现代推荐):

    <div class="container-grid">
      <div class="left">Left Fixed</div>
      <div class="middle">Middle Auto-width</div>
      <div class="right">Right Fixed</div>
    </div>
    .container-grid {
      display: grid;
      grid-template-columns: 200px 1fr 200px; /* 左边200px,中间自适应,右边200px */
      height: 200px; /* 示例高度 */
    }
    .left { background-color: lightblue; }
    .middle { background-color: lightgreen; }
    .right { background-color: lightblue; }
  • Positioning + Margin (传统方式):

    <div class="container-pos">
      <div class="left">Left Fixed</div>
      <div class="right">Right Fixed</div>
      <div class="middle">Middle Auto-width</div>
    </div>
    .container-pos {
      position: relative; /* 确保中间元素能够正确计算宽度 */
      height: 200px; /* 示例高度 */
      padding: 0 200px; /* 为左右两边留出空间 */
    }
    .left {
      position: absolute;
      left: 0;
      top: 0;
      width: 200px;
      height: 100%;
      background-color: lightblue;
    }
    .right {
      position: absolute;
      right: 0;
      top: 0;
      width: 200px;
      height: 100%;
      background-color: lightblue;
    }
    .middle {
      height: 100%;
      background-color: lightgreen;
      /* 宽度由父元素的padding撑开,无需设置 */
    }

3. 如何实现一边固定宽度,另一边自适应?

这可以看作是“两边固定,中间自适应”的简化版。

  • Flexbox (最推荐):

    <div class="container-flex">
      <div class="fixed-side">Fixed Side</div>
      <div class="auto-side">Auto-width Side</div>
    </div>
    .container-flex {
      display: flex;
      height: 150px;
    }
    .fixed-side {
      width: 200px; /* 固定宽度 */
      background-color: lightcoral;
      flex-shrink: 0;
    }
    .auto-side {
      flex: 1; /* 占据剩余所有空间 */
      background-color: lightgoldenrodyellow;
    }
  • Grid (现代推荐):

    <div class="container-grid">
      <div class="fixed-side">Fixed Side</div>
      <div class="auto-side">Auto-width Side</div>
    </div>
    .container-grid {
      display: grid;
      grid-template-columns: 200px 1fr; /* 左边200px,右边自适应 */
      height: 150px;
    }
    .fixed-side { background-color: lightcoral; }
    .auto-side { background-color: lightgoldenrodyellow; }
  • Float + Margin (传统方式):

    <div class="container-float">
      <div class="fixed-side">Fixed Side</div>
      <div class="auto-side">Auto-width Side</div>
    </div>
    .container-float {
      overflow: hidden; /* 触发BFC,防止父元素高度塌陷 */
      height: 150px;
    }
    .fixed-side {
      float: left; /* 或 right */
      width: 200px; /* 固定宽度 */
      background-color: lightcoral;
      height: 100%; /* 确保高度 */
    }
    .auto-side {
      margin-left: 200px; /* 为浮动元素留出空间 */
      background-color: lightgoldenrodyellow;
      height: 100%;
    }

11. 如何实现height占满全屏?

实现元素高度占满全屏通常有以下几种方法:

  1. 使用视口单位 vh (Viewport Height) - 最常用且推荐:

    • vh 单位表示视口高度的百分比。1vh 等于视口高度的 1%。
    • 100vh 就是视口高度的 100%。
    html, body {
      margin: 0; /* 移除浏览器默认的margin */
      padding: 0;
      height: 100%; /* 确保html和body的高度也为100% */
    }
    .full-height-element {
      height: 100vh; /* 元素高度占满整个视口 */
      background-color: lightblue;
    }

    优点: 简单直接,不依赖父元素高度。 缺点: 移动端浏览器地址栏可能会影响 vh 的计算(某些浏览器中 100vh 会包含地址栏,导致内容被遮挡或出现滚动条)。

  2. 使用百分比 height: 100% (需要父元素有明确高度) - 传统方法:

    • 当一个元素的 height 设置为百分比时,它是相对于其 包含块 (containing block) 的高度来计算的。
    • 这意味着,如果父元素没有明确的高度,子元素的 height: 100% 将无效。要让一个元素的高度占满全屏,需要确保从 htmlbody 再到目标元素的所有祖先元素都设置了 height: 100%
    html, body {
      margin: 0;
      padding: 0;
      height: 100%; /* 关键:确保html和body高度为100% */
    }
    .full-height-element {
      height: 100%; /* 元素高度占满其父元素(body)的高度 */
      background-color: lightgreen;
    }

    优点: 兼容性好。 缺点: 需要层层设置 height: 100%,如果中间有某个父元素没有设置,则会失效。

  3. Flexbox 布局:

    • 如果父元素是 Flex 容器,可以通过 flex-growheight: 100% 来实现子元素占满剩余空间。
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
    }
    body {
      display: flex;
      flex-direction: column; /* 让子元素垂直排列 */
    }
    .header {
      height: 50px;
      background-color: gray;
    }
    .full-height-content {
      flex-grow: 1; /* 占据剩余所有空间 */
      background-color: lightcoral;
      /* 或者 height: 100%; 如果父元素高度确定 */
    }
    .footer {
      height: 50px;
      background-color: gray;
    }

    优点: 适用于复杂布局中,某个区域需要占据剩余空间的情况。

  4. Grid 布局:

    • 类似 Flexbox,Grid 也可以方便地分配空间。
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
    }
    body {
      display: grid;
      grid-template-rows: auto 1fr auto; /* header高度自适应, content占据剩余, footer高度自适应 */
      height: 100vh; /* 或 100% */
    }
    .header {
      background-color: gray;
    }
    .full-height-content {
      background-color: lightseagreen;
    }
    .footer {
      background-color: gray;
    }

    优点: 适用于需要精确控制行和列的复杂页面布局。

总结:

  • 对于一个独立元素简单地占满整个视口,height: 100vh; 是最简洁和常用的方法。
  • 如果需要考虑兼容性或移动端地址栏问题,或者页面有固定头部/底部,需要内容区域占据中间剩余空间,则 height: 100%; 结合 html, body { height: 100%; }Flex/Grid 布局 更为合适。

二、 JavaScript 基础

1. JS数组常用的方法

JavaScript 数组方法可以分为改变原数组(Mutating Methods)和不改变原数组(Non-mutating Methods)以及迭代/查找方法。

改变原数组的方法(Mutating Methods):

  1. push(...items): 在数组末尾添加一个或多个元素,并返回新数组的长度。
    let arr = [1, 2];
    arr.push(3, 4); // arr: [1, 2, 3, 4]
  2. pop(): 删除数组的最后一个元素,并返回该元素。
    let arr = [1, 2, 3];
    let last = arr.pop(); // last: 3, arr: [1, 2]
  3. shift(): 删除数组的第一个元素,并返回该元素。
    let arr = [1, 2, 3];
    let first = arr.shift(); // first: 1, arr: [2, 3]
  4. unshift(...items): 在数组开头添加一个或多个元素,并返回新数组的长度。
    let arr = [3, 4];
    arr.unshift(1, 2); // arr: [1, 2, 3, 4]
  5. splice(start, deleteCount, ...items):
    • 从指定位置删除元素。
    • 从指定位置插入元素。
    • 替换元素。
    • 返回一个包含被删除元素的数组。
    let arr = [1, 2, 3, 4, 5];
    arr.splice(2, 1); // 从索引2开始删除1个元素 (删除3),arr: [1, 2, 4, 5]
    arr.splice(1, 0, 'a', 'b'); // 从索引1开始删除0个元素,插入'a','b',arr: [1, 'a', 'b', 2, 4, 5]
    arr.splice(0, 2, 'x', 'y'); // 从索引0开始删除2个元素,插入'x','y',arr: ['x', 'y', 'b', 2, 4, 5]
  6. sort([compareFunction]): 对数组元素进行排序。默认按字符串 Unicode 码点排序。
    let arr = [3, 1, 4, 1, 5, 9];
    arr.sort(); // arr: [1, 1, 3, 4, 5, 9] (默认行为)
    arr.sort((a, b) => a - b); // 升序,arr: [1, 1, 3, 4, 5, 9]
  7. reverse(): 反转数组中元素的顺序。
    let arr = [1, 2, 3];
    arr.reverse(); // arr: [3, 2, 1]
  8. fill(value, start, end): 用一个固定值填充数组中从起始索引到终止索引内的全部元素。
    let arr = [1, 2, 3, 4];
    arr.fill(0, 1, 3); // arr: [1, 0, 0, 4]

不改变原数组的方法(Non-mutating Methods):

  1. concat(...arrays): 合并两个或多个数组,返回一个新数组。
    let arr1 = [1, 2];
    let arr2 = [3, 4];
    let newArr = arr1.concat(arr2); // newArr: [1, 2, 3, 4], arr1, arr2 不变
  2. slice(start, end): 截取数组的一部分,返回一个新数组。
    let arr = [1, 2, 3, 4, 5];
    let newArr = arr.slice(1, 4); // newArr: [2, 3, 4], arr 不变
  3. join(separator): 将数组的所有元素连接成一个字符串。
    let arr = ['Hello', 'World'];
    let str = arr.join(' '); // str: "Hello World"
  4. toString(): 返回表示数组的字符串。
    let arr = [1, 2, 3];
    let str = arr.toString(); // str: "1,2,3"

迭代/遍历方法:

  1. forEach(callback(currentValue, index, array)): 遍历数组的每个元素,执行回调函数。没有返回值。
    let arr = [1, 2, 3];
    arr.forEach((item, index) => console.log(`Item ${item} at index ${index}`));
  2. map(callback(currentValue, index, array)): 遍历数组的每个元素,执行回调函数,并返回一个由回调函数返回值组成的新数组。
    let arr = [1, 2, 3];
    let newArr = arr.map(item => item * 2); // newArr: [2, 4, 6]
  3. filter(callback(currentValue, index, array)): 遍历数组的每个元素,根据回调函数的返回值(true/false)过滤元素,返回一个包含通过测试的新数组。
    let arr = [1, 2, 3, 4, 5];
    let evenNumbers = arr.filter(item => item % 2 === 0); // evenNumbers: [2, 4]
  4. reduce(callback(accumulator, currentValue, index, array), initialValue): 对数组中的所有元素执行一个由您提供的 reducer 函数,将其结果汇总为单个返回值。
    let arr = [1, 2, 3, 4];
    let sum = arr.reduce((acc, current) => acc + current, 0); // sum: 10
  5. some(callback(currentValue, index, array)): 测试数组中是否至少有一个元素通过了回调函数的测试。返回布尔值。
    let arr = [1, 2, 3];
    let hasEven = arr.some(item => item % 2 === 0); // hasEven: true
  6. every(callback(currentValue, index, array)): 测试数组中的所有元素是否都通过了回调函数的测试。返回布尔值。
    let arr = [2, 4, 6];
    let allEven = arr.every(item => item % 2 === 0); // allEven: true

查找/搜索方法:

  1. indexOf(searchElement, fromIndex): 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1。
    let arr = [1, 2, 3, 2];
    arr.indexOf(2); // 1
    arr.indexOf(5); // -1
  2. lastIndexOf(searchElement, fromIndex): 返回在数组中可以找到一个给定元素的最后一个索引,如果不存在,则返回 -1。
    let arr = [1, 2, 3, 2];
    arr.lastIndexOf(2); // 3
  3. includes(valueToFind, fromIndex): 判断一个数组是否包含一个指定的值,返回布尔值。
    let arr = [1, 2, 3];
    arr.includes(2); // true
    arr.includes(5); // false
  4. find(callback(currentValue, index, array)): 返回数组中满足提供的测试函数的第一个元素的值。否则返回undefined
    let arr = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
    let item = arr.find(obj => obj.id === 2); // item: {id: 2, name: 'B'}
  5. findIndex(callback(currentValue, index, array)): 返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1。
    let arr = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
    let index = arr.findIndex(obj => obj.id === 2); // index: 1

2. 判断变量为数组的方法

有多种方法可以判断一个变量是否为数组:

  1. Array.isArray() (推荐)

    • 这是最可靠和推荐的方法,因为它直接检查一个值是否为 Array 类型。
    • 它能正确处理跨 iframe 的数组。
    Array.isArray([1, 2, 3]); // true
    Array.isArray({a: 1});    // false
    Array.isArray(null);      // false
    Array.isArray(undefined); // false
  2. instanceof Array

    • 检查一个对象是否是特定类的实例。
    • 当只有一个全局执行环境时,它通常是有效的。
    • 缺点: 在多全局执行环境(如 iframe 或 Web Workers)中,如果数组是从另一个 iframe 创建的,arr instanceof Array 可能会返回 false,因为它们有不同的 Array 构造函数。
    let arr = [1, 2, 3];
    arr instanceof Array; // true
  3. Object.prototype.toString.call() (兼容性好,但稍显繁琐)

    • 利用 Object.prototype.toString 方法返回一个表示该对象类型的字符串。对于数组,它会返回 "[object Array]".
    • 这是判断内置对象类型的通用方法,也能够正确处理跨 iframe 的情况。
    Object.prototype.toString.call([1, 2, 3]) === '[object Array]'; // true
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"

总结:

  • 最佳实践: 始终优先使用 Array.isArray()
  • 兼容性好: 如果需要兼容老旧浏览器或特定环境,Object.prototype.toString.call() 是一个可靠的备选方案。
  • 谨慎使用: instanceof Array 在单全局环境有效,但在多全局环境可能出现问题。

3. 如何合并数组去重

合并数组并去重是常见的操作,有多种方法可以实现:

方法一:使用 Set (ES6+) - 最简洁高效

Set 是 ES6 引入的一种新的数据结构,它只存储唯一的值。

function mergeAndDeduplicate(arr1, arr2) {
  const combinedArray = arr1.concat(arr2); // 合并数组
  // 或者使用展开运算符: const combinedArray = [...arr1, ...arr2];
  return [...new Set(combinedArray)]; // 使用Set去重,再转回数组
}

const arrA = [1, 2, 3, 2];
const arrB = [3, 4, 5, 1];
const result = mergeAndDeduplicate(arrA, arrB);
console.log(result); // [1, 2, 3, 4, 5]

优点: 代码简洁,性能好(尤其是对于大数据量)。 缺点: 无法直接处理对象数组的去重(除非对象引用完全相同)。

方法二:使用 filter + indexOf

这种方法先合并数组,然后遍历新数组,利用 indexOf 判断当前元素是否是第一次出现。

function mergeAndDeduplicateFilter(arr1, arr2) {
  const combinedArray = arr1.concat(arr2);
  return combinedArray.filter((item, index, self) => {
    return self.indexOf(item) === index; // 判断当前元素是否是第一次出现
  });
}

const arrA = [1, 2, 3, 2];
const arrB = [3, 4, 5, 1];
const result = mergeAndDeduplicateFilter(arrA, arrB);
console.log(result); // [1, 2, 3, 4, 5]

优点: 兼容性好(ES5)。 缺点: 性能相对较差,因为 indexOf 在每次迭代中都会遍历数组。对于包含对象的数组,indexOf 同样是基于引用比较。

方法三:使用 reduce + 对象/Map (适用于复杂数据去重,如对象数组)

如果数组中包含对象,并且需要根据对象的某个属性进行去重,SetindexOf 就不适用了。这时可以使用 reduce 结合一个 Map 或普通对象来记录已出现的值。

// 示例:根据对象的id属性去重
function mergeAndDeduplicateObjects(arr1, arr2, key) {
  const combinedArray = arr1.concat(arr2);
  const seen = new Map(); // 使用Map来存储已出现的key
  return combinedArray.filter(item => {
    const id = item[key];
    if (!seen.has(id)) {
      seen.set(id, true);
      return true;
    }
    return false;
  });
}

const arrObjA = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
const arrObjB = [{id: 2, name: 'BB'}, {id: 3, name: 'C'}];
const resultObj = mergeAndDeduplicateObjects(arrObjA, arrObjB, 'id');
console.log(resultObj);
// [ { id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 3, name: 'C' } ]
// 注意:对于id为2的对象,保留的是arrObjA中的第一个,因为它是先出现的

优点: 能够处理复杂数据类型的去重,性能较好。 缺点: 代码相对复杂。

总结:

  • 基本数据类型数组去重: 优先使用 Set
  • 对象数组去重(根据某个属性): 使用 filter 结合 Map 或普通对象。

4. 深拷贝如何实现

什么是深拷贝? 深拷贝是指在复制一个对象时,不仅复制对象本身,还递归地复制对象内部所有嵌套的对象和数组。深拷贝后的新对象与原对象之间完全独立,修改新对象不会影响原对象。

为什么需要深拷贝? JavaScript 中的对象(包括数组)是引用类型。当进行浅拷贝时,如果对象内部包含引用类型(如另一个对象或数组),那么新对象和原对象会共享这些内部引用。修改其中一个会影响另一个,这可能导致意外的副作用。深拷贝可以避免这种问题。

实现深拷贝的方法:

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

    • 原理: 将对象转换为 JSON 字符串,再将 JSON 字符串解析回新的对象。
    • 优点: 简单、方便,对于大部分纯数据对象(不包含函数、Symbol、undefined 等)非常有效。
    • 缺点:
      • 无法复制函数、undefinedSymbol 值。它们在序列化过程中会被忽略或转换为 null
      • 无法处理 RegExpDate 对象。Date 对象会变成字符串,RegExp 会变成空对象。
      • 无法处理循环引用(Circular References)。如果对象中存在循环引用,会报错。
      • 无法复制原型链上的属性和方法。
    • 适用场景: 当对象只包含基本数据类型、普通对象和数组,且没有循环引用时。
    const obj1 = {
      a: 1,
      b: { c: 2 },
      d: [3, 4],
      e: undefined, // 会丢失
      f: function() {}, // 会丢失
      g: new Date() // 会变成字符串
    };
    const obj2 = JSON.parse(JSON.stringify(obj1));
    console.log(obj2);
    // { a: 1, b: { c: 2 }, d: [ 3, 4 ], g: '2023-10-27T06:00:00.000Z' }
  2. 递归实现 (最常用和推荐的手动实现方式)

    • 原理: 遍历对象的每一个属性,如果属性值是基本类型,则直接赋值;如果属性值是对象或数组,则递归调用深拷贝函数。
    • 优点: 可以处理更复杂的数据类型,可以处理循环引用(通过存储已复制的对象引用)。
    • 缺点: 实现相对复杂,需要考虑多种数据类型和特殊情况。
    function deepClone(obj, hash = new WeakMap()) {
      // 处理基本类型和 null
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
    
      // 处理日期对象
      if (obj instanceof Date) {
        return new Date(obj);
      }
    
      // 处理正则表达式
      if (obj instanceof RegExp) {
        return new RegExp(obj);
      }
    
      // 处理循环引用
      if (hash.has(obj)) {
        return hash.get(obj);
      }
    
      // 根据是数组还是对象创建新的容器
      const cloneObj = Array.isArray(obj) ? [] : {};
      // 存储已复制的对象,避免循环引用
      hash.set(obj, cloneObj);
    
      // 遍历对象的属性进行递归拷贝
      for (let key in obj) {
        // 确保只复制对象自身的属性,不复制原型链上的属性
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          cloneObj[key] = deepClone(obj[key], hash);
        }
      }
      return cloneObj;
    }
    
    // 示例
    const obj1 = {
      a: 1,
      b: { c: 2 },
      d: [3, { e: 4 }],
      f: function() { console.log('hello'); },
      g: new Date(),
      h: /abc/g
    };
    obj1.i = obj1; // 循环引用
    
    const obj2 = deepClone(obj1);
    obj2.b.c = 99;
    obj2.d[1].e = 88;
    console.log(obj1.b.c); // 2
    console.log(obj1.d[1].e); // 4
    console.log(obj2.f); // [Function: f]
    console.log(obj2.g instanceof Date); // true
    console.log(obj2.h instanceof RegExp); // true
    console.log(obj2.i === obj2); // true (循环引用被正确处理)
  3. structuredClone() API (现代浏览器)

    • 原理: 浏览器内置的深拷贝算法,与 postMessageIndexedDB 内部使用的算法相同。
    • 优点:
      • 支持多种数据类型(包括 Date, RegExp, Map, Set, ArrayBuffer, TypedArray, ImageBitmap, ImageData 等)。
      • 自动处理循环引用。
      • 性能通常优于手动递归实现。
    • 缺点:
      • 不支持函数、Error 对象、DOM 节点。
      • IE 浏览器不支持。
    • 适用场景: 现代浏览器环境下,需要拷贝复杂数据结构(不含函数/DOM)时的最佳选择。
    // 检查浏览器是否支持
    if (typeof structuredClone === 'function') {
      const obj1 = {
        a: 1,
        b: { c: 2 },
        d: [3, { e: 4 }],
        g: new Date(),
        h: /abc/g,
        m: new Map([[1, 'one']]),
        s: new Set([1, 2])
      };
      obj1.i = obj1; // 循环引用
    
      const obj2 = structuredClone(obj1);
      console.log(obj2.g instanceof Date); // true
      console.log(obj2.h instanceof RegExp); // true
      console.log(obj2.m instanceof Map); // true
      console.log(obj2.s instanceof Set); // true
      console.log(obj2.i === obj2); // true
    } else {
      console.log("Your browser does not support structuredClone.");
    }
  4. 第三方库

    • Lodash 的 _.cloneDeep():
      • 优点: 功能强大,考虑了各种边缘情况,包括函数、循环引用等,非常健壮。
      • 缺点: 需要引入整个 Lodash 库(或按需引入 cloneDeep 模块)。
    // npm install lodash --save
    import _ from 'lodash';
    const obj1 = { a: 1, b: { c: 2 } };
    const obj2 = _.cloneDeep(obj1);

5. JS数据类型,null和undefined区别

JavaScript 数据类型:

JavaScript 中的数据类型分为两大类:基本数据类型 (Primitive Types)引用数据类型 (Reference Types)

基本数据类型 (7种):

  1. Number: 用于表示整数和浮点数。
    • 示例:10, 3.14, NaN (Not a Number), Infinity
  2. String: 用于表示文本数据。
    • 示例:"hello", 'world'
  3. Boolean: 用于表示逻辑实体,只有两个值:truefalse
    • 示例:true, false
  4. Undefined: 表示一个变量声明了但未被赋值时的默认值,或函数没有返回值时返回的值。
    • 示例:let x; console.log(x); // undefined
  5. Null: 表示一个空值或者“没有对象”。它是一个表示缺少的特殊关键字。
    • 示例:let y = null;
  6. Symbol (ES6 新增): 表示一个独一无二的值,主要用于对象的属性名,防止属性名冲突。
    • 示例:const s1 = Symbol('desc');
  7. BigInt (ES2020 新增): 用于表示任意大的整数,可以突破 Number 类型的安全整数限制。
    • 示例:10n, BigInt(9007199254740991)

引用数据类型 (Object):

所有非基本数据类型的值都是对象。对象是属性的集合,每个属性都有一个键和一个值。

  1. Object: 最基本的对象类型。
    • 示例:{}, new Object()
  2. Array: 有序的键值对集合,键是数字索引。
    • 示例:[], new Array()
  3. Function: 可调用的对象。
    • 示例:function() {}, () => {}
  4. Date: 日期和时间对象。
    • 示例:new Date()
  5. RegExp: 正则表达式对象。
    • 示例:/abc/, new RegExp('abc')
  6. Map (ES6 新增): 键值对的集合,键可以是任意类型。
    • 示例:new Map()
  7. Set (ES6 新增): 值的集合,只存储唯一值。
    • 示例:new Set()
  8. WeakMap / WeakSet (ES6 新增): 弱引用版本的 Map 和 Set。
  9. 其他内置对象:如 Math, JSON, Promise, Proxy, Reflect 等。

nullundefined 的区别:

特性undefinednull
含义表示“未定义”或“未初始化”的值。表示“空值”或“没有对象”。
类型 (typeof)string (值为 "undefined")object (这是 JavaScript 的一个历史遗留 bug,但被保留)
转换布尔值falsefalse
数值转换NaN0
发生场景- 变量已声明但未赋值。
- 访问对象不存在的属性。
- 函数没有明确返回值。
- 函数参数没有传值。
- 作为空值的占位符,通常由开发者手动赋值。
- DOM 查询没有找到元素时返回。
相等性undefined == nulltrue (值相等)undefined === nullfalse (类型和值都不等)

总结:

  • undefined 更多是系统层面的“缺失”,表示变量还没有赋值,或者属性不存在。
  • null 更多是开发者层面的“空”,表示一个变量有意地被赋值为“没有值”或“没有对象”。
  • 在实际开发中,当需要表示一个变量“没有值”或“空”时,通常建议使用 null 而不是 undefined,因为 undefined 更多是系统自动生成的。

6. 闭包

什么是闭包?

闭包 (Closure) 是 JavaScript 中一个非常重要的概念,它指的是:函数和声明该函数的词法环境(Lexical Environment)的组合。

简单来说,当一个函数被定义时,它会“记住”其创建时的作用域,即使该函数在它被创建的作用域之外执行,它仍然能够访问和操作该作用域中的变量。

闭包的特点:

  1. 函数嵌套: 闭包通常发生在函数内部定义另一个函数。
  2. 内部函数引用外部函数的变量: 内部函数引用了外部(父)函数的局部变量。
  3. 外部函数返回内部函数: 外部函数执行完毕后,其内部函数被返回并在外部调用。
  4. 变量持久化: 被内部函数引用的外部函数变量不会被垃圾回收机制销毁,而是会一直保存在内存中,直到内部函数不再被引用。

闭包的形成条件:

  • 函数作为返回值。
  • 函数作为参数传递。
  • 在定时器、事件监听器、Ajax 请求等异步操作中。

闭包的常见应用场景:

  1. 创建私有变量和方法: 模块化开发中,可以隐藏内部实现细节,只暴露公共接口。

    function createCounter() {
      let count = 0; // 私有变量
    
      return {
        increment: function() {
          count++;
          console.log(count);
        },
        decrement: function() {
          count--;
          console.log(count);
        },
        getCount: function() {
          return count;
        }
      };
    }
    
    const counter1 = createCounter();
    counter1.increment(); // 1
    counter1.increment(); // 2
    console.log(counter1.getCount()); // 2
    
    const counter2 = createCounter(); // 独立的计数器
    counter2.increment(); // 1
  2. 延长变量的生命周期: 使局部变量在函数执行完毕后仍然能被访问。

    for (var i = 0; i < 5; i++) {
      (function(j) { // 使用立即执行函数创建闭包
        setTimeout(function() {
          console.log(j);
        }, j * 1000);
      })(i);
    }
    // 输出:0, 1, 2, 3, 4 (每隔1秒)
    // 如果没有闭包,会输出5个5,因为setTimeout回调执行时i已经变成5
  3. 函数柯里化 (Currying) 和偏函数应用:

    function add(x) {
      return function(y) {
        return x + y;
      };
    }
    const add5 = add(5);
    console.log(add5(10)); // 15
  4. 缓存/记忆化 (Memoization):

    function memoize(fn) {
      const cache = {};
      return function(...args) {
        const key = JSON.stringify(args); // 简单的缓存key
        if (cache[key]) {
          console.log('from cache');
          return cache[key];
        } else {
          console.log('calculating');
          const result = fn(...args);
          cache[key] = result;
          return result;
        }
      };
    }
    
    const slowAdd = (a, b) => {
      console.log('doing slow add...');
      return a + b;
    };
    const cachedAdd = memoize(slowAdd);
    console.log(cachedAdd(1, 2)); // doing slow add... 3
    console.log(cachedAdd(1, 2)); // from cache 3

闭包的缺点/注意事项:

  1. 内存泄漏: 由于闭包会使外部函数的变量一直保存在内存中,如果使用不当,可能会导致内存占用过大,甚至造成内存泄漏。特别是在循环中创建大量闭包时需要注意。
  2. 性能开销: 闭包的创建和维护比普通函数有更高的性能开销,因为它们需要额外的内存来存储其词法环境。

如何避免内存泄漏:

  • 及时解除对闭包的引用,让垃圾回收机制能够回收内存。
  • 对于事件监听器,在不再需要时移除监听。
  • 对于定时器,在不再需要时清除定时器。

8. 箭头函数和普通函数区别

箭头函数 (Arrow Function) 是 ES6 (ECMAScript 2015) 引入的一种新的函数定义方式,它与传统的 function 关键字定义的函数在语法和行为上都有显著区别。

主要区别:

  1. this 的绑定:

    • 普通函数: this 的指向是动态的,取决于函数被调用时的上下文。
      • 作为对象方法调用时,this 指向该对象。
      • 作为普通函数调用时,this 指向全局对象(浏览器中是 window,严格模式下是 undefined)。
      • 通过 call(), apply(), bind() 可以改变 this 的指向。
      • 作为构造函数调用时,this 指向新创建的实例。
    • 箭头函数: this 的指向是静态的(词法作用域),它在定义时就绑定了外层(最近一层非箭头函数)作用域的 this,并且不可改变。
      • 它没有自己的 this,会捕获其所在上下文的 this 值作为自己的 this
      • call(), apply(), bind() 无法改变箭头函数的 this
    const obj = {
      name: 'Alice',
      greetNormal: function() {
        setTimeout(function() {
          console.log('Normal function this:', this.name); // this指向window (或undefined)
        }, 100);
      },
      greetArrow: function() {
        setTimeout(() => {
          console.log('Arrow function this:', this.name); // this指向obj
        }, 100);
      }
    };
    obj.greetNormal(); // Normal function this: undefined (或空)
    obj.greetArrow();  // Arrow function this: Alice
  2. 没有 arguments 对象:

    • 普通函数: 拥有自己的 arguments 对象,可以访问函数的所有参数。
    • 箭头函数: 没有自己的 arguments 对象。如果需要访问所有参数,可以使用 ES6 的剩余参数 (Rest Parameters) ...args
    function normalFunc() {
      console.log(arguments); // [1, 2, 3]
    }
    normalFunc(1, 2, 3);
    
    const arrowFunc = (...args) => {
      console.log(args); // [1, 2, 3]
      // console.log(arguments); // ReferenceError: arguments is not defined
    };
    arrowFunc(1, 2, 3);
  3. 不能作为构造函数 (不能使用 new 关键字):

    • 普通函数: 可以作为构造函数,通过 new 关键字创建实例。
    • 箭头函数: 不能作为构造函数,因为它们没有 prototype 属性,也没有自己的 this。使用 new 调用会报错。
    function Person(name) {
      this.name = name;
    }
    const p = new Person('Bob'); // Works
    
    // const ArrowPerson = (name) => { this.name = name; };
    // const ap = new ArrowPerson('Charlie'); // TypeError: ArrowPerson is not a constructor
  4. 没有 prototype 属性:

    • 普通函数: 拥有 prototype 属性,用于实现原型继承。
    • 箭头函数: 没有 prototype 属性。
    function normalFunc() {}
    console.log(normalFunc.prototype); // {constructor: f}
    
    const arrowFunc = () => {};
    console.log(arrowFunc.prototype); // undefined
  5. 语法更简洁:

    • 当函数体只有一行表达式时,可以省略 {}return
    • 当只有一个参数时,可以省略 ()
    // 普通函数
    function add(a, b) {
      return a + b;
    }
    // 箭头函数
    const add = (a, b) => a + b;
    
    // 普通函数
    function greet(name) {
      return 'Hello ' + name;
    }
    // 箭头函数
    const greet = name => 'Hello ' + name;

总结:

特性普通函数 (function)箭头函数 (=>)
this 绑定动态绑定,取决于调用方式词法绑定,继承自外层作用域,不可改变
arguments有自己的 arguments 对象没有 arguments 对象 (可用剩余参数 ...args)
构造函数可以作为构造函数 (new)不能作为构造函数 (new 会报错)
prototypeprototype 属性没有 prototype 属性
语法相对繁琐简洁 (单表达式可省略 {}return,单参数可省略 ())
super有自己的 super (在类方法中)没有自己的 super (继承自外层)
yield可以作为 Generator 函数 (function*)不能作为 Generator 函数

选择建议:

  • 使用箭头函数: 当你需要一个匿名函数,并且希望 this 能够自动绑定到父级上下文时(例如回调函数、数组方法的回调等)。
  • 使用普通函数:
    • 作为对象的方法,需要访问对象自身的属性时。
    • 作为构造函数。
    • 需要使用 arguments 对象时。
    • 需要定义 Generator 函数时。
    • 需要动态改变 this 的指向时。

9. new Promise是同步还是异步的,promise状态有哪些

new Promise 的执行是同步的,但其内部的 .then().catch().finally() 回调是异步的

解释:

  • new Promise 构造函数被调用时,它内部的执行器函数(executor function,即 (resolve, reject) => { ... })会立即、同步地执行。
  • 只有当执行器函数中调用 resolvereject 后,相应的 .then().catch() 中注册的回调函数才会被放入微任务队列 (Microtask Queue) 中,等待当前宏任务执行完毕后,在下一个事件循环的微任务阶段执行。

示例:

console.log('script start'); // 1

new Promise((resolve, reject) => {
  console.log('promise executor'); // 2 (同步执行)
  resolve('Promise resolved!');
}).then(res => {
  console.log('promise then'); // 5 (异步执行,微任务)
});

console.log('script end'); // 3

// 预期输出顺序:
// script start
// promise executor
// script end
// promise then

Promise 的状态 (States):

一个 Promise 对象有三种状态:

  1. pending (待定):

    • Promise 对象的初始状态。
    • 表示异步操作正在进行中,既没有成功也没有失败。
    • 可以从 pending 状态转换为 fulfilledrejected 状态。
  2. fulfilled (已成功 / 已完成):

    • 表示异步操作成功完成。
    • Promise 会带有一个结果值 (value)。
    • 一旦 Promise 变为 fulfilled 状态,它就不能再改变状态,也不能再改变其结果值(不可逆)。
  3. rejected (已失败):

    • 表示异步操作失败。
    • Promise 会带有一个拒绝原因 (reason,通常是一个 Error 对象)。
    • 一旦 Promise 变为 rejected 状态,它就不能再改变状态,也不能再改变其拒绝原因(不可逆)。

状态转换图:

      +-----------+
      |  pending  |
      +-----------+
            |
            | resolve()
            V
      +-----------+
      | fulfilled |
      +-----------+
            ^
            | reject()
            |
      +-----------+
      |  rejected |
      +-----------+

总结:

  • new Promise 构造函数及其执行器是同步的。
  • Promise.prototype.then(), Promise.prototype.catch(), Promise.prototype.finally() 注册的回调是异步的,它们被放入微任务队列。
  • Promise 有三种状态:pending (初始状态), fulfilled (成功状态), rejected (失败状态)。一旦从 pending 变为 fulfilledrejected,状态就不可逆转。

10. forEach、for of、for in区别

在 JavaScript 中,for...infor...offorEach 都是用于遍历集合的方法,但它们各有特点,适用于不同的场景。

1. for...in 循环

  • 用途: 主要用于遍历对象的可枚举属性(包括原型链上的属性,除非被 hasOwnProperty 过滤)。
  • 遍历内容: 遍历的是键 (key)索引 (index)
  • 适用对象: 适用于普通对象、数组、字符串等。
  • 注意事项:
    • 不推荐用于遍历数组,因为它会遍历到非数字属性以及原型链上的属性,顺序也不保证。
    • 遍历对象时,通常需要配合 hasOwnProperty() 方法来过滤掉原型链上的属性。
// 遍历对象
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, key)) { // 推荐使用hasOwnProperty过滤
    console.log(`Key: ${key}, Value: ${obj[key]}`);
  }
}
// Key: a, Value: 1
// Key: b, Value: 2
// Key: c, Value: 3

// 遍历数组 (不推荐)
const arr = ['apple', 'banana', 'orange'];
arr.customProp = 'test'; // 添加一个非数字属性
for (const index in arr) {
  console.log(`Index: ${index}, Value: ${arr[index]}`);
}
// Index: 0, Value: apple
// Index: 1, Value: banana
// Index: 2, Value: orange
// Index: customProp, Value: test

2. for...of 循环 (ES6)

  • 用途: 遍历可迭代对象 (Iterable) 的值。
  • 遍历内容: 遍历的是值 (value)
  • 适用对象: 字符串、数组、Map、Set、arguments 对象、DOM NodeList 等具有 [Symbol.iterator] 属性的对象。
  • 特点:
    • 能够正确遍历数组的元素,并且顺序是正确的。
    • 不能直接遍历普通对象(因为普通对象默认不可迭代)。
    • 可以配合 entries(), keys(), values() 方法遍历 Map 和 Set。
// 遍历数组
const arr = ['apple', 'banana', 'orange'];
for (const value of arr) {
  console.log(`Value: ${value}`);
}
// Value: apple
// Value: banana
// Value: orange

// 遍历字符串
const str = 'hello';
for (const char of str) {
  console.log(`Char: ${char}`);
}
// Char: h
// Char: e
// Char: l
// Char: l
// Char: o

// 遍历 Map
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
  console.log(`Key: ${key}, Value: ${value}`);
}
// Key: a, Value: 1
// Key: b, Value: 2

// 遍历 Set
const set = new Set([1, 2, 3]);
for (const value of set) {
  console.log(`Value: ${value}`);
}
// Value: 1
// Value: 2
// Value: 3

3. forEach() 方法

  • 用途: 数组的内置方法,用于遍历数组的每个元素,并对每个元素执行回调函数。
  • 遍历内容: 遍历的是值 (value)索引 (index)数组本身 (array)
  • 适用对象: 只能用于数组 (以及类数组对象通过 Array.prototype.forEach.call())。
  • 特点:
    • 不能使用 break, continue 跳出循环,也不能使用 return 提前结束循环(return 只是跳出当前迭代)。
    • 没有返回值(或者说返回值是 undefined)。
    • 回调函数中的 this 可以通过 forEach 的第二个参数指定。
const arr = ['apple', 'banana', 'orange'];
arr.forEach((value, index, array) => {
  console.log(`Index: ${index}, Value: ${value}, Array: ${array}`);
});
// Index: 0, Value: apple, Array: apple,banana,orange
// Index: 1, Value: banana, Array: apple,banana,orange
// Index: 2, Value: orange, Array: apple,banana,orange

总结对比:

特性for...infor...offorEach()
遍历对象可枚举属性的键 (key)不适用 (普通对象不可迭代)不适用 (不是数组方法)
遍历数组索引 (index),包括非数字属性,顺序不保证元素的值 (value),顺序正确元素的值 (value),索引 (index),数组本身
遍历字符串索引 (index)字符 (value)不适用 (不是字符串方法)
遍历 Map/Set不适用Map 的 [key, value] 对,Set 的值不适用 (不是 Map/Set 方法)
跳出循环可以 (break, continue)可以 (break, continue)不可以 (return 只跳过当前迭代)
返回值无 (undefined)
性能遍历对象性能较高,遍历数组性能较差遍历数组性能通常优于 forEachfor...in性能良好,但无法中断循环
兼容性ES1ES6ES5

选择建议:

  • 遍历数组:
    • 如果需要中断循环或有性能要求:for 循环 (for (let i = 0; i < arr.length; i++)) 或 for...of
    • 如果不需要中断循环,且代码简洁:forEach()
  • 遍历可迭代对象 (如字符串、Map、Set、NodeList): for...of 是最佳选择。
  • 遍历普通对象: for...in 配合 hasOwnProperty,或者使用 Object.keys(), Object.values(), Object.entries() 结合 forEachfor...of

11. new的原理, Object.create 和 new 创建对象有什么区别?

1. new 的原理

当使用 new 关键字调用一个构造函数时(例如 new MyConstructor()),JavaScript 引擎会执行以下四个步骤:

  1. 创建一个新的空对象: JavaScript 会在内存中创建一个全新的、空的对象。
  2. 设置原型链: 这个新创建的对象的 [[Prototype]] (即 __proto__ 属性) 会被链接到构造函数的 prototype 对象上。这意味着新对象可以访问构造函数原型上的属性和方法。 新对象.__proto__ = 构造函数.prototype;
  3. 绑定 this 并执行构造函数: 构造函数被调用,并且其内部的 this 会被绑定到这个新创建的对象上。构造函数中的代码会执行,通常会为新对象添加属性和方法。
  4. 返回新对象:
    • 如果构造函数没有显式地返回一个对象(即返回 undefined 或基本类型值),那么 new 操作符会默认返回这个新创建的对象。
    • 如果构造函数显式地返回了一个对象(非 null),那么 new 操作符会返回这个显式返回的对象,而不是新创建的对象。
    • 如果构造函数显式地返回了一个非对象的基本类型值(如数字、字符串、布尔值),这个返回值会被忽略,仍然返回新创建的对象。

模拟 new 的实现:

function myNew(Constructor, ...args) {
  // 1. 创建一个新对象
  const obj = {};

  // 2. 将新对象的原型链连接到构造函数的原型上
  obj.__proto__ = Constructor.prototype;
  // 或者:const obj = Object.create(Constructor.prototype);

  // 3. 绑定 this 并执行构造函数
  const result = Constructor.apply(obj, args);

  // 4. 返回新对象
  // 如果构造函数返回了一个对象,则返回该对象,否则返回新创建的obj
  return typeof result === 'object' && result !== null ? result : obj;
}

// 示例构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
  // return { custom: 'object' }; // 如果返回一个对象,new会返回这个对象
  // return 123; // 如果返回基本类型,new会忽略,仍然返回实例
}
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('Alice', 30);
console.log(person1.name); // Alice
person1.sayHello(); // Hello, my name is Alice

const person2 = myNew(Person, 'Bob', 25);
console.log(person2.name); // Bob
person2.sayHello(); // Hello, my name is Bob

2. Object.create()new 创建对象的区别

特性new Constructor()Object.create(prototypeObject)
目的通过构造函数创建实例,并执行构造函数内部的初始化逻辑。创建一个新对象,并指定其原型对象
执行过程1. 创建空对象。
2. 链接原型。
3. 绑定 this执行构造函数
4. 返回新对象。
1. 创建空对象。
2. 链接原型(将传入的 prototypeObject 设置为新对象的 __proto__)。
3. 不执行任何构造函数
构造函数需要一个构造函数(一个普通的函数,通常首字母大写)。不需要构造函数。直接指定新对象的原型。
this 绑定构造函数内部的 this 会绑定到新创建的实例。不涉及 this 绑定,因为没有构造函数被执行。
初始化可以在构造函数内部进行实例属性的初始化。无法直接在创建时初始化实例属性。需要手动添加。
使用场景- 创建类的实例。
- 需要在创建时执行初始化逻辑。
- 模拟传统面向对象编程。
- 实现纯粹的原型继承
- 创建一个对象,其原型是另一个现有对象。
- 避免执行构造函数的副作用。

示例对比:

function Parent(name) {
  this.name = name;
  this.hobbies = ['reading']; // 实例属性
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};

// --- 使用 new ---
const instance1 = new Parent('Alice');
console.log(instance1); // Parent { name: 'Alice', hobbies: ['reading'] }
instance1.sayName(); // Alice
instance1.hobbies.push('coding');
console.log(instance1.hobbies); // ['reading', 'coding']

const instance2 = new Parent('Bob');
console.log(instance2.hobbies); // ['reading'] (每个实例有独立的hobbies数组)


// --- 使用 Object.create ---
// Object.create 只能设置原型,无法直接初始化实例属性
const protoObj = {
  sayName: function() {
    console.log(this.name);
  },
  hobbies: ['reading'] // 放在原型上的属性是共享的
};

const obj1 = Object.create(protoObj);
obj1.name = 'Charlie'; // 手动添加实例属性
obj1.sayName(); // Charlie
obj1.hobbies.push('swimming'); // 修改的是原型上的数组
console.log(obj1.hobbies); // ['reading', 'swimming']

const obj2 = Object.create(protoObj);
obj2.name = 'David';
obj2.sayName(); // David
console.log(obj2.hobbies); // ['reading', 'swimming'] (obj2也受影响)

// 如果需要类似new的初始化,可以结合使用
function createAndInitialize(proto, ...args) {
  const obj = Object.create(proto);
  // 模拟构造函数的初始化逻辑
  if (typeof obj.init === 'function') { // 假设原型上有一个init方法
    obj.init(...args);
  }
  return obj;
}

const MyProto = {
  init: function(name) {
    this.name = name;
    this.hobbies = ['reading']; // 确保实例有自己的hobbies
  },
  sayName: function() {
    console.log(this.name);
  }
};

const obj3 = createAndInitialize(MyProto, 'Eve');
obj3.sayName(); // Eve
obj3.hobbies.push('drawing');
console.log(obj3.hobbies); // ['reading', 'drawing']

const obj4 = createAndInitialize(MyProto, 'Frank');
console.log(obj4.hobbies); // ['reading'] (obj4有独立的hobbies数组)

总结:

  • new 关键字是用于调用构造函数来创建对象实例的,它会自动处理原型链的连接和 this 的绑定,并执行构造函数中的初始化逻辑。
  • Object.create() 是一个更底层的创建对象的方式,它只负责指定新对象的原型,而不会执行任何构造函数。它更适合实现纯粹的原型继承,或者当你需要精确控制对象原型链时。

12. 什么是原型?__proto__prototype 区别

在 JavaScript 中,原型(Prototype)是实现继承和共享属性与方法的核心机制。理解 __proto__prototype 是理解原型链的关键。

1. prototype 属性 (函数特有)

  • 定义: prototype函数 (Function) 才有的一个属性。它是一个对象,被称为原型对象 (Prototype Object)
  • 作用: 当一个函数被用作构造函数(即通过 new 关键字调用)时,新创建的实例对象的 __proto__ 属性就会指向这个构造函数的 prototype 对象。
  • 用途: 构造函数通过 prototype 属性来为所有由它创建的实例共享属性和方法,从而实现继承和节省内存。
function Person(name) {
  this.name = name;
}

// 在 Person 的原型上添加方法
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};
Person.prototype.species = 'human';

console.log(typeof Person.prototype); // object
console.log(Person.prototype); // { sayHello: [Function], species: 'human', constructor: [Function: Person] }

2. __proto__ 属性 (对象特有)

  • 定义: __proto__ (双下划线 proto) 是对象 (Object) 才有的一个非标准属性(但被广泛支持,现在推荐使用 Object.getPrototypeOf()Object.setPrototypeOf())。它指向该对象的原型
  • 作用: 它表示一个对象的原型链的上一层。当试图访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 引擎就会沿着 __proto__ 链向上查找,直到找到该属性或者到达原型链的顶端(null)。
  • 用途: 构成了原型链,实现了属性和方法的继承。
const alice = new Person('Alice');

console.log(typeof alice.__proto__); // object
console.log(alice.__proto__); // { sayHello: [Function], species: 'human', constructor: [Function: Person] }

// 验证 alice 的 __proto__ 是否指向 Person.prototype
console.log(alice.__proto__ === Person.prototype); // true

// 访问继承的属性和方法
console.log(alice.species); // human
alice.sayHello(); // Hello, my name is Alice

__proto__prototype 的区别总结:

特性prototype__proto__
拥有者函数 (Function) 独有对象 (Object) 独有 (包括函数也是对象)
指向指向原型对象,这个原型对象是所有通过该函数构造出来的实例的共享属性和方法的来源。指向该对象的原型,即其继承自哪个对象。
作用用于构造函数定义所有实例共享的属性和方法。用于实例对象查找其继承的属性和方法,构成原型链。
标准性标准属性非标准属性 (但广泛实现),推荐使用 Object.getPrototypeOf()

原型链 (Prototype Chain):

每个对象都有一个 __proto__ 属性,指向它的原型。这个原型又可能有一个自己的 __proto__ 属性,指向更上层的原型,直到最终指向 null。这种一层一层的链接就构成了原型链。

  • 实例对象__proto__ 指向 构造函数.prototype
  • 构造函数.prototype__proto__ 指向 Object.prototype
  • Object.prototype__proto__ 指向 null (原型链的终点)
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

当访问 alice.sayHello() 时,JS 引擎会:

  1. 首先在 alice 对象自身查找 sayHello
  2. 如果没有找到,就沿着 alice.__proto__ (即 Person.prototype) 查找 sayHello
  3. 找到并执行 Person.prototype.sayHello

这就是 JavaScript 实现继承的本质。

13. Symbol的应用场景

Symbol 是 ES6 引入的一种新的基本数据类型,它表示一个独一无二的值。Symbol 值可以作为对象的属性名,主要目的是为了解决属性名冲突的问题。

Symbol 的特点:

  1. 独一无二: 即使创建多个 Symbol,只要它们不是同一个 Symbol 值,它们就永远不相等。
    const s1 = Symbol('foo');
    const s2 = Symbol('foo');
    console.log(s1 === s2); // false
  2. 不可枚举: Symbol 属性默认是不可枚举的,这意味着它们不会出现在 for...in 循环、Object.keys()Object.values()Object.entries() 中。
  3. 不能被隐式转换: Symbol 值不能与字符串或数字进行隐式转换,否则会报错。

Symbol 的主要应用场景:

  1. 作为对象的唯一属性名,防止属性名冲突: 这是 Symbol 最主要的应用场景。当你想给一个对象添加一些私有或不希望被外部轻易访问和修改的属性时,或者当你在一个对象上添加属性,但又担心与现有或未来可能添加的属性名冲突时,可以使用 Symbol 作为属性名。

    const MY_KEY = Symbol('my_unique_key');
    const obj = {
      name: 'Alice',
      [MY_KEY]: 'This is a private value'
    };
    
    console.log(obj.name); // Alice
    console.log(obj[MY_KEY]); // This is a private value
    
    // 无法通过常规方式访问或遍历
    for (let key in obj) {
      console.log(key); // 只输出 name
    }
    console.log(Object.keys(obj)); // ['name']
    console.log(Object.getOwnPropertyNames(obj)); // ['name']
    
    // 只能通过 Object.getOwnPropertySymbols() 获取
    console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(my_unique_key)]

    应用案例:

    • 模块化开发: 在一个模块中定义一些内部使用的常量或属性,不暴露给外部。
    • 避免第三方库的属性名冲突: 当你向一个由第三方库提供的对象添加自定义属性时,使用 Symbol 可以确保不会覆盖或被覆盖库中的属性。
  2. 定义常量,避免魔术字符串: 使用 Symbol 来定义一组常量,确保每个常量都是独一无二的,避免字符串常量可能带来的冲突和误解。

    const LOG_LEVEL = {
      DEBUG: Symbol('debug'),
      INFO: Symbol('info'),
      WARN: Symbol('warn'),
      ERROR: Symbol('error')
    };
    
    function log(level, message) {
      if (level === LOG_LEVEL.DEBUG) {
        console.log(`[DEBUG] ${message}`);
      } else if (level === LOG_LEVEL.INFO) {
        console.info(`[INFO] ${message}`);
      }
      // ...
    }
    
    log(LOG_LEVEL.DEBUG, 'This is a debug message.');
  3. 作为内置 Symbol 值 (Well-known Symbols),用于修改 JavaScript 语言的内部行为: JavaScript 提供了许多内置的 Symbol 值,它们作为对象的属性,可以改变对象的某些默认行为。

    • Symbol.iterator: 定义对象的默认迭代器,使其可以通过 for...of 循环遍历。
    • Symbol.hasInstance: 定义 instanceof 运算符的行为。
    • Symbol.toStringTag: 定义 Object.prototype.toString 返回的字符串值。
    • Symbol.toPrimitive: 定义对象转换为原始值时的行为。
    • Symbol.asyncIterator: 定义对象的异步迭代器,使其可以通过 for await...of 循环遍历。
    • Symbol.isConcatSpreadable: 定义数组在 concat() 方法中是否展开。
    • Symbol.species: 用于创建派生对象时,指定构造函数。

    示例 (Symbol.iterator):

    class MyCollection {
      constructor(...elements) {
        this.elements = elements;
      }
      // 使 MyCollection 实例可迭代
      [Symbol.iterator]() {
        let index = 0;
        let elements = this.elements;
        return {
          next: () => {
            if (index < elements.length) {
              return { value: elements[index++], done: false };
            } else {
              return { value: undefined, done: true };
            }
          }
        };
      }
    }
    
    const collection = new MyCollection(1, 2, 3);
    for (const item of collection) {
      console.log(item); // 1, 2, 3
    }

总结:

Symbol 为 JavaScript 提供了创建独一无二标识符的能力,极大地增强了语言的灵活性和安全性,特别是在处理对象属性冲突和扩展语言行为方面发挥着重要作用。

14. JS中如何实现继承,ES5和ES6中的区别

JavaScript 中实现继承的方式多种多样,从 ES5 的原型链继承到 ES6 的 class 语法糖,体现了语言的演进。

ES5 实现继承:

ES5 主要通过原型链 (Prototype Chain) 来实现继承。核心思想是让子类的原型对象指向父类的实例。

  1. 原型链继承 (Prototype Chaining)

    • 原理: 让子类的原型对象指向父类的实例。
    • 优点: 简单易懂,能够继承父类原型上的方法。
    • 缺点:
      • 父类的所有实例属性(引用类型)会被所有子类实例共享,修改一个子类实例的引用属性会影响所有其他子类实例。
      • 创建子类实例时,无法向父类构造函数传递参数。
    function Animal() {
      this.species = 'Animal';
      this.colors = ['black', 'white']; // 引用类型
    }
    Animal.prototype.getSpecies = function() {
      return this.species;
    };
    
    function Dog() {}
    Dog.prototype = new Animal(); // 核心:让Dog的原型指向Animal的实例
    Dog.prototype.constructor = Dog; // 修复constructor指向
    
    const dog1 = new Dog();
    const dog2 = new Dog();
    
    console.log(dog1.species); // Animal
    console.log(dog1.getSpecies()); // Animal
    dog1.colors.push('brown');
    console.log(dog1.colors); // ['black', 'white', 'brown']
    console.log(dog2.colors); // ['black', 'white', 'brown'] -- 共享了!
  2. 构造函数继承 (Constructor Stealing / Call)

    • 原理: 在子类构造函数中调用父类构造函数,并改变 this 指向。
    • 优点:
      • 解决了引用类型属性共享的问题,每个子类实例都有独立的父类实例属性。
      • 可以在创建子类实例时向父类构造函数传递参数。
    • 缺点:
      • 只能继承父类构造函数中定义的属性和方法,不能继承父类原型上的方法。
      • 每次创建子类实例时,都会重新执行父类构造函数,可能造成性能开销。
    function Animal(name) {
      this.name = name;
      this.colors = ['black', 'white'];
    }
    Animal.prototype.getSpecies = function() { // 原型上的方法无法继承
      return this.species;
    };
    
    function Cat(name, age) {
      Animal.call(this, name); // 核心:调用父类构造函数,并改变this指向
      this.age = age;
    }
    
    const cat1 = new Cat('Tom', 3);
    const cat2 = new Cat('Jerry', 2);
    
    console.log(cat1.name); // Tom
    cat1.colors.push('red');
    console.log(cat1.colors); // ['black', 'white', 'red']
    console.log(cat2.colors); // ['black', 'white'] -- 独立了!
    // console.log(cat1.getSpecies()); // TypeError: cat1.getSpecies is not a function
  3. 组合继承 (Combination Inheritance) - ES5 最常用

    • 原理: 结合了原型链继承和构造函数继承的优点。
      • 使用构造函数继承来继承父类的实例属性(解决引用类型共享和传参问题)。
      • 使用原型链继承来继承父类原型上的方法(解决方法复用问题)。
    • 优点:
      • 解决了引用类型属性共享的问题。
      • 可以向父类构造函数传参。
      • 能够继承父类原型上的方法。
    • 缺点: 会调用两次父类构造函数:一次在子类构造函数中(Animal.call(this, name)),另一次在设置子类原型时(Dog.prototype = new Animal())。这会导致子类实例上有一份父类实例属性,子类原型上又有一份父类实例属性。
    function Animal(name) {
      this.name = name;
      this.colors = ['black', 'white'];
    }
    Animal.prototype.sayName = function() {
      console.log(`My name is ${this.name}`);
    };
    
    function Dog(name, age) {
      Animal.call(this, name); // 第一次调用:继承实例属性
      this.age = age;
    }
    
    Dog.prototype = new Animal(); // 第二次调用:继承原型方法
    Dog.prototype.constructor = Dog; // 修复constructor指向
    
    const dog1 = new Dog('Buddy', 5);
    dog1.colors.push('brown');
    console.log(dog1.name, dog1.age, dog1.colors); // Buddy 5 ['black', 'white', 'brown']
    dog1.sayName(); // My name is Buddy
    
    const dog2 = new Dog('Lucy', 3);
    console.log(dog2.name, dog2.age, dog2.colors); // Lucy 3 ['black', 'white']
    dog2.sayName(); // My name is Lucy
  4. 寄生组合继承 (Prototypal Inheritance / Parasitic Combination Inheritance) - 最佳 ES5 继承方案

    • 原理: 解决了组合继承调用两次父类构造函数的问题。不直接使用 new Animal() 来设置子类原型,而是使用 Object.create() 创建一个中间对象作为子类原型,这个中间对象的原型指向父类原型。
    • 优点:
      • 只调用一次父类构造函数。
      • 避免了子类原型上不必要的父类实例属性。
      • 保留了组合继承的所有优点。
    function inheritPrototype(subType, superType) {
      const prototype = Object.create(superType.prototype); // 创建父类原型的一个副本
      prototype.constructor = subType; // 修复constructor指向
      subType.prototype = prototype; // 将子类原型指向这个副本
    }
    
    function Animal(name) {
      this.name = name;
      this.colors = ['black', 'white'];
    }
    Animal.prototype.sayName = function() {
      console.log(`My name is ${this.name}`);
    };
    
    function Dog(name, age) {
      Animal.call(this, name); // 第一次调用:继承实例属性
      this.age = age;
    }
    
    inheritPrototype(Dog, Animal); // 核心:实现原型继承
    
    const dog1 = new Dog('Buddy', 5);
    dog1.colors.push('brown');
    console.log(dog1.name, dog1.age, dog1.colors);
    dog1.sayName();
    
    const dog2 = new Dog('Lucy', 3);
    console.log(dog2.name, dog2.age, dog2.colors);
    dog2.sayName();

ES6 实现继承:

ES6 引入了 class 关键字,提供了更接近传统面向对象语言的语法糖来定义类和实现继承。底层仍然是基于原型链的。

  • classextends 关键字
    • 原理: class 声明创建了一个新的“类”,extends 关键字用于指定继承的父类。在子类构造函数中,必须先调用 super() 来调用父类的构造函数,才能使用 this
    • 优点:
      • 语法简洁清晰,更易读和理解。
      • 解决了 ES5 继承中的许多痛点(如 this 指向、原型链设置、重复调用父类构造函数)。
      • super 关键字提供了对父类构造函数和方法的方便访问。
    class Animal {
      constructor(name) {
        this.name = name;
        this.colors = ['black', 'white'];
      }
      sayName() {
        console.log(`My name is ${this.name}`);
      }
      static staticMethod() { // 静态方法
        console.log('This is a static method of Animal');
      }
    }
    
    class Dog extends Animal {
      constructor(name, age) {
        super(name); // 核心:调用父类构造函数,必须在this之前调用
        this.age = age;
      }
      bark() {
        console.log('Woof!');
      }
      sayName() { // 重写父类方法
        console.log(`Woof! My name is ${this.name}`);
      }
      get parentName() { // getter
        return super.sayName(); // 调用父类方法
      }
    }
    
    const dog1 = new Dog('Buddy', 5);
    dog1.colors.push('brown');
    console.log(dog1.name, dog1.age, dog1.colors); // Buddy 5 ['black', 'white', 'brown']
    dog1.sayName(); // Woof! My name is Buddy
    dog1.bark(); // Woof!
    
    const dog2 = new Dog('Lucy', 3);
    console.log(dog2.name, dog2.age, dog2.colors); // Lucy 3 ['black', 'white']
    dog2.sayName(); // Woof! My name is Lucy
    
    Animal.staticMethod(); // This is a static method of Animal
    // Dog.staticMethod(); // Dog 也会继承 Animal 的静态方法

ES5 和 ES6 继承的区别总结:

特性ES5 实现继承 (基于原型链)ES6 实现继承 (基于 class/extends 语法糖)
语法复杂,需要手动处理原型链和 constructor 指向,代码冗长。简洁清晰,更接近传统 OOP 语言,易于理解和维护。
this 绑定需要手动使用 call/apply 来绑定 thissuper() 自动处理 this 的绑定和父类构造函数调用。
原型设置手动设置 Child.prototype = new Parent()Object.create(Parent.prototype)extends 关键字自动处理原型链的连接。
构造函数调用组合继承会调用两次父类构造函数,寄生组合继承解决。super() 只调用一次父类构造函数。
静态方法需要手动将方法添加到构造函数本身 (Parent.staticMethod = function(){...})。使用 static 关键字直接定义,子类自动继承。
本质基于原型的继承,class 只是语法糖,底层仍是原型链。基于原型的继承,class 只是语法糖,底层仍是原型链。
可读性/维护性较差,理解原型链需要一定门槛。更好,更符合直觉。

总结:

ES6 的 class 继承是 ES5 寄生组合继承的语法糖,它提供了更优雅、更符合直觉的面向对象编程体验,解决了 ES5 继承中的许多痛点。在现代 JavaScript 开发中,推荐使用 ES6 的 class 语法来实现继承。

15. super.x() 执行 super 做了什么

在 ES6 的 class 语法中,super 关键字用于访问和调用父类(超类)的属性和方法。它有两种主要的使用方式:

  1. 作为函数调用:super()

    • 在子类构造函数 constructor() 中,super() 必须在 this 关键字之前调用。
    • 它用于调用父类的构造函数,并继承父类的实例属性。
    • 如果子类有自己的 constructor,而没有调用 super(),则会报错。
    class Parent {
      constructor(name) {
        this.name = name;
      }
    }
    
    class Child extends Parent {
      constructor(name, age) {
        super(name); // 调用父类的构造函数,并把 name 传给父类
        this.age = age; // 必须在 super() 之后才能使用 this
      }
    }
    
    const child = new Child('Alice', 10);
    console.log(child.name); // Alice
    console.log(child.age);  // 10
  2. 作为对象使用:super.propertyNamesuper.methodName()

    • 在子类的普通方法中,super 可以作为对象使用,指向父类的原型对象 (Parent.prototype)。
    • 通过 super.methodName() 可以调用父类原型上的方法。
    • 通过 super.propertyName 可以访问父类原型上的属性。
    class Parent {
      constructor(name) {
        this.name = name;
      }
      greet() {
        console.log(`Hello from Parent, my name is ${this.name}`);
      }
      get parentName() {
        return this.name.toUpperCase();
      }
    }
    
    class Child extends Parent {
      constructor(name, age) {
        super(name);
        this.age = age;
      }
      greet() {
        super.greet(); // 调用父类的 greet 方法
        console.log(`Hello from Child, I am ${this.age} years old.`);
      }
      get childParentName() {
        return super.parentName; // 访问父类的 getter
      }
    }
    
    const child = new Child('Bob', 5);
    child.greet();
    // Output:
    // Hello from Parent, my name is Bob
    // Hello from Child, I am 5 years old.
    
    console.log(child.childParentName); // BOB

super.x() 执行 super 做了什么?

super 作为对象使用(例如 super.methodName()super.propertyName)时,它的行为有点特殊:

  • super 实际上指向的是父类的原型对象。 例如,在 Child 类的方法中,super 指向的是 Parent.prototype
  • super 调用方法时,其内部的 this 仍然指向当前子类的实例。 这意味着,当你在子类中调用 super.greet() 时,虽然 greet 方法定义在 Parent.prototype 上,但它执行时 this 仍然是 child 实例,所以 this.name 会正确地取到 child 实例上的 name 属性。

总结 super.x() 的行为:

  1. 查找属性/方法: super.x 会首先在父类的原型链上查找 x 属性或方法。
  2. this 绑定: 如果 x 是一个方法,当它被调用时,其内部的 this 不会指向父类的实例,而是仍然指向当前子类的实例。这是 superParent.prototype.x.call(this) 的主要区别。
  3. 读写属性:
    • 读取 super.x 属性时,x 属性在父类原型链上查找。
    • 设置 super.x = value 属性时,x 属性会设置到子类的实例上,而不是父类的原型上。

这种 this 的自动绑定特性,使得 super 在实现方法重写和扩展时非常方便和直观。

16. call/apply/bind 的区别

call, apply, bind 都是 JavaScript 中用于改变函数执行上下文(即 this 的指向)的方法,它们都继承自 Function.prototype

1. call()

  • 语法: func.call(thisArg, arg1, arg2, ...)
  • 作用: 调用一个函数,并将其 this 关键字设置为 thisArg 所指定的值。
  • 参数: 接受一个 thisArg 作为第一个参数,后面跟着的是一系列独立的参数
  • 执行: 立即执行函数。
  • 返回值: 函数的返回值。
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!'); // Hello, Alice!
// 等同于:
// person.greet = greet;
// person.greet('Hello', '!');
// delete person.greet;

2. apply()

  • 语法: func.apply(thisArg, [argsArray])
  • 作用: 调用一个函数,并将其 this 关键字设置为 thisArg 所指定的值。
  • 参数: 接受一个 thisArg 作为第一个参数,后面跟着的是一个数组或类数组对象,其中的元素将作为函数的参数。
  • 执行: 立即执行函数。
  • 返回值: 函数的返回值。
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Bob' };

greet.apply(person, ['Hi', '.']); // Hi, Bob.

// 常见应用:结合 Math.max/min 查找数组最大/最小值
const numbers = [1, 5, 2, 8, 3];
const max = Math.max.apply(null, numbers); // null 或 undefined 表示 this 指向全局对象
console.log(max); // 8

call()apply() 的共同点:

  • 都用于改变 this 的指向。
  • 都立即执行函数。
  • 如果 thisArgnullundefinedthis 会指向全局对象(浏览器中是 window,严格模式下是 undefined)。

call()apply() 的主要区别:

  • 传参方式: call() 接受独立参数列表,apply() 接受一个参数数组。

3. bind()

  • 语法: func.bind(thisArg, arg1, arg2, ...)
  • 作用: 创建一个新的函数,这个新函数在被调用时,其 this 关键字会被永久绑定到 thisArg
  • 参数: 接受一个 thisArg 作为第一个参数,后面跟着的是一系列独立的参数,这些参数会作为新函数的前置参数。
  • 执行: 不立即执行函数,而是返回一个绑定了 this 的新函数。
  • 返回值: 一个新的函数。
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Charlie' };

const boundGreet = greet.bind(person, 'Hey'); // 绑定 this 和第一个参数

boundGreet('!'); // Hey, Charlie! (此时传入的 '!' 是第二个参数)
boundGreet('?'); // Hey, Charlie?

// bind 的参数可以分多次传入(柯里化)
const boundGreet2 = greet.bind(person);
boundGreet2('Hello', '!'); // Hello, Charlie!

bind() 的特点:

  • 永久绑定: 一旦函数被 bind 绑定,它的 this 就无法再通过 callapply 改变。
  • 惰性执行: 不会立即执行函数,而是返回一个新的函数,需要手动调用。
  • 柯里化: bind 可以在绑定 this 的同时,预设一部分参数。

总结对比:

特性call()apply()bind()
执行时机立即执行函数立即执行函数返回一个新函数,不立即执行
传参方式独立参数列表 (arg1, arg2, ...)数组或类数组 ([arg1, arg2, ...])独立参数列表 (arg1, arg2, ...),作为新函数的前置参数
返回值函数的返回值函数的返回值一个新的函数
this 绑定临时绑定,执行一次临时绑定,执行一次永久绑定,返回的新函数 this 无法再改变
主要用途改变 this 并立即执行,参数数量确定且不多改变 this 并立即执行,参数数量不确定或以数组形式存在创建一个预设 this 和部分参数的新函数,用于回调或延迟执行

选择建议:

  • 需要立即执行函数,且参数数量确定: 使用 call()
  • 需要立即执行函数,且参数数量不确定或以数组形式存在: 使用 apply()
  • 需要创建一个新函数,并永久绑定 this 和/或预设部分参数,用于后续调用或作为回调函数: 使用 bind()

17. 实现bind

实现 bind 函数需要理解其核心功能:

  1. 返回一个新的函数。
  2. 新函数被调用时,其 this 指向被永久绑定到 thisArg
  3. bind 方法可以接受预设参数,这些参数会作为新函数的前置参数。
  4. 新函数如果作为构造函数被 new 调用,this 应该指向新创建的实例,而不是 bind 时指定的 thisArg,同时能够继承原函数的原型。
Function.prototype.myBind = function(thisArg, ...args) {
  // this 指向调用 myBind 的函数 (即原函数)
  const originalFunc = this;

  // 确保 thisArg 是一个对象,如果传入 null/undefined,则指向全局对象
  // 严格模式下,如果 thisArg 是 null/undefined,this 仍为 null/undefined
  // 这里为了简化,我们让它指向对象,或者保持原样
  // const context = thisArg === null || thisArg === undefined ? globalThis : Object(thisArg);

  // 返回一个新的函数
  const boundFunc = function(...innerArgs) {
    // 当 boundFunc 作为普通函数被调用时,this 指向 context
    // 当 boundFunc 作为构造函数被 new 调用时,this 指向新创建的实例
    // 所以需要判断当前调用方式:
    // 如果 this instanceof boundFunc 为 true,说明 boundFunc 被 new 调用
    // 此时 this 指向新创建的实例,我们应该让原函数中的 this 指向这个实例
    // 否则,this 指向 bind 时传入的 thisArg
    const finalContext = this instanceof boundFunc ? this : thisArg;

    // 合并 bind 时传入的参数和调用时传入的参数
    const allArgs = args.concat(innerArgs);

    // 使用 apply 调用原函数,并绑定正确的 this 和参数
    return originalFunc.apply(finalContext, allArgs);
  };

  // 关键步骤:处理作为构造函数被 new 调用时的原型继承
  // 1. 创建一个空对象,其原型指向原函数的原型
  //    这样,新函数作为构造函数时,其创建的实例就能继承原函数的原型方法
  if (originalFunc.prototype) { // 确保原函数有prototype (箭头函数没有)
    boundFunc.prototype = Object.create(originalFunc.prototype);
  }


  return boundFunc;
};

// --- 测试 ---

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.`);
};

const obj = {
  name: 'Global',
  age: 99
};

// 1. 普通函数绑定
function greet(city, country) {
  console.log(`Hello, I'm ${this.name}, from ${city}, ${country}.`);
}

const boundGreet = greet.myBind(obj, 'Beijing');
boundGreet('China'); // Hello, I'm Global, from Beijing, China.

const boundGreet2 = greet.myBind(null, 'Shanghai'); // thisArg为null/undefined时,指向全局对象
boundGreet2('China'); // Hello, I'm undefined, from Shanghai, China. (浏览器中可能是 Hello, I'm Window, from Shanghai, China.)

// 2. 作为构造函数使用
const BoundPerson = Person.myBind(null, 'BoundName'); // 绑定第一个参数 'BoundName'
const instance = new BoundPerson(25); // 传入 age
console.log(instance.name); // BoundName
console.log(instance.age);  // 25
instance.sayHello(); // Hello, my name is BoundName and I am 25 years old.

// 3. 验证原型链
console.log(instance instanceof Person); // true
console.log(instance instanceof BoundPerson); // true

实现细节解释:

  1. originalFunc = this;: thismyBind 方法中指向调用它的函数(即需要被绑定的原函数)。
  2. boundFunc = function(...innerArgs) { ... };: 这是 myBind 返回的新函数。当这个新函数被调用时,innerArgs 是它接收到的参数。
  3. this instanceof boundFunc ? this : thisArg: 这是处理 new 调用的关键。
    • 如果 boundFuncnew 关键字调用 (new boundFunc()),那么 this 会指向新创建的实例对象。此时,我们希望原函数 originalFunc 内部的 this 也指向这个新实例,而不是 bind 时传入的 thisArg
    • 如果 boundFunc 只是作为普通函数被调用 (boundFunc()),那么 this 在非严格模式下通常指向 window (或 undefined 在严格模式下)。此时,我们希望原函数 originalFunc 内部的 this 指向 bind 时传入的 thisArg
  4. allArgs = args.concat(innerArgs);: 将 bind 时预设的参数 (args) 和新函数调用时传入的参数 (innerArgs) 合并起来,作为最终传递给原函数的参数。
  5. originalFunc.apply(finalContext, allArgs);: 使用 apply 来执行原函数,并正确地绑定 this (finalContext) 和传递参数 (allArgs)。
  6. boundFunc.prototype = Object.create(originalFunc.prototype);: 这是为了确保当 boundFunc 作为构造函数被 new 调用时,它创建的实例能够正确地继承原函数原型上的方法。Object.create() 创建一个新对象,并将其原型设置为 originalFunc.prototype,然后将这个新对象赋值给 boundFunc.prototype。这样就建立了正确的原型链,避免了直接 boundFunc.prototype = originalFunc.prototype 导致原型共享的问题。

18. Promise的理解

Promise 是 JavaScript 中处理异步操作的一种机制,它代表了一个异步操作的最终完成(或失败)及其结果值。它解决了传统回调函数(Callback Hell)带来的嵌套过深、难以维护和错误处理困难的问题。

Promise 的核心概念:

  1. 状态 (States): 一个 Promise 对象有三种互斥的状态:

    • pending (待定): 初始状态,既没有成功也没有失败。
    • fulfilled (已成功 / Resolved): 异步操作成功完成。
    • rejected (已失败): 异步操作失败。 一旦 Promise 从 pending 变为 fulfilledrejected,它的状态就不可逆转(即“凝固”)。
  2. 结果 (Value/Reason):

    • 当 Promise 变为 fulfilled 状态时,它会带有一个结果值 (value)。
    • 当 Promise 变为 rejected 状态时,它会带有一个拒绝原因 (reason,通常是一个 Error 对象)。

Promise 的基本用法:

  1. 创建 Promise:Promise 构造函数接收一个执行器函数 (executor function) 作为参数,该执行器函数会立即同步执行。执行器函数接收两个参数:resolvereject,它们都是函数。

    • 调用 resolve(value) 会使 Promise 变为 fulfilled 状态,并传递一个结果值。
    • 调用 reject(reason) 会使 Promise 变为 rejected 状态,并传递一个拒绝原因。
    const myPromise = new Promise((resolve, reject) => {
      // 模拟异步操作
      setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
          resolve('数据获取成功!'); // 成功时调用resolve
        } else {
          reject(new Error('数据获取失败!')); // 失败时调用reject
        }
      }, 1000);
    });
  2. 处理 Promise 结果:

    • .then(onFulfilled, onRejected): 用于注册 Promise 状态改变后的回调函数。
      • onFulfilled (可选): Promise 成功时调用的回调函数,接收 Promise 的结果值。
      • onRejected (可选): Promise 失败时调用的回调函数,接收 Promise 的拒绝原因。
    • .catch(onRejected): 专门用于处理 Promise 失败的情况,是 .then(null, onRejected) 的语法糖。
    • .finally(onFinally) (ES9/ES2018): 无论 Promise 成功或失败,都会执行的回调函数。它不接收任何参数,并且其返回值不会影响 Promise 的最终结果(除非返回一个 rejected Promise)。
    myPromise
      .then(data => {
        console.log('成功:', data);
        return '处理后的数据'; // 返回一个值,会传递给下一个then
      })
      .then(processedData => {
        console.log('进一步处理:', processedData);
      })
      .catch(error => {
        console.error('捕获到错误:', error.message);
      })
      .finally(() => {
        console.log('Promise 结束,无论成功或失败都会执行。');
      });

Promise 的链式调用 (Chaining):

then()catch()finally() 方法都会返回一个新的 Promise 对象,这使得 Promise 可以进行链式调用,从而避免回调地狱。

  • 返回普通值: then 回调中返回一个普通值(非 Promise),这个值会作为下一个 then 的成功结果。
  • 返回 Promise: then 回调中返回一个 Promise,下一个 then 将等待这个新的 Promise 解决(resolve 或 reject)。
  • 抛出错误: then 回调中抛出错误,会直接跳过后续的 then,被最近的 catch 捕获。
fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json(); // 返回一个Promise
  })
  .then(data => {
    console.log('数据:', data);
    // 可以在这里进行数据处理,并返回新的Promise或值
    return processData(data); // 假设 processData 返回一个Promise
  })
  .then(processedResult => {
    console.log('处理结果:', processedResult);
  })
  .catch(error => {
    console.error('请求或处理过程中发生错误:', error);
  });

Promise 的静态方法:

  • Promise.all(iterable): 接收一个 Promise 数组(或其他可迭代对象),当所有 Promise 都成功时,返回一个包含所有结果的数组 Promise。如果有任何一个 Promise 失败,则立即返回失败的 Promise。
  • Promise.race(iterable): 接收一个 Promise 数组,返回第一个解决(成功或失败)的 Promise 的结果。
  • Promise.allSettled(iterable) (ES11/ES2020): 接收一个 Promise 数组,当所有 Promise 都完成(无论成功或失败)时,返回一个包含每个 Promise 状态和结果/原因的数组。
  • Promise.any(iterable) (ES12/ES2021): 接收一个 Promise 数组,只要其中任何一个 Promise 成功,就返回该 Promise 的结果。只有当所有 Promise 都失败时,才返回一个 AggregateError
  • Promise.resolve(value): 返回一个已成功 (fulfilled) 的 Promise 对象。
  • Promise.reject(reason): 返回一个已失败 (rejected) 的 Promise 对象。

Promise 的优点:

  • 链式调用: 解决了回调地狱问题,使异步代码更扁平化、可读性更高。
  • 错误处理: 统一的错误处理机制 (.catch()),可以捕获链中任何位置的错误。
  • 状态管理: 清晰的状态 (pending, fulfilled, rejected),使异步操作的状态更易追踪。
  • 可组合性: 提供了 Promise.all 等方法,可以方便地组合多个异步操作。

async/await 的关系:

async/await 是基于 Promise 的语法糖,提供了一种更同步、更直观的方式来编写异步代码,进一步提高了可读性。async 函数总是返回一个 Promise,而 await 关键字则用于等待一个 Promise 解决。

19. Promise.all

Promise.all()Promise 对象的一个静态方法,它接收一个 Promise 实例的可迭代对象(通常是一个数组),并返回一个新的 Promise

核心功能:

  • 当且仅当所有传入的 Promise 都成功(fulfilled)时,Promise.all() 返回的 Promise 才会成功。
  • 成功时,它返回的 Promise 会带着一个数组,数组中包含所有传入 Promise 的结果,且结果的顺序与传入 Promise 的顺序保持一致
  • 只要传入的 Promise 中有任何一个失败(rejected),Promise.all() 返回的 Promise 就会立即失败。
  • 失败时,它返回的 Promise 会带着第一个失败 Promise 的拒绝原因。

语法:

Promise.all(iterable);
  • iterable: 一个可迭代对象,例如 Array。它的所有成员都应该是 Promise 实例,或者可以被 Promise.resolve() 转换为 Promise 的值。

示例:

const promise1 = Promise.resolve(3);
const promise2 = 42; // 非 Promise 值会被 Promise.resolve 包装
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 100);
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // [3, 42, "foo"]
    // 顺序与传入的 Promise 顺序一致
  })
  .catch((error) => {
    console.error('至少一个 Promise 失败:', error);
  });

// 示例:一个 Promise 失败的情况
const promiseA = new Promise((resolve) => setTimeout(() => resolve('A'), 100));
const promiseB = new Promise((_, reject) => setTimeout(() => reject('B failed'), 50)); // 50ms 后失败
const promiseC = new Promise((resolve) => setTimeout(() => resolve('C'), 200));

Promise.all([promiseA, promiseB, promiseC])
  .then((values) => {
    console.log(values);
  })
  .catch((error) => {
    console.error('第一个失败的 Promise 捕获到:', error); // 输出: B failed
  });

应用场景:

  1. 并行发送多个请求,等待所有请求完成后统一处理结果: 例如,页面需要同时加载多个数据源(用户信息、订单列表、通知),只有当所有数据都加载成功后才渲染页面。
  2. 文件上传: 当需要同时上传多个文件,并等待所有文件都上传成功后才显示上传完成信息。
  3. 资源预加载: 在应用启动前,预加载多个图片、字体或脚本文件。

注意事项:

  • Promise.all() 是“全有或全无”的模式。如果有一个 Promise 失败,整个 all 就会失败。
  • 如果需要即使有 Promise 失败也等待所有 Promise 完成并获取每个 Promise 的状态和结果,可以使用 Promise.allSettled() (ES2020)。
  • 如果只需要获取最快完成的 Promise 的结果,可以使用 Promise.race()

20. ES6的新方法

ES6 (ECMAScript 2015) 引入了大量新特性,极大地提升了 JavaScript 的开发效率和代码可读性。以下是一些主要的新方法和语法:

  1. 声明变量:letconst

    • let: 块级作用域变量,解决了 var 的变量提升和重复声明问题。
    • const: 块级作用域常量,声明后不能再修改其值(对于对象和数组,其引用不可变,但内部属性可变)。
  2. 箭头函数 (Arrow Functions)

    • 语法简洁,特别是对于单行表达式。
    • 没有自己的 thisargumentssuper,也没有 prototypethis 继承自外层词法作用域。
  3. 模板字符串 (Template Literals)

    • 使用反引号 ` 定义,支持多行字符串和嵌入表达式 ${expression}
    • const name = 'Alice';
      console.log(`Hello, ${name}!
      How are you today?`);
  4. 解构赋值 (Destructuring Assignment)

    • 从数组或对象中提取值,对变量进行赋值,语法简洁。
    • const [a, b] = [1, 2];
      const { name, age } = { name: 'Bob', age: 30 };
  5. 展开运算符 (Spread Operator) ...

    • 数组: 展开数组元素,用于合并数组、复制数组。
      const arr1 = [1, 2];
      const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]
    • 对象: 展开对象属性,用于合并对象、复制对象(浅拷贝)。
      const obj1 = { a: 1, b: 2 };
      const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }
    • 函数参数: 传递数组元素作为函数参数。
      function sum(a, b, c) { return a + b + c; }
      const numbers = [1, 2, 3];
      sum(...numbers); // 6
  6. 剩余参数 (Rest Parameters) ...

    • 用于函数定义中,将不定数量的参数收集到一个数组中。
    • function logArgs(first, ...rest) {
        console.log(first, rest);
      }
      logArgs(1, 2, 3, 4); // 1, [2, 3, 4]
  7. 默认参数 (Default Parameters)

    • 允许为函数参数设置默认值。
    • function greet(name = 'Guest') {
        console.log(`Hello, ${name}`);
      }
      greet(); // Hello, Guest
      greet('Alice'); // Hello, Alice
  8. 类 (Classes)

    • 提供了更清晰、更简洁的语法来创建构造函数和实现基于原型的继承。
    • 本质上是原型继承的语法糖。
    • class Person {
        constructor(name) {
          this.name = name;
        }
        sayHello() {
          console.log(`Hello, my name is ${this.name}`);
        }
      }
      class Student extends Person {
        constructor(name, id) {
          super(name);
          this.id = id;
        }
      }
  9. 模块 (Modules) importexport

    • 原生支持模块化开发,允许将代码分割成独立的文件,提高代码组织性和复用性。
    • // math.js
      export const add = (a, b) => a + b;
      export function subtract(a, b) { return a - b; }
      
      // main.js
      import { add, subtract } from './math.js';
      import * as MathUtils from './math.js'; // 导入所有
  10. Promise

    • 用于更优雅地处理异步操作,解决了回调地狱问题。
  11. for...of 循环

    • 遍历可迭代对象(如数组、字符串、Map、Set)的值。
  12. Map 和 Set 数据结构

    • Map: 键值对的集合,键可以是任意类型。
    • Set: 值的集合,只存储唯一值。
  13. WeakMap 和 WeakSet

    • 弱引用版本的 Map 和 Set,键是弱引用,有助于垃圾回收。
  14. Symbol 数据类型

    • 表示独一无二的值,主要用于对象的唯一属性名,防止属性名冲突。
  15. 迭代器 (Iterators) 和生成器 (Generators)

    • 提供了自定义遍历行为的能力。

这些 ES6 的新特性共同构成了现代 JavaScript 开发的基础,极大地提高了开发效率和代码质量。

21. JS面向对象原理

JavaScript 是一种基于原型的面向对象语言,与传统的基于类的面向对象语言(如 Java、C++)有所不同。尽管 ES6 引入了 class 关键字作为语法糖,但其底层仍然是基于原型的。

面向对象编程 (OOP) 的三大基本特征:

  1. 封装 (Encapsulation):

    • 定义: 将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元(对象)。并对外部隐藏对象的内部实现细节,只暴露必要的接口。
    • JavaScript 中的实现:
      • 对象字面量/构造函数: 将属性和方法定义在对象内部。
      • 闭包: 利用闭包创建私有变量和方法,外部无法直接访问,只能通过暴露的公共方法进行操作。
      • ES6 Class: class 语法糖提供了更结构化的方式来定义属性和方法。虽然 JavaScript 没有真正的私有修饰符(如 private),但可以通过约定(如 _ 开头)或使用 SymbolWeakMap、或 ES2022 的私有类字段 (#) 来模拟私有性。
    // 闭包实现私有变量
    function createCounter() {
      let count = 0; // 私有变量
    
      return {
        increment: function() { count++; },
        getCount: function() { return count; }
      };
    }
    const counter = createCounter();
    counter.increment();
    console.log(counter.getCount()); // 1
    // console.log(counter.count); // undefined (无法直接访问)
    
    // ES6 Class (私有字段 #) - ES2022
    class BankAccount {
      #balance = 0; // 私有字段
    
      constructor(initialBalance) {
        if (initialBalance > 0) {
          this.#balance = initialBalance;
        }
      }
    
      deposit(amount) {
        if (amount > 0) {
          this.#balance += amount;
        }
      }
    
      getBalance() {
        return this.#balance;
      }
    }
    const account = new BankAccount(100);
    account.deposit(50);
    console.log(account.getBalance()); // 150
    // console.log(account.#balance); // SyntaxError
  2. 继承 (Inheritance):

    • 定义: 允许一个对象(子类/派生类)获取另一个对象(父类/基类)的属性和方法。这促进了代码的重用。
    • JavaScript 中的实现:
      • 原型链 (Prototype Chain): JavaScript 继承的本质。每个对象都有一个内部链接 [[Prototype]](通过 __proto__ 访问,或 Object.getPrototypeOf()),指向其原型对象。当访问一个对象的属性或方法时,如果自身没有,就会沿着原型链向上查找。
      • ES5 继承模式: 组合继承、寄生组合继承等。
      • ES6 Class extends 语法糖,底层仍然是基于原型链的继承。
    // ES6 Class 继承
    class Animal {
      constructor(name) {
        this.name = name;
      }
      speak() {
        console.log(`${this.name} makes a sound.`);
      }
    }
    
    class Dog extends Animal {
      constructor(name, breed) {
        super(name); // 调用父类构造函数
        this.breed = breed;
      }
      speak() { // 方法重写
        console.log(`${this.name} barks.`);
      }
      fetch() {
        console.log(`${this.name} fetches the ball.`);
      }
    }
    
    const myDog = new Dog('Buddy', 'Golden Retriever');
    myDog.speak(); // Buddy barks. (调用子类方法)
    myDog.fetch(); // Buddy fetches the ball.
    console.log(myDog instanceof Dog); // true
    console.log(myDog instanceof Animal); // true
  3. 多态 (Polymorphism):

    • 定义: 允许不同类的对象对同一消息(方法调用)做出不同的响应。简单来说,就是同一个方法名,在不同对象上表现出不同的行为。
    • JavaScript 中的实现:
      • 方法重写 (Method Overriding): 子类可以定义与父类同名的方法,当子类实例调用该方法时,会执行子类自己的实现。
      • 鸭子类型 (Duck Typing): JavaScript 是一种动态类型语言,它不关心对象的具体类型,只关心对象是否具有某个方法。如果一个对象“看起来像鸭子,叫起来像鸭子,走起来像鸭子”,那么它就是鸭子。 这意味着只要对象有相同的方法名,就可以被相同的方式处理,而无需显式继承或实现接口。
    // 方法重写示例 (见继承中的 Dog 类的 speak 方法)
    
    // 鸭子类型示例
    class Cat {
      speak() {
        console.log('Meow!');
      }
    }
    
    class Cow {
      speak() {
        console.log('Moo!');
      }
    }
    
    function makeAnimalSpeak(animal) {
      if (typeof animal.speak === 'function') {
        animal.speak();
      } else {
        console.log('This animal cannot speak.');
      }
    }
    
    makeAnimalSpeak(myDog); // Buddy barks.
    makeAnimalSpeak(new Cat()); // Meow!
    makeAnimalSpeak(new Cow()); // Moo!
    makeAnimalSpeak({}); // This animal cannot speak.

JavaScript 面向对象的特点:

  • 基于原型: 核心是原型链继承,而不是基于类的继承。
  • 动态性: 对象可以在运行时动态添加或删除属性和方法。
  • 函数是第一公民: 函数可以作为值传递、赋值给变量、作为参数或返回值。函数也可以作为构造函数。
  • 没有接口和抽象类: JavaScript 没有内置的接口或抽象类概念,但可以通过约定或 TypeScript 等工具来模拟。

理解这些原理对于编写健壮、可维护的 JavaScript 代码至关重要。

22. 异步和同步的区别

在编程中,同步 (Synchronous) 和异步 (Asynchronous) 是描述程序执行流程的两种基本方式。

1. 同步 (Synchronous):

  • 定义: 代码按照顺序一条一条地执行。当一个任务开始执行时,它会阻塞(暂停)后续所有任务的执行,直到当前任务完成并返回结果。

  • 特点:

    • 阻塞: 一个任务的执行会等待另一个任务完成。
    • 顺序执行: 任务的执行顺序与代码的编写顺序一致。
    • 易于理解和调试: 流程线性,错误容易追踪。
  • 示例:

    • 函数调用:result = add(a, b); 只有 add 函数执行完毕,result 才能被赋值。
    • 简单的赋值、计算操作。
    • alert(), prompt(), confirm() 等会阻塞浏览器主线程的 API。
    console.log('Start'); // 1
    function syncTask() {
      console.log('Executing synchronous task...'); // 2
      // 模拟耗时操作
      let i = 0;
      while (i < 1000000000) { // 大量计算,阻塞
        i++;
      }
      console.log('Synchronous task finished.'); // 3
    }
    syncTask();
    console.log('End'); // 4
    // 输出顺序:Start -> Executing synchronous task... -> Synchronous task finished. -> End

2. 异步 (Asynchronous):

  • 定义: 代码在执行某个任务时,不会等待该任务的完成。它会立即继续执行后续的任务,当异步任务完成后,会通过回调函数、Promise 或 async/await 等机制通知并处理结果。

  • 特点:

    • 非阻塞: 任务的执行不会阻塞主线程,程序可以继续响应用户操作或执行其他任务。
    • 并发: 多个任务可以同时进行(尽管在单线程 JavaScript 中,是通过事件循环模拟并发)。
    • 复杂性: 流程可能不线性,错误处理和调试相对复杂(尤其是在回调地狱中)。
  • 示例:

    • 网络请求: fetch(), XMLHttpRequest
    • 定时器: setTimeout(), setInterval()
    • 事件监听: addEventListener()
    • 文件读写: Node.js 中的文件 I/O 操作。
    • Promise, async/await。
    console.log('Start'); // 1
    
    setTimeout(() => {
      console.log('Asynchronous task finished after 0ms.'); // 3 (被放入任务队列,等待主线程空闲)
    }, 0); // 尽管是0ms,它仍然是异步的
    
    fetch('https://api.example.com/data') // 模拟网络请求
      .then(response => response.json())
      .then(data => {
        console.log('Data fetched:', data); // 4 (网络请求完成后执行)
      })
      .catch(error => {
        console.error('Fetch error:', error);
      });
    
    console.log('End'); // 2
    // 预期输出顺序:Start -> End -> Asynchronous task finished after 0ms. -> Data fetched: ...

同步与异步的对比:

特性同步 (Synchronous)异步 (Asynchronous)
阻塞性阻塞主线程,等待任务完成不阻塞主线程,任务在后台执行
执行顺序严格按照代码顺序执行任务启动后立即返回,结果通过回调等方式处理,执行顺序可能与代码顺序不符
响应性差,UI 可能卡顿好,UI 保持响应
复杂性简单,易于理解和调试相对复杂,需要处理回调、Promise 等机制
适用场景简单、快速、不涉及 I/O 或耗时操作的任务耗时操作(网络请求、文件读写)、定时器、事件处理等

JavaScript 中的单线程与异步:

JavaScript 引擎在浏览器中是单线程的,这意味着它一次只能执行一个任务。异步操作并不是多线程,而是通过事件循环 (Event Loop) 机制来实现非阻塞的。当异步任务(如 setTimeoutfetch)被触发时,它们会被交给浏览器或 Node.js 的 Web API(或 C++ 模块)去处理,主线程继续执行后续代码。当异步任务完成时,其回调函数会被放入任务队列(宏任务或微任务),等待主线程空闲时被事件循环取出并执行。

23. 删除一个属性

在 JavaScript 中,删除对象的属性有几种方法,最常用的是 delete 运算符。

  1. delete 运算符 (最常用)

    • delete 运算符用于删除对象的属性。
    • 它会从对象中移除指定的属性。如果删除成功,它会返回 true;如果属性不存在或无法删除(例如,原型链上的属性、不可配置的属性),它会返回 false
    • delete 运算符只能删除对象自身的属性,不能删除原型链上的属性。
    • delete 只能删除对象的属性,不能删除变量
    const obj = {
      name: 'Alice',
      age: 30,
      address: {
        city: 'New York'
      }
    };
    
    console.log(obj.name); // Alice
    delete obj.name; // 删除属性
    console.log(obj.name); // undefined
    console.log(obj); // { age: 30, address: { city: 'New York' } }
    
    // 删除嵌套属性
    delete obj.address.city;
    console.log(obj.address); // {}
    
    // 删除不存在的属性,返回 true
    console.log(delete obj.gender); // true
    
    // 无法删除原型链上的属性
    function Person() {}
    Person.prototype.species = 'human';
    const p = new Person();
    console.log(p.species); // human
    console.log(delete p.species); // true (但实际上删除的是p自身可能存在的同名属性,而不是原型上的)
    console.log(p.species); // human (原型上的还在)
    
    // 无法删除通过 var/let/const 声明的变量
    let myVar = 10;
    // delete myVar; // SyntaxError: Delete of an unqualified identifier in strict mode.
    // console.log(delete window.myVar); // false (在浏览器非严格模式下,var声明的全局变量可以被删除,但let/const不行)
  2. 将属性设置为 undefinednull (不推荐用于“删除”)

    • 这种方法并不会真正删除属性,而是将属性的值设置为 undefinednull
    • 属性键仍然存在于对象上,并且仍然可以被枚举。
    • 这不会释放属性占用的内存,除非整个对象被垃圾回收。
    const obj2 = { a: 1, b: 2 };
    obj2.a = undefined; // 将值设为 undefined
    console.log(obj2); // { a: undefined, b: 2 }
    console.log('a' in obj2); // true (属性仍然存在)
  3. 使用对象解构和剩余操作符 (Rest Operator) (创建新对象,适用于不可变操作)

    • 这种方法不会修改原对象,而是创建一个不包含指定属性的新对象。
    • 适用于需要保持原对象不变的场景(如 React/Vue 的状态管理)。
    const originalObj = { name: 'Alice', age: 30, city: 'New York' };
    const { city, ...newObj } = originalObj; // 提取 city,其余的放入 newObj
    
    console.log(newObj); // { name: 'Alice', age: 30 }
    console.log(originalObj); // { name: 'Alice', age: 30, city: 'New York' } (原对象未变)

总结:

  • 要真正从对象中移除一个属性,应该使用 delete 运算符。
  • 如果需要创建一个不包含某些属性的新对象而不修改原对象,可以使用对象解构和剩余操作符。
  • 将属性设置为 undefinednull 只是改变了属性的值,属性本身仍然存在。

24. 页面倒计时的时间实现的哪里的时间

页面倒计时通常是基于客户端(用户浏览器)的时间来实现的。

实现原理:

  1. 获取目标时间: 首先确定倒计时结束的未来某个时间点。这个时间点可以是:
    • 服务器返回的时间戳: 从后端获取一个精确的未来时间戳(例如,活动结束时间、订单支付截止时间)。这是最推荐的方式,因为它能避免客户端时间不准确的问题。
    • 客户端本地时间: 如果倒计时只是针对用户本地操作(例如,一个本地缓存的有效期),可以直接基于用户设备的当前时间来计算。
  2. 获取当前时间: 使用 JavaScript 的 new Date() 对象获取用户设备的当前时间。
  3. 计算时间差: 将目标时间减去当前时间,得到剩余的毫秒数。
  4. 转换为天/小时/分/秒: 将毫秒数转换为更易读的单位(天、小时、分钟、秒)。
  5. 定时更新: 使用 setInterval(function, 1000) 每秒更新一次倒计时显示,重复步骤 2-4。
  6. 倒计时结束处理: 当时间差小于等于 0 时,清除定时器,并执行倒计时结束后的逻辑(例如,显示“已结束”)。

示例代码:

// 假设目标时间是 2025年7月5日 23:59:59
const targetDate = new Date('2025-07-05T23:59:59').getTime(); // 获取目标时间的毫秒数

function updateCountdown() {
  const now = new Date().getTime(); // 获取当前时间的毫秒数
  const timeLeft = targetDate - now; // 计算剩余毫秒数

  if (timeLeft <= 0) {
    clearInterval(countdownInterval); // 倒计时结束,清除定时器
    document.getElementById('countdown').innerHTML = '活动已结束!';
    return;
  }

  const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
  const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
  const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);

  document.getElementById('countdown').innerHTML =
    `${days}${hours}小时 ${minutes}${seconds}`;
}

// 首次调用,立即显示倒计时
updateCountdown();
// 每秒更新一次
const countdownInterval = setInterval(updateCountdown, 1000);

为什么通常基于客户端时间?

  • 实时性: 倒计时需要每秒更新,频繁地与服务器交互会增加服务器负载和网络延迟。
  • 用户体验: 客户端计算可以立即响应,提供流畅的倒计时体验。

客户端时间可能存在的问题及解决方案:

  1. 用户设备时间不准确: 用户可能手动修改了设备时间,导致倒计时不准确。
    • 解决方案: 始终从服务器获取一个标准的未来时间戳作为倒计时目标。 客户端只负责根据这个标准时间戳和自己的本地时间进行计算和显示。这样即使客户端时间不准,计算出的剩余时间也是相对于服务器的准确时间。
  2. 网络延迟: 首次获取服务器时间戳时可能存在延迟。
    • 解决方案: 可以在获取到服务器时间戳后,记录下客户端获取到该时间戳时的本地时间,然后在此基础上进行校准。例如:剩余时间 = (服务器目标时间 - 服务器当前时间) + (客户端当前时间 - 客户端获取服务器时间时的本地时间)

因此,虽然倒计时在客户端实现,但其基准时间(目标时间) 最好从服务器获取,以保证准确性。

25. 事件循环

JavaScript 是单线程的,这意味着它一次只能执行一个任务。为了处理异步操作(如网络请求、定时器、用户事件),JavaScript 引入了事件循环 (Event Loop) 机制。

事件循环的核心概念:

  1. 调用栈 (Call Stack):

    • 一个 LIFO(后进先出)的数据结构,用于存储正在执行的函数。
    • 当一个函数被调用时,它被推入栈顶;当函数执行完毕返回时,它被从栈中弹出。
    • JavaScript 引擎的主线程会不断地从调用栈中取出任务并执行。
  2. Web APIs / Node.js APIs:

    • 浏览器或 Node.js 提供的异步功能,例如 setTimeout, setInterval, fetch, DOM 事件监听 (addEventListener), XMLHttpRequest 等。
    • 当 JavaScript 引擎遇到这些异步任务时,它会将这些任务交给对应的 Web API 或 Node.js API 处理,然后主线程继续执行调用栈中的下一个任务,不会阻塞。
  3. 任务队列 (Task Queue / Callback Queue):

    • 当 Web API 或 Node.js API 完成其异步任务后,会将对应的回调函数(以及其结果)放入一个任务队列中等待。
    • 任务队列分为两种:
      • 宏任务队列 (Macrotask Queue):存放宏任务的回调,如 setTimeout, setInterval, setImmediate (Node.js), I/O, UI 渲染等。
      • 微任务队列 (Microtask Queue):存放微任务的回调,如 Promise.then(), catch(), finally(), MutationObserver, process.nextTick (Node.js)。
  4. 事件循环 (Event Loop):

    • 事件循环是一个持续运行的进程。
    • 它的工作是不断地检查调用栈是否为空。
    • 如果调用栈为空,它会首先检查微任务队列。如果微任务队列中有任务,它会把所有微任务一次性地全部推入调用栈执行,直到微任务队列清空。
    • 微任务队列清空后,事件循环会检查宏任务队列。如果宏任务队列中有任务,它会从队列中取出一个宏任务(只取一个),将其推入调用栈执行。
    • 执行完这个宏任务后,事件循环会再次重复上述过程:清空微任务队列,然后取下一个宏任务,如此循环往复。

事件循环的执行顺序:

  1. 执行同步代码,直到调用栈清空。
  2. 执行所有微任务队列中的任务,直到微任务队列清空。
  3. 从宏任务队列中取出一个任务执行。
  4. 重复步骤 2 和 3,直到宏任务队列和微任务队列都清空。

示例:

console.log('script start'); // 1 (同步任务)

setTimeout(function () {
  console.log('setTimeout'); // 5 (宏任务)
}, 0);

new Promise(function (resolve) {
  console.log('promise1'); // 2 (同步任务,Promise 构造函数立即执行)
  resolve();
}).then(function () {
  console.log('promise2'); // 4 (微任务)
});

console.log('script end'); // 3 (同步任务)

// 预期输出:
// script start
// promise1
// script end
// promise2
// setTimeout

解析:

  1. console.log('script start') 同步执行,输出 script start
  2. setTimeout 被推入 Web API,其回调函数在 0ms 后被放入宏任务队列。
  3. new Promise 构造函数同步执行,输出 promise1
  4. Promise.then 的回调被放入微任务队列。
  5. console.log('script end') 同步执行,输出 script end
  6. 此时,调用栈清空。事件循环开始工作:
    • 检查微任务队列,发现 promise2 回调。将其推入调用栈执行,输出 promise2
    • 微任务队列清空。
    • 检查宏任务队列,发现 setTimeout 回调。将其推入调用栈执行,输出 setTimeout
    • 宏任务队列清空。
  7. 事件循环继续,但队列已空。

总结:

事件循环是 JavaScript 实现非阻塞 I/O 和并发的关键机制。它通过不断地在调用栈、Web APIs 和任务队列之间调度任务,确保了单线程 JavaScript 能够高效地处理异步操作,同时保持用户界面的响应性。理解宏任务和微任务的优先级是正确预测异步代码执行顺序的关键。

26. Node下的事件循环,以及优先级关系

Node.js 的事件循环与浏览器环境的事件循环在核心概念上是相似的(都是基于 V8 引擎和 Libuv 库),但由于 Node.js 面向服务器端,其事件循环有自己独特的阶段 (phases) 和优先级机制。

Node.js 事件循环的阶段 (Phases):

Node.js 的事件循环是分阶段执行的,每个阶段都有一个 FIFO(先进先出)队列来执行回调函数。当一个阶段完成后,它会进入下一个阶段,如果所有阶段都已完成,并且没有待处理的异步操作,事件循环就会退出。

  1. timers (定时器阶段):

    • 执行 setTimeout()setInterval() 的回调。
    • 这些回调在指定的时间阈值过后,会尽可能早地执行。
  2. pending callbacks (待定回调阶段):

    • 执行一些系统操作的回调,例如 TCP ECONNREFUSED 错误的回调。
  3. idle, prepare (空闲/准备阶段):

    • 仅在内部使用。
  4. poll (轮询阶段):

    • 核心阶段。 大多数 I/O 回调(除了定时器、setImmediateclose 回调)都在此阶段执行。
    • 作用:
      • 检查新的 I/O 事件。
      • 执行 I/O 相关的回调(例如文件读取、网络请求完成的回调)。
      • 如果存在 setImmediate 回调,并且 poll 队列为空,它会结束 poll 阶段并进入 check 阶段。
      • 如果 poll 队列不为空,它会遍历并执行队列中的回调,直到队列为空或达到系统限制。
      • 如果 poll 队列为空,它会等待新的 I/O 事件到来,直到有新的 I/O 事件或达到定时器的阈值。
  5. check (检查阶段):

    • 执行 setImmediate() 的回调。
    • setImmediate 的回调会在 poll 阶段结束后立即执行。
  6. close callbacks (关闭回调阶段):

    • 执行 close 事件的回调,例如 socket.on('close', ...)

微任务队列 (Microtask Queue) 的优先级:

在 Node.js 中,微任务(process.nextTickPromise.then 等)的优先级非常高。

  • process.nextTick():
    • 这是 Node.js 特有的微任务,优先级最高
    • 它会在当前阶段的任何其他操作之前执行。也就是说,只要调用栈空了,process.nextTick 的回调就会立即执行,甚至在当前阶段的宏任务队列清空之前。
  • Promise.then() / async/await:
    • 与浏览器类似,Promise 的回调也是微任务。
    • 它们会在当前阶段的所有 process.nextTick 回调执行完毕后,但在进入下一个事件循环阶段之前执行。

Node.js 事件循环的执行流程(简化):

  1. 执行所有同步代码。
  2. 执行所有 process.nextTick 回调。
  3. 执行所有 Promise 微任务 (.then/.catch/.finally)。
  4. 进入事件循环的下一个阶段(timers -> pending -> poll -> check -> close)。
    • 执行当前阶段的宏任务回调。
    • 在每个阶段执行完当前阶段的宏任务后,会再次检查并清空 process.nextTick 队列和 Promise 微任务队列,然后才进入下一个阶段。
  5. 如果所有阶段都已完成,并且没有待处理的异步操作,事件循环退出。

示例:

console.log('start'); // 1

setTimeout(() => {
  console.log('setTimeout'); // 7 (timers 阶段)
}, 0);

setImmediate(() => {
  console.log('setImmediate'); // 8 (check 阶段)
});

Promise.resolve().then(() => {
  console.log('promise.then'); // 4 (微任务,在 nextTick 之后)
});

process.nextTick(() => {
  console.log('nextTick'); // 3 (最高优先级微任务)
});

const fs = require('fs');
fs.readFile(__filename, () => {
  console.log('readFile callback'); // 6 (poll 阶段)
  process.nextTick(() => {
    console.log('readFile nextTick'); // 6.1 (poll 阶段内部的 nextTick)
  });
  Promise.resolve().then(() => {
    console.log('readFile promise.then'); // 6.2 (poll 阶段内部的 promise.then)
  });
});

console.log('end'); // 2

预期输出:

start
end
nextTick
promise.then
readFile callback
readFile nextTick
readFile promise.then
setTimeout
setImmediate

解释:

  1. startend 同步执行。
  2. process.nextTick 优先级最高,在当前同步代码执行完后立即执行。
  3. promise.then 其次,在所有 nextTick 后执行。
  4. fs.readFile 的回调是 I/O 任务,会在 poll 阶段执行。
  5. setTimeouttimers 阶段。
  6. setImmediatecheck 阶段。

由于 readFile 的回调(I/O)通常会在 poll 阶段执行,而 setTimeoutsetImmediate 都在其各自的阶段等待。如果 poll 阶段没有其他 I/O 任务,它可能会在等待 I/O 期间直接进入 check 阶段执行 setImmediate。但在这个例子中,readFile 的回调会先执行,因为它是一个 I/O 任务。

理解 Node.js 事件循环的阶段和微任务的优先级对于编写高性能和无 Bug 的 Node.js 应用至关重要,尤其是在处理大量 I/O 和并发任务时。

27. 垃圾回收

垃圾回收(Garbage Collection, GC)是编程语言中一种自动管理内存的机制。它的主要目的是识别和回收程序中不再使用的内存,从而避免内存泄漏和内存溢出,让开发者无需手动管理内存。

JavaScript 中的垃圾回收:

JavaScript 引擎(如 V8)内置了垃圾回收器。由于 JavaScript 是高级语言,开发者通常不需要直接进行内存管理(如 C/C++ 中的 mallocfree)。垃圾回收器会自动跟踪内存分配和使用情况,并在适当的时候回收不再被引用的对象。

垃圾回收的常见算法:

  1. 引用计数 (Reference Counting) - 已废弃或辅助算法:

    • 原理: 跟踪每个对象被引用的次数。当一个对象的引用次数变为 0 时,就认为该对象不再被需要,可以被回收。
    • 优点: 简单,实时性高,一旦引用计数为 0 就可以立即回收。
    • 缺点:
      • 循环引用 (Circular References) 问题: 如果两个或多个对象相互引用,即使它们都不再被外部引用,它们的引用计数也永远不会降到 0,导致它们无法被回收,从而造成内存泄漏。
      let obj1 = {};
      let obj2 = {};
      obj1.a = obj2;
      obj2.a = obj1; // 循环引用
      obj1 = null;
      obj2 = null; // 此时 obj1 和 obj2 仍然无法被回收
      • 额外的开销:需要维护每个对象的引用计数。
    • 现状: 现代 JavaScript 引擎基本不再使用纯粹的引用计数作为主要垃圾回收算法,因为它无法解决循环引用问题。
  2. 标记-清除 (Mark-and-Sweep) - 现代 JavaScript 引擎主要算法:

    • 原理: 这是 JavaScript 引擎中最常用的垃圾回收算法。它分为两个阶段:
      1. 标记阶段 (Marking):
        • 垃圾回收器从一组“根对象 (roots)”(如全局对象 windowglobal,以及当前调用栈中的变量)开始,递归地遍历所有它们能访问到的对象,并标记这些对象为“可达 (reachable)”。
        • 所有从根对象无法访问到的对象都被认为是“不可达 (unreachable)”,即垃圾。
      2. 清除阶段 (Sweeping):
        • 垃圾回收器遍历堆内存,清除所有未被标记(即不可达)的对象,释放它们所占用的内存空间。
    • 优点:
      • 解决了循环引用问题:即使对象之间存在循环引用,只要它们从根对象不可达,就会被标记并清除。
      • 实现相对简单。
    • 缺点:
      • 内存碎片化: 清除后会留下不连续的内存空间,可能导致后续分配大对象时效率降低。
      • 暂停执行 (Stop-the-world): 在标记和清除过程中,JavaScript 应用程序的执行会暂停,这可能导致用户界面出现卡顿(尤其是对于大型应用)。
  3. 标记-整理 (Mark-and-Compact) - 解决内存碎片化:

    • 原理: 标记阶段与标记-清除相同。在清除阶段之后,它会进行一个“整理 (Compacting)”步骤,将所有存活的对象移动到内存的一端,从而消除内存碎片。
    • 优点: 解决了内存碎片化问题,提高了内存分配效率。
    • 缺点: 整理阶段会增加额外的开销,可能导致更长的暂停时间。
  4. 分代回收 (Generational Collection) - 优化暂停时间:

    • 原理: 发现“大部分对象在创建后很快就变得不可达,而少数对象会存活很长时间”的经验法则。
    • 将堆内存划分为不同的区域(代):
      • 新生代 (Young Generation / New Space): 存放新创建的对象。大多数对象在这里被回收,回收频率高,但每次回收时间短。通常使用 Scavenge (复制) 算法。
      • 老生代 (Old Generation / Old Space): 存放经过多次新生代回收后仍然存活的对象(即“晋升”到老生代的对象)。回收频率低,但每次回收时间可能较长。通常使用 标记-清除标记-整理 算法。
    • Scavenge 算法(新生代): 将新生代内存分为 From Space 和 To Space。新对象分配在 From Space。回收时,将 From Space 中存活的对象复制到 To Space,然后清空 From Space,交换 From 和 To 的角色。这种算法高效且没有碎片,但需要两倍的内存空间。
    • 优点: 大幅减少了每次垃圾回收的暂停时间,因为大部分回收发生在小而快的新生代。
    • 现状: 现代 JavaScript 引擎(如 V8)都采用了分代回收策略,结合了多种算法。

V8 引擎的垃圾回收策略:

V8 引擎采用了分代回收的思想,并结合了多种算法来优化性能:

  • 新生代: 使用 Scavenge 算法(一种复制算法)。
  • 老生代: 主要使用 标记-清除标记-整理。为了减少“Stop-the-world”时间,V8 还引入了:
    • 增量标记 (Incremental Marking): 将标记工作分解为小块,穿插在 JavaScript 执行过程中,减少单次暂停时间。
    • 并发标记 (Concurrent Marking): 标记工作由单独的后台线程完成,与 JavaScript 主线程并行执行。
    • 惰性清除 (Lazy Sweeping): 清除工作也可以延迟或分批进行。

总结:

JavaScript 的垃圾回收是自动进行的,开发者无需手动干预。现代垃圾回收器通过分代回收、标记-清除、标记-整理、增量/并发标记等多种策略和算法的组合,致力于在回收内存的同时,最大限度地减少对应用程序执行的干扰,提供流畅的用户体验。


三、 浏览器 / Web API

1. 前端跨域问题; (同源策略)

跨域是指当一个请求的协议、域名、端口号三者中任意一个与当前页面不同时,浏览器就会阻止该请求。这是浏览器的同源策略(Same-Origin Policy)所限制的,目的是为了保护用户的信息安全,防止恶意网站窃取数据。

同源的定义:

如果两个 URL 的协议 (protocol)、域名 (host) 和端口号 (port) 都相同,则称它们同源。

URL协议域名端口是否同源于 http://www.example.com:80/dir/page.html
http://www.example.com/dir/other.htmlhttpwww.example.com80
http://www.example.com:81/dir/page.htmlhttpwww.example.com81否 (端口不同)
https://www.example.com:80/dir/page.htmlhttpswww.example.com80否 (协议不同)
http://blog.example.com:80/dir/page.htmlhttpblog.example.com80否 (域名不同)

常见的跨域解决方案:

  1. CORS (Cross-Origin Resource Sharing) - 跨域资源共享 (推荐)

    • 原理: 服务器设置响应头,允许浏览器进行跨域请求。这是 W3C 标准,是主流的跨域解决方案。
    • 实现:
      • 简单请求: 浏览器直接发送请求,服务器在响应头中添加 Access-Control-Allow-Origin 字段,指定允许访问的源。
        Access-Control-Allow-Origin: http://your-frontend-domain.com
        // 或允许所有源 (不推荐生产环境):
        Access-Control-Allow-Origin: *
      • 预检请求 (Preflight Request): 对于非简单请求(如 PUT/DELETE 请求,或自定义请求头),浏览器会先发送一个 OPTIONS 请求(预检请求),询问服务器是否允许该跨域请求。服务器在响应 OPTIONS 请求时,需要返回一系列 Access-Control-Allow-* 响应头,告知浏览器允许的请求方法、请求头等。如果预检通过,浏览器才会发送实际请求。
        Access-Control-Allow-Methods: GET, POST, PUT, DELETE
        Access-Control-Allow-Headers: Content-Type, Authorization
        Access-Control-Max-Age: 86400 // 预检请求结果的缓存时间
    • 优点: 标准、安全、灵活,由服务器控制权限。
    • 缺点: 需要后端配合修改配置。
  2. 代理 (Proxy) - 常用,特别是开发环境

    • 原理: 浏览器向同源的代理服务器发送请求,代理服务器再将请求转发给目标跨域服务器,然后将响应返回给浏览器。由于浏览器认为请求是同源的,所以不会阻止。
    • 分类:
      • 前端代理 (开发服务器代理): 在开发环境中,通过 Webpack Dev Server、Vite 等工具配置代理。
        // webpack.config.js 或 vue.config.js (或 vite.config.js)
        module.exports = {
          devServer: {
            proxy: {
              '/api': {
                target: 'http://api.example.com', // 目标跨域服务器
                changeOrigin: true, // 改变源,将请求头中的 Host 字段设置为目标 URL
                pathRewrite: { '^/api': '' }, // 重写路径,例如 /api/users -> /users
              },
            },
          },
        };
      • 后端代理 (Nginx 反向代理): 在生产环境中,通过 Nginx、Apache 等 Web 服务器配置反向代理。
        # Nginx 配置示例
        server {
            listen 80;
            server_name your-frontend-domain.com;
        
            location / {
                root /path/to/your/frontend/dist; # 前端静态文件路径
                index index.html;
            }
        
            location /api/ { # 匹配所有以 /api/ 开头的请求
                proxy_pass http://api.example.com/; # 转发到后端API服务器
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                # ... 其他代理配置
            }
        }
    • 优点: 对前端透明,无需修改前端代码;生产环境稳定可靠。
    • 缺点: 需要额外的服务器配置。
  3. JSONP (JSON with Padding) - 已不推荐

    • 原理: 利用 <script> 标签没有同源限制的特性。前端定义一个回调函数,通过 <script> 标签向服务器发送带回调函数名的请求,服务器返回的数据会包裹在回调函数中执行。
    • 实现:
      // 前端
      function handleData(data) {
        console.log(data);
      }
      const script = document.createElement('script');
      script.src = 'http://api.example.com/data?callback=handleData';
      document.body.appendChild(script);
      
      // 后端 (返回)
      // handleData({"name": "jsonp data"})
    • 优点: 兼容性好,支持老旧浏览器。
    • 缺点:
      • 只支持 GET 请求。
      • 不安全: 容易受到 XSS 攻击,因为服务器返回的代码会被直接执行。
      • 需要后端配合返回特定格式的数据。
      • 错误处理不方便。
    • 现状: 几乎被 CORS 和代理取代。
  4. WebSocket

    • 原理: WebSocket 协议本身就没有同源限制,它建立在 TCP 连接之上,允许全双工通信。
    • 优点: 适用于实时通信场景。
    • 缺点: 并非所有场景都适用,需要服务器支持 WebSocket 协议。
  5. document.domain + iframe (仅限主域相同,子域不同)

    • 原理: 如果两个页面是不同子域但同主域(如 a.example.comb.example.com),可以设置 document.domain = 'example.com',使它们同源,从而进行通信。
    • 优点: 简单。
    • 缺点: 只能解决主域相同的情况,且安全性较低。
  6. postMessage (跨文档消息)

    • 原理: HTML5 新增的 API,允许不同源的窗口或 iframe 之间安全地发送消息。
    • 优点: 安全,支持任意窗口/iframe 之间的通信。
    • 缺点: 只能发送字符串消息,需要双方都监听和发送。

总结:

在现代 Web 开发中,CORS 是最推荐的后端解决方案,而代理(特别是开发环境的 Webpack Dev Server 代理和生产环境的 Nginx 反向代理)是最常用的前端/部署解决方案。JSONP 已基本被淘汰,其他方法适用于特定场景。

2. sessionStorage和localStorage的区别

sessionStoragelocalStorage 都是 Web Storage API 的一部分,用于在客户端(浏览器)存储数据。它们都使用键值对的形式存储字符串数据,并且提供相同的 API (setItem, getItem, removeItem, clear, key)。然而,它们在生命周期作用域上存在显著差异。

特性localStoragesessionStorage
生命周期持久化存储。数据在浏览器关闭后仍然保留,除非手动清除(通过 JavaScript 代码或浏览器设置)。会话存储。数据只在当前浏览器会话(tab 或 window)有效。当用户关闭浏览器标签页或窗口时,数据会被清除。
作用域同源共享。在同一个浏览器、同一个域名下,所有标签页和窗口都可以访问和修改同一份 localStorage 数据。同源不同窗口/标签页隔离。在同一个浏览器、同一个域名下,不同标签页或窗口的 sessionStorage 数据是独立的,互不影响。即使是同一个网站,在不同的标签页或窗口中打开,它们的 sessionStorage 也是独立的。
存储大小通常为 5MB 到 10MB(不同浏览器可能有所不同)。通常为 5MB 到 10MB(与 localStorage 类似)。
与服务器通信不会自动随 HTTP 请求发送到服务器。需要手动通过 JavaScript 读取并发送。不会自动随 HTTP 请求发送到服务器。需要手动通过 JavaScript 读取并发送。
安全性存储的数据容易被 XSS 攻击获取。不应存储敏感信息。存储的数据容易被 XSS 攻击获取。不应存储敏感信息。
使用场景- 长期保存用户偏好设置(如主题、语言)
- 缓存不经常变动的大量数据
- 离线应用数据存储
- 记住用户登录状态(配合后端 token 验证)
- 临时保存用户在当前会话中的操作状态(如表单填写进度)
- 防止意外刷新丢失数据
- 单页应用(SPA)中的页面状态管理(当前路由下的数据)
- 用户在一次会话中的浏览历史

共同点:

  • 都属于客户端存储。
  • 都只能存储字符串。如果需要存储对象,需要使用 JSON.stringify() 序列化,读取时使用 JSON.parse() 反序列化。
  • API 相同:
    • setItem(key, value): 存储键值对。
    • getItem(key): 获取指定键的值。
    • removeItem(key): 删除指定键的值。
    • clear(): 清除所有存储的数据。
    • key(index): 获取指定索引的键名。
    • length: 获取存储的键值对数量。

示例:

// localStorage
localStorage.setItem('username', 'Alice');
console.log(localStorage.getItem('username')); // Alice
// 关闭浏览器再打开,数据还在

// sessionStorage
sessionStorage.setItem('currentTabId', 'tab-123');
console.log(sessionStorage.getItem('currentTabId')); // tab-123
// 关闭当前标签页再打开,数据丢失

安全性考虑:

由于 localStoragesessionStorage 都容易受到 XSS 攻击(攻击者可以通过注入恶意脚本来读取或修改存储的数据),因此不应在其中存储敏感信息,如用户的密码或会话令牌(Session ID)。对于敏感信息,更安全的做法是使用 HTTP Only 的 Cookie 或者在后端进行管理。

3. 常见的HTTP状态码

HTTP 状态码是服务器对 HTTP 请求的响应状态。它们是三位数字,分为 5 大类。

1xx (信息性状态码): 表示请求已被接收,继续处理。

  • 100 Continue: 客户端应继续其请求。
  • 101 Switching Protocols: 服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求。

2xx (成功状态码): 表示请求已被成功接收、理解、接受。

  • 200 OK: 请求成功。通常用于 GET 和 POST 请求。
  • 201 Created: 请求已经被成功处理,并且创建了新的资源。通常用于 POST 或 PUT 请求。
  • 202 Accepted: 请求已被接受进行处理,但处理尚未完成。
  • 204 No Content: 服务器成功处理了请求,但不需要返回任何实体内容。通常用于 PUT 或 DELETE 请求。
  • 206 Partial Content: 服务器成功处理了部分 GET 请求(用于断点续传或分块下载)。

3xx (重定向状态码): 表示需要客户端采取进一步的操作才能完成请求。

  • 301 Moved Permanently: 资源已被永久移动到新的 URL。搜索引擎会更新其链接。
  • 302 Found (或 Moved Temporarily): 资源临时移动到新的 URL。搜索引擎不会更新其链接。
  • 303 See Other: 参见其他。请求的响应可以在另一个 URI 上找到,并且应该使用 GET 方法检索。
  • 304 Not Modified: 客户端缓存的资源是最新的,无需再次传输。通常用于协商缓存。
  • 307 Temporary Redirect: 临时重定向,与 302 类似,但要求客户端使用相同的 HTTP 方法进行重定向。

4xx (客户端错误状态码): 表示客户端可能发生了错误,服务器无法处理请求。

  • 400 Bad Request: 服务器无法理解请求,通常是由于请求语法错误。
  • 401 Unauthorized: 请求需要用户认证。通常在用户未登录或认证失败时返回。
  • 403 Forbidden: 服务器理解请求,但拒绝执行。通常是由于权限不足。
  • 404 Not Found: 服务器找不到请求的资源。
  • 405 Method Not Allowed: 请求方法不允许。例如,对只允许 GET 的资源发送了 POST 请求。
  • 408 Request Timeout: 客户端在服务器准备等待的时间内未能发送完整的请求。
  • 409 Conflict: 请求与服务器的当前状态冲突。例如,尝试创建已存在的资源。
  • 429 Too Many Requests: 客户端在给定时间内发送了太多请求(速率限制)。

5xx (服务器错误状态码): 表示服务器在处理请求时发生了错误。

  • 500 Internal Server Error: 服务器遇到了一个意外情况,阻止它完成请求。这是通用的服务器端错误。
  • 501 Not Implemented: 服务器不支持当前请求所需要的某个功能。
  • 502 Bad Gateway: 作为网关或代理的服务器从上游服务器收到无效响应。
  • 503 Service Unavailable: 服务器目前无法处理请求,通常是由于过载或停机维护。
  • 504 Gateway Timeout: 作为网关或代理的服务器在等待上游服务器响应时超时。

理解这些状态码对于调试网络请求和理解 Web 应用的行为至关重要。

4. 为什么将css/img/js放在cdn

将 CSS、图片 (img)、JavaScript (JS) 等静态资源放在 CDN (Content Delivery Network,内容分发网络) 上,是为了优化网站性能、提高用户体验和降低服务器负载。

CDN 的原理:

CDN 是一组分布在不同地理位置的服务器网络。当用户请求一个资源时,CDN 会将请求导向离用户最近的边缘节点(Edge Server),从而提供更快的响应速度。

将静态资源放在 CDN 的主要原因和好处:

  1. 提高加载速度 (地理位置优化):

    • 用户从离他们最近的 CDN 节点获取资源,而不是从原始服务器获取。这大大减少了数据传输的物理距离,从而降低了网络延迟 (latency) 和传输时间。
    • 对于全球用户而言,无论他们身在何处,都能获得较快的加载速度。
  2. 减轻源服务器负载:

    • CDN 缓存了静态资源。大部分用户请求可以直接由 CDN 边缘节点响应,无需回源到原始服务器。
    • 这大大减少了源服务器的带宽消耗和处理请求的压力,使其能够更专注于处理动态内容和业务逻辑。
  3. 提高并发限制:

    • 浏览器对同一个域名下的并发请求数量有限制(通常是 6-8 个)。
    • 如果将静态资源放在不同的 CDN 域名下(例如 static1.cdn.com, static2.cdn.com),可以突破这个限制,允许浏览器同时下载更多的资源,进一步加快页面加载速度。
  4. 增强可用性和稳定性:

    • CDN 具有分布式架构和冗余机制。即使某个 CDN 节点出现故障,用户的请求也会自动路由到其他可用节点,确保服务的高可用性。
    • 这降低了单点故障的风险,提高了网站的整体稳定性。
  5. 安全性:

    • CDN 通常具备 DDoS 攻击防护、Web 应用防火墙 (WAF) 等安全功能,可以抵御常见的网络攻击,保护源服务器的安全。
  6. 带宽成本降低:

    • 由于大部分流量由 CDN 处理,源服务器的出口带宽需求降低,从而可能节省带宽费用。
  7. SEO 优化:

    • 网站加载速度是搜索引擎排名的一个重要因素。更快的加载速度有助于提升用户体验,从而间接改善 SEO。

总结:

将 CSS、JS、图片等静态资源部署到 CDN 是一种标准的 Web 性能优化实践。它通过利用分布式网络、缓存机制和并发下载等技术,显著提升了网站的加载速度、稳定性和可扩展性,为用户提供了更流畅的访问体验。

5. 了解哪些攻击?

前端安全是 Web 安全的重要组成部分,主要关注浏览器端可能发生的攻击。以下是一些常见的 Web 攻击类型:

  1. XSS (Cross-Site Scripting) - 跨站脚本攻击

    • 原理: 攻击者将恶意脚本(通常是 JavaScript)注入到网页中,当用户访问该网页时,恶意脚本会在用户的浏览器上执行。
    • 危害: 窃取用户 Cookie/Session、劫持用户会话、篡改页面内容、钓鱼、传播恶意软件等。
    • 分类:
      • 反射型 XSS (Reflected XSS): 恶意脚本通过 URL 参数注入,服务器端没有对参数进行过滤,直接返回到页面中。
      • 存储型 XSS (Stored XSS): 恶意脚本被存储到服务器端(如数据库),当用户访问包含该脚本的页面时,脚本被读取并执行。
      • DOM 型 XSS (DOM-based XSS): 恶意脚本不经过服务器,而是直接在浏览器端修改 DOM 结构,导致脚本执行。
    • 防御:
      • 对所有用户输入进行严格的输入验证 (Input Validation)。
      • 对所有输出到页面的内容进行适当的输出编码/转义 (Output Encoding/Escaping)。 特别是 <script>, <img>, <a> 标签内的内容。
      • 设置 HttpOnly 属性的 Cookie: 防止 JavaScript 访问 Cookie,降低 Cookie 窃取的风险。
      • 内容安全策略 (CSP - Content Security Policy): 限制页面可以加载的资源来源,阻止恶意脚本的执行。
      • 使用安全的 JavaScript API: 避免使用 innerHTML, document.write, eval 等不安全的 API。
  2. CSRF (Cross-Site Request Forgery) - 跨站请求伪造

    • 原理: 攻击者诱导用户点击一个恶意链接或访问一个恶意网站,利用用户在目标网站已登录的身份,发送伪造的请求到目标网站。由于浏览器会自动携带目标网站的 Cookie,服务器会误认为这是用户的合法操作。
    • 危害: 盗取用户资金、修改用户密码、发送恶意邮件、执行非法操作等。
    • 防御:
      • CSRF Token: 在表单或请求中添加一个随机生成的 Token,服务器验证 Token 的合法性。攻击者无法伪造这个 Token。
      • SameSite Cookie 属性: 设置 Cookie 的 SameSite 属性(Lax, Strict, None),限制 Cookie 只能在同站请求中发送。
      • Referer 检查: 验证 HTTP 请求头中的 Referer 字段,确保请求来源于合法页面。
      • 双重 Cookie 提交: 客户端 JavaScript 读取 Cookie 中的 Token,并将其作为请求头或请求体的一部分发送,服务器进行比对。
      • 验证码: 对于敏感操作,要求用户输入验证码。
  3. 点击劫持 (Clickjacking)

    • 原理: 攻击者将一个透明的、恶意的 iframe 覆盖在合法页面的上方,诱导用户点击表面上的合法按钮,实际上点击的是 iframe 中的恶意内容。
    • 危害: 诱导用户进行非自愿的操作,如点赞、关注、转账等。
    • 防御:
      • X-Frame-Options HTTP 响应头: 设置为 DENYSAMEORIGIN,阻止页面被嵌入到 iframe 中。
      • JavaScript 防御 (Frame Busting): 通过 JavaScript 判断当前页面是否被 iframe 嵌入,如果是则跳出 iframe
  4. DDoS (Distributed Denial of Service) - 分布式拒绝服务攻击

    • 原理: 攻击者利用大量被控制的计算机(僵尸网络)向目标服务器发送大量请求,耗尽服务器资源,导致服务不可用。
    • 前端防御: 前端无法直接防御 DDoS,但可以通过一些措施间接减轻影响,如:
      • CDN (内容分发网络): 分发静态资源,减轻源服务器压力。
      • 前端限流: 对 API 请求进行限流(如防抖、节流),避免短时间内发送大量请求。
    • 主要防御: 后端和网络层面(防火墙、流量清洗、CDN 服务商)。
  5. SQL 注入 (SQL Injection)

    • 原理: 攻击者在用户输入字段中插入恶意的 SQL 代码,这些代码被后端应用程序执行,从而绕过安全限制、访问或修改数据库。
    • 前端防御: 前端无法直接防御 SQL 注入(因为最终是在后端执行),但前端的输入验证和过滤可以作为第一道防线,减少无效或可疑的输入。
    • 主要防御: 后端对所有用户输入进行参数化查询 (Prepared Statements)、ORM 框架、输入验证和转义。
  6. 文件上传漏洞

    • 原理: 攻击者上传恶意文件(如 WebShell),然后通过访问这个文件来执行恶意代码,控制服务器。
    • 前端防御: 前端可以进行文件类型、大小、名称的初步校验,但这些校验很容易被绕过。
    • 主要防御: 后端严格校验文件类型(白名单)、文件内容、文件大小,重命名文件,将文件上传到非 Web 可访问的目录,对上传目录设置执行权限限制。

总结:

前端安全是一个持续的挑战,需要前端和后端开发人员共同努力。前端主要负责防御 XSS、CSRF、点击劫持等发生在浏览器端的攻击,并通过输入验证、CSP、Cookie 属性等手段来增强安全性。对于 SQL 注入、DDoS 等攻击,前端可以做辅助性工作,但主要防御措施在后端和网络层面。

6. 前端性能优化:知道哪些优化方法

前端性能优化是提高网站加载速度、响应速度和用户体验的关键。以下是一些常见的前端性能优化方法:

  1. 资源加载优化:

    • 减少 HTTP 请求: 合并 CSS/JS 文件,使用雪碧图 (Sprite Images),使用 Icon Font 或 SVG。
    • 文件压缩:
      • Gzip/Brotli 压缩: 开启服务器 Gzip/Brotli 压缩,减少传输文件大小。
      • 代码压缩 (Minification): 移除 JS/CSS/HTML 中的空格、注释、换行符等,缩短变量名。
      • 图片压缩: 使用 TinyPNG、ImageOptim 等工具压缩图片,选择合适的图片格式(WebP 优于 JPG/PNG)。
    • 浏览器缓存:
      • 强缓存 (Cache-Control, Expires): 设置 HTTP 响应头,让浏览器直接从缓存中读取资源,不发送请求。
      • 协商缓存 (Last-Modified/If-Modified-Since, ETag/If-None-Match): 资源过期后,浏览器带上标识符向服务器询问资源是否更新,如果未更新则返回 304。
    • CDN 加速: 将静态资源部署到 CDN,利用其分布式特性,使用户从最近的节点获取资源。
    • 懒加载 (Lazy Loading):
      • 图片/视频懒加载: 只加载进入视口(或即将进入视口)的图片/视频,减少首屏加载时间。
      • 组件/模块懒加载 (Code Splitting): 按需加载 JS 模块或组件,减少初始包大小。
    • 预加载/预渲染 (Preload/Preconnect/Prefetch/Prerender):
      • rel="preload": 预先加载关键资源。
      • rel="preconnect": 提前建立与域名的连接。
      • rel="prefetch": 预先获取用户可能访问的下一个页面资源。
      • rel="prerender": 预渲染整个页面。
    • 关键 CSS (Critical CSS): 提取首屏所需的 CSS,内联到 HTML 中,实现快速渲染。
    • 异步加载 JS: 使用 deferasync 属性,避免 JS 阻塞 HTML 解析和渲染。
      • defer: 脚本在 HTML 解析完成后执行,保持脚本的相对顺序。
      • async: 脚本下载完成后立即执行,不保证脚本的相对顺序。
  2. 渲染优化:

    • 减少重排 (Reflow/Layout) 和重绘 (Repaint):
      • 避免频繁操作 DOM,批量修改样式。
      • 使用 transformopacity 等属性进行动画,因为它们不会触发重排。
      • 避免在循环中读取会触发重排的属性(如 offsetHeight)。
    • CSS 选择器优化: 避免使用过于复杂的选择器,从右向左匹配,提高匹配效率。
    • 硬件加速: 使用 transform: translateZ(0)will-change 开启 GPU 硬件加速。
    • 离屏渲染: 将复杂动画或不常变化的元素移出主渲染流程,例如使用 canvaswebgl
    • 虚拟列表/长列表优化: 对于大量数据的列表,只渲染可视区域内的部分,减少 DOM 节点数量。
  3. JavaScript 优化:

    • 减少 DOM 操作: 缓存 DOM 元素,使用文档碎片 (DocumentFragment) 进行批量 DOM 操作。
    • 事件委托 (Event Delegation): 将事件监听器绑定到父元素,减少事件监听器的数量。
    • 防抖 (Debounce) 和节流 (Throttle): 限制事件处理函数的执行频率,减少不必要的计算。
    • Web Workers: 将耗时计算放到 Web Worker 中,避免阻塞主线程。
    • 内存优化: 避免创建不必要的全局变量,及时解除对不再使用的对象的引用,防止内存泄漏。
    • Tree Shaking: 打包时移除未使用的代码。
  4. SSR/SSG (服务器端渲染 / 静态站点生成):

    • SSR: 在服务器端生成 HTML,直接返回给浏览器,减少首次内容绘制 (FCP) 时间,有利于 SEO。
    • SSG: 在构建时生成所有页面的 HTML,部署到 CDN,提供极致的加载速度。
  5. 用户体验指标优化:

    • LCP (Largest Contentful Paint): 最大内容绘制,衡量加载性能。
    • FID (First Input Delay): 首次输入延迟,衡量交互性。
    • CLS (Cumulative Layout Shift): 累计布局偏移,衡量视觉稳定性。
    • FCP (First Contentful Paint): 首次内容绘制,衡量用户看到页面内容的时间。
    • TTI (Time to Interactive): 可交互时间,衡量页面何时变得完全可交互。

通过综合运用以上方法,可以显著提升前端应用的性能和用户体验。

7. Ajax过程

Ajax (Asynchronous JavaScript and XML) 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。它通过在后台与服务器进行少量数据交换,使网页实现异步更新。

Ajax 的核心是 XMLHttpRequest (XHR) 对象。

Ajax 请求的基本过程:

  1. 创建 XMLHttpRequest 对象: 这是 Ajax 的核心,用于与服务器进行通信。

    const xhr = new XMLHttpRequest();
  2. 设置请求参数 (open() 方法):xhr.open(method, url, async, user, password);

    • method: HTTP 请求方法,如 GET, POST, PUT, DELETE 等。
    • url: 请求的目标 URL。
    • async: (可选,默认为 true) 是否异步执行请求。true 为异步,false 为同步。强烈建议使用异步。
    • user: (可选) 用户名,用于认证。
    • password: (可选) 密码,用于认证。
    xhr.open('GET', 'https://api.example.com/data', true);
    // 或 POST 请求
    // xhr.open('POST', 'https://api.example.com/submit', true);
  3. 设置请求头 (setRequestHeader() 方法 - 仅在 open() 之后,send() 之前): 如果需要发送 POST 请求或自定义请求头,需要设置 Content-Type 等。 xhr.setRequestHeader(header, value);

    // 如果发送 JSON 数据
    xhr.setRequestHeader('Content-Type', 'application/json');
    // 如果需要认证
    // xhr.setRequestHeader('Authorization', 'Bearer your_token');
  4. 监听请求状态变化 (onreadystatechange 事件或 onload/onerror):xhr.readyState 属性表示请求的当前状态,从 0 到 4 变化。每次 readyState 改变时,都会触发 onreadystatechange 事件。

    • readyState 的值:

      • 0 (UNSENT): 初始状态,open() 方法还未被调用。
      • 1 (OPENED): open() 方法已被调用,但 send() 方法还未被调用。
      • 2 (HEADERS_RECEIVED): send() 方法已被调用,并且头部和状态已经可获得。
      • 3 (LOADING): 下载中,responseText 属性已经包含部分数据。
      • 4 (DONE): 请求完成,数据传输完毕。
    • status 属性: HTTP 状态码(如 200, 404, 500 等)。

    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) { // 请求完成
        if (xhr.status >= 200 && xhr.status < 300) { // HTTP 状态码表示成功
          console.log('请求成功:', xhr.responseText);
          // 如果是 JSON 数据,需要解析
          // const data = JSON.parse(xhr.responseText);
        } else {
          console.error('请求失败:', xhr.status, xhr.statusText);
        }
      }
    };
    
    // 现代浏览器更推荐使用 onload 和 onerror
    /*
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        console.log('请求成功:', xhr.responseText);
      } else {
        console.error('请求失败:', xhr.status, xhr.statusText);
      }
    };
    xhr.onerror = function() {
      console.error('网络错误或请求被阻止');
    };
    xhr.ontimeout = function() { // 监听超时
      console.error('请求超时');
    };
    xhr.ontimeout = 5000; // 设置超时时间为5秒
    */
  5. 发送请求 (send() 方法):xhr.send(data);

    • data: (可选) 作为请求主体发送的数据。
      • 对于 GET 请求,data 应为 null 或省略。
      • 对于 POST 请求,data 可以是字符串、FormData 对象、BlobArrayBuffer 等。
    // GET 请求
    xhr.send();
    
    // POST 请求发送 JSON 数据
    // const postData = { id: 1, value: 'test' };
    // xhr.send(JSON.stringify(postData));
    
    // POST 请求发送 FormData
    // const formData = new FormData();
    // formData.append('key', 'value');
    // xhr.send(formData);

完整示例 (GET 请求):

function ajaxGet(url, successCallback, errorCallback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true); // true 表示异步

  xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
      successCallback(xhr.responseText);
    } else {
      errorCallback(xhr.status, xhr.statusText);
    }
  };

  xhr.onerror = function() {
    errorCallback(0, 'Network Error');
  };

  xhr.send();
}

// 使用示例
ajaxGet(
  'https://jsonplaceholder.typicode.com/todos/1',
  function(response) {
    console.log('数据获取成功:', JSON.parse(response));
  },
  function(status, statusText) {
    console.error('数据获取失败:', status, statusText);
  }
);

现代 Ajax 替代方案:

尽管 XMLHttpRequest 是 Ajax 的基石,但在现代 Web 开发中,通常更推荐使用以下更高级的 API 或库:

  • fetch API (Promise-based): 现代浏览器内置的 API,基于 Promise,提供更简洁、更强大的网络请求功能。
  • axios 等第三方库: 封装了 XMLHttpRequestfetch,提供更友好的 API、拦截器、请求取消等高级功能。

8. open 函数的参数

XMLHttpRequest 对象的 open() 方法用于初始化一个请求。

语法:

xhr.open(method, url, async, user, password);

参数详解:

  1. method (字符串,必需)

    • 指定 HTTP 请求方法,例如:
      • "GET": 用于请求指定资源。
      • "POST": 用于向指定资源提交数据。
      • "PUT": 用于更新或创建资源。
      • "DELETE": 用于删除指定资源。
      • "HEAD": 与 GET 类似,但只请求响应头,不返回响应主体。
      • "OPTIONS": 用于描述目标资源的通信选项。
      • "PATCH": 用于对资源进行部分修改。
  2. url (字符串,必需)

    • 指定请求发送到的 URL。
    • 可以是相对 URL 或绝对 URL。
    • 对于 GET 请求,查询参数可以直接附加在 URL 后面,例如 "/api/data?id=123"
  3. async (布尔值,可选,默认为 true)

    • 指定请求是否异步执行。
    • true (默认值): 表示请求将异步执行。XMLHttpRequest 会在后台发送请求,不阻塞主线程。当请求完成时,会触发 onreadystatechangeonload 事件。这是推荐且常用的方式。
    • false: 表示请求将同步执行。XMLHttpRequest 会阻塞主线程,直到服务器响应返回。这意味着在请求完成之前,浏览器将停止响应用户交互,页面会“冻结”。不推荐在主线程中使用同步请求,因为它会导致糟糕的用户体验。
  4. user (字符串,可选)

    • 用于认证的用户名。
    • 如果提供了此参数,并且服务器需要认证,浏览器会在请求头中包含 Authorization 字段。
    • 通常与 password 参数一起使用。
  5. password (字符串,可选)

    • 用于认证的密码。
    • 通常与 user 参数一起使用。
    • 注意: 在实际应用中,直接在 URL 或 open() 方法中传递用户名和密码是不安全的,因为它们可能会被轻易拦截。更安全的做法是使用基于 Token 的认证(如 JWT)或 OAuth。

示例:

const xhr = new XMLHttpRequest();

// GET 请求,异步
xhr.open('GET', '/api/users', true);

// POST 请求,异步,带认证信息
// xhr.open('POST', '/api/login', true, 'myUsername', 'myPassword');

// GET 请求,同步 (不推荐)
// xhr.open('GET', '/api/status', false);

在调用 open() 方法后,请求对象的状态会从 UNSENT (0) 变为 OPENED (1)。此时,你可以设置请求头 (setRequestHeader()),然后才能发送请求 (send())。

9. readyState 的值是什么含义

XMLHttpRequest 对象的 readyState 属性表示请求的当前状态。它是一个整数,从 0 到 4 变化。每次状态改变时,都会触发 onreadystatechange 事件。

readyState 的五个值及其含义:

  1. 0 (UNSENT)

    • 含义: 初始状态。XMLHttpRequest 对象已创建,但 open() 方法还未被调用。
    • 描述: 客户端还没有发出请求。
  2. 1 (OPENED)

    • 含义: open() 方法已被调用。请求的配置(方法、URL、是否异步)已完成。
    • 描述: 客户端已建立请求,但 send() 方法还未被调用。此时可以设置请求头 (setRequestHeader())。
  3. 2 (HEADERS_RECEIVED)

    • 含义: send() 方法已被调用,并且服务器已经接收到请求并返回了响应头。
    • 描述: 客户端已发送请求,服务器已响应,并且响应头和状态码已经可用。此时可以通过 xhr.statusxhr.getAllResponseHeaders() 获取响应信息。
  4. 3 (LOADING)

    • 含义: 正在下载响应主体。responseText 属性中已经包含部分数据。
    • 描述: 客户端正在接收响应数据。对于大型文件下载,可以在此阶段获取部分数据进行处理。
  5. 4 (DONE)

    • 含义: 请求已完成,数据传输完毕。
    • 描述: 客户端已经完全接收到服务器的响应。此时可以安全地访问 xhr.responseTextxhr.responseXML 获取完整的响应数据,并根据 xhr.status 判断请求是否成功。

示例:

const xhr = new XMLHttpRequest();
console.log('State 0:', xhr.readyState); // 0 (UNSENT)

xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
console.log('State 1:', xhr.readyState); // 1 (OPENED)

xhr.onreadystatechange = function() {
  console.log('Current State:', xhr.readyState);
  if (xhr.readyState === 4) {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('Request successful! Response:', xhr.responseText);
    } else {
      console.error('Request failed! Status:', xhr.status);
    }
  }
};

xhr.send();
// 预期输出顺序可能类似:
// State 0: 0
// State 1: 1
// Current State: 2
// Current State: 3
// Current State: 4
// Request successful! Response: ...

在实际开发中,当使用 onreadystatechange 监听请求状态时,通常只关心 readyState === 4 的情况,因为这是请求完成的最终状态。对于更现代的浏览器,可以直接使用 onloadonerror 事件,它们分别在请求成功和失败时触发,更简洁明了。

10. options 协议请求方式

OPTIONS 是一种 HTTP 请求方法,它用于获取目标资源所支持的通信选项。它通常在以下两种主要场景中使用:

  1. CORS (跨域资源共享) 中的预检请求 (Preflight Request) - 最常见用途

    • 当浏览器发起一个非简单跨域请求时(例如,使用 PUTDELETE 等 HTTP 方法,或者请求头中包含自定义字段,或者 Content-Typeapplication/json 等),浏览器会先自动发送一个 OPTIONS 请求到服务器。
    • 这个 OPTIONS 请求的目的是询问服务器:
      • 是否允许来自当前源的跨域请求?
      • 允许使用哪些 HTTP 方法?
      • 允许使用哪些请求头?
    • 服务器接收到 OPTIONS 请求后,如果允许这些操作,会在响应头中包含 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 等字段。
    • 浏览器根据 OPTIONS 响应头判断是否允许发送实际请求。如果预检失败,实际请求就不会被发送。
    • 特点: 这个 OPTIONS 请求是由浏览器自动发出的,开发者通常不需要手动编写。

    示例 (CORS 预检请求流程):

    • 前端代码 (尝试发送非简单请求):
      fetch('http://api.example.com/data', {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          'X-Custom-Header': 'value'
        },
        body: JSON.stringify({ id: 1, name: 'test' })
      });
    • 浏览器发出的第一个请求 (预检请求):
      OPTIONS /data HTTP/1.1
      Host: api.example.com
      Origin: http://your-frontend-domain.com
      Access-Control-Request-Method: PUT
      Access-Control-Request-Headers: Content-Type, X-Custom-Header
    • 服务器响应预检请求:
      HTTP/1.1 204 No Content
      Access-Control-Allow-Origin: http://your-frontend-domain.com
      Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
      Access-Control-Allow-Headers: Content-Type, X-Custom-Header
      Access-Control-Max-Age: 86400 // 缓存预检结果的时间
    • 浏览器发出的第二个请求 (实际请求,如果预检通过):
      PUT /data HTTP/1.1
      Host: api.example.com
      Origin: http://your-frontend-domain.com
      Content-Type: application/json
      X-Custom-Header: value
      ...
  2. 获取服务器支持的通信选项:

    • 除了 CORS 预检,OPTIONS 请求也可以用于客户端直接查询服务器,了解某个 URL 支持哪些 HTTP 方法、可以发送哪些请求头等信息。
    • 例如,你可以向一个 API 资源发送 OPTIONS 请求,服务器可能会在响应头中告诉你它支持 GETPOSTPUTDELETE 等方法。

    示例 (手动发送 OPTIONS 请求):

    fetch('https://api.example.com/users', {
      method: 'OPTIONS'
    })
    .then(response => {
      // 检查响应头,例如 Access-Control-Allow-Methods
      console.log('Allowed Methods:', response.headers.get('Access-Control-Allow-Methods'));
      console.log('Allowed Headers:', response.headers.get('Access-Control-Allow-Headers'));
      // 或者直接查看所有响应头
      for (let [key, value] of response.headers) {
        console.log(`${key}: ${value}`);
      }
    })
    .catch(error => {
      console.error('Error fetching OPTIONS:', error);
    });

总结:

OPTIONS 请求方法在 Web 开发中扮演着重要角色,尤其是在跨域通信中作为预检请求,确保了浏览器在发送实际请求前的安全性。它允许客户端和服务器在不实际执行操作的情况下,协商请求的合法性。


四、 前端框架 (React / Vue)

1. useEffect理解 (React)

useEffect 是 React Hooks 中的一个 Hook,它允许你在函数组件中执行副作用 (side effects)。副作用是指那些在组件渲染过程中发生,但又不在渲染主流程中的操作,例如:

  • 数据获取 (data fetching)
  • 订阅/取消订阅 (subscriptions)
  • 手动修改 DOM (manual DOM manipulations)
  • 设置定时器 (timers)
  • 日志记录 (logging)

在类组件中,这些副作用通常在 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期方法中处理。useEffect 将这些生命周期逻辑合并到了一个 API 中。

useEffect 的基本用法:

useEffect(setup, dependencies?)

  • setup (必需): 这是一个函数,包含你想要执行的副作用逻辑。
    • 它会在组件首次渲染后每次更新后执行。
    • 它可以选择性地返回一个清理函数 (cleanup function)
  • dependencies (可选): 这是一个数组,包含 effect 所依赖的所有值(props, state, 函数等)。
    • React 会在每次渲染后比较 dependencies 数组中的值。
    • 如果数组中的任何值发生变化,setup 函数就会重新执行。
    • 如果数组中的值没有变化,effect 就不会重新执行,从而避免不必要的副作用。

useEffect 的执行时机:

  1. 首次渲染后: setup 函数会执行。
  2. 每次更新后:
    • 如果提供了 dependencies 数组:React 会比较数组中的值。如果值发生变化,则先执行上一次 effect 返回的清理函数(如果有),然后执行新的 setup 函数。
    • 如果没有提供 dependencies 数组:setup 函数会在每次渲染后都重新执行,并且每次执行前都会先执行上一次的清理函数。
  3. 组件卸载前: 如果 setup 函数返回了一个清理函数,该清理函数会在组件卸载前执行。

useEffect 的三种常见用法模式:

  1. 无依赖项 (每次渲染后都执行):

    • useEffect(() => { /* do something */ });
    • 用途: 极少使用,因为这会导致副作用在每次渲染后都执行,可能造成性能问题。适用于需要频繁执行的副作用,但通常有更好的优化方式。
  2. 空依赖项数组 (只在组件挂载和卸载时执行,模拟 componentDidMountcomponentWillUnmount):

    • useEffect(() => { /* do something */ return () => { /* cleanup */ }; }, []);
    • 用途:
      • 数据获取 (只加载一次)。
      • 添加事件监听器 (并在清理函数中移除)。
      • 设置一次性定时器。
      • 初始化第三方库。
    import React, { useEffect, useState } from 'react';
    
    function DataFetcher() {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        console.log('Component Mounted: Fetching data...');
        fetch('https://jsonplaceholder.typicode.com/todos/1')
          .then(response => {
            if (!response.ok) {
              throw new Error('Network response was not ok');
            }
            return response.json();
          })
          .then(json => {
            setData(json);
            setLoading(false);
          })
          .catch(err => {
            setError(err);
            setLoading(false);
          });
    
        // 清理函数:模拟组件卸载时取消订阅或清除资源
        return () => {
          console.log('Component Unmounted: Cleaning up...');
          // 例如:取消正在进行的网络请求,清除定时器等
        };
      }, []); // 空数组表示只在挂载和卸载时执行
    
      if (loading) return <div>Loading...</div>;
      if (error) return <div>Error: {error.message}</div>;
      return <div>Data: {JSON.stringify(data)}</div>;
    }
  3. 有依赖项数组 (在依赖项变化时执行,模拟 componentDidUpdatecomponentWillUnmount):

    • useEffect(() => { /* do something */ return () => { /* cleanup */ }; }, [dep1, dep2]);
    • 用途:
      • 根据 props 或 state 的变化重新获取数据。
      • 根据 props 或 state 的变化重新订阅。
      • 根据路由参数变化执行逻辑。
    import React, { useEffect, useState } from 'react';
    
    function UserProfile({ userId }) {
      const [user, setUser] = useState(null);
      const [loading, setLoading] = useState(true);
    
      useEffect(() => {
        console.log(`Fetching user data for userId: ${userId}`);
        setLoading(true);
        // 模拟数据获取
        const fetchData = async () => {
          try {
            const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
            const json = await response.json();
            setUser(json);
          } catch (error) {
            console.error('Failed to fetch user:', error);
          } finally {
            setLoading(false);
          }
        };
    
        fetchData();
    
        // 清理函数:在 userId 变化或组件卸载时执行
        return () => {
          console.log(`Cleaning up for old userId: ${userId}`);
          // 例如,取消上一个 userId 的请求
        };
      }, [userId]); // 当 userId 变化时重新执行 effect
    
      if (loading) return <div>Loading user {userId}...</div>;
      if (!user) return <div>User not found.</div>;
      return (
        <div>
          <h2>{user.name}</h2>
          <p>Email: {user.email}</p>
        </div>
      );
    }

useEffect 的注意事项:

  • 闭包陷阱: useEffectsetup 函数会形成闭包,捕获其定义时的 props 和 state。如果依赖项数组不完整,可能会导致 effect 使用了过时的值。
  • 完整依赖项: 确保 dependencies 数组包含了 effect 内部所有用到的来自组件作用域的变量(props, state, 函数)。可以使用 ESLint 的 exhaustive-deps 规则来帮助检查。
  • 函数依赖: 如果 effect 依赖于一个函数,并且这个函数在每次渲染时都会重新创建(例如,定义在组件内部的函数),那么 effect 也会重新运行。可以使用 useCallback 来记忆化这些函数,避免不必要的 effect 运行。
  • 清理函数: 如果 effect 订阅了外部资源或创建了定时器,务必在清理函数中进行取消订阅或清除,以防止内存泄漏。

useEffect 是一个非常强大的 Hook,但正确理解和使用它对于避免常见问题和编写高性能的 React 应用至关重要。

2. react的key作用

在 React 中,key 是一个特殊的字符串属性,当你渲染一个列表(例如,通过 map 方法生成多个组件)时,需要为每个列表项提供一个稳定且唯一key

key 的作用:

key 的主要作用是帮助 React 识别列表中哪些项被添加、移除、修改或重新排序了。

当列表项的顺序发生变化时,React 会使用 key 来匹配旧的虚拟 DOM 树中的列表项和新的虚拟 DOM 树中的列表项。通过 key,React 能够:

  1. 高效地更新 DOM:

    • 如果没有 keykey 不稳定,React 在更新列表时会倾向于原地修改 DOM 元素的内容,而不是移动或删除/重建元素。
    • 如果列表项的顺序发生变化,这种原地修改可能会导致性能问题(因为需要大量不必要的 DOM 操作)和潜在的 Bug(例如,表单输入框的状态错乱)。
    • 有了 key,React 能够精确地知道哪个元素对应哪个数据,从而进行最小化的 DOM 操作(插入、删除、移动),提高更新效率。
  2. 保持组件状态:

    • 当列表项被重新排序时,如果使用 key,React 会识别出这是同一个组件实例(只是位置变了),从而保留该组件内部的状态(如输入框的值、滚动位置等)。
    • 如果没有 keykey 不稳定,React 可能会认为这是一个新的组件,并销毁旧的组件实例,导致状态丢失。

key 的选择原则:

  1. 唯一性: key 在同一层级的兄弟节点中必须是唯一的。
  2. 稳定性: key 应该是一个在列表项的生命周期中保持不变的标识符。
  3. 避免使用 index 作为 key (除非列表是静态且不会变化):
    • 问题: 如果列表项的顺序会改变,或者列表中会添加/删除项,使用 index 作为 key 会导致问题。因为当列表项顺序变化时,同一个数据项的 index 会改变,React 会认为这是一个新的组件,从而导致不必要的 DOM 重绘和状态丢失。
    • 适用场景: 只有当列表是完全静态的,即列表项的顺序和数量永远不会改变时,才可以使用 index 作为 key
  4. 推荐使用数据项的唯一 ID:
    • 如果你的数据源有唯一的 ID(例如数据库中的 ID),这是作为 key 的最佳选择。
    • const items = [
        { id: 1, text: 'Item A' },
        { id: 2, text: 'Item B' },
        { id: 3, text: 'Item C' },
      ];
      
      {items.map(item => (
        <li key={item.id}>{item.text}</li> // 使用 item.id 作为 key
      ))}
  5. 如果实在没有唯一 ID,可以考虑使用一些库生成唯一 ID (如 uuid),但要确保其稳定性。

示例:index 作为 key 的问题

假设有一个列表,可以删除项:

// 初始状态
const list = ['A', 'B', 'C'];
// 渲染:
// <li key="0">A</li>
// <li key="1">B</li>
// <li key="2">C</li>

// 删除 'B'
const newList = ['A', 'C'];
// 如果 key 仍然是 index:
// <li key="0">A</li>  // React 认为这是旧的 key="0" 的元素,内容从 'A' 变为 'A' (没变)
// <li key="1">C</li>  // React 认为这是旧的 key="1" 的元素,内容从 'B' 变为 'C' (变了,原地更新)
// <li key="2"></li>   // 旧的 key="2" 的元素被删除了

// 实际上我们想要的是:
// <li key="id_A">A</li>
// <li key="id_C">C</li>
// React 会识别出 key="id_B" 的元素被删除了,key="id_C" 的元素位置变了。

当使用 index 作为 key 并且列表项顺序或数量发生变化时,React 无法准确识别哪些是新增的、哪些是删除的、哪些是移动的。它会倾向于原地更新内容,而不是移动 DOM 节点,这可能导致性能下降和组件状态混乱。

总结:

key 是 React 列表渲染中一个非常重要的属性,它不是为了给开发者用的,而是给 React 内部算法用的。正确使用 key 可以显著提高列表的渲染性能和稳定性,避免不必要的 DOM 操作和组件状态问题。始终使用稳定且唯一的 ID 作为 key

3. react组件通信

React 组件通信是构建复杂应用的关键。组件之间需要共享数据、触发事件和协调行为。以下是 React 中常见的组件通信方式:

  1. Props (属性) - 父子组件通信 (单向数据流)

    • 原理: 父组件通过 props 向子组件传递数据、函数或 JSX 元素。这是 React 最基本、最常用的通信方式。
    • 特点: 单向数据流(从父到子),数据不可逆向传递。子组件不能直接修改父组件传递的 props。
    • 适用场景: 绝大多数父子组件之间的数据传递。
    • 示例:
      // ParentComponent.js
      function ParentComponent() {
        const message = "Hello from Parent!";
        const handleClick = () => console.log("Button clicked in child!");
      
        return (
          <ChildComponent message={message} onButtonClick={handleClick} />
        );
      }
      
      // ChildComponent.js
      function ChildComponent(props) {
        return (
          <div>
            <p>{props.message}</p>
            <button onClick={props.onButtonClick}>Click Me</button>
          </div>
        );
      }
  2. 回调函数 (Callback Functions) - 子传父

    • 原理: 父组件将一个函数作为 prop 传递给子组件。子组件在特定事件发生时调用这个函数,并将数据作为参数传递给父组件。
    • 特点: 实现了从子组件向父组件的数据传递。
    • 适用场景: 子组件需要通知父组件某个事件发生了,或者需要将子组件内部的数据传递给父组件。
    • 示例: (同上 onButtonClick 示例)
  3. Context API - 跨组件通信 (祖孙/兄弟组件)

    • 原理: Context 提供了一种在组件树中共享数据的方式,而无需通过 props 一层层手动传递。它允许你创建一个“上下文”,所有位于该上下文提供者(Provider)下的组件都可以消费(Consumer)这个上下文中的数据。
    • 特点: 解决了“props drilling”(逐层传递 props)的问题。
    • 适用场景: 共享全局数据(如主题、认证信息、语言设置),或者在组件树中深度嵌套的组件之间传递数据。
    • 示例:
      // ThemeContext.js
      import React, { createContext, useContext } from 'react';
      const ThemeContext = createContext('light');
      
      // App.js
      function App() {
        return (
          <ThemeContext.Provider value="dark">
            <Toolbar />
          </ThemeContext.Provider>
        );
      }
      
      // Toolbar.js
      function Toolbar() {
        return <ThemedButton />;
      }
      
      // ThemedButton.js
      function ThemedButton() {
        const theme = useContext(ThemeContext); // 使用 useContext Hook 消费 Context
        return <button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>Themed Button</button>;
      }
  4. Redux / Zustand / MobX 等状态管理库 - 复杂应用全局状态管理

    • 原理: 这些库提供了集中式的状态管理解决方案。它们通常有一个全局的 Store 来存储应用的所有状态,组件可以从 Store 中读取状态,并派发 Action 来修改状态。
    • 特点: 适用于大型、复杂应用,状态逻辑集中、可预测、易于调试。
    • 适用场景: 跨多个组件、多个层级的复杂状态共享和管理。
    • 示例 (Redux 概念):
      • Store: 存储应用状态的单一数据源。
      • Actions: 描述发生了什么事件的普通 JavaScript 对象。
      • Reducers: 纯函数,接收当前状态和 Action,返回新的状态。
  5. Refs (引用) - 父组件直接操作子组件 DOM 或实例

    • 原理: Refs 提供了一种访问 DOM 节点或 React 组件实例的方式。父组件可以通过 ref 直接调用子组件的方法或访问其 DOM 元素。
    • 特点: 应该谨慎使用,因为它打破了 React 的声明式编程范式和单向数据流原则。
    • 适用场景:
      • 管理焦点、文本选择或媒体播放。
      • 触发强制性动画。
      • 集成第三方 DOM 库。
    • 示例:
      import React, { useRef } from 'react';
      
      function MyInput() {
        const inputRef = useRef(null);
      
        const focusInput = () => {
          inputRef.current.focus(); // 直接调用 DOM 元素的 focus 方法
        };
      
        return (
          <div>
            <input type="text" ref={inputRef} />
            <button onClick={focusInput}>Focus Input</button>
          </div>
        );
      }
  6. Render Props / Higher-Order Components (HOCs) - 共享逻辑

    • 原理:
      • Render Props: 子组件接收一个函数作为 prop,该函数返回 JSX。子组件在内部调用这个函数,并将自己的状态作为参数传递给它,从而将渲染控制权交给父组件。
      • HOCs (高阶组件): 一个函数,接收一个组件作为参数,并返回一个新的组件。新的组件可以注入额外的 props 或行为。
    • 特点: 都是用于共享组件逻辑(而非数据)的模式。
    • 适用场景: 共享非视觉的逻辑,如数据获取、订阅、认证等。
    • 现代替代: 随着 React Hooks 的出现,这些模式在共享逻辑方面很多时候被自定义 Hooks 所取代,因为 Hooks 更简洁、更直观。
  7. Portals (传送门) - 渲染到 DOM 树的其他位置

    • 原理: Portals 允许你将子组件渲染到父组件 DOM 层次结构之外的 DOM 节点。
    • 特点: 虽然渲染位置不同,但事件冒泡仍然遵循 React 组件树的层次结构。
    • 适用场景: 模态框 (Modals)、浮层 (Popovers)、提示框 (Tooltips) 等需要脱离父组件布局的组件,以避免 CSS z-indexoverflow 问题。

选择哪种通信方式?

  • 父子组件: 优先使用 Props回调函数
  • 跨层级/全局状态:
    • 少量、不频繁变化的全局数据:Context API
    • 复杂、频繁变化、需要严格管理的状态:Redux / Zustand / MobX 等状态管理库。
  • 直接操作子组件实例或 DOM: 谨慎使用 Refs
  • 共享逻辑: 自定义 Hooks 是现代 React 的首选。
  • 脱离 DOM 结构渲染: Portals

4. vue3、2 diff算法的区别

Vue 2 和 Vue 3 都使用了虚拟 DOM (Virtual DOM) 和 Diff 算法来优化页面渲染性能。它们的 Diff 算法核心思想都是通过比较新旧虚拟 DOM 树的差异,然后最小化地更新真实 DOM。然而,Vue 3 在 Diff 算法上进行了多项优化,使其性能比 Vue 2 更高。

Vue 2 Diff 算法简述:

Vue 2 的 Diff 算法主要基于双端比较同级比较

  • 同级比较: 只比较同一层级的节点,不进行跨层级比较(因为跨层级移动 DOM 成本很高)。
  • 双端比较 (Two Pointers):
    • 对新旧 VNode 列表的头部和尾部各设置两个指针(oldStart, oldEnd, newStart, newEnd)。
    • 通过这四个指针,尝试进行四种比较:
      1. oldStartnewStart
      2. oldEndnewEnd
      3. oldStartnewEnd
      4. oldEndnewStart
    • 如果找到相同的节点(根据 keytag 判断),则移动指针并进行 patch。
    • 如果四种比较都未匹配,则遍历 oldChildren 查找与 newStart 匹配的节点。如果找到,则移动旧节点到新位置;如果未找到,则创建新节点。
    • 最后处理剩余的节点(添加或删除)。

Vue 3 Diff 算法的优化:

Vue 3 在 Vue 2 的基础上,引入了多项编译时优化和运行时优化,使得 Diff 算法更加高效。

  1. 编译时优化 (Compiler Optimization) - 静态提升 (Static Hoisting) 和 PatchFlags:

    • 静态提升 (Static Hoisting): Vue 3 的编译器在编译模板时,会识别出那些永远不会改变的静态节点(如纯文本节点、没有动态绑定的元素),并将它们提升到渲染函数之外。这意味着在每次重新渲染时,这些静态节点不需要创建新的 VNode,也不需要参与 Diff 比较,直接复用。
    • PatchFlags (块级作用域的 PatchFlags): Vue 3 的编译器会给每个 VNode 标记一个 PatchFlag,指示该 VNode 哪些部分是动态的(例如,文本内容变化、属性变化、事件变化等)。
      • 在运行时 Diff 过程中,Vue 3 会根据 PatchFlag 跳过那些没有变化的 VNode 属性,只比较和更新有标记变化的部分。这大大减少了 Diff 算法的比较范围,提高了效率。
      • 例如,一个元素只有 text 发生变化,那么 Diff 算法就只关注 text 属性,而不会去比较其他静态属性。
  2. Fragment (片段) 支持:

    • Vue 3 支持 Fragment,允许组件返回多个根节点而无需包裹在一个额外的 div 中。这在一定程度上简化了 VNode 树的结构,也避免了不必要的 DOM 节点。
  3. 最长递增子序列 (Longest Increasing Subsequence, LIS) 算法:

    • 在处理非同序的节点移动时,Vue 2 的双端 Diff 算法在某些复杂场景下(如大量乱序的节点)性能不佳,会进行较多的 DOM 移动操作。
    • Vue 3 引入了 LIS 算法来优化节点的移动。它会计算出新旧 VNode 列表中最长的、不需要移动的子序列。然后,只移动那些不在这个最长递增子序列中的节点。这使得 DOM 移动操作的次数达到理论上的最小值,大大减少了 DOM 操作。
    • 这个优化主要体现在处理带有 key 的列表项的乱序更新上。
  4. 缓存事件处理函数:

    • Vue 3 默认会缓存事件处理函数。在 Vue 2 中,如果事件处理函数是内联的或每次渲染都重新创建,可能会导致不必要的 Diff 和更新。Vue 3 通过编译器优化,避免了这种重复创建和 Diff。

总结 Vue 3 Diff 算法的优势:

  • 更细粒度的更新: 通过 PatchFlags,Vue 3 能够精确知道 VNode 的哪些部分是动态的,只 Diff 和更新必要的部分,跳过静态部分。
  • 更少的 DOM 操作:
    • 静态提升减少了 VNode 的创建和 Diff。
    • LIS 算法优化了节点移动,实现了最小化的 DOM 移动操作。
  • 更高的性能: 综合以上优化,Vue 3 的渲染性能在很多场景下比 Vue 2 有显著提升。
  • 更小的包体积: 尽管功能更强大,但通过 Tree-shaking 等优化,Vue 3 的运行时核心库体积更小。

Vue 3 的 Diff 算法是其性能提升的关键之一,它充分利用了编译时信息来指导运行时优化,使得其在处理复杂应用和大量数据时表现更出色。

5. 讲一下vue生命周期

Vue 实例从创建到销毁的过程,就是它的生命周期。在这个过程中,Vue 会运行一系列的生命周期钩子函数,允许开发者在特定阶段执行自定义逻辑。

Vue 2 和 Vue 3 的生命周期钩子函数在命名和行为上略有不同,但核心概念是相似的。


Vue 2 生命周期 (Options API):

Vue 2 生命周期图
Vue 2 生命周期图
  1. 创建阶段 (Creation):

    • beforeCreate:
      • 实例刚在内存中被创建,但数据观测 (data observation) 和事件机制都尚未初始化。
      • 此时 datamethods 都不可用。
    • created:
      • 实例已经创建完成,数据观测 (data observation)、属性和方法的计算、事件回调都已经初始化。
      • 此时 datamethods 已经可用,可以进行数据请求等操作。
      • $el 属性(DOM)还未生成。
  2. 挂载阶段 (Mounting):

    • beforeMount:
      • 在挂载开始之前被调用,相关的 render 函数首次被调用。
      • 模板编译/解析已完成,但尚未挂载到 DOM 上。
      • 此时 $el 属性还未可用。
    • mounted:
      • 实例已被挂载到 DOM 上,$el 属性现在可用。
      • 组件的模板已经渲染到页面上。
      • 可以进行 DOM 操作、集成第三方库(如 ECharts、Swiper)等。
      • 注意: mounted 不保证所有子组件也都挂载完成,如果需要等待所有子组件挂载,可以使用 $nextTick
  3. 更新阶段 (Updating):

    • beforeUpdate:
      • 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
      • 此时 data 中的数据已是最新,但 DOM 尚未更新。
      • 可以在此阶段访问更新前的 DOM。
    • updated:
      • 由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。
      • 此时 data 和 DOM 都已是最新。
      • 注意: 避免在此阶段进行会再次触发数据更新的操作,可能会导致死循环。
  4. 销毁阶段 (Destruction):

    • beforeDestroy:
      • 实例销毁之前调用。
      • 实例仍然完全可用,可以进行清理操作,如清除定时器、取消订阅、解绑事件监听器等。
    • destroyed:
      • 实例销毁之后调用。
      • 实例的所有指令都被解绑,所有事件监听器都被移除,所有子实例都被销毁。
      • 组件已从 DOM 中完全移除。

Vue 3 生命周期 (Composition API):

Vue 3 的 Composition API 提供了新的生命周期钩子函数,它们以 on 开头,并且需要在 setup() 函数中导入并调用。它们与 Vue 2 的 Options API 钩子有对应关系。

Vue 3 生命周期图
Vue 3 生命周期图
  1. 创建阶段 (Creation):

    • setup(): (特殊的钩子,不是传统意义上的生命周期钩子)
      • 这是 Composition API 的入口点,在 beforeCreatecreated 之间执行。
      • 在组件实例创建之前执行,因此无法访问 this
      • 用于定义响应式数据、方法、计算属性、侦听器,并注册其他生命周期钩子。
    • onBeforeMount: (对应 Vue 2 的 beforeMount)
      • 在组件挂载到 DOM 之前调用。
    • onMounted: (对应 Vue 2 的 mounted)
      • 组件挂载到 DOM 之后调用。
  2. 更新阶段 (Updating):

    • onBeforeUpdate: (对应 Vue 2 的 beforeUpdate)
      • 在组件更新 DOM 之前调用。
    • onUpdated: (对应 Vue 2 的 updated)