likes
comments
collection
share

?北京七年前端大专找工作竟如此坎坷?近N个月面试复盘(附总结答案),快来学习呀!系列更新中(一)

作者站长头像
站长
· 阅读数 10

我的面试复盘系列

面试复盘一(本篇):

面试复盘二:

面试复盘三(上):

正文

7 月中旬,从某非著名程序员在线学习平台领了大礼包后,一直没有马上找工作的想法。玩儿了俩月,考了个摩托 D 本。国庆节后开始刷题投简历,到今天也有一个月了。算上今天这个面试,一共只有两个面试。

我都无语了,boss 打招呼已读不回,我开的会员强提醒他们,已读不回,要不不合适。前几天有带佬说可能是我简历问题,找他改了改,改完昨天又找他过了一遍,一会儿发完文章我赶紧改简历。周日还有场考试,还得再刷刷题。

A 公司是前同事内推的;一面线上,二面线下

B 公司是半个月前投的简历,可能是一直没有招到合适的人?才把我捞起来?B 司 995。线上面。

俩公司都没过,也就不说那么多了,只探讨面试题部分。

最近考取了业余无线电操作证,沉迷业余无线电,有对业余无线电感兴趣的可以加我微信

PS:突然想建讨论群,如果你也最近在面试,或者有些心态崩了,或者有什么想探讨的可以加微信群,入口在文末。

PS3:我微信也在文末,有想加的可以加我。

PS4:如果二维码过期或者人满,就加我微信,拉你,二维码在文末。

PS5:最后可以在文末加我微信拉你进群,也可以顺便关注下我 N 年前注册的公众号

另:求内推机会,给个孩子个机会吧

求内推机会,给个机会吧,我打招呼刷岗位都没空刷八股文了。


A 公司

一面,线上

1. 浏览器拿到数据后的渲染过程

浏览器在接收到网页的 HTML、CSS 和 JavaScript 数据后,经历一系列步骤来渲染网页,这个过程通常被称为浏览器的渲染过程。以下是浏览器渲染过程的主要步骤:

  1. 解析 HTML 和构建 DOM 树
  • 浏览器开始解析 HTML 数据,并构建文档对象模型(DOM)树,表示网页的结构和内容。DOM 树是一个树状结构,其中每个 HTML 元素都被表示为一个节点,包括文本、标签、属性等。
  1. 解析 CSS 和构建 CSSOM 树
  • 同时,浏览器也开始解析 CSS 数据,并构建 CSS 对象模型(CSSOM)树,表示网页的样式和布局信息。CSSOM 树与 DOM 树一一对应,每个元素都有对应的样式信息。
  1. 合并 DOM 和 CSSOM,构建渲染树
  • 浏览器将 DOM 树和 CSSOM 树合并,构建渲染树(Render Tree)。渲染树包含了需要渲染的页面内容,但不包括那些被 CSS 隐藏的元素。
  1. 布局计算
  • 浏览器开始计算每个元素在页面中的精确位置和尺寸,这个过程称为布局计算。浏览器确定元素如何放置和相互布局。
  1. 绘制页面
  • 浏览器使用计算出的位置和尺寸信息来绘制页面。这包括将页面内容绘制到屏幕上的像素点上,以及处理字体渲染、图像显示等。
  1. 处理 JavaScript
  • 如果页面包含 JavaScript,浏览器将执行 JavaScript 代码。JavaScript 可能会修改 DOM 树和样式,从而触发重新布局和绘制。
  1. 反复执行布局和绘制
  • 如果 JavaScript 或用户交互导致页面内容发生变化,浏览器会根据需要执行布局和绘制的步骤。这个过程可能会多次发生。
  1. 渲染完毕
  • 一旦浏览器完成所有的布局和绘制,页面就会呈现给用户,用户可以看到并与页面进行交互。

这个渲染过程是高度优化的,浏览器会尽力减少布局和绘制的次数,以提供更快的性能。同时,浏览器还可以通过缓存和其他技术来加速页面的加载和渲染。不过,开发人员也可以通过优化 HTML、CSS 和 JavaScript 代码来改善页面的加载速度和性能。

2. 重绘和回流,什么情况下会回流,重绘

重绘(Repaint)和回流(Reflow)是与浏览器渲染引擎相关的概念,它们可以影响网页的性能。以下是它们的定义和触发情况:

  1. 重绘 (Repaint)
  • 重绘是指更新页面元素的可见样式,而不影响其布局。这包括改变颜色、背景、字体等属性,使元素的外观发生变化。
    • 重绘不会影响元素的位置或大小,因此开销较小。
    • 触发重绘的情况:改变元素的样式,但不影响布局,如修改背景颜色、文本颜色等。
  1. 回流 (Reflow)
  • 回流是指更新页面元素的布局,通常涉及大小、位置或结构的变化。当页面布局发生变化时,浏览器需要重新计算元素的位置和大小。
    • 回流是开销较大的操作,因为它会触发整个页面的重新布局计算,可能涉及多个元素。
    • 触发回流的情况:改变页面布局的操作,如修改元素的宽度、高度、边距、添加/删除元素等。

常见触发回流和重绘的操作包括:

  • 修改元素的几何属性,如宽度、高度、位置等。
  • 改变字体大小、字体样式等文本属性。
  • 增加、删除或改变 DOM 节点。
  • 改变浏览器窗口大小。
  • 获取某些计算值,如元素的宽度或高度。
  • 使用 JavaScript 计算样式或布局。

为了提高性能,应尽量减少回流和重绘的次数。一种方法是尽量合并多个 DOM 操作,以减少回流的频率。另外,使用 CSS3 动画和过渡效果,以及使用 transform 和 opacity 属性,可以减少回流和重绘的开销,提高页面性能。此外,懒加载和异步加载可以帮助减少不必要的渲染操作。

3. CSS 实现左侧定宽 200px,右侧撑满,五种方式

要实现一个左侧定宽 200px,右侧撑满的布局,可以使用以下五种方式:

  1. Flex 布局
.container {
    display: flex;
}

.left {
    width: 200px;
}

.right {
    flex: 1;
}
  1. Grid 布局
.container {
    display: grid;
    grid-template-columns: 200px 1fr;
}
  1. 浮动 (Float)
.left {
    width: 200px;
    float: left;
}

.right {
    overflow: hidden; /* 清除浮动 */
}
  1. 绝对定位 (Absolute Positioning)
.container {
    position: relative;
}

.left {
    width: 200px;
    position: absolute;
}

.right {
    margin-left: 200px;
}
  1. CSS 表格布局
.container {
    display: table;
}

.left {
    width: 200px;
    display: table-cell;
}

.right {
    display: table-cell;
}

以上方法中,Flex 布局和 Grid 布局是最现代和灵活的方式,特别适用于复杂布局。浮动、绝对定位和 CSS 表格布局也是传统的布局方式,它们在某些情况下仍然有用,但需要更多的样式规则来处理边界情况。选择哪种方式取决于项目的具体需求和所支持的浏览器版本。

4. CSS 实现 0.5px 边框

在标准 CSS 中,通常无法直接设置 0.5px 的边框宽度,因为 CSS 像素是浏览器的最小渲染单位,而不支持亚像素级的渲染。但是,你可以通过一些技巧来模拟实现一个看起来像 0.5px 边框的效果,具体方式如下:

  1. 使用伪元素: 你可以使用伪元素 ::before::after 来创建一个额外的边框层,并设置其高度为 0.5px。例如:
.element {
    position: relative;
    border: 1px solid #000; /* 1px 主要边框 */
}

.element::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 0.5px; /* 模拟 0.5px 边框 */
    background: #000; /* 边框颜色 */
}

这样,你在元素的上边框下面会创建一个看起来像 0.5px 的额外边框。

  1. 使用阴影效果: 你可以使用 box-shadow 属性来模拟一个细边框的效果:
.element {
    border: 1px solid transparent; /* 隐藏主边框 */
    box-shadow: 0 0 0 0.5px #000; /* 模拟 0.5px 边框 */
}

这将在元素周围创建一个 0.5px 的模拟边框效果。

请注意,这些方法都是模拟 0.5px 边框的效果,实际上浏览器仍然以整数像素为单位渲染,但这些技巧可以欺骗视觉,使边框看起来像 0.5px。这在某些情况下很有用,但不适用于所有情况。在移动设备和高分辨率屏幕上,这些技巧可能不会像预期的那样工作。

在标准 CSS 中,模拟 0.5px 边框的方法有限,因为 CSS 的渲染机制通常以整数像素为单位。除了上述伪元素和阴影效果的方法,还可以使用 CSS 的 transform 属性来实现一种类似的效果,但请注意这些方法在不同浏览器和设备上可能会有不同的表现。

  1. 使用 transform 缩放: 你可以使用 transform 缩放来调整元素的高度,以模拟 0.5px 边框。这个方法可能对某些情况有效,但在某些情况下可能会产生不一致的效果。例如:
.element {
    border: 1px solid #000; /* 1px 主要边框 */
    transform: scaleY(0.5); /* 垂直方向缩放 */
}

这将使元素的高度减小一半,从视觉上看起来像是一个 0.5px 的边框。这个方法的效果可能因浏览器和设备而异,因此需要测试和调整以满足特定需求。

请注意,这些方法都是模拟效果,因为 CSS 的渲染机制通常不支持亚像素级别的渲染。这些技巧适用于某些情况,但在不同浏览器和设备上可能会有不同的表现,因此需要谨慎使用并进行测试。如果需要确切的 0.5px 边框,通常需要使用图形处理软件生成具有所需效果的图像或使用其他技术。

5. 判断对象是空对象

var data = {};
var b = JSON.stringify(data) == "{}";
console.log(b); // true

Object.getOwnPropertyNames()

Object 对象的 getOwnPropertyNames 方法,获取到对象中的属性名,存到一个数组中,返回数组对象,我们可以通过判断数组的 length 来判断此对象是否为空。

var data = {};
var arr = Object.getOwnPropertyNames(data);
console.log(arr.length == 0); // true

getOwnPropertySymbols

console.log(Object.getOwnPropertySymbols(a).length === 0) // false

最终解决方案

结合 getOwnPropertySymbols 和 getOwnPropertyNames

const a = { [Symbol()]: 'a' }
const b = { a: 'a' }
const c = {}

console.log(Object.getOwnPropertyNames(a).length === 0 && Object.getOwnPropertySymbols(a).length === 0) // false
console.log(Object.getOwnPropertyNames(b).length === 0 && Object.getOwnPropertySymbols(b).length === 0)  // false
console.log(Object.getOwnPropertyNames(c).length === 0 && Object.getOwnPropertySymbols(c).length === 0)  // true

Reflect.ownKeys()

const a = { [Symbol()]: 'a' }
const b = { a: 'a' }
const c = {}
console.log(Reflect.ownKeys(a).length === 0) // false
console.log(Reflect.ownKeys(b).length === 0) // false
console.log(Reflect.ownKeys(c).length === 0) // true

6. 如何判断页面滚动到底部

判断页面是否滚动到底部通常是为了实现无限滚动加载(Infinite Scroll)等功能。你可以使用 JavaScript 来检测页面是否滚动到底部,以下是一种常见的方法:

window.onscroll = function() {
    // 可见窗口的高度
    var windowHeight = window.innerHeight;

    // 整个页面的高度
    var documentHeight = document.documentElement.scrollHeight || document.body.scrollHeight;

    // 已经滚动的高度
    var scrollTop = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;

    // 判断是否滚动到底部
    if (windowHeight + scrollTop >= documentHeight) {
        // 滚动到底部的处理逻辑
        console.log("已滚动到底部");
    }
};

这段代码使用 onscroll 事件监听页面的滚动。在事件处理函数中,首先获取可见窗口的高度 windowHeight、整个页面的高度 documentHeight 以及已经滚动的高度 scrollTop。然后,通过比较它们的和是否等于 documentHeight,判断是否滚动到底部。

当页面滚动到底部时,你可以在处理逻辑中执行加载更多内容的操作。这是实现无限滚动加载的基本思路。

注意:这只是一种基本方法,具体情况可能因项目需求和页面结构而异。在实际应用中,你可能需要根据你的页面结构和需求进行更复杂的滚动检测和加载逻辑。

7. DsBridge 如何理解,以及原理

DSBridge 是一种用于实现 JavaScript 与原生移动应用之间通信的桥接库。它的原理是在原生应用和 WebView(Web视图)之间建立一个通信通道,使得 JavaScript 和原生代码能够互相调用和传递数据。

DSBridge 的原理基本如下:

  1. 在原生应用中
  • 在原生应用中,你需要引入 DSBridge 的原生库(通常是一个 SDK 或库文件)。
  • 该库提供了一组 API,允许你注册原生方法,以便 JavaScript 可以调用这些方法。这些注册的方法通常是被 JavaScript 调用的接口,以便执行原生功能。
  • 此外,原生库还提供了一种方式来执行 JavaScript 代码,以便原生代码可以调用 JavaScript 函数。
  1. 在 WebView 中
  • WebView 是一个用于显示网页内容的组件,通常嵌入在原生应用中。
  • 在 WebView 中,你需要引入 DSBridge 的 JavaScript 库。这个库提供了用于与原生通信的接口。
  • JavaScript 通过 DSBridge 提供的接口注册函数,以便供原生应用调用。
  • JavaScript 可以使用 DSBridge 发送请求,请求原生应用执行注册的原生方法,并将数据传递给原生应用。
  1. 通信
  • 当 JavaScript 发送请求到原生应用时,请求中包含方法名称、参数等信息。
  • 原生应用接收到请求后,根据请求的方法名称调用相应的原生方法,并传递参数。
  • 原生方法执行完成后,可以返回结果给 JavaScript。
  • 同样,原生应用也可以通过执行 JavaScript 代码来与 WebView 中的 JavaScript 通信。

这种双向通信机制使得 JavaScript 和原生代码可以相互调用,从而实现了 WebView 中的 JavaScript 与原生应用之间的紧密集成。DSBridge 的原理类似于其他类似桥接库,如WebView JavaScript Bridge(WebViewJavascriptBridge)、React Native Bridge 等,它们都提供了一种在 Web 应用和原生应用之间进行通信的方式,从而实现了混合应用的开发。

8. 最快方式寻找一个有序数组的给定值

在有序数组中寻找给定值,最快的方式通常是使用二分查找(Binary Search),也称为折半查找。二分查找是一种高效的搜索算法,它的时间复杂度为 O(log n),其中 n 是数组的大小。以下是使用二分查找来寻找一个有序数组中给定值的示例 JavaScript 代码:

function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;

    while (left <= right) {
        const mid = Math.floor((left + right) / 2);

        if (arr[mid] === target) {
            return mid; // 找到目标值,返回索引
        } else if (arr[mid] < target) {
            left = mid + 1; // 目标值在右半部分
        } else {
            right = mid - 1; // 目标值在左半部分
        }
    }

    return -1; // 没有找到目标值
}

const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15, 17];
const targetValue = 7;
const index = binarySearch(sortedArray, targetValue);

if (index !== -1) {
    console.log(`找到目标值 ${targetValue},位于索引 ${index}`);
} else {
    console.log(`未找到目标值 ${targetValue}`);
}

二分查找通过不断将搜索范围减半,迅速找到目标值或确认其不存在。这使得它成为在有序数组中寻找值的最快方式之一。请注意,前提是数组必须有序,否则二分查找将不适用。

9. 洗牌算法

洗牌算法(Shuffle Algorithm)用于随机排列或打乱数组或列表中的元素顺序,以产生随机性。下面是一种常见的洗牌算法,称为 Fisher-Yates 洗牌算法(也称为 Knuth 洗牌算法),它可以在 O(n) 的时间内完成洗牌操作,其中 n 是数组的大小。

function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
        // 生成 0 到 i 之间的随机索引
        const j = Math.floor(Math.random() * (i + 1));

        // 交换 array[i] 和 array[j]
        [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
}

// 使用示例
const originalArray = [1, 2, 3, 4, 5];
const shuffledArray = shuffleArray(originalArray);

console.log(shuffledArray);

这段代码中,shuffleArray 函数遍历数组,从最后一个元素开始,依次随机选择一个索引 j(在 0 到 i 之间),然后交换 array[i]array[j] 的位置。通过重复这个过程,每个元素都有平等的机会被放置在数组的不同位置,从而实现了随机洗牌。

Fisher-Yates 洗牌算法确保了每个排列都有相等的概率,是一种高效且常用的洗牌算法。这对于游戏、随机播放音乐列表以及生成随机样本等应用非常有用。

二面,线下

人事问完问题就开始上机操作

实现一个自适应列表

  • 通过改变内容元素块的宽度,实现自适应
  • 元素问隙固定20px
  • 元素最大宽度250px,最小宽度xxx px
  • 容器与两边元素无间隙
  • 最后一行左对齐,元素宽度和上面元素都一样
  • 屏幕宽度没有限制

grid 布局实现,还有 js 也可以实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      ul {
        display: grid;
        grid-template-columns: repeat( auto-fill, minmax(100px, 1fr) );
        grid-gap: 20px;
      }

      li {
        list-style: none;
        width: 100%;
        height: 100px;
        border: 1px solid #000;
      }
    </style>
  </head>
  <body>
    <ul>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  </body>
</html>

三面,线下

CTO 谈天说地 等等,几乎没聊项目

结果 没成,想招前端架构

B 公司

一面,线上

1. 什么是 MVVM?比之 MVC 有什么区别?

MVVM(Model-View-ViewModel)和 MVC(Model-View-Controller)都是软件设计模式,用于组织和架构应用程序的代码。它们之间的主要区别在于如何组织和管理用户界面的代码。

a. MVC(Model-View-Controller):

  1. Model(模型): 负责应用程序的数据和业务逻辑。它表示应用程序的状态和行为,与数据库通信以检索或更新数据。
  2. View(视图): 负责显示数据和用户界面元素。它接收来自控制器的数据,并将其渲染成用户界面。
  3. Controller(控制器): 负责处理用户输入、更新模型和调整视图。它充当用户界面和应用程序逻辑之间的协调者。

在传统的 MVC 模式中,View 和 Controller 之间的关系是相对紧密的,Controller 负责管理用户输入并更新 Model,同时直接影响 View。

b. MVVM(Model-View-ViewModel):

  1. Model(模型): 负责应用程序的数据和业务逻辑,与 MVC 中的 Model 类似。
  2. View(视图): 负责显示数据和用户界面元素,与 MVC 中的 View 类似。
  3. ViewModel(视图模型): 介于 View 和 Model 之间,负责处理用户输入、与 Model 交互,并为 View 提供渲染所需的数据。ViewModel 是一个用于封装视图状态和行为的抽象层。

在 MVVM 中,View 和 ViewModel 是通过数据绑定关联的,ViewModel 通常会包含一个或多个属性,这些属性与 View 中的元素绑定。当 ViewModel 的属性变化时,View 会自动更新,而用户的操作也会直接影响到 ViewModel 中的数据。

区别:

  1. 数据绑定: MVVM 强调数据绑定,通过双向绑定实现视图和数据的同步。MVC 中,视图和控制器之间的关系通常是单向的。
  2. 视图模型: MVVM 引入了 ViewModel,它是一个专门用于管理视图状态和行为的组件。MVC 中,控制器负责处理用户输入和更新模型,直接影响视图。
  3. 松散耦合: MVVM 中的各个组件之间相对较为松散耦合,每个组件都有明确定义的职责。MVC 中,视图和控制器之间的联系相对较紧密。

总体而言,MVVM 强调数据驱动视图,通过数据绑定实现自动更新,而 MVC 强调用户输入驱动视图,通过控制器更新视图。 MVVM 的数据绑定使得前端开发更加简洁和高效,特别适用于大规模的前端应用。

2. Vue 和 React 之间的区别

Vue.js 和 React 都是流行的前端 JavaScript 框架,用于构建用户界面。以下是它们之间的一些主要区别:

a. 设计理念和开发范式:

  1. 组件化:
  • Vue: Vue.js 将组件化作为其核心思想,提供了更直观和灵活的组件系统。单文件组件(SFC)是一种将模板、样式和脚本封装在一个文件中的方式。
  • React: React 也支持组件化,但它更加强调的是一切皆是组件的思想。React 组件使用 JSX 语法,将模板和逻辑紧密结合在一起。

b. 数据绑定:

  1. 双向数据绑定:
  • Vue: Vue 提供了简单的双向数据绑定,通过 v-model 指令可以轻松实现表单元素和数据的双向绑定。
  • React: React 采用的是单向数据流,通过状态(state)和属性(props)来管理数据流向。

c. 模板语法:

  1. 模板语法:
  • Vue: Vue 使用模板语法,模板中可以包含直接渲染的 HTML 代码。Vue 的模板更接近传统的 HTML 结构。
  • React: React 使用 JSX 语法,它是一种在 JavaScript 中嵌套 XML 结构的语法。这使得模板和逻辑更紧密地结合在一起,但也需要习惯这种嵌套结构。

d. 学习曲线:

  1. 学习曲线:
  • Vue: Vue 被认为是相对容易学习的框架,尤其适合初学者。其文档清晰,API 直观,上手较快。
  • React: React 在一开始可能对新手有一定的学习曲线,尤其是对于 JSX 语法和一些概念的理解。但一旦掌握,React 提供了强大的灵活性。

e. 生态系统和社区:

  1. 生态系统:
  • Vue: Vue 的生态系统相对较小,但也在不断壮大。它有一些优秀的插件和工具,但相比 React,生态规模较小。
  • React: React 拥有更大的生态系统和庞大的社区支持。有丰富的第三方库和组件,适用于各种应用场景。

f. 状态管理:

  1. 状态管理:
  • Vue: Vue 提供了 Vuex 作为官方的状态管理库,用于管理组件之间的共享状态。
  • React: React 可以使用 Context API 和第三方库(如 Redux)来进行状态管理。

g. 组件通信:

  1. 组件通信:
  • Vue: Vue 提供了 props 和自定义事件等机制来实现组件之间的通信。
  • React: React 通过 props 和回调函数来传递数据,同时也可以使用 Context API 或 Redux 等进行全局状态管理。

h. 社区和企业应用:

  1. 应用场景:
  • Vue: Vue 在中小型项目和单页面应用中表现良好,也适用于快速原型开发。
  • React: React 更常用于大型和复杂的项目,且在大型企业应用中更为普遍。

总体而言,Vue 和 React 都是强大的前端框架,选择其中之一通常取决于项目的需求、团队的经验和开发者的偏好。

3. Vue2,3 的区别

Vue 2 和 Vue 3 之间有一些显著的区别。以下是它们的一些主要区别:

  1. 性能优化: Vue 3 在性能方面有显著的改进。它引入了响应式系统的重写,使用 Proxy 替代 Object.defineProperty,这使得 Vue 3 的性能比 Vue 2 更好。
  2. Composition API: Vue 3 引入了 Composition API,这是一个新的组织组件逻辑的方式。相比于 Vue 2 的选项 API,Composition API 更灵活,更容易组织和重用代码。
  3. Teleport: Vue 3 引入了 Teleport 特性,允许你在 DOM 中的任何位置渲染组件的内容。这对于在应用程序中移动组件的位置或在不同的层次结构中渲染组件很有用。
  4. 新的特性和改进: Vue 3 引入了一些新的特性,如 Fragments(片段)、Custom Directives(自定义指令)等,并对一些现有特性进行了改进。这使得开发者在构建复杂应用时更加方便。
  5. Tree-shaking 支持: Vue 3 更好地支持 tree-shaking,这意味着在构建时可以更有效地剔除未使用的代码,从而减小应用程序的体积。
  6. TypeScript 集成: Vue 3 对 TypeScript 的支持更加紧密和友好,包括完全的 TypeScript 类型定义。

这些只是一些主要的区别,具体取决于你的项目需求和个人偏好。如果你已经使用 Vue 2,迁移到 Vue 3 可能需要一些时间,但从性能和开发体验的角度来看,它可能是值得的。

4. Vue3 性能更好在哪里

Vue 3 相对于 Vue 2 在性能方面有一些改进,主要体现在以下几个方面:

  1. Proxy 替代 Object.defineProperty: Vue 3 中的响应式系统采用 Proxy 替代了 Vue 2 中使用的 Object.defineProperty。Proxy 允许更细粒度的拦截操作,这使得 Vue 3 能够更高效地追踪属性的变化,提高了响应式系统的性能。
  2. 编译器优化: Vue 3 的编译器经过优化,生成的代码更加精简和高效。这有助于减小应用程序的体积并提高运行时的性能。
  3. 静态树提升(Static Tree Hoisting): Vue 3 引入了静态树提升的概念,可以将一些静态节点在渲染时提升为常量,减少不必要的重复渲染,从而提高性能。
  4. 优化的事件处理: Vue 3 对事件处理进行了优化,使其更加高效。特别是对于频繁触发的事件,Vue 3 的性能相对更好。
  5. Teleport 特性的引入: Vue 3 的 Teleport 特性允许在 DOM 中的任何位置渲染组件的内容,这在性能优化方面提供了更多的灵活性,能够更高效地处理组件的渲染和移动。
  6. Tree-shaking 支持: Vue 3 更好地支持 tree-shaking,使得在构建时可以更有效地剔除未使用的代码,减小应用程序的体积,从而提高加载和运行时的性能。

总体而言,这些改进和优化使得 Vue 3 在性能方面相对于 Vue 2 有所提升,特别是在处理大型和复杂的应用程序时。

5. Vue2,3 diff 算法原理

Vue 2 和 Vue 3 在数据响应方面采用了不同的 diff 算法,用于比较虚拟 DOM 树并更新视图。以下是 Vue 2 和 Vue 3 的 diff 算法原理的概述:

a. Vue 2 的 Diff 算法:

Vue 2 使用的是经典的Virtual DOM和基于深度优先的双端比较的算法。它的主要步骤如下:

  1. 创建虚拟 DOM 树: 将组件的状态映射到虚拟 DOM 树,这是一个 JavaScript 对象表示的抽象层次结构。
  2. 渲染为真实 DOM: 将虚拟 DOM 树渲染为真实 DOM。
  3. 数据变更时的 Diff: 当组件状态变化时,生成新的虚拟 DOM 树,然后和之前的虚拟 DOM 树进行比较。
  4. 差异计算: 通过深度优先的算法,逐层比较新旧虚拟 DOM 树,找到两者之间的差异。
  5. 更新: 根据差异,只对需要更新的部分进行实际 DOM 操作,最小化了对真实 DOM 的修改,提高了性能。

Vue 2 使用的是经典的 Virtual DOM 和基于深度优先的双端比较的 diff 算法。下面是 Vue 2 中 diff 算法的一般步骤:

  1. 创建虚拟 DOM 树(Virtual DOM Tree): 当组件的状态发生变化时,Vue 2 会首先根据新的状态生成一个新的虚拟 DOM 树。
  2. 比较新旧虚拟 DOM 树: Vue 2 通过逐层比较新旧虚拟 DOM 树的节点,找出两者之间的差异。这一步是通过深度优先的算法实现的。
  3. 生成差异(Diff): 在比较过程中,当发现节点有差异时,Vue 2 会生成一个差异对象,该对象描述了如何更新真实 DOM 以保持与虚拟 DOM 的一致性。
  4. 应用差异(Patch): Vue 2 将生成的差异对象应用到真实 DOM 上,从而更新视图。这一步是通过对真实 DOM 进行相应的操作,比如修改节点的属性、插入新节点、删除不需要的节点等来实现的。
  5. 更新组件状态: 最后,Vue 2 会更新组件的状态,确保虚拟 DOM 和组件的状态保持同步。

在这个过程中,Vue 2 采用了一些优化措施,例如通过限制比较的深度,只对同一层级的节点进行比较,以减小比较的范围。同时,对于列表渲染,Vue 2 采用了一种叫做“key”的机制,通过给列表中的元素添加唯一的标识符,可以帮助 Vue 更准确地追踪列表中元素的变化。

总体而言,Vue 2 的 diff 算法是一个高效的、基于 Virtual DOM 的算法,可以有效地减小对真实 DOM 的操作,提高视图更新的性能。然而,它并不是完美的,特别是在处理大型列表时,一些额外的优化措施可能是必要的。

b. Vue 3 的 Diff 算法:

Vue 3 引入了基于 Proxy 的响应式系统,同时也对 diff 算法进行了优化。其主要变化有:

  1. Proxy 响应式系统: Vue 3 使用 Proxy 替代 Object.defineProperty 来实现响应式系统。Proxy 提供了更细粒度的拦截能力,使得 Vue 3 能够更精确地追踪属性的变化。
  2. 静态树提升: Vue 3 引入了静态树提升的概念,通过提升静态节点,可以减少比较的复杂性,提高 diff 算法的效率。
  3. Patch Flag: Vue 3 使用 Patch Flag 来标记需要更新的节点类型,从而减小比较的范围,提高了性能。
  4. 缓存节点: Vue 3 在 diff 过程中对一些中间结果进行缓存,避免不必要的重复计算,提高了性能。

总体而言,Vue 3 的 diff 算法在基于 Proxy 的响应式系统和一些优化手段的支持下,相对于 Vue 2 在性能方面有所提升。这些优化使得 Vue 3 更高效地处理组件状态变更并更新视图。

Vue 3 的 diff 算法相对于 Vue 2 有一些改进,采用了一种称为“优化的渲染器”(Optimized Renderer)的策略。以下是 Vue 3 中 diff 算法的一般步骤:

  1. 生成虚拟 DOM(Virtual DOM): 当组件的状态发生变化时,Vue 3 会生成一个新的虚拟 DOM 树,该树表示组件的当前状态。
  2. 计算新旧虚拟 DOM 之间的差异: Vue 3 使用 Patch Flag 机制,通过在虚拟 DOM 节点上标记一些信息,例如节点的静态性、是否有子节点等,来帮助 diff 算法更快速地找到差异。这使得 Vue 3 在比较新旧虚拟 DOM 时能够更高效地确定需要更新的节点。
  3. 生成差异(Diff): 在比较过程中,Vue 3 生成一个差异对象,描述了需要对真实 DOM 进行的操作,如添加、移动、删除节点等。Vue 3 放弃了双端比较,采用的是更为高效的单端比较。
  4. 应用差异(Patch): Vue 3 将生成的差异对象应用到真实 DOM 上,通过对真实 DOM 进行相应的操作,更新视图。与 Vue 2 不同,Vue 3 的渲染器对 DOM 操作进行了优化,采用了更高效的算法,如可选的静态树提升等。
  5. 更新组件状态: 最后,Vue 3 更新组件的状态,确保虚拟 DOM 和组件的状态保持同步。

Vue 3 引入了一些新的概念和优化,如 Patch Flag、静态树提升等,以提高 diff 算法的性能。总体而言,Vue 3 的 diff 算法在性能上有一些提升,尤其是在处理大型组件和复杂视图时。

6. Vue 生命周期

Vue.js 的生命周期钩子函数是在组件的不同阶段执行的一组函数,允许你在组件的生命周期中执行特定的操作。以下是 Vue 2.x 中常用的生命周期钩子函数:

  1. beforeCreate(创建前): 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。在这个阶段,组件的数据、事件等都还没有初始化。
  2. created(创建后): 实例已经创建完成之后被调用。在这个阶段,可以访问数据、计算属性等,但 DOM 元素还未生成。
  3. beforeMount(挂载前): 在挂载开始之前被调用:相关的 render 函数首次被调用。
  4. mounted(挂载后): el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。在这一步,组件已经渲染完成,DOM 元素也已经生成。
  5. beforeUpdate(更新前): 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步修改数据,但避免直接操作 DOM。
  6. updated(更新后): 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。在这一阶段,组件的 DOM 已经更新。
  7. beforeDestroy(销毁前): 实例销毁之前调用。在这一步,实例仍然完全可用。
  8. destroyed(销毁后): Vue 实例销毁后调用。在这一步,组件的所有指令都已经解绑,所有事件监听器已经移除,所有子实例也已经销毁。

以上是 Vue 2.x 的生命周期钩子函数。在 Vue 3 中,由于引入了 Composition API,生命周期钩子的使用可能会有所不同。确保查阅 Vue 3 文档以获取最新的信息。

Vue 3 中的生命周期钩子函数与 Vue 2.x 有些许不同。Vue 3 引入了 Composition API,使组件的逻辑更加灵活。以下是 Vue 3 中常用的生命周期钩子函数:

  1. setup 在组件实例初始化之后,返回 render 函数之前执行。这是 Composition API 的一部分,用于设置组件的状态、引入响应式数据等。
  2. beforeCreate (创建前): 与 Vue 2.x 相同,在实例初始化之后,数据观测 (data observer) 和事件/watcher 事件配置之前调用。
  3. created (创建后): 与 Vue 2.x 相同,在实例已经创建完成之后调用。
  4. beforeMount (挂载前): 与 Vue 2.x 相同,在挂载开始之前调用。
  5. onBeforeMount Vue 3 新增的生命周期钩子,在 beforeMount 之前调用。
  6. mounted (挂载后): 与 Vue 2.x 相同,在挂载完成之后调用。
  7. onMounted Vue 3 新增的生命周期钩子,在 mounted 之后调用。
  8. beforeUpdate (更新前): 与 Vue 2.x 相同,在数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  9. onBeforeUpdate Vue 3 新增的生命周期钩子,在 beforeUpdate 之前调用。
  10. updated (更新后): 与 Vue 2.x 相同,在数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。
  11. onUpdated Vue 3 新增的生命周期钩子,在 updated 之后调用。
  12. beforeUnmount (卸载前): 在实例卸载之前调用。在这一步,实例仍然可用。
  13. onBeforeUnmount Vue 3 新增的生命周期钩子,在 beforeUnmount 之前调用。
  14. unmounted (卸载后): 在实例销毁后调用。在这一步,组件的所有指令都已经解绑,所有事件监听器已经移除,所有子实例也已经销毁。

这些生命周期钩子函数的命名和调用时机与 Vue 2.x 大致相似,但在使用 Composition API 的情况下,可以更灵活地使用 setup 函数来组织组件的逻辑。

7. Vue v-for 的 key 值是什么,没有唯一性的 key 值怎么办

在 Vue 中,key 是用于识别 v-for 循环中的每个节点的特殊属性。它的作用是帮助 Vue 识别每个节点的身份,从而在列表发生变化时更高效地更新 DOM。key 应该具有唯一性,即在同一列表中,key 的值不能重复。

<div v-for="(item, index) in items" :key="item.id">
  {{ item.name }}
</div>

在上面的例子中,item.id 被用作 key,假设 item.id 具有唯一性,这有助于 Vue 在更新列表时更准确地识别每个节点。

如果数据中没有唯一标识符可供用作 key,可以使用索引作为 key。但要注意,最好在有唯一标识符的情况下使用唯一标识符,因为使用索引作为 key 有时可能引发一些问题,特别是在列表项的顺序发生变化时。

<div v-for="(item, index) in items" :key="index">
  {{ item.name }}
</div>

如果列表项没有唯一标识符且不适合使用索引,可以考虑使用 v-for 的特殊语法,直接迭代对象的值而不是索引。这样,Vue 将使用对象的属性名作为 key

<div v-for="item in items" :key="item.id">
  {{ item.name }}
</div>

在任何情况下,尽量确保 key 具有唯一性,以确保 Vue 在处理列表时能够正确追踪和更新节点。

8. css 0.5px

上边实现过,就不讲了

9. css 实现三角形

在 CSS 中,可以使用伪元素(::before::after)以及适当的 border 属性来创建三角形。以下是一个简单的例子:

.triangle {
  width: 0;
  height: 0;
  border-left: 50px solid transparent; /* 左边的边 */
  border-right: 50px solid transparent; /* 右边的边 */
  border-bottom: 100px solid #3498db; /* 底边,可以设置成你想要的颜色 */
}

在这个例子中,通过设置 border-leftborder-rightborder-bottom 的宽度,我们可以创建一个等腰三角形。你可以调整 border 的宽度和颜色以满足你的需求。

然后,你可以在 HTML 中使用这个类:

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

这是一个基本的例子,可以根据实际需求进行调整。这种方法是创建三角形的一种简单而常见的方式。

10. transform 和 animation 的区别

transformanimation 是 CSS 中用于实现动画效果的两种不同属性,它们有不同的用途和特点。

a. transform:

transform 是一个 CSS 属性,用于对元素进行平移、旋转、缩放和倾斜等变换。常用的值包括 translaterotatescaleskew。这些变换可以通过 transform 来实现,而不需要改变元素的文档流。

.element {
  transform: translate(50px, 50px); /* 平移 */
  transform: rotate(45deg); /* 旋转 */
  transform: scale(1.5); /* 缩放 */
  transform: skew(30deg, 20deg); /* 倾斜 */
}

transform 主要用于实现静态或基本的元素变换,不涉及到时间的概念。它通常在响应用户交互或在元素初始状态和最终状态之间切换时使用。

b. animation:

animation 是用于创建更复杂的动画效果的 CSS 属性。通过定义关键帧(keyframes)和指定动画的各个阶段,可以在元素上应用 animation 属性来实现复杂的动画。

@keyframes move {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(100px);
  }
}

.element {
  animation: move 2s ease-in-out infinite; /* 应用名为 "move" 的动画 */
}

上述代码中,@keyframes 定义了一个名为 "move" 的关键帧,元素 .element 通过 animation 属性引用了这个动画。animation 具有多个属性,如动画名称、持续时间、缓动函数等。

总的来说,transform 主要用于简单的静态变换,而 animation 更适合复杂的、涉及到时间的动画效果。在实际开发中,这两者可以结合使用,例如在 @keyframes 中使用 transform 实现关键帧之间的变换。

11. svg 懂吗,可以讲一下

SVG 是可缩放矢量图形(Scalable Vector Graphics)的缩写,它是一种用于描述二维矢量图形的 XML 标记语言。SVG 图形可以缩放到任意大小而不失真,并且可以在 Web 页面上通过 HTML、CSS 和 JavaScript 进行交互。SVG 是一种基于 XML 的开放标准,由 W3C(World Wide Web Consortium)制定。

以下是 SVG 的一些特点和用途:

  1. 矢量图形: SVG 是矢量图形格式,图形是通过数学公式描述的,因此可以无损缩放,不会失真。这与像素图形(如 JPEG、PNG)不同,后者在缩放时可能会失去清晰度。
  2. 可编辑性: SVG 文件可以通过文本编辑器直接编辑,也可以通过图形编辑软件(如 Adobe Illustrator、Inkscape)进行编辑。这使得 SVG 图形易于修改和定制。
  3. 支持图形元素: SVG 支持各种基本图形元素,如线段、矩形、圆、椭圆、路径等,以及一些高级图形元素,如滤镜、渐变、符号等。
  4. 嵌入性: SVG 可以直接嵌入到 HTML 中,也可以通过 CSS 样式和 JavaScript 进行控制。这使得 SVG 成为 Web 页面中图形和图表的常见选择。
  5. 交互性: 通过 JavaScript,可以对 SVG 图形进行事件处理和交互操作,使其更加动态和生动。
  6. 动画: SVG 支持通过 CSS 或 JavaScript 实现简单到复杂的动画效果,使得图形在页面上可以有更多的表现力。
  7. 性能: 由于是矢量图形,SVG 文件通常相对较小,加载速度较快,适用于在 Web 页面上展示复杂图形或图表。

SVG 在 Web 开发中广泛应用于图标、图表、地图、动画等领域,它提供了一种灵活而强大的方式来创建丰富的图形和用户界面。

其他想不起来了

二面,线上

面试官前端专家,擅长八股文攻势,令人防不胜防

1. print(fb) 执行后打印什么,我最先说 100,然后做完第二题又改口 200,哈哈哈哈哈哈

function print(fb) {
	const b = 200;
	fb();
}
const b = 100;
function fb() {
	console.log(b); // 100
}
print(fb);

在这个例子中,print 函数接受一个函数 fb 作为参数,并在内部定义了一个局部变量 b,然后调用传入的函数 fb。同时,在全局作用域中定义了一个变量 b,其值为 100。在 fb 函数内部使用 console.log(b) 打印变量 b 的值。

让我们逐步分析代码执行过程:

  1. 全局作用域:
  • 定义了全局变量 b,其值为 100
  • 定义了全局函数 fb,该函数内部打印全局变量 b
  1. 执行 print(fb)
  • print 函数内部,定义了局部变量 b,其值为 200
  • 调用传入的函数 fb()
  1. 执行 fb()
  • fb 函数内部,使用 console.log(b) 打印变量 b
  • 由于 fb 函数内部没有定义局部变量 b,因此会从外部作用域查找。在这里,fb 函数内部的 b 引用的是全局作用域中的变量 b,其值为 100
  1. 打印结果:
  • 执行 console.log(b) 打印的结果为 100

因此,最终的打印结果是 100。在 fb 函数内部,它引用的是全局作用域中的变量 b,而不是 print 函数内部定义的局部变量 b

2.fn() 执行后打印什么

function create() {
	let a = 100;
 	return function () {
		console.log(a); // 100
  }
}
const fn = create()
const a = 200;
fn();

在这个例子中,create 函数返回了一个内部函数,内部函数引用了外部函数 create 中的变量 a。在全局作用域中,又定义了一个变量 a,其值为 200

让我们逐步分析代码执行过程:

  1. 调用 create()
  • create 函数内部,定义了局部变量 a,其值为 100
  • 返回了一个内部函数。
  1. 执行 const fn = create()
  • create 返回的内部函数赋值给变量 fn
  1. 定义全局变量 a,其值为 200

  2. 调用 fn()

  • 执行 fn 函数,该函数内部使用 console.log(a) 打印变量 a
  • 在内部函数中,由于没有局部定义变量 a,会从外部函数 create 中的作用域查找。在这个作用域中,a 的值是外部函数调用时定义的 100
  1. 打印结果:
  • 执行 console.log(a) 打印的结果为 100

因此,最终的打印结果是 100。内部函数 fn 保留了其创建时的词法作用域,可以访问外部函数 create 中的变量 a,而不受全局作用域中的同名变量 a 影响。

3. 打印什么

console.log("script start");

const promiseA = new Promise((resolve, reject) => {
	console.log("init promiseA")
  resolve("promiseA");
});

const promiseB = new Promise((resolve, reject) => {
	console.log("init promiseB");
	resolve("promiseB");
});

setTimeout(()=> {
	console. log("set Timeout run"),
	promiseB.then(res => {
		console.log("promiseB res => ", res);
  })
	console.log("setTimeout end");
}, 0);

promiseA.then(res => {
	console.log("promiseA res =>> ",res);
});

console.log("script end");

// VM15077:1 script start
// VM15077:4 init promiseA
// VM15077:9 init promiseB
// VM15077:25 script end
// VM15077:22 promiseA res =>>  promiseA
// VM15077:14 set Timeout run
// VM15077:18 setTimeout end
// VM15077:16 promiseB res =>  promiseB

这段代码涉及到了 Promise 和 setTimeout,让我们逐步分析代码执行过程:

  1. 同步部分:
  • 执行 console.log("script start")

  • 创建 promiseApromiseB,并分别输出 "init promiseA""init promiseB"

  • 调用 setTimeout,设置一个异步任务,但此时不会执行其回调函数。

    输出:

    script start
    init promiseA
    init promiseB
    script end
    
  1. Microtask 阶段(Promise 执行):
  • promiseA 是已经被 resolve 的 Promise,因此会执行其 then 方法的回调函数,输出 "promiseA res =>> promiseA"

    输出:

    promiseA res =>> promiseA
    
  1. MacroTask 阶段(setTimeout 回调执行):
  • 执行 setTimeout 的回调函数,输出 "set Timeout run"

  • 输出 "setTimeout end"

  • 执行 promiseB.then,由于 promiseB 是已经被 resolve 的 Promise,输出 "promiseB res =>> promiseB"

    输出:

    set Timeout run
    setTimeout end
    promiseB res =>> promiseB
    
    

最终输出:

script start
init promiseA
init promiseB
script end
promiseA res =>> promiseA
set Timeout run
setTimeout end
promiseB res =>> promiseB

总结:

  • Promise 的回调函数是在 Microtask 阶段执行的,而 setTimeout 的回调函数是在 MacroTask 阶段执行的。
  • 在同步执行完成后,Microtasks 会在同一事件循环内优先执行完,然后才执行下一个 MacroTask(这里是 setTimeout 的回调函数)。

4. 问:打印什么?之前看过,忘完了

这个在阮一峰 ES6 里有介绍的

function* test(x) {
	const y = 2 * (yield (x + 1))
	const z = yield (y / 3)
	console.log('x', x, 'y', y, '2', z)
	return x + y + z
}
const b = test(5)
console.log(b.next())
console.log(b.next(12))
console.log(b.next(13))

// VM16133:8 {value: 6, done: false}
// VM16133:9 {value: 8, done: false}
// VM16133:4 x 5 y 24 2 13
// VM16133:10 {value: 42, done: true}

这是一个使用 Generator 函数的示例,该函数包含了 yield 表达式,允许你在迭代的过程中暂停执行,同时传递和接收值。让我们逐步解释这个过程:

function* test(x) {
  // 第一次调用 next(),执行到第一个 yield 表达式
  const y = 2 * (yield (x + 1));
  // 第二次调用 next(12),将 12 传递给上一个 yield 表达式,计算 y,并执行到第二个 yield 表达式
  const z = yield (y / 3);
  // 第三次调用 next(13),将 13 传递给上一个 yield 表达式,计算 z,并执行到函数结束
  console.log('x', x, 'y', y, 'z', z);
  // 返回 x + y + z
  return x + y + z;
}

// 创建 Generator 对象,但不执行函数体
const b = test(5);

// 第一次调用 next(),开始执行 Generator 函数体,执行到第一个 yield 表达式
console.log(b.next());
// 输出: { value: 6, done: false },其中 value 是第一个 yield 表达式的结果(5 + 1)

// 第二次调用 next(12),将 12 传递给上一个 yield 表达式,继续执行函数体,执行到第二个 yield 表达式
console.log(b.next(12));
// 输出: { value: 8, done: false },其中 value 是第二个 yield 表达式的结果(2 * 6 / 3)

// 第三次调用 next(13),将 13 传递给上一个 yield 表达式,继续执行函数体,执行到函数结束
console.log(b.next(13));
// 输出: x 5 y 24 z 13,其中 x 是初始参数 5,y 是上一个 yield 表达式的结果(2 * 12),z 是上一个 yield 表达式的传入值(13)
// 最终返回 x + y + z,即 5 + 24 + 13 = 42
// 输出: { value: 42, done: true },其中 value 是函数的返回值,done 表示 Generator 函数是否执行结束

在这个过程中,yield 表达式的值是通过调用 next() 方法时传递的参数确定的,它会暂停函数执行并返回一个对象,包含 valuedone 属性。当再次调用 next() 方法时,会继续执行函数体,直到下一个 yield 表达式或函数结束。这种通过迭代器协议实现的逐步执行的方式使得 Generator 函数可以用来创建可暂停的、交互式的代码。

回答二

Generator 函数是 ES6 引入的一种新型函数,它通过 function* 关键字定义。Generator 函数可以被看作是可以暂停和继续执行的函数。在执行 Generator 函数时,通过调用其 next 方法,可以逐步执行 Generator 函数体内的代码。

让我们逐步分析你提供的代码:

function* test(x) {
	const y = 2 * (yield (x + 1))
	const z = yield (y / 3)
	console.log('x', x, 'y', y, 'z', z)
	return x + y + z
}

const b = test(5)
console.log(b.next())   // { value: 6, done: false }
console.log(b.next(12)) // { value: 8, done: false }
console.log(b.next(13)) // x 5 y 24 z 13 { value: 18, done: true }
  1. 第一次调用 b.next()
  • Generator 函数开始执行,执行到 yield (x + 1) 时暂停,返回 { value: 6, done: false }。此时 yield 的表达式的值为 x + 1,即 5 + 1
  1. 第二次调用 b.next(12)
  • 继续执行 Generator 函数,将传入的参数 12 赋给上一个 yield 表达式的结果。执行到 yield (y / 3) 时暂停,返回 { value: 8, done: false }。此时 y 的值为 2 * 12,即 24
  1. 第三次调用 b.next(13)
  • 继续执行 Generator 函数,将传入的参数 13 赋给上一个 yield 表达式的结果。执行到函数末尾,即 console.log 语句,打印 'x 5 y 24 z 13'。最终返回 { value: 18, done: true },表示 Generator 函数执行完毕。

注意:

  • 第一次调用 next 时传入的参数并不会被使用,因为第一次调用主要用于启动生成器。
  • 每次调用 next 时,传入的参数将会成为上一个 yield 表达式的结果,即它将影响到 Generator 函数中的赋值语句。

5. 共享屏幕,实现一个 Promise.all

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

promiseAll([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // 输出 [1, 2, 3]
  })
  .catch((error) => {
    console.error(error);
  });

之前看的八股忘完了,磕磕绊绊的,没写全

function MyPromiseAll (promiseList) {
  return new Promise((resolve, reject) => {
    let thenList = []

    promiseList.forEach(item => {
      Promise.resolve(item).then((data) =>{
        thenList.push(data)

        resolve(thenList)
      }, (err) => {
        reject(err)
      })
    })
  });
}

附:简单版,理解意思就行

 static all (promises) {
    return new Promise((resolve, reject)=>{
      let result = [];
      let times = 0;
      
      // 将成功结果放入数组中对应的位置
      const processSuccess = (index, val)=>{
        result[index] = val;
        if(++times === promises.length){  
          resolve(result); // 全部执行成功,返回 result
        }
      }
  
      // 遍历处理集合中的每一个 promise
      for(let i = 0;i < promises.length; i++){
        let p = promises[i];
        if(p && typeof p.then ==='function'){
          // 调用这个p的 then 方法
          p.then((data)=>{
            // 按照执行顺序存放执行结果
            processSuccess(i, data)
          }, reject);
        }else{
          // 普通值,直接按照执行顺序放入数组对应位置
          processSuccess(i, p)
        }
      }
    })
  }

从网上找了一个

// 输入不仅仅只有Array
function promiseAll (args) {
  return new Promise((resolve, reject) => {
    const promiseResults = [];
    let iteratorIndex = 0;
    // 已完成的数量,用于最终的返回,不能直接用完成数量作为iteratorIndex
    // 输出顺序和完成顺序是两码事
    let fullCount = 0;
    // 用于迭代iterator数据
    for (const item of args) {
      // for of 遍历顺序,用于返回正确顺序的结果
      // 因iterator用forEach遍历后的key和value一样,所以必须存一份for of的 iteratorIndex
      let resultIndex = iteratorIndex;
      iteratorIndex += 1;
      // 包一层,以兼容非promise的情况
      Promise.resolve(item).then(res => {
        promiseResults[resultIndex] = res;
        fullCount += 1;
        // Iterator 接口的数据无法单纯的用length和size判断长度,不能局限于Array和 Map类型中
        if (fullCount === iteratorIndex) {
          resolve(promiseResults)
        }
      }).catch(err => {
        reject(err)
      })
    }
    // 处理空 iterator 的情况
    if(iteratorIndex===0){
      resolve(promiseResults)
    }
  }
                    )
}
if (!Promise.all) Promise.all = promiseAll;

附上 GPT 的回答

Promise.all 是一个常用的 Promise 方法,它接受一个包含多个 Promise 的可迭代对象,并在所有 Promise 都完成时返回一个新的 Promise,该 Promise 的结果是所有 Promise 结果的数组,顺序与传入的 Promise 的顺序一致。

以下是一个简单的实现示例:

function customPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      reject(new TypeError('Promises must be an array'));
    }

    const results = [];
    let completedCount = 0;

    const handleCompletion = (index, result) => {
      results[index] = result;
      completedCount++;

      if (completedCount === promises.length) {
        resolve(results);
      }
    };

    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i])
        .then((result) => {
          handleCompletion(i, result);
        })
        .catch((error) => {
          reject(error);
        });
    }

    if (promises.length === 0) {
      resolve(results);
    }
  });
}

// 示例用法
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

customPromiseAll([promise1, promise2, promise3])
  .then((results) => {
    console.log('All promises resolved:', results);
  })
  .catch((error) => {
    console.error('One or more promises rejected:', error);
  });

这个实现会返回一个新的 Promise,该 Promise 在所有传入的 Promise 都成功时将解析为一个数组,包含每个 Promise 的结果。如果其中任何一个 Promise 被拒绝,它会立即将新的 Promise 拒绝,并传递拒绝的原因。请注意,此实现仅处理 Promise 对象,并且不支持其他可迭代对象。在实际应用中,可能需要更多的边界检查和错误处理。

最后,Generator那个一时做不出来,之前看过,忘了已经。

PromiseAll写的不好,磕磕绊绊的,最后被叫停了。

最后结果是没过, 虽然是八股文,但是背的还是不好

我的面试复盘系列

面试复盘一(本篇):

面试复盘二:

面试复盘三(上):

讨论交流群

?北京七年前端大专找工作竟如此坎坷?近N个月面试复盘(附总结答案),快来学习呀!系列更新中(一)

?北京七年前端大专找工作竟如此坎坷?近N个月面试复盘(附总结答案),快来学习呀!系列更新中(一)

本人公众号

?北京七年前端大专找工作竟如此坎坷?近N个月面试复盘(附总结答案),快来学习呀!系列更新中(一)