interview(2)

Posted on:June 8, 2024 at 09:47 AM
预计阅读时长:94 min read 字数:18696
面试(其二)—2023-06-08=>…

目录

其一见此

g-steps

position

Click to expand

position 属性被指定为从下面的值列表中选择的单个关键字.

取值

  • static

该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置.此时 top, right, bottom, left 和 z-index 属性无效.

  • relative

该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白).position:relative 对 table-*-group, table-row, table-column, table-cell, table-caption 元素无效.

  • absolute

元素会被移出正常文档流,并不为元素预留空间,通过指定元素相对于最近的非 static 定位祖先元素的偏移,来确定元素位置.绝对定位的元素可以设置外边距(margins),且不会与其他边距合并.

  • fixed

元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置.元素的位置在屏幕滚动时不会改变.打印时,元素会出现在的每页的固定位置.fixed 属性会创建新的层叠上下文.当元素祖先的 transform、perspective、filter 或 backdrop-filter 属性非 none 时,容器由视口改为该祖先.

  • sticky

元素根据正常文档流进行定位,然后相对它的最近滚动祖先(nearest scrolling ancestor)和 containing block(最近块级祖先 nearest block-level ancestor),包括 table-related 元素,基于 top、right、bottom 和 left 的值进行偏移.偏移值不会影响任何其他元素的位置. 该值总是创建一个新的层叠上下文(stacking context).注意,一个 sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的 overflow 是 hidden、scroll、auto 或 overlay 时),即便这个祖先不是最近的真实可滚动祖先.这有效地抑制了任何“sticky”行为(详情见 Github issue on W3C CSSWG).

长列表/大数据渲染的优化方案

Click to expand 在前端开发中,处理长列表或大数据渲染时,需要采取优化策略以提高性能和用户体验.以下是几种常见的优化方案:

1. 虚拟滚动(Virtual Scrolling)

虚拟滚动是一种只渲染可视区域内的列表项的方法,避免了浏览器处理过多的DOM元素.只有当前可见的元素和少量的预渲染元素存在于DOM中,其余的元素则是虚拟的,不会被渲染.

  • 实现:通过计算可视区域和滚动位置,仅渲染可见的元素.可以使用一些库,如react-windowreact-virtualizedvue-virtual-scroll-list等.

2. 分页(Pagination)

分页将数据分成多个页面,每次只加载一个页面的数据.用户可以通过分页控件来切换页面,避免一次性加载过多数据.

  • 实现:通过后端接口获取分页数据,根据当前页码请求对应的数据并渲染.

3. 懒加载(Lazy Loading)

懒加载是指在用户滚动到列表的底部或即将到达底部时,才加载新的数据项.这种方法可以减少初始加载时间和内存消耗.

  • 实现:监听滚动事件,当滚动到接近底部时,触发数据加载请求.

4. 优化数据结构和算法

处理大数据时,选择合适的数据结构和算法,可以显著提高性能.

  • 实现:使用高效的数据结构(如树、图、哈希表)和算法(如二分查找、动态规划).

5. 使用Web Workers

Web Workers允许你在后台线程中运行脚本,避免阻塞主线程,适用于数据处理或计算密集型任务.

  • 实现:将计算密集型任务移到Web Worker中,主线程负责更新UI和处理用户交互.

6. 使用Intersection Observer API

Intersection Observer API可以用于实现懒加载,通过检测元素是否进入视口来触发加载.

  • 实现:创建一个Intersection Observer,当列表项进入视口时,加载并渲染数据.

示例代码:虚拟滚动实现

以下是一个使用Vue.js实现虚拟滚动的示例:

1. 虚拟滚动组件

<template>
  <div class="virtual-list" ref="list" @scroll="handleScroll">
    <div class="spacer" :style="{ height: totalHeight + 'px' }"></div>
    <div
      v-for="(item, index) in visibleItems"
      :key="item.id"
      class="item"
      :style="{
        transform: `translateY(${(startIndex + index) * itemHeight}px)`,
      }"
    >
      {{ item.name }}
    </div>
  </div>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true,
    },
    itemHeight: {
      type: Number,
      required: true,
    },
    visibleCount: {
      type: Number,
      required: true,
    },
  },
  data() {
    return {
      startIndex: 0,
    };
  },
  computed: {
    totalHeight() {
      return this.items.length * this.itemHeight;
    },
    visibleItems() {
      return this.items.slice(
        this.startIndex,
        this.startIndex + this.visibleCount
      );
    },
  },
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.list.scrollTop;
      this.startIndex = Math.floor(scrollTop / this.itemHeight);
    },
  },
};
</script>

<style>
.virtual-list {
  position: relative;
  overflow-y: auto;
  height: 400px; /* Example height */
}
.spacer {
  width: 100%;
}
.item {
  position: absolute;
  width: 100%;
  box-sizing: border-box;
}
</style>

2. 使用虚拟滚动组件

<template>
  <div>
    <VirtualList :items="items" :itemHeight="50" :visibleCount="10" />
  </div>
</template>

<script>
import VirtualList from "./components/VirtualList.vue";

export default {
  components: {
    VirtualList,
  },
  data() {
    return {
      items: Array.from({ length: 1000 }, (v, k) => ({
        id: k,
        name: `Item ${k}`,
      })),
    };
  },
};
</script>

总结

长列表和大数据渲染的优化方案有很多种,具体选择哪种方案要根据实际需求和场景.虚拟滚动、分页、懒加载、优化数据结构和算法、使用Web Workers和Intersection Observer API都是常见的优化方法.通过合理运用这些方法,可以显著提高前端应用的性能和用户体验.

垂直居中方案

Click to expand

在前端开发中,垂直居中是一个常见的需求,根据不同的场景和要求,可以采用多种方法来实现.以下是几种常用的垂直居中方案:

1. Flexbox

使用Flexbox是实现垂直居中的最常用和最简便的方法之一.

<div class="container">
  <div class="content">Centered Content</div>
</div>

<style>
  .container {
    display: flex;
    justify-content: center; /* 水平居中 */
    align-items: center; /* 垂直居中 */
    height: 100vh; /* 示例高度 */
    border: 1px solid black; /* 示例边框 */
  }
  .content {
    border: 1px solid red; /* 示例内容边框 */
  }
</style>

2. Grid Layout

使用CSS Grid布局也可以轻松实现垂直居中.

<div class="container">
  <div class="content">Centered Content</div>
</div>

<style>
  .container {
    display: grid;
    place-items: center; /* 水平和垂直居中 */
    height: 100vh; /* 示例高度 */
    border: 1px solid black; /* 示例边框 */
  }
  .content {
    border: 1px solid red; /* 示例内容边框 */
  }
</style>

3. 使用绝对定位和transform

结合绝对定位和transform可以实现垂直居中.

<div class="container">
  <div class="content">Centered Content</div>
</div>

<style>
  .container {
    position: relative;
    height: 100vh; /* 示例高度 */
    border: 1px solid black; /* 示例边框 */
  }
  .content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: 1px solid red; /* 示例内容边框 */
  }
</style>

4. Table布局

使用表格布局的垂直居中方法适用于需要兼容旧版浏览器的情况.

<div class="container">
  <div class="content">Centered Content</div>
</div>

<style>
  .container {
    display: table;
    height: 100vh; /* 示例高度 */
    width: 100%; /* 示例宽度 */
    border: 1px solid black; /* 示例边框 */
  }
  .content {
    display: table-cell;
    vertical-align: middle;
    text-align: center; /* 水平居中 */
    border: 1px solid red; /* 示例内容边框 */
  }
</style>

5. Line-Height方法(适用于单行文本)

对于单行文本,可以通过设置line-height等于容器高度来实现垂直居中.

<div class="container">
  <div class="content">Centered Text</div>
</div>

<style>
  .container {
    height: 100px; /* 示例高度 */
    line-height: 100px; /* 行高等于高度 */
    text-align: center; /* 水平居中 */
    border: 1px solid black; /* 示例边框 */
  }
  .content {
    display: inline-block;
    vertical-align: middle; /* 兼容内联元素 */
    border: 1px solid red; /* 示例内容边框 */
  }
</style>

6. Flexbox with Column Direction

当子元素高度不确定时,可以使用Flexbox的列方向布局来实现垂直居中.

<div class="container">
  <div class="content">Centered Content</div>
</div>

<style>
  .container {
    display: flex;
    flex-direction: column;
    justify-content: center; /* 垂直居中 */
    align-items: center; /* 水平居中 */
    height: 100vh; /* 示例高度 */
    border: 1px solid black; /* 示例边框 */
  }
  .content {
    border: 1px solid red; /* 示例内容边框 */
  }
</style>

总结

以上几种方法可以根据不同的场景和需求选择使用.Flexbox和Grid是最推荐的方法,因为它们简单且强大,适用于大多数现代浏览器.对于需要兼容旧版浏览器的项目,可以选择使用表格布局或绝对定位的方法.通过合理运用这些方法,可以轻松实现元素的垂直居中.

正则表达式

Click to expand

在JavaScript中,使用正则表达式(Regular Expression)来匹配邮箱地址是一种常见需求.一个匹配邮箱的正则表达式需要能够处理各种常见的邮箱格式,并且考虑一些边缘情况.以下是一个较为常用和健壮的匹配邮箱地址的正则表达式:

const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

// 测试一些邮箱地址
const emails = [
  "example@example.com",
  "user.name+tag+sorting@example.com",
  "user_name@example.co.in",
  "user-name@sub.example.com",
  "user@sub-domain.example.com",
  "invalid-email@.com",
  "@example.com",
  "plainaddress",
  "email.example.com",
  "email@example@example.com",
  "email@111.222.333.44444",
];

emails.forEach(email => {
  console.log(`${email}: ${emailRegex.test(email)}`);
});

解释正则表达式

  • ^:匹配输入的开始位置.
  • [a-zA-Z0-9._%+-]+:匹配邮箱用户名部分,可以包含字母、数字、点(.)、下划线(_)、百分号(%)、加号(+)和减号(-).这部分要求至少一个字符.
  • @:匹配邮箱地址中的@符号.
  • [a-zA-Z0-9.-]+:匹配域名部分,可以包含字母、数字、点(.)和减号(-).这部分要求至少一个字符.
  • \.:匹配域名与顶级域之间的点(.).
  • [a-zA-Z]{2,}:匹配顶级域名部分,要求至少两个字母.
  • $:匹配输入的结束位置.

使用示例

可以使用这个正则表达式来验证邮箱地址,如下:

const email = "example@example.com";
const isValid = emailRegex.test(email);
console.log(`Is the email valid? ${isValid}`); // 输出: Is the email valid? true

注意事项

  • 这个正则表达式没有考虑所有可能的邮箱格式,因为邮箱地址规范非常复杂.如果需要更复杂的验证,可以使用一些库,比如validator库中的isEmail函数.
  • 不同的应用场景可能需要不同的验证规则,使用前请根据具体需求进行调整.

cookie包含哪些字段

Click to expand

HTTP Cookie 是在服务器和客户端之间存储和传递少量数据的一种方式.Cookie 包含多个字段,每个字段都携带不同的信息.以下是Cookie的主要字段:

  1. Name(名称)

    • 描述:Cookie 的名称,标识这个 Cookie 的唯一键.
    • 示例:sessionId
  2. Value(值)

    • 描述:Cookie 的值,存储具体的数据.
    • 示例:38afes7a8
  3. Domain(域)

    • 描述:指定哪些域可以访问这个 Cookie.如果没有指定,则默认是创建该 Cookie 的域(不含子域).
    • 示例:.example.com(允许所有子域访问)
  4. Path(路径)

    • 描述:指定这个 Cookie 对哪个路径可见.默认是创建该 Cookie 的路径(不含子路径).
    • 示例:/account(只有在访问 /account 路径时,浏览器才会发送这个 Cookie)
  5. Expires(过期时间)

    • 描述:设置 Cookie 的过期时间,绝对时间.如果设置了这个字段,当时间到期时,浏览器会自动删除这个 Cookie.
    • 示例:Expires=Wed, 09 Jun 2021 10:18:14 GMT
  6. Max-Age

    • 描述:设置 Cookie 的过期时间,相对时间(秒).比 Expires 更精确,优先级也更高.
    • 示例:Max-Age=3600(表示 Cookie 会在 3600 秒后过期)
  7. Secure(安全)

    • 描述:指定 Cookie 只能通过 HTTPS 协议发送.设置了这个字段后,Cookie 只能在加密的请求中发送.
    • 示例:Secure
  8. HttpOnly

    • 描述:指定 Cookie 不能通过 JavaScript 的 Document.cookie 访问.防止跨站脚本(XSS)攻击获取 Cookie.
    • 示例:HttpOnly
  9. SameSite

    • 描述:防止跨站请求伪造(CSRF)攻击,指定 Cookie 的发送策略.
      • Strict:完全禁止第三方网站请求带上这个 Cookie.
      • Lax:大部分情况禁止第三方网站请求带上这个 Cookie,除非是导航到目标网址的 GET 请求.
      • None:允许跨站请求携带这个 Cookie,但必须设置 Secure 字段.
    • 示例:SameSite=Lax

示例

下面是一个包含所有主要字段的 Cookie 示例:

Set-Cookie: sessionId=38afes7a8; Domain=example.com; Path=/; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=3600; Secure; HttpOnly; SameSite=Lax

详细解释

  • sessionId=38afes7a8: Cookie 的名称是 sessionId,值是 38afes7a8.
  • Domain=example.com: 这个 Cookie 对 example.com 及其子域可见.
  • Path=/: 这个 Cookie 对整个网站可见.
  • Expires=Wed, 09 Jun 2021 10:18:14 GMT: 这个 Cookie 会在 2021 年 6 月 9 日 10:18:14 到期.
  • Max-Age=3600: 这个 Cookie 会在 3600 秒后过期.
  • Secure: 这个 Cookie 只能通过 HTTPS 发送.
  • HttpOnly: 这个 Cookie 不能被 JavaScript 访问.
  • SameSite=Lax: 这个 Cookie 的发送策略是 Lax.

注意事项

  • Domain 和 Path:确保设置正确,否则 Cookie 可能不会被发送到服务器.
  • Secure:在处理敏感信息时,应设置此属性以确保 Cookie 仅通过加密连接发送.
  • HttpOnly:保护 Cookie 免受 XSS 攻击.
  • SameSite:根据需要选择合适的策略,以防范 CSRF 攻击.

了解并正确设置这些字段,有助于确保 Cookie 的安全和正确传输.

微信扫码登录模块的流程

Click to expand

微信扫码登录是一种常见的第三方登录方式,通过扫描二维码,用户可以快速登录到网站或应用.以下是微信扫码登录模块的详细流程:

1. 申请并配置微信开放平台

  • 申请微信开放平台账号:注册并登录微信开放平台.
  • 创建应用:在微信开放平台创建一个新的应用,获取AppIDAppSecret.
  • 配置域名:在微信开放平台的应用设置中配置授权回调域名.

2. 在前端显示二维码

  • 向微信请求二维码:前端向后端发起请求,后端请求微信接口生成登录二维码.
  • 显示二维码:前端将生成的二维码展示给用户.

3. 用户扫描二维码

  • 打开微信扫一扫:用户使用微信客户端扫描二维码.
  • 确认授权:用户在微信客户端确认授权登录.

4. 微信回调并获取临时登录凭证(code

  • 微信回调:用户确认授权后,微信会重定向到回调地址,并携带临时凭证(code).
  • 获取临时凭证:前端通过URL参数获取code.

5. 后端使用临时凭证获取访问令牌

  • 请求微信接口:后端使用AppIDAppSecretcode请求微信的接口,获取访问令牌和用户信息.

6. 获取用户信息并登录

  • 请求用户信息:后端使用访问令牌请求微信用户信息接口,获取用户的详细信息.
  • 创建或更新用户:根据获取的用户信息,创建新用户或更新已有用户的信息.
  • 生成会话:后端生成登录会话,返回给前端,完成登录流程.

详细流程图

  1. 前端显示二维码

    • 前端请求后端API:GET /api/wechat/qrcode
    • 后端调用微信接口生成二维码并返回给前端
    • 前端显示二维码
  2. 用户扫描二维码

    • 用户使用微信客户端扫描二维码
  3. 微信回调并获取code

    • 微信重定向到回调地址:https://yourdomain.com/callback?code=CODE
    • 前端获取URL中的code
  4. 后端获取访问令牌

    • 前端将code发送给后端:POST /api/wechat/login
    • 后端请求微信接口:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
    • 微信返回访问令牌和用户信息
  5. 后端获取用户信息并登录

    • 后端使用访问令牌请求微信用户信息接口:https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
    • 微信返回用户信息
    • 后端创建或更新用户信息,生成会话并返回给前端
    • 前端接收会话信息,完成登录

示例代码

前端代码

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>微信扫码登录</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  </head>
  <body>
    <div id="qrcode"></div>

    <script>
      $(document).ready(function () {
        $.get("/api/wechat/qrcode", function (data) {
          $("#qrcode").html('<img src="' + data.qrCodeUrl + '" />');
        });

        function checkLoginStatus() {
          $.get("/api/wechat/check-login", function (data) {
            if (data.loggedIn) {
              window.location.href = "/dashboard"; // 登录成功跳转
            } else {
              setTimeout(checkLoginStatus, 1000); // 未登录,继续轮询
            }
          });
        }

        checkLoginStatus();
      });
    </script>
  </body>
</html>

后端代码(Node.js 示例)

const express = require("express");
const axios = require("axios");
const app = express();
const port = 3000;

const appID = "YOUR_APPID";
const appSecret = "YOUR_APPSECRET";
const redirectUri = "https://yourdomain.com/callback";

let qrCodeUrl;

// 生成二维码
app.get("/api/wechat/qrcode", (req, res) => {
  const state = "random_state_string"; // 用于防止CSRF攻击
  qrCodeUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=${appID}&redirect_uri=${encodeURIComponent(
    redirectUri
  )}&response_type=code&scope=snsapi_login&state=${state}#wechat_redirect`;
  res.json({ qrCodeUrl });
});

// 回调处理
app.get("/callback", async (req, res) => {
  const code = req.query.code;
  const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appID}&secret=${appSecret}&code=${code}&grant_type=authorization_code`;
  const response = await axios.get(url);
  const accessToken = response.data.access_token;
  const openId = response.data.openid;

  // 获取用户信息
  const userInfoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}`;
  const userInfoResponse = await axios.get(userInfoUrl);
  const userInfo = userInfoResponse.data;

  // 创建或更新用户信息,生成会话(示例代码)
  // 假设你有一个用户数据库,可以根据 userInfo.openid 查找或创建用户
  // 这里直接返回成功登录页面
  res.send(`<script>window.location.href='/dashboard';</script>`);
});

// 检查登录状态(轮询示例)
app.get("/api/wechat/check-login", (req, res) => {
  // 根据你的应用逻辑,检查用户是否已登录
  const loggedIn = true; // 这里是示例,应该根据实际登录状态返回
  res.json({ loggedIn });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

总结

微信扫码登录的流程主要包括:

  1. 申请并配置微信开放平台.
  2. 前端显示二维码.
  3. 用户扫描二维码并确认授权.
  4. 微信回调并获取临时登录凭证.
  5. 后端使用凭证获取访问令牌和用户信息.
  6. 后端创建或更新用户信息,并生成登录会话.

通过上述步骤,可以实现一个完整的微信扫码登录模块.

HTTP 响应状态码

Click to expand

HTTP 响应状态码是服务器返回给客户端的三位数字代码,用于表示 HTTP 请求的处理结果.以下是常见的 HTTP 响应状态码及其解释:

1xx: Informational (信息性状态码)

  • 100 Continue:服务器已接收到请求头,客户端可以继续发送请求体.
  • 101 Switching Protocols:服务器将遵从客户端的请求切换到另一个协议.

2xx: Success (成功状态码)

  • 200 OK:请求成功,服务器返回请求的资源.
  • 201 Created:请求成功并且服务器创建了新的资源.
  • 202 Accepted:服务器已接受请求,但尚未处理.
  • 204 No Content:服务器成功处理了请求,但没有返回任何内容.

3xx: Redirection (重定向状态码)

  • 301 Moved Permanently:请求的资源已永久移动到新 URL.
  • 302 Found:请求的资源临时移动到新 URL.
  • 303 See Other:请求的资源可以在另一个 URL 通过 GET 方法获取.
  • 304 Not Modified:资源未修改,自上次请求以来未改变.
  • 307 Temporary Redirect:请求的资源临时移动到新 URL,且应该使用原有的 HTTP 方法进行请求.
  • 308 Permanent Redirect:请求的资源永久移动到新 URL,且应该使用原有的 HTTP 方法进行请求.

4xx: Client Error (客户端错误状态码)

  • 400 Bad Request:请求无效或语法错误.
  • 401 Unauthorized:请求需要身份验证.
  • 403 Forbidden:服务器拒绝请求,即使身份验证成功.
  • 404 Not Found:请求的资源不存在.
  • 405 Method Not Allowed:请求方法不被允许.
  • 408 Request Timeout:服务器等待客户端发送请求时间过长,超时.
  • 409 Conflict:请求与服务器的当前状态冲突.
  • 410 Gone:请求的资源已永久删除,不再可用.
  • 413 Payload Too Large:请求体过大,服务器无法处理.
  • 414 URI Too Long:请求的 URI 过长,服务器无法处理.
  • 415 Unsupported Media Type:请求的媒体格式不受支持.
  • 429 Too Many Requests:客户端在一定时间内发送了太多请求.

5xx: Server Error (服务器错误状态码)

  • 500 Internal Server Error:服务器内部错误,无法完成请求.
  • 501 Not Implemented:服务器不支持请求的功能.
  • 502 Bad Gateway:服务器作为网关或代理时,从上游服务器接收到无效响应.
  • 503 Service Unavailable:服务器暂时不可用(超载或维护).
  • 504 Gateway Timeout:服务器作为网关或代理时,未能及时从上游服务器接收到响应.
  • 505 HTTP Version Not Supported:服务器不支持请求中使用的 HTTP 版本.

详细解释和使用场景

200 OK

  • 解释:请求已成功,服务器返回请求的资源.
  • 使用场景:GET 请求成功返回资源、POST 请求成功返回处理结果等.

201 Created

  • 解释:请求已成功,并且服务器创建了一个新的资源.
  • 使用场景:POST 请求成功创建资源,如新用户注册、添加新条目等.

301 Moved Permanently

  • 解释:请求的资源已永久移动到新 URL,客户端应使用新 URL 进行访问.
  • 使用场景:网站重构,旧链接永久重定向到新链接.

401 Unauthorized

  • 解释:请求需要用户身份验证.
  • 使用场景:访问受保护的资源时,需要用户登录或提供 API 密钥.

403 Forbidden

  • 解释:服务器理解请求但拒绝执行.
  • 使用场景:用户权限不足,无法访问资源.

404 Not Found

  • 解释:请求的资源在服务器上不存在.
  • 使用场景:用户访问了不存在的网页或资源.

500 Internal Server Error

  • 解释:服务器遇到未知错误,无法完成请求.
  • 使用场景:服务器端代码错误、异常处理失败等.

503 Service Unavailable

  • 解释:服务器当前无法处理请求,可能会在短时间内恢复.
  • 使用场景:服务器维护、过载时返回.

总结

HTTP 响应状态码提供了丰富的信息,帮助开发者理解和处理客户端与服务器之间的通信.了解和正确使用这些状态码,有助于提升应用程序的健壮性和用户体验.

美*面试准备

深拷贝、浅拷贝

Click to expand

在编程中,拷贝对象时有两种主要方式:浅拷贝和深拷贝.了解这两种拷贝方式对于处理复杂数据结构非常重要,特别是在需要维护数据一致性和防止意外修改时.

浅拷贝

浅拷贝是对对象的顶层属性进行拷贝,如果属性是引用类型(如对象或数组),那么拷贝的是引用地址,因此修改其中一个对象的引用类型属性,会影响到另一个对象.

实现方式

  1. 使用Object.assign()
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);

copy.b.c = 3;
console.log(original.b.c); // 输出: 3,原对象也被修改
  1. 使用扩展运算符(spread syntax)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

copy.b.c = 3;
console.log(original.b.c); // 输出: 3,原对象也被修改

深拷贝

深拷贝会递归地拷贝对象的所有层级,包括所有嵌套的对象和数组,因此拷贝后的对象与原对象完全独立,互不影响.

实现方式

  1. 使用 JSON 序列化和反序列化
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));

copy.b.c = 3;
console.log(original.b.c); // 输出: 2,原对象未被修改

注意:这种方法不适用于拷贝包含函数、undefinedDate等特殊对象类型的对象.

  1. 使用递归函数
function deepClone(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  if (Array.isArray(obj)) {
    const copy = [];
    for (let i = 0; i < obj.length; i++) {
      copy[i] = deepClone(obj[i]);
    }
    return copy;
  }

  const copy = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key]);
    }
  }
  return copy;
}

const original = { a: 1, b: { c: 2 } };
const copy = deepClone(original);

copy.b.c = 3;
console.log(original.b.c); // 输出: 2,原对象未被修改

何时使用浅拷贝和深拷贝

  • 浅拷贝

    • 适用于只需要拷贝对象的顶层属性时.
    • 当对象的嵌套层次较浅,并且嵌套对象不需要独立于原对象时.
    • 性能相对较好,因为不需要递归处理嵌套对象.
  • 深拷贝

    • 适用于需要完全独立的对象副本时,尤其是处理嵌套对象时.
    • 对象层次较深,且嵌套对象需要独立于原对象时.
    • 性能相对较差,因为需要递归处理所有层级的对象.

总结

理解浅拷贝和深拷贝的区别以及如何实现它们,对于避免数据引用带来的副作用非常重要.在实际开发中,应根据具体需求选择适合的拷贝方式,以确保程序的正确性和性能.

垂直居中

Click to expand

使用 Tailwind CSS 实现垂直居中可以通过多种方法完成.以下是一些常见的实现方式:

1. 使用 Flexbox

<div class="h-screen flex items-center">
  <div class="content">居中内容</div>
</div>

解释:

  • h-screen: 设置容器的高度为视口高度.
  • flex: 使容器成为 flex 容器.
  • items-center: 垂直居中内容.

2. 使用 Grid 布局

<div class="h-screen grid place-items-center">
  <div class="content">居中内容</div>
</div>

解释:

  • h-screen: 设置容器的高度为视口高度.
  • grid: 使容器成为 grid 容器.
  • place-items-center: 同时水平和垂直居中内容.

3. 使用绝对定位和 transform

<div class="relative h-screen">
  <div class="absolute top-1/2 transform -translate-y-1/2">居中内容</div>
</div>

解释:

  • relative: 使容器相对定位.
  • h-screen: 设置容器的高度为视口高度.
  • absolute: 使内容绝对定位.
  • top-1/2: 将内容的顶部定位在容器高度的一半.
  • transform -translate-y-1/2: 使用 transform 将内容向上移动自身高度的一半,实现垂直居中.

4. 使用内联块和负边距

这种方法适用于已知元素高度的情况.

<div class="relative h-screen">
  <div class="inline-block align-middle">
    <!-- This empty span is used to vertically center the inline-block element -->
    <span class="inline-block h-full align-middle"></span>
    <div class="inline-block align-middle">居中内容</div>
  </div>
</div>

解释:

  • relative: 使容器相对定位.
  • h-screen: 设置容器的高度为视口高度.
  • inline-block align-middle: 使用 inline-blockalign-middle 来实现垂直居中.

5. 使用表格布局

<div class="table w-full h-screen">
  <div class="table-cell align-middle">居中内容</div>
</div>

解释:

  • table: 使容器显示为表格.
  • w-full h-screen: 设置容器的宽度为 100% 和高度为视口高度.
  • table-cell align-middle: 使用表格单元格的垂直居中对齐方式.

示例总结

以上示例中,使用 Flexbox 和 Grid 是最推荐的方法,因为它们更现代化且灵活,能够适应各种布局需求.Tailwind CSS 提供了便捷的工具类,可以轻松实现这些布局.

选择哪种方法取决于你的具体需求和布局情况,通常情况下 Flexbox 是最常用的方式.

Click to expand
  • flex
<div class="h-[100px] flex items-center bg-black my-10">
  <div class="bg-white">居中内容</div>
</div>
居中内容
  • transform
<div class="relative bg-black h-[100px]">
  <div class="absolute top-1/2 -translate-y-1/2 bg-white">居中内容</div>
</div>

居中内容

闭包

Click to expand 闭包是 JavaScript(以及其他编程语言)中的一个重要概念,它允许函数访问并操作它们外部作用域(即定义它们的环境)中的变量.理解闭包对于编写高效、模块化和可维护的代码非常重要.以下是对闭包的详细解释:

闭包的定义

闭包是指函数在定义时,能够记住并访问其词法作用域(即在函数定义时的环境)中的变量,即使函数在其定义的词法作用域之外执行.

闭包的原理

在 JavaScript 中,每当一个函数被创建时,闭包就会被创建.函数内部可以访问到其外部的变量,即使在该函数外部执行时,仍然能够访问这些变量.

例子

基本例子

function outerFunction() {
  let outerVariable = "I am outside!";

  function innerFunction() {
    console.log(outerVariable); // 可以访问到 outerVariable
  }

  return innerFunction;
}

const myInnerFunction = outerFunction();
myInnerFunction(); // 输出: I am outside!

在这个例子中,innerFunction 是一个闭包,因为它可以访问并“记住” outerFunction 作用域中的 outerVariable,即使 outerFunction 已经执行完毕.

更复杂的例子

function createCounter() {
  let count = 0;

  return {
    increment: function () {
      count++;
      console.log(count);
    },
    decrement: function () {
      count--;
      console.log(count);
    },
  };
}

const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1

在这个例子中,createCounter 返回一个对象,这个对象包含两个方法 incrementdecrement.这两个方法都是闭包,因为它们可以访问 createCounter 作用域中的 count 变量.

闭包的应用场景

  1. 数据封装: 闭包可以用于创建私有变量和方法,从而实现数据封装.

    function createPrivateCounter() {
      let count = 0;
      return {
        increment: function () {
          count++;
          console.log(count);
        },
        getCount: function () {
          return count;
        },
      };
    }
    
    const privateCounter = createPrivateCounter();
    privateCounter.increment(); // 输出: 1
    console.log(privateCounter.getCount()); // 输出: 1
    
  2. 函数工厂: 闭包可以用于创建函数工厂,根据传入的参数生成不同的函数.

    function createGreeting(greeting) {
      return function (name) {
        console.log(greeting + ", " + name);
      };
    }
    
    const sayHello = createGreeting("Hello");
    const sayHi = createGreeting("Hi");
    
    sayHello("Alice"); // 输出: Hello, Alice
    sayHi("Bob"); // 输出: Hi, Bob
    
  3. 回调函数和事件处理: 闭包常用于回调函数和事件处理,确保在异步操作中仍然能够访问正确的变量.

    function delayedGreeting(name) {
      setTimeout(function () {
        console.log("Hello, " + name);
      }, 1000);
    }
    
    delayedGreeting("Alice"); // 1秒后输出: Hello, Alice
    

闭包的注意事项

  1. 内存泄漏: 由于闭包会保留对其外部作用域变量的引用,可能会导致内存泄漏.因此,需要注意避免过度使用闭包,尤其是在创建大量对象时.

  2. 性能问题: 使用闭包可能会增加内存使用,影响性能.在高性能要求的场景中,需要谨慎使用闭包.

总结

闭包是 JavaScript 中非常强大且重要的概念,理解闭包有助于编写更灵活、模块化和安全的代码.通过闭包,可以实现数据封装、创建函数工厂、处理回调函数和事件等功能.然而,在使用闭包时,也需要注意可能带来的内存泄漏和性能问题.

ajax和fetch的区别

Click to expand

Ajax(Asynchronous JavaScript and XML)和 Fetch 都是用于在 Web 应用程序中进行网络请求的技术,但它们之间有一些重要的区别.

1. 发展历史

  • Ajax:Ajax 是一种传统的技术,最初是由 Microsoft 的工程师 Jesse James Garrett 在 2005 年提出的概念.它通过使用 XMLHttpRequest 对象实现在不重新加载整个页面的情况下与服务器进行数据交换.
  • Fetch:Fetch 是 Web API 的一部分,是现代浏览器原生提供的一种方式,用于发起和处理 HTTP 请求.Fetch 在 ES6 中被引入,旨在提供一种更简洁、更强大的方式来进行网络请求.

2. API 设计

  • Ajax:Ajax 使用 XMLHttpRequest 对象来执行异步请求,并提供了一种事件驱动的编程模型,例如 onreadystatechange 事件和回调函数.
  • Fetch:Fetch 提供了一组现代化的 API,使用 Promise 对象进行异步操作.它使用 fetch() 方法来发送请求,并返回一个 Promise 对象,可以通过 .then() 方法处理响应.

3. 使用方法

  • Ajax:使用 Ajax 时,需要手动创建 XMLHttpRequest 对象,然后设置请求的参数(例如 URL、请求方法、请求头等),最后通过调用 open()send() 方法来发送请求.此外,需要在 onreadystatechange 事件中处理响应.

  • Fetch:Fetch 使用更简单的 API.只需调用 fetch() 方法,并传入要请求的 URL 和选项参数即可.Fetch 会返回一个 Promise 对象,可以通过 .then() 方法来处理响应,也可以使用 async/await 语法来处理异步操作.

4. 支持性

  • Ajax:Ajax 是一个老旧的技术,但由于其广泛的应用,目前几乎所有的主流浏览器都支持 XMLHttpRequest 对象,因此可以在大多数环境中使用.

  • Fetch:Fetch 是一种现代化的 API,它的支持性取决于浏览器对 ES6 和 Fetch API 的支持.目前,大多数主流浏览器(包括 Chrome、Firefox、Safari 和 Edge)都支持 Fetch,但在某些老旧的浏览器中可能需要使用 Polyfill 进行兼容处理.

5. 功能和特性

  • Ajax:Ajax 是一个相对底层的技术,只提供了基本的异步请求和响应处理功能.对于复杂的功能,例如跨域请求、请求取消、请求超时等,需要额外的代码来实现.

  • Fetch:Fetch 是一个更高级、更现代化的 API,提供了更多的功能和特性.例如,Fetch 支持 Promise 对象、可自定义请求头、可配置请求缓存模式、支持跨域请求和请求取消等功能.

6. 使用场景

  • Ajax:由于其传统的、较低级别的 API 设计,Ajax 更适用于简单的数据交换场景,例如从服务器加载一些数据或提交表单等.

  • Fetch:由于其现代化的 API 设计和丰富的功能特性,Fetch 更适用于复杂的数据交互场景,例如处理 RESTful API、执行 GraphQL 查询、使用 Service Worker 进行离线缓存等.

总结

Ajax 和 Fetch 都是用于在 Web 应用程序中进行网络请求的技术,但它们之间有着明显的区别.Ajax 是一种传统的技术,使用 XMLHttpRequest 对象进行异步请求;而 Fetch 是一种现代化的 API,使用 Promise 对象进行异步操作,并提供了更简洁、更强大的功能和特性.在选择使用哪种技术时,需要根据具体的需求和环境来进行考虑.

示例

下面我将为你提供一个简单的示例,分别演示如何使用 Ajax 和 Fetch 发起 GET 请求,获取远程服务器上的数据.

使用 Ajax 发起 GET 请求

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Ajax GET Request</title>
  </head>
  <body>
    <div id="result"></div>

    <script>
      // 创建 XMLHttpRequest 对象
      var xhr = new XMLHttpRequest();

      // 配置请求
      xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1", true);

      // 注册事件处理程序,处理请求完成时的操作
      xhr.onreadystatechange = function () {
        if (xhr.readyState === XMLHttpRequest.DONE) {
          if (xhr.status === 200) {
            // 处理成功响应
            document.getElementById("result").textContent = xhr.responseText;
          } else {
            // 处理请求失败
            console.error("Request failed:", xhr.status);
          }
        }
      };

      // 发送请求
      xhr.send();
    </script>
  </body>
</html>

使用 Fetch 发起 GET 请求

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Fetch GET Request</title>
  </head>
  <body>
    <div id="result"></div>

    <script>
      // 发起 GET 请求
      fetch("https://jsonplaceholder.typicode.com/posts/1")
        .then(response => {
          // 检查请求状态
          if (!response.ok) {
            throw new Error("Network response was not ok");
          }
          // 将响应转换为 JSON 格式
          return response.json();
        })
        .then(data => {
          // 处理 JSON 数据
          document.getElementById("result").textContent = JSON.stringify(data);
        })
        .catch(error => {
          // 处理请求错误
          console.error("There was a problem with the fetch operation:", error);
        });
    </script>
  </body>
</html>

这两个示例都会向 https://jsonplaceholder.typicode.com/posts/1 发起 GET 请求,并将响应数据显示在页面上.Ajax 示例使用 XMLHttpRequest 对象,而 Fetch 示例使用 Fetch API,你可以对比两者之间的差异.

箭头函数与普通函数的区别

Click to expand 箭头函数和普通函数在 JavaScript 中有一些重要的区别,这些区别涉及到函数的行为、作用域、this 关键字等方面.

1. 语法形式

  • 普通函数:使用 function 关键字定义函数.

    function regularFunction() {
      // 函数体
    }
    
  • 箭头函数:使用 => 符号定义函数.

    const arrowFunction = () => {
      // 函数体
    };
    

2. this 关键字的绑定

  • 普通函数:普通函数的 this 关键字在函数被调用时动态绑定,取决于调用它的上下文.

  • 箭头函数:箭头函数的 this 关键字指向其定义时的词法作用域,而不是调用时的上下文.

3. arguments 对象

  • 普通函数:普通函数中可以使用 arguments 对象来访问传递给函数的参数列表.

  • 箭头函数:箭头函数没有自己的 arguments 对象,但可以使用剩余参数语法 ...args 来获取参数列表.

4. new 关键字的使用

  • 普通函数:普通函数可以作为构造函数使用,通过 new 关键字调用,创建一个新的对象.

  • 箭头函数:箭头函数不能被用作构造函数,不能通过 new 关键字来调用.

5. 嵌套函数的行为

  • 普通函数:普通函数的嵌套函数中的 this 关键字会动态绑定,取决于调用嵌套函数的上下文.

  • 箭头函数:箭头函数的嵌套函数中的 this 关键字继承自外部箭头函数,指向外部词法作用域.

6. 返回值

  • 普通函数:普通函数可以有多个返回值,并且可以使用 return 语句来返回值.

  • 箭头函数:箭头函数可以有一个返回值,并且如果函数体只有一条表达式,则可以省略 {}return 关键字.

总结

箭头函数和普通函数之间有一些重要的区别,特别是在 this 关键字的绑定、arguments 对象的使用、作为构造函数的行为等方面.根据具体的需求和上下文,选择合适的函数形式是非常重要的.箭头函数适用于简单的函数表达式,以及在不需要动态绑定 this 的情况下;而普通函数适用于更复杂的函数逻辑,以及需要作为构造函数使用的情况.

关于图形图像开发需要掌握的知识

canvas相关的api,面试题

在面试中,涉及 HTML5 Canvas 的问题可能会涵盖基础知识、API 的使用、实际应用场景以及性能优化等方面.以下是一些常见的面试问题以及相应的解答和示例代码:

基础知识

  1. 什么是 Canvas?Canvas 的用途是什么?

    • 回答:Canvas 是 HTML5 提供的一个用于绘制图形的元素.通过 JavaScript 可以在上面绘制各种图形,如线条、矩形、圆形、文本、图像等,常用于游戏开发、数据可视化、图像处理等.
  2. 如何在 HTML 中创建 Canvas 元素?

    • 回答:可以使用 <canvas> 标签创建 Canvas 元素,并设置宽度和高度.
    • 示例
<canvas id="myCanvas" width="500" height="400"></canvas>
  1. 如何获取 Canvas 的绘图上下文?有什么类型的绘图上下文?
    • 回答:可以使用 getContext 方法获取绘图上下文,常用的上下文类型是 “2d” 和 “webgl”.
    • 示例
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

API 使用

  1. 如何绘制一个矩形?
    • 回答:可以使用 fillRect 方法绘制填充的矩形,或使用 strokeRect 方法绘制矩形边框.
    • 示例
ctx.fillStyle = "blue";
ctx.fillRect(50, 50, 150, 100);

ctx.strokeStyle = "red";
ctx.strokeRect(50, 50, 150, 100);
  1. 如何绘制一条直线?
    • 回答:可以使用 moveTo 方法设置起点,使用 lineTo 方法设置终点,然后用 stroke 方法绘制.
    • 示例
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 200);
ctx.stroke();
  1. 如何绘制圆形或弧形? - 回答:可以使用 arc 方法绘制圆形或弧形. - 示例
ctx.beginPath();
ctx.arc(150, 100, 50, 0, Math.PI * 2); // 圆形 ctx.fill();

void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

  1. 如何绘制文本?
    • 回答:可以使用 fillTextstrokeText 方法绘制填充或边框文本.
    • 示例
ctx.font = "30px Arial";
ctx.fillStyle = "green";
ctx.fillText("Hello Canvas", 50, 150);

ctx.strokeStyle = "blue";
ctx.strokeText("Hello Canvas", 50, 200);
  1. 如何绘制图像?
    • 回答:可以使用 drawImage 方法绘制图像.
    • 示例
const img = new Image();
img.src = "path/to/image.jpg";
img.onload = () => {
  ctx.drawImage(img, 50, 50, 200, 150);
};

实际应用

  1. 如何清空 Canvas?
    • 回答:可以使用 clearRect 方法清空 Canvas.
    • 示例
ctx.clearRect(0, 0, canvas.width, canvas.height);
  1. 如何在 Canvas 中处理动画?
    • 回答:可以使用 requestAnimationFrame 方法创建动画循环.
    • 示例
let x = 0;
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(x, 50, 50, 50);
  x += 1;
  requestAnimationFrame(animate);
}
animate();

性能优化

  1. Canvas 性能优化的一些方法是什么?
    • 回答:可以使用离屏 Canvas、减少重绘区域、优化绘图算法、使用 requestAnimationFrame 进行动画控制等方法进行性能优化.

高级问题

  1. Canvas 与 SVG 的区别是什么?什么时候使用 Canvas,什么时候使用 SVG?
    • 回答:Canvas 是立即模式绘图,适合大量像素操作和动画,SVG 是保留模式绘图,适合静态矢量图和可交互的图形.Canvas 用于游戏、图像处理等需要高性能绘制的场景,SVG 用于需要高质量和可缩放图形的场景.

示例项目

  1. 如何在 Canvas 上实现一个简单的绘图应用?
    • 回答:需要处理鼠标事件来绘制图形.
    • 示例
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
let drawing = false;

canvas.addEventListener("mousedown", () => {
  drawing = true;
});
canvas.addEventListener("mouseup", () => {
  drawing = false;
  ctx.beginPath();
});
canvas.addEventListener("mousemove", draw);

function draw(event) {
  if (!drawing) return;
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.strokeStyle = "black";
  ctx.lineTo(
    event.clientX - canvas.offsetLeft,
    event.clientY - canvas.offsetTop
  );
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(
    event.clientX - canvas.offsetLeft,
    event.clientY - canvas.offsetTop
  );
}

优化一下的astro组件

---

---

<canvas id="drawCanvas" class="bg-white m-auto"></canvas>

<script>
  document.addEventListener("DOMContentLoaded", () => {
    const canvas = document.getElementById("drawCanvas") as HTMLCanvasElement;

    canvas.width = window.innerWidth*3/4;
    canvas.height = window.innerHeight;
    const ctx = canvas.getContext("2d");
    let drawing = false;

    canvas.addEventListener("mousedown", startDrawing);
    canvas.addEventListener("mouseup", stopDrawing);
    canvas.addEventListener("mousemove", draw);

    function startDrawing(event) {
      drawing = true;
      draw(event); // 开始绘图时立即画一个点
    }

    function stopDrawing() {
      drawing = false;
      ctx.beginPath(); // 开始一个新的路径,以防止连接上一个绘图
    }

    function draw(event) {
      if (!drawing) return;

      // 获取 Canvas 元素的边界框
      const rect = canvas.getBoundingClientRect();

      // 计算相对于 Canvas 的坐标
      const x = (event.clientX - rect.left) * (canvas.width / rect.width);
      const y = (event.clientY - rect.top) * (canvas.height / rect.height);

      ctx.lineWidth = 5;
      ctx.lineCap = "round";
      ctx.strokeStyle = "black";
      ctx.lineTo(x, y); // 画到新的点
      ctx.stroke(); // 绘制路径
      ctx.beginPath(); // 开始一个新的路径
      ctx.moveTo(x, y); // 将路径移动到新的点
    }
  });
</script>

通过这些问题和回答,你可以系统地复习和准备与 Canvas 相关的面试内容.

ultipa

简述

笔试,图的遍历,wasm,promise等原理,重写array.prototype.foreach,最高效方案解决数组交集,对象扁平化,gpu工作原理
面试,原型链/事件循环/深浅拷贝/获取数据类型

gpu原理

图形处理器(GPU,Graphics Processing Unit)是一种专门用于处理图形计算任务的处理器.与中央处理器(CPU)相比,GPU具有更多的并行处理能力,能够更高效地处理大量数据和复杂计算.GPU的工作原理涉及以下几个关键概念:

1. 并行计算架构

GPU设计的核心理念是并行计算.传统的CPU有少量的高性能核心,专注于串行计算任务,而GPU拥有数百甚至数千个较小的处理核心,能够同时处理大量的并行任务.这使得GPU非常适合用于图形渲染、科学计算和机器学习等需要大量并行计算的领域.

2. 流处理器(Streaming Processors)

GPU中的基本计算单元是流处理器(也称为着色器核心).每个流处理器可以独立地执行指令集合,但通常这些处理器被组织成块,称为“线程块”或“工作组”,这些块内的处理器可以共享数据和同步.

3. 硬件线程

GPU的另一个重要特性是其硬件线程支持.每个处理核心能够管理多个硬件线程,当一个线程在等待数据时,核心可以切换到另一个线程继续工作,从而有效地隐藏内存访问延迟.这种特性使得GPU在处理需要大量内存访问的任务时效率更高.

4. 内存架构

GPU通常有多个层次的内存,包括寄存器、共享内存、全局内存和常量内存等.共享内存和寄存器具有较低的访问延迟,适合频繁访问的数据,而全局内存则提供大容量存储,适合存放需要大量访问的数据.GPU的内存架构设计也是为了优化并行计算性能.

5. 图形渲染管线

在图形处理方面,GPU使用了图形渲染管线的概念.图形渲染管线包括多个阶段,如顶点着色、曲面细分、几何着色、光栅化、片段着色和后处理等.每个阶段负责特定的图形处理任务,这些任务可以在多个处理器上并行执行,从而提高渲染效率.

6. 编程模型

为了充分利用GPU的并行计算能力,需要使用专门的编程模型和API,如CUDA(Compute Unified Device Architecture)和OpenCL(Open Computing Language).这些工具允许开发者编写能够在GPU上高效运行的并行代码.

总结

GPU通过其高度并行的架构、硬件线程支持、多层次的内存架构以及专门的编程模型,能够高效地处理大量并行计算任务.这使得GPU在图形渲染、科学计算和机器学习等领域具有显著的优势.

js实现图的遍历,从任意某个节点出发

在JavaScript中,实现图的遍历有两种常见的方法:深度优先搜索(DFS)和广度优先搜索(BFS).下面分别展示这两种方法的实现.

深度优先搜索(DFS)

深度优先搜索从起始节点开始,沿着路径一直走到底部(没有未访问的邻居),然后回溯,寻找下一条路径.

以下是DFS的实现代码:

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    if (!this.adjacencyList.has(vertex)) {
      this.adjacencyList.set(vertex, []);
    }
  }

  addEdge(vertex1, vertex2) {
    if (this.adjacencyList.has(vertex1) && this.adjacencyList.has(vertex2)) {
      this.adjacencyList.get(vertex1).push(vertex2);
      this.adjacencyList.get(vertex2).push(vertex1); // For undirected graph
    }
  }

  dfs(start) {
    const visited = new Set();
    this.dfsRecursive(start, visited);
  }

  dfsRecursive(vertex, visited) {
    if (!vertex) return;
    visited.add(vertex);
    console.log(vertex);
    const neighbors = this.adjacencyList.get(vertex);
    for (let neighbor of neighbors) {
      if (!visited.has(neighbor)) {
        this.dfsRecursive(neighbor, visited);
      }
    }
  }
}

// 示例
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");
graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "D");
graph.addEdge("C", "E");
graph.addEdge("D", "E");

console.log("DFS Traversal:");
graph.dfs("A");

广度优先搜索(BFS)

广度优先搜索从起始节点开始,首先访问所有直接邻居,然后依次访问每个邻居的邻居.

以下是BFS的实现代码:

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    if (!this.adjacencyList.has(vertex)) {
      this.adjacencyList.set(vertex, []);
    }
  }

  addEdge(vertex1, vertex2) {
    if (this.adjacencyList.has(vertex1) && this.adjacencyList.has(vertex2)) {
      this.adjacencyList.get(vertex1).push(vertex2);
      this.adjacencyList.get(vertex2).push(vertex1); // For undirected graph
    }
  }

  bfs(start) {
    const visited = new Set();
    const queue = [start];
    visited.add(start);

    while (queue.length > 0) {
      const vertex = queue.shift();
      console.log(vertex);
      const neighbors = this.adjacencyList.get(vertex);
      for (let neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }
  }
}

// 示例
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");
graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "D");
graph.addEdge("C", "E");
graph.addEdge("D", "E");

console.log("BFS Traversal:");
graph.bfs("A");

总结

以上代码展示了如何在JavaScript中实现图的遍历,包括深度优先搜索(DFS)和广度优先搜索(BFS).DFS通过递归访问每个节点的邻居,直到没有未访问的邻居为止;BFS通过队列依次访问每个节点及其所有邻居.你可以根据需要选择合适的方法来遍历图结构.

wasm原理

WebAssembly(Wasm)是一种二进制指令格式,旨在作为可移植、性能高效的目标代码,用于浏览器和其他环境.它的设计初衷是为了在网页上运行性能接近原生的应用程序,同时具有跨平台和安全性的特性.以下是WebAssembly的基本原理和关键特性:

1. 编译目标

WebAssembly并不是一种编程语言,而是一种可以被多种高级语言编译成的目标格式.例如,C、C++、Rust等语言可以通过编译器(如Emscripten、Rust编译器等)编译成Wasm代码.这些编译器将高级语言代码转换成WebAssembly的二进制格式.

2. 二进制格式

WebAssembly使用紧凑的二进制格式,这种格式不仅可以提高加载速度和解析速度,还可以减少网络传输时间.与传统的JavaScript代码相比,二进制格式的WebAssembly文件通常更小、更高效.

3. 线性内存

WebAssembly使用线性内存模型,这是一块连续的内存块,可以通过指针进行访问.WebAssembly模块可以在初始化时请求一定量的内存,并可以在运行时动态增长.这种内存模型使得WebAssembly非常适合用于需要高性能和高效内存管理的应用程序.

4. 栈机器架构

WebAssembly采用了栈机器架构,这意味着指令大多在栈上操作.操作数被压入栈中,指令执行时从栈中弹出操作数并将结果压回栈中.栈机器架构的优势在于其实现简单,易于生成代码,并且能有效利用现代CPU的性能特性.

5. 模块化设计

WebAssembly模块是WebAssembly代码的基本单位.一个模块可以包含多个函数、全局变量、内存和表.模块之间可以相互调用,并通过导入和导出进行交互.这种模块化设计使得WebAssembly可以方便地进行代码重用和分离.

6. 安全性

WebAssembly通过沙箱执行环境提供高安全性.每个WebAssembly模块在一个独立的执行环境中运行,无法直接访问宿主环境的资源.这种设计防止了恶意代码对宿主系统的破坏,确保了运行时的安全性.

7. 与JavaScript的互操作性

WebAssembly与JavaScript紧密集成,可以相互调用.JavaScript代码可以通过WebAssembly的JavaScript API加载和执行WebAssembly模块,而WebAssembly模块也可以调用JavaScript函数.这种互操作性使得WebAssembly可以与现有的JavaScript应用无缝集成.

工作原理示例

  1. 编译代码:将C/C++/Rust等代码编译为WebAssembly模块(.wasm文件).
  2. 加载和实例化:在浏览器中使用JavaScript API加载并实例化WebAssembly模块.
  3. 调用函数:通过JavaScript调用WebAssembly模块中的函数,或者让WebAssembly模块调用JavaScript函数.
// JavaScript代码加载和使用WebAssembly模块
fetch("example.wasm")
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes))
  .then(results => {
    const instance = results.instance;
    // 调用WebAssembly模块中的函数
    instance.exports.myFunction();
  });

总结

WebAssembly是一种高效的二进制格式,旨在提供接近原生性能的网页应用程序.它通过编译目标、多语言支持、紧凑的二进制格式、线性内存模型、栈机器架构、模块化设计、安全性以及与JavaScript的互操作性,为开发者提供了一种强大的工具,用于在浏览器和其他环境中运行高性能的应用程序.

重写Array.prototype.forEach

重写 Array.prototype.forEach 方法的核心思路是保存原来的 forEach 方法,然后定义一个新的方法来实现我们想要的功能.新的方法应该遵循原始 forEach 方法的规范,同时添加我们自己的逻辑.

下面是用原生 JavaScript 重写 Array.prototype.forEach 的具体步骤:

  1. 保存原始的 Array.prototype.forEach 方法.
  2. 定义新的 forEach 方法,添加自定义逻辑,同时调用原始方法来确保原有功能不丢失.

以下是实现代码:

// 保存原始的 Array.prototype.forEach 方法
const originalForEach = Array.prototype.forEach;

// 重写 Array.prototype.forEach 方法
Array.prototype.forEach = function (callback, thisArg) {
  // 检查 callback 是否为函数
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  // 获取数组长度
  const length = this.length;

  // 自定义逻辑:打印数组长度
  console.log("Array length:", length);

  // 使用 for 循环实现遍历
  for (let i = 0; i < length; i++) {
    // 检查数组是否包含当前索引
    if (i in this) {
      const element = this[i];

      // 自定义逻辑:打印索引和元素
      console.log("Index:", i, "Element:", element);

      // 调用用户提供的回调函数
      callback.call(thisArg, element, i, this);
    }
  }

  // 自定义逻辑:打印遍历结束
  console.log("Traversal complete.");
};

// 测试新定义的 forEach 方法
const arr = [1, 2, 3, 4, 5];
arr.forEach(function (element, index, array) {
  console.log("Callback:", element);
});

代码解释

  1. 保存原始方法

    const originalForEach = Array.prototype.forEach;
    

    这一步保存了原始的 forEach 方法,以防止丢失其原有功能.

  2. 重写 forEach 方法

    Array.prototype.forEach = function (callback, thisArg) {
      // 检查 callback 是否为函数
      if (typeof callback !== "function") {
        throw new TypeError(callback + " is not a function");
      }
    
      // 获取数组长度
      const length = this.length;
    
      // 自定义逻辑:打印数组长度
      console.log("Array length:", length);
    
      // 使用 for 循环实现遍历
      for (let i = 0; i < length; i++) {
        // 检查数组是否包含当前索引
        if (i in this) {
          const element = this[i];
    
          // 自定义逻辑:打印索引和元素
          console.log("Index:", i, "Element:", element);
    
          // 调用用户提供的回调函数
          callback.call(thisArg, element, i, this);
        }
      }
    
      // 自定义逻辑:打印遍历结束
      console.log("Traversal complete.");
    };
    
  3. 测试新的 forEach 方法

    const arr = [1, 2, 3, 4, 5];
    arr.forEach(function (element, index, array) {
      console.log("Callback:", element);
    });
    

    这段代码演示了如何使用新的 forEach 方法遍历数组,并打印出自定义信息和回调函数中的元素.

注意事项

  • 类型检查:新的 forEach 方法中添加了对 callback 是否为函数的检查,以确保调用时不会出现类型错误.
  • 索引检查:在遍历过程中检查索引是否在数组中(if (i in this)),以处理稀疏数组的情况.
  • 性能影响:重写 Array.prototype.forEach 会影响所有数组的遍历行为,因此要谨慎使用,特别是在性能敏感的代码中.
  • 兼容性问题:如果多个库或代码片段都尝试重写 forEach,可能会导致冲突和不可预期的行为.

通过上述步骤,你可以在确保不破坏原始功能的前提下,重写 Array.prototype.forEach 方法,并添加自定义逻辑.

Click to expand

要高效地解决两个数组的交集问题,最常见的方法是利用集合(Set)的特性.集合可以高效地进行查找操作,从而使交集运算变得更快.以下是详细步骤:

  1. 将一个数组转换为集合(Set),以利用集合的高效查找特性.
  2. 遍历另一个数组,检查元素是否在集合中.

这种方法的时间复杂度接近 O(n + m),其中 n 和 m 分别是两个数组的长度.

示例代码

以下是用 JavaScript 实现的高效求两个数组交集的方法:

function intersection(arr1, arr2) {
  // 将较小的数组转换为集合
  const set1 = new Set(arr1);
  const result = [];

  // 遍历较大的数组,检查元素是否在集合中
  for (let item of arr2) {
    if (set1.has(item)) {
      result.push(item);
      // 移除已找到的元素,避免重复结果
      set1.delete(item);
    }
  }

  return result;
}

// 示例
const array1 = [1, 2, 2, 3, 4, 5];
const array2 = [2, 2, 3, 5, 6];
const result = intersection(array1, array2);
console.log(result); // 输出: [2, 3, 5]

代码解释

  1. 转换为集合

    const set1 = new Set(arr1);
    

    这一步将第一个数组转换为集合,以利用集合的高效查找特性.

  2. 遍历和查找

    for (let item of arr2) {
      if (set1.has(item)) {
        result.push(item);
        set1.delete(item); // 移除已找到的元素,避免重复结果
      }
    }
    

    这一步遍历第二个数组,并检查元素是否在第一个数组转换的集合中.如果在集合中,则将该元素添加到结果数组中,并从集合中删除该元素以避免重复结果.

优化

为了进一步优化,可以在转换数组为集合之前,先比较两个数组的长度,将较小的数组转换为集合.这可以减少集合的构建和查找操作的时间.

function intersection(arr1, arr2) {
  // 将较小的数组转换为集合
  if (arr1.length > arr2.length) {
    [arr1, arr2] = [arr2, arr1];
  }

  const set1 = new Set(arr1);
  const result = [];

  // 遍历较大的数组,检查元素是否在集合中
  for (let item of arr2) {
    if (set1.has(item)) {
      result.push(item);
      set1.delete(item); // 移除已找到的元素,避免重复结果
    }
  }

  return result;
}

// 示例
const array1 = [1, 2, 2, 3, 4, 5];
const array2 = [2, 2, 3, 5, 6];
const result = intersection(array1, array2);
console.log(result); // 输出: [2, 3, 5]

这种优化确保了在处理数组交集时,始终将较小的数组转换为集合,从而减少了不必要的查找操作,提高了整体效率.

结论

利用集合来高效求解两个数组的交集是一个常见且高效的方案.它利用了集合的高效查找特性,使得交集运算的时间复杂度接近 O(n + m),是处理这种问题的最佳实践之一.

对象扁平化,且合并相同项

要实现对象的扁平化并合并相同项,可以采用递归的方式来处理.首先,需要遍历对象的每个属性,将嵌套的属性展开为一级属性,并将具有相同键的属性值合并为一个数组.以下是一个实现对象扁平化并合并相同项的 JavaScript 函数:

function flattenAndMerge(obj) {
  const result = {};

  function flatten(obj, prefix = "") {
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        const newKey = prefix ? `${prefix}.${key}` : key;
        if (typeof obj[key] === "object" && obj[key] !== null) {
          flatten(obj[key], newKey);
        } else {
          if (result[newKey]) {
            result[newKey].push(obj[key]);
          } else {
            result[newKey] = [obj[key]];
          }
        }
      }
    }
  }

  flatten(obj);

  // 将合并的数组拼接为单个值
  for (let key in result) {
    if (result[key].length === 1) {
      result[key] = result[key][0];
    }
  }

  return result;
}

// 示例
const obj = {
  a: {
    b: 1,
    c: {
      d: 2,
      e: 3,
    },
  },
  f: {
    g: 4,
    h: {
      i: 5,
    },
  },
  j: 6,
};

const flattened = flattenAndMerge(obj);
console.log(flattened);

函数解释

  1. flattenAndMerge 函数接受一个嵌套对象作为参数,并返回扁平化并合并相同项后的结果.
  2. flatten 函数是一个递归函数,用于遍历对象并将其扁平化.它接受两个参数:要扁平化的对象和当前属性的前缀.
  3. flatten 函数中,使用 for...in 循环遍历对象的每个属性.
  4. 如果属性值是一个对象,则递归调用 flatten 函数.
  5. 如果属性值是一个基本类型,则将其添加到结果对象中,如果已经存在相同键的属性,则将其值合并为数组.
  6. 最后,将合并的数组拼接为单个值.

示例结果

对于给定的示例对象:

const obj = {
  a: {
    b: 1,
    c: {
      d: 2,
      e: 3,
    },
  },
  f: {
    g: 4,
    h: {
      i: 5,
    },
  },
  j: 6,
};

函数的输出结果为:

{
    "a.b": 1,
    "a.c.d": 2,
    "a.c.e": 3,
    "f.g": 4,
    "f.h.i": 5,
    "j": 6
}

其中,属性已经被扁平化,并且具有相同键的属性值被合并为数组.

js事件循环机制

JavaScript 的事件循环(Event Loop)是一种实现并发的机制,用于处理异步操作.它是 JavaScript 运行时环境(如浏览器或 Node.js)提供的一种机制,用于协调执行代码和处理事件的顺序.事件循环机制主要包括以下几个部分:

1. 调用栈(Call Stack)

调用栈是一个存储函数调用的栈结构,用于跟踪程序的执行状态.当调用函数时,会将该函数推入调用栈中,当函数执行完成后,会将其从调用栈中弹出.调用栈是 JavaScript 单线程执行代码的基础.

2. 任务队列(Task Queue)

任务队列是一个先进先出(FIFO)的队列,用于存储待执行的任务.在浏览器环境中,任务队列通常分为宏任务队列(macrotask queue)和微任务队列(microtask queue)两种类型.

  • 宏任务队列(macrotask queue):包含一些宏观任务,例如 setTimeout、setInterval、I/O 操作等.
  • 微任务队列(microtask queue):包含一些微观任务,例如 Promise 的回调、MutationObserver 等.

3. 事件循环(Event Loop)

事件循环是一个持续运行的循环,负责从任务队列中取出任务并执行.它的工作原理如下:

  1. 从宏任务队列中取出一个任务,将其推入调用栈中执行,直到调用栈为空.
  2. 当调用栈为空时,事件循环会检查微任务队列是否为空,如果不为空,则依次取出微任务执行,直到微任务队列为空.
  3. 如果宏任务队列和微任务队列都为空,则事件循环会等待新的任务被添加到任务队列中,然后继续执行.

示例

下面是一个简单的示例,演示了 JavaScript 中的事件循环机制:

console.log("Script Start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(function () {
  console.log("Promise");
});

console.log("Script End");

上述示例中,首先会打印出 "Script Start",然后执行 setTimeoutPromise,最后打印出 "Script End".具体执行顺序如下:

  1. 执行同步代码,打印 "Script Start""Script End".
  2. setTimeout 的回调函数推入宏任务队列.
  3. 将 Promise 的回调函数推入微任务队列.
  4. 执行微任务队列中的所有任务,打印 "Promise".
  5. 事件循环检查宏任务队列中是否有任务,发现有任务,取出 setTimeout 的回调函数执行,打印 "setTimeout".

总结

JavaScript 的事件循环机制是一种实现并发的机制,用于协调执行代码和处理事件的顺序.它由调用栈、任务队列和事件循环组成,通过不断地从任务队列中取出任务并执行,来实现 JavaScript 的异步编程.理解事件循环机制对于编写高效的异步代码非常重要.

美图

感觉像来刷业绩的…

自我介绍

react组件通信方式

react useEffect 以及第二个参数的问题

函数组件和类式组件

vue2与vue3

ts有什么好处

es6

js数组不改变原数组的方法

项目开发选型

混沌学*

函数判断字符串是否包含回文子串

Click to expand

要判断一个字符串是否包含回文子串,可以使用几种不同的方法.下面介绍两种常见的方法:暴力搜索和中心扩展.

方法一:暴力搜索

这种方法遍历字符串的所有子串,并检查这些子串是否是回文.尽管这种方法在性能上不是最优的,但它实现起来非常简单.

function hasPalindromeSubstring(s) {
  // Helper function to check if a given string is a palindrome
  function isPalindrome(str) {
    const len = str.length;
    for (let i = 0; i < len / 2; i++) {
      if (str[i] !== str[len - 1 - i]) {
        return false;
      }
    }
    return true;
  }

  const n = s.length;
  // Iterate through all possible substrings
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j <= n; j++) {
      const substring = s.slice(i, j);
      if (isPalindrome(substring) && substring.length > 1) {
        return true;
      }
    }
  }
  return false;
}

// Example usage:
console.log(hasPalindromeSubstring("racecar")); // true
console.log(hasPalindromeSubstring("abcdefg")); // false
console.log(hasPalindromeSubstring("abba")); // true
console.log(hasPalindromeSubstring("a")); // false
console.log(hasPalindromeSubstring("abcba")); // true

方法二:中心扩展算法

这种方法利用中心扩展来查找所有可能的回文子串.对于每个字符,尝试将其作为中心向外扩展,以找到回文子串.

function hasPalindromeSubstring(s) {
  if (!s || s.length < 2) return false;

  // Helper function to expand around the center and check for palindrome
  function isPalindromeAroundCenter(left, right) {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      if (right - left + 1 > 1) {
        return true;
      }
      left--;
      right++;
    }
    return false;
  }

  for (let i = 0; i < s.length; i++) {
    // Check for odd length palindromes (single character center)
    if (isPalindromeAroundCenter(i, i)) {
      return true;
    }
    // Check for even length palindromes (two character center)
    if (isPalindromeAroundCenter(i, i + 1)) {
      return true;
    }
  }

  return false;
}

// Example usage:
console.log(hasPalindromeSubstring("racecar")); // true
console.log(hasPalindromeSubstring("abcdefg")); // false
console.log(hasPalindromeSubstring("abba")); // true
console.log(hasPalindromeSubstring("a")); // false
console.log(hasPalindromeSubstring("abcba")); // true

解释

  1. 暴力搜索方法:

    • isPalindrome 辅助函数检查一个字符串是否为回文.
    • 双重循环遍历所有子串,使用 isPalindrome 检查是否为回文.
    • 如果找到一个长度大于 1 的回文子串,返回 true.
  2. 中心扩展方法:

    • isPalindromeAroundCenter 辅助函数通过扩展中心点检查是否有回文子串.
    • 主循环遍历字符串中的每个字符,检查以当前字符为中心(奇数长度回文)或当前字符与下一个字符为中心(偶数长度回文)的回文子串.
    • 如果找到一个回文子串,返回 true.

这两种方法都能有效地检查字符串中是否包含回文子串.选择哪种方法取决于具体需求和性能考虑.

js设计函数返回传入参数的类型

Click to expand
function getType(value) {
  // Handle null and array cases separately
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";

  // Use typeof for most types
  let type = typeof value;

  // Further distinguish objects, functions, etc.
  if (type === "object" || type === "function") {
    let typeString = Object.prototype.toString.call(value);
    // Remove [object ] and return the specific type
    return typeString.slice(8, -1).toLowerCase();
  }

  return type;
}

// Example usage:
console.log(getType("Hello, World!")); // "string"
console.log(getType(42)); // "number"
console.log(getType(true)); // "boolean"
console.log(getType(undefined)); // "undefined"
console.log(getType(null)); // "null"
console.log(getType([1, 2, 3])); // "array"
console.log(getType({ key: "value" })); // "object"
console.log(getType(function () {})); // "function"
console.log(getType(new Date())); // "date"
console.log(getType(/abc/)); // "regexp"
console.log(getType(new Map())); // "map"
console.log(getType(new Set())); // "set"
console.log(getType(Symbol("symbol"))); // "symbol"

最重要的是这个Object.prototype.toString.call,没答上,Object.prototype.toString()

一道考察 JavaScript 事件循环的题目

Click to expand
console.log(1);

setTimeout(function () {
  console.log(2);
  process.nextTick(function () {
    console.log(3);
  });
  new Promise(function (resolve) {
    console.log(4);
    resolve();
  }).then(function () {
    console.log(5);
  });
});

process.nextTick(function () {
  console.log(6);
});

new Promise(function (resolve) {
  console.log(7);
  resolve();
}).then(function () {
  console.log(8);
});

执行顺序和解释

  1. 同步代码执行:所有同步代码首先执行.

    • console.log(1); 输出 1
    • console.log(7); 输出 7
    • new Promise(...).then(...);then 回调 console.log(8); 添加到微任务队列.
  2. process.nextTick 回调:在当前事件循环结束前执行所有的 process.nextTick 回调.

    • process.nextTick(function() { console.log(6); }); 输出 6
  3. Promise 微任务:在当前事件循环结束前,执行所有的 Promise 微任务.

    • Promise.resolve().then(function() { console.log(8); }); 输出 8
  4. setTimeout 宏任务:所有的 setTimeout 回调在下一次事件循环中执行.

    • setTimeout(function() { ... }); 宏任务在下一次事件循环执行.
      • console.log(2); 输出 2
      • process.nextTick(function() { console.log(3); });console.log(3); 添加到微任务队列.
      • console.log(4); 输出 4
      • new Promise(...).then(...);then 回调 console.log(5); 添加到微任务队列.
  5. 第二次事件循环的微任务:在当前事件循环结束前,执行所有的 process.nextTickPromise 微任务.

    • process.nextTick(function() { console.log(3); }); 输出 3
    • Promise.resolve().then(function() { console.log(5); }); 输出 5

最终输出顺序:

1
7
6
8
2
4
3
5

解释

  1. 同步代码首先执行

    • console.log(1);console.log(7); 是同步代码,因此最先执行.
  2. process.nextTick 回调

    • process.nextTick 回调在当前事件循环的尾部执行,优先于 Promise 微任务.
    • process.nextTick(function() { console.log(6); }); 输出 6
  3. Promise 微任务

    • Promisethen 回调作为微任务,在 process.nextTick 回调之后执行.
    • Promise.resolve().then(function() { console.log(8); }); 输出 8
  4. setTimeout 宏任务

    • setTimeout 回调在下一次事件循环中执行.
    • console.log(2); 输出 2
    • console.log(4); 输出 4
    • process.nextTick(function() { console.log(3); });Promise.resolve().then(function() { console.log(5); }); 添加到微任务队列.
  5. 第二次事件循环的微任务

    • 在当前事件循环结束前,执行所有的 process.nextTickPromise 微任务.
    • process.nextTick(function() { console.log(3); }); 输出 3
    • Promise.resolve().then(function() { console.log(5); }); 输出 5

通过上述执行顺序的分析,可以理解 JavaScript 事件循环中微任务和宏任务的执行优先级.

vue2 vs vue3

Click to expand

Vue.js 是一个用于构建用户界面的渐进式框架.Vue 2 和 Vue 3 是 Vue.js 的两个主要版本,它们在使用和底层原理上有一些显著的区别.

使用上的区别

Composition API vs. Options API

  • Vue 2: 主要使用 Options API,通过 data, methods, computed, watch 等选项来定义组件.
  • Vue 3: 引入了 Composition API,这是一种新的方式,通过 setup 函数来组织逻辑,使代码更具可读性和复用性.
// Vue 2 Options API
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}

// Vue 3 Composition API
import { ref } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    return { count, increment };
  }
}

Teleport

  • Vue 2: 没有内置的 Teleport 功能.
  • Vue 3: 引入了 Teleport,可以将子组件渲染到 DOM 树的其他位置.
<!-- Vue 3 Teleport -->
<template>
  <teleport to="body">
    <div>This will be teleported to the body element.</div>
  </teleport>
</template>

Fragments

  • Vue 2: 一个组件的模板只能有一个根元素.
  • Vue 3: 支持多个根元素,即支持 fragments.
<!-- Vue 3 Fragment -->
<template>
  <div>Fragment part 1</div>
  <div>Fragment part 2</div>
</template>

Composition API Hooks

  • Vue 3: 提供了新的生命周期钩子,比如 onMounted, onUpdated, onUnmounted 等,在 Composition API 中使用.
import { onMounted, ref } from "vue";
export default {
  setup() {
    const count = ref(0);
    onMounted(() => {
      console.log("Component is mounted");
    });
    return { count };
  },
};

底层原理上的区别

Proxy 代替 DefineProperty

  • Vue 2: 使用 Object.defineProperty 实现响应式系统,对对象的每个属性进行劫持.这种方法有一些限制,比如不能检测属性的添加或删除.
  • Vue 3: 使用 Proxy 进行响应式处理,Proxy 可以直接监听对象的读写操作,因此能够更好地处理对象属性的添加和删除,并且对数组和对象的性能更高.

Virtual DOM 和优化

  • Vue 2: Virtual DOM 的性能优化有限,复杂组件树的更新效率较低.
  • Vue 3: 对 Virtual DOM 进行了重写,提供了更多的编译时优化,减少了运行时的开销,提高了复杂组件树的更新效率.

Tree Shaking

  • Vue 2: Vue 2 的核心代码是一个整体包,Tree Shaking 的效果有限.
  • Vue 3: Vue 3 的设计更加模块化,所有的功能都是按需导入的,因此 Tree Shaking 更加有效,这使得最终打包的代码体积更小.

Composition API 的内存优化

  • Vue 2: Options API 中,所有定义的属性和方法都会挂载到 Vue 实例上,即使不需要时也会占用内存.
  • Vue 3: Composition API 允许更加灵活地定义和使用状态和方法,不需要时不会占用额外的内存,有利于减少内存消耗.

总结

Vue 3 相比 Vue 2 在使用上更具灵活性,底层实现也更加高效.Vue 3 的新特性如 Composition API、Teleport、Fragments 等提供了更强大的功能,使得开发者可以编写更清晰、更具复用性的代码.同时,底层采用 Proxy 实现响应式系统,并进行了许多性能优化和改进,使得 Vue 3 相较于 Vue 2 更加现代和高效.

call,apply,bind

Click to expand callapplybind 是 JavaScript 中的三个方法,常用于改变函数的 this 上下文.这三个方法主要用于函数调用时设置 this 的值,并在不同场景下有各自的使用方法.下面详细介绍它们的使用和区别.

call

call 方法可以调用一个函数,并且在调用时为其指定 this 值和参数列表.

语法

function.call(thisArg, arg1, arg2, ...)

示例

function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

const person = { name: "Alice" };

greet.call(person, "Hello", "!");

输出

Hello, Alice!

apply

apply 方法与 call 类似,但它接收一个参数数组而不是一组参数.

语法

function.apply(thisArg, [argsArray])

示例

function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

const person = { name: "Alice" };

greet.apply(person, ["Hello", "!"]);

输出

Hello, Alice!

bind

bind 方法创建一个新函数,当这个新函数被调用时,其 this 值是绑定到 bind 的第一个参数,并且其余参数是新函数的参数.

语法

const boundFunction = function.bind(thisArg, arg1, arg2, ...)

示例

function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

const person = { name: "Alice" };

const greetPerson = greet.bind(person, "Hello");
greetPerson("!");

输出

Hello, Alice!

区别总结

  1. 参数传递

    • call: 参数逐个传递.
    • apply: 参数以数组形式传递.
    • bind: 参数逐个传递,可以预先绑定部分参数.
  2. 调用时机

    • callapply: 立即调用函数.
    • bind: 返回一个新函数,不会立即调用,需要手动调用返回的新函数.
  3. 使用场景

    • call: 当你知道参数列表并且希望立即调用函数时使用.
    • apply: 当你有一个参数数组时使用,特别是参数数量不固定的情况.
    • bind: 当你希望创建一个函数稍后调用,并且可能部分参数已预先知道时使用.

实际应用

示例 1:使用 call 改变 this 上下文

const person1 = { name: "Alice" };
const person2 = { name: "Bob" };

function sayName() {
  console.log(this.name);
}

sayName.call(person1); // Alice
sayName.call(person2); // Bob

示例 2:使用 apply 传递参数数组

const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);
const min = Math.min.apply(null, numbers);

console.log(max); // 7
console.log(min); // 2

示例 3:使用 bind 创建一个新函数

const module = {
  x: 42,
  getX: function () {
    return this.x;
  },
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined, 因为 `this` 是全局对象或 undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42, 因为 `this` 被绑定到 module

通过了解 callapplybind 的用法和区别,可以在不同的场景下灵活地改变函数的 this 上下文,编写更加灵活和高效的代码.

TypeScript 参数默认值语法

Click to expand

在 TypeScript 中,函数的参数默认值语法允许我们在定义函数时为参数提供默认值.这样做的好处是,在调用函数时可以不必传递该参数,而是使用预设的默认值.下面详细讲解 TypeScript 中参数默认值的语法和使用方法.

基本语法

在 TypeScript 中,函数参数默认值的语法与 JavaScript 相似,使用等号 = 来为参数设定默认值.例如:

function greet(name: string = "Guest") {
  console.log(`Hello, ${name}!`);
}

在上面的例子中,函数 greet 接收一个参数 name,并为其设置了默认值 'Guest'.如果调用 greet 函数时没有传递 name 参数,它将会使用默认值 'Guest'.

示例用法

下面展示了一些不同情况下的函数调用及其输出:

// 使用默认值
greet(); // Output: Hello, Guest!

// 提供参数值
greet("Alice"); // Output: Hello, Alice!

// 可以传递 undefined 或 null 来触发默认值
greet(undefined); // Output: Hello, Guest!
greet(null); // Output: Hello, null! (null 作为参数,不会触发默认值)

// 使用其他假值来触发默认值
greet(""); // Output: Hello, ! (空字符串不会触发默认值)

注意事项

  1. 默认值类型与参数类型一致

    • 默认值的类型必须与参数类型兼容.例如,如果函数参数 name 的类型为 string,那么默认值也必须是 string 类型或者允许隐式转换为 string 类型的值.
  2. 默认值计算

    • 默认值可以是常量、表达式或函数调用的结果.例如:

      function calculateTotal(price: number, taxRate: number = 0.2) {
        const total = price + price * taxRate;
        console.log(`Total: ${total}`);
      }
      
      calculateTotal(100); // Output: Total: 120
      

      在这个例子中,默认的 taxRate0.2,这是一个常量值.

  3. 默认值在可选参数中的应用

    • 如果参数声明为可选(即参数名后跟一个 ?),则它可以在未提供时是 undefined,而不会触发默认值.

      function greet(name?: string) {
        console.log(`Hello, ${name || "Guest"}!`);
      }
      
      greet(); // Output: Hello, Guest!
      

      在这个例子中,参数 name 是可选的,如果没有传递,则默认为 undefined,这时会触发 'Guest' 作为默认值.

  4. 注意 nullundefined 的区别

    • 默认值不会在参数显式传递 null 时触发,但会在参数未提供时触发.例如:

      function greet(name: string = "Guest") {
        console.log(`Hello, ${name}!`);
      }
      
      greet(null); // Output: Hello, null! (null 作为参数,不会触发默认值)
      

使用建议

  • 在编写函数时,考虑哪些参数可以有合理的默认值,以简化函数的使用.
  • 注意参数默认值的类型和逻辑,确保其行为符合预期.

参数默认值是 TypeScript 中提高函数可用性和灵活性的重要特性之一,合理使用可以提升代码的可读性和可维护性.

链接