目录
其一见此
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-window
、react-virtualized
、vue-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的主要字段:
-
Name(名称)
- 描述:Cookie 的名称,标识这个 Cookie 的唯一键.
- 示例:
sessionId
-
Value(值)
- 描述:Cookie 的值,存储具体的数据.
- 示例:
38afes7a8
-
Domain(域)
- 描述:指定哪些域可以访问这个 Cookie.如果没有指定,则默认是创建该 Cookie 的域(不含子域).
- 示例:
.example.com
(允许所有子域访问)
-
Path(路径)
- 描述:指定这个 Cookie 对哪个路径可见.默认是创建该 Cookie 的路径(不含子路径).
- 示例:
/account
(只有在访问/account
路径时,浏览器才会发送这个 Cookie)
-
Expires(过期时间)
- 描述:设置 Cookie 的过期时间,绝对时间.如果设置了这个字段,当时间到期时,浏览器会自动删除这个 Cookie.
- 示例:
Expires=Wed, 09 Jun 2021 10:18:14 GMT
-
Max-Age
- 描述:设置 Cookie 的过期时间,相对时间(秒).比
Expires
更精确,优先级也更高. - 示例:
Max-Age=3600
(表示 Cookie 会在 3600 秒后过期)
- 描述:设置 Cookie 的过期时间,相对时间(秒).比
-
Secure(安全)
- 描述:指定 Cookie 只能通过 HTTPS 协议发送.设置了这个字段后,Cookie 只能在加密的请求中发送.
- 示例:
Secure
-
HttpOnly
- 描述:指定 Cookie 不能通过 JavaScript 的
Document.cookie
访问.防止跨站脚本(XSS)攻击获取 Cookie. - 示例:
HttpOnly
- 描述:指定 Cookie 不能通过 JavaScript 的
-
SameSite
- 描述:防止跨站请求伪造(CSRF)攻击,指定 Cookie 的发送策略.
Strict
:完全禁止第三方网站请求带上这个 Cookie.Lax
:大部分情况禁止第三方网站请求带上这个 Cookie,除非是导航到目标网址的 GET 请求.None
:允许跨站请求携带这个 Cookie,但必须设置Secure
字段.
- 示例:
SameSite=Lax
- 描述:防止跨站请求伪造(CSRF)攻击,指定 Cookie 的发送策略.
示例
下面是一个包含所有主要字段的 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. 申请并配置微信开放平台
- 申请微信开放平台账号:注册并登录微信开放平台.
- 创建应用:在微信开放平台创建一个新的应用,获取
AppID
和AppSecret
. - 配置域名:在微信开放平台的应用设置中配置授权回调域名.
2. 在前端显示二维码
- 向微信请求二维码:前端向后端发起请求,后端请求微信接口生成登录二维码.
- 显示二维码:前端将生成的二维码展示给用户.
3. 用户扫描二维码
- 打开微信扫一扫:用户使用微信客户端扫描二维码.
- 确认授权:用户在微信客户端确认授权登录.
4. 微信回调并获取临时登录凭证(code
)
- 微信回调:用户确认授权后,微信会重定向到回调地址,并携带临时凭证(
code
). - 获取临时凭证:前端通过URL参数获取
code
.
5. 后端使用临时凭证获取访问令牌
- 请求微信接口:后端使用
AppID
、AppSecret
和code
请求微信的接口,获取访问令牌和用户信息.
6. 获取用户信息并登录
- 请求用户信息:后端使用访问令牌请求微信用户信息接口,获取用户的详细信息.
- 创建或更新用户:根据获取的用户信息,创建新用户或更新已有用户的信息.
- 生成会话:后端生成登录会话,返回给前端,完成登录流程.
详细流程图
-
前端显示二维码
- 前端请求后端API:
GET /api/wechat/qrcode
- 后端调用微信接口生成二维码并返回给前端
- 前端显示二维码
- 前端请求后端API:
-
用户扫描二维码
- 用户使用微信客户端扫描二维码
-
微信回调并获取
code
- 微信重定向到回调地址:
https://yourdomain.com/callback?code=CODE
- 前端获取URL中的
code
- 微信重定向到回调地址:
-
后端获取访问令牌
- 前端将
code
发送给后端:POST /api/wechat/login
- 后端请求微信接口:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
- 微信返回访问令牌和用户信息
- 前端将
-
后端获取用户信息并登录
- 后端使用访问令牌请求微信用户信息接口:
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}`);
});
总结
微信扫码登录的流程主要包括:
- 申请并配置微信开放平台.
- 前端显示二维码.
- 用户扫描二维码并确认授权.
- 微信回调并获取临时登录凭证.
- 后端使用凭证获取访问令牌和用户信息.
- 后端创建或更新用户信息,并生成登录会话.
通过上述步骤,可以实现一个完整的微信扫码登录模块.
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
在编程中,拷贝对象时有两种主要方式:浅拷贝和深拷贝.了解这两种拷贝方式对于处理复杂数据结构非常重要,特别是在需要维护数据一致性和防止意外修改时.
浅拷贝
浅拷贝是对对象的顶层属性进行拷贝,如果属性是引用类型(如对象或数组),那么拷贝的是引用地址,因此修改其中一个对象的引用类型属性,会影响到另一个对象.
实现方式
- 使用Object.assign()
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
copy.b.c = 3;
console.log(original.b.c); // 输出: 3,原对象也被修改
- 使用扩展运算符(spread syntax)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.b.c = 3;
console.log(original.b.c); // 输出: 3,原对象也被修改
深拷贝
深拷贝会递归地拷贝对象的所有层级,包括所有嵌套的对象和数组,因此拷贝后的对象与原对象完全独立,互不影响.
实现方式
- 使用 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,原对象未被修改
注意:这种方法不适用于拷贝包含函数、undefined
、Date
等特殊对象类型的对象.
- 使用递归函数
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-block
和align-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
返回一个对象,这个对象包含两个方法 increment
和 decrement
.这两个方法都是闭包,因为它们可以访问 createCounter
作用域中的 count
变量.
闭包的应用场景
-
数据封装: 闭包可以用于创建私有变量和方法,从而实现数据封装.
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
-
函数工厂: 闭包可以用于创建函数工厂,根据传入的参数生成不同的函数.
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
-
回调函数和事件处理: 闭包常用于回调函数和事件处理,确保在异步操作中仍然能够访问正确的变量.
function delayedGreeting(name) { setTimeout(function () { console.log("Hello, " + name); }, 1000); } delayedGreeting("Alice"); // 1秒后输出: Hello, Alice
闭包的注意事项
-
内存泄漏: 由于闭包会保留对其外部作用域变量的引用,可能会导致内存泄漏.因此,需要注意避免过度使用闭包,尤其是在创建大量对象时.
-
性能问题: 使用闭包可能会增加内存使用,影响性能.在高性能要求的场景中,需要谨慎使用闭包.
总结
闭包是 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 的使用、实际应用场景以及性能优化等方面.以下是一些常见的面试问题以及相应的解答和示例代码:
基础知识
-
什么是 Canvas?Canvas 的用途是什么?
- 回答:Canvas 是 HTML5 提供的一个用于绘制图形的元素.通过 JavaScript 可以在上面绘制各种图形,如线条、矩形、圆形、文本、图像等,常用于游戏开发、数据可视化、图像处理等.
-
如何在 HTML 中创建 Canvas 元素?
- 回答:可以使用
<canvas>
标签创建 Canvas 元素,并设置宽度和高度. - 示例:
- 回答:可以使用
<canvas id="myCanvas" width="500" height="400"></canvas>
- 如何获取 Canvas 的绘图上下文?有什么类型的绘图上下文?
- 回答:可以使用
getContext
方法获取绘图上下文,常用的上下文类型是 “2d” 和 “webgl”. - 示例:
- 回答:可以使用
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
API 使用
- 如何绘制一个矩形?
- 回答:可以使用
fillRect
方法绘制填充的矩形,或使用strokeRect
方法绘制矩形边框. - 示例:
- 回答:可以使用
ctx.fillStyle = "blue";
ctx.fillRect(50, 50, 150, 100);
ctx.strokeStyle = "red";
ctx.strokeRect(50, 50, 150, 100);
- 如何绘制一条直线?
- 回答:可以使用
moveTo
方法设置起点,使用lineTo
方法设置终点,然后用stroke
方法绘制. - 示例:
- 回答:可以使用
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 200);
ctx.stroke();
- 如何绘制圆形或弧形? - 回答:可以使用
arc
方法绘制圆形或弧形. - 示例:
ctx.beginPath();
ctx.arc(150, 100, 50, 0, Math.PI * 2); // 圆形 ctx.fill();
void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
- 如何绘制文本?
- 回答:可以使用
fillText
或strokeText
方法绘制填充或边框文本. - 示例:
- 回答:可以使用
ctx.font = "30px Arial";
ctx.fillStyle = "green";
ctx.fillText("Hello Canvas", 50, 150);
ctx.strokeStyle = "blue";
ctx.strokeText("Hello Canvas", 50, 200);
- 如何绘制图像?
- 回答:可以使用
drawImage
方法绘制图像. - 示例:
- 回答:可以使用
const img = new Image();
img.src = "path/to/image.jpg";
img.onload = () => {
ctx.drawImage(img, 50, 50, 200, 150);
};
实际应用
- 如何清空 Canvas?
- 回答:可以使用
clearRect
方法清空 Canvas. - 示例:
- 回答:可以使用
ctx.clearRect(0, 0, canvas.width, canvas.height);
- 如何在 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();
性能优化
- Canvas 性能优化的一些方法是什么?
- 回答:可以使用离屏 Canvas、减少重绘区域、优化绘图算法、使用
requestAnimationFrame
进行动画控制等方法进行性能优化.
- 回答:可以使用离屏 Canvas、减少重绘区域、优化绘图算法、使用
高级问题
- Canvas 与 SVG 的区别是什么?什么时候使用 Canvas,什么时候使用 SVG?
- 回答:Canvas 是立即模式绘图,适合大量像素操作和动画,SVG 是保留模式绘图,适合静态矢量图和可交互的图形.Canvas 用于游戏、图像处理等需要高性能绘制的场景,SVG 用于需要高质量和可缩放图形的场景.
示例项目
- 如何在 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应用无缝集成.
工作原理示例
- 编译代码:将C/C++/Rust等代码编译为WebAssembly模块(
.wasm
文件). - 加载和实例化:在浏览器中使用JavaScript API加载并实例化WebAssembly模块.
- 调用函数:通过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
的具体步骤:
- 保存原始的
Array.prototype.forEach
方法. - 定义新的
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);
});
代码解释
-
保存原始方法:
const originalForEach = Array.prototype.forEach;
这一步保存了原始的
forEach
方法,以防止丢失其原有功能. -
重写
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); });
这段代码演示了如何使用新的
forEach
方法遍历数组,并打印出自定义信息和回调函数中的元素.
注意事项
- 类型检查:新的
forEach
方法中添加了对callback
是否为函数的检查,以确保调用时不会出现类型错误. - 索引检查:在遍历过程中检查索引是否在数组中(
if (i in this)
),以处理稀疏数组的情况. - 性能影响:重写
Array.prototype.forEach
会影响所有数组的遍历行为,因此要谨慎使用,特别是在性能敏感的代码中. - 兼容性问题:如果多个库或代码片段都尝试重写
forEach
,可能会导致冲突和不可预期的行为.
通过上述步骤,你可以在确保不破坏原始功能的前提下,重写 Array.prototype.forEach
方法,并添加自定义逻辑.
Click to expand
要高效地解决两个数组的交集问题,最常见的方法是利用集合(Set)的特性.集合可以高效地进行查找操作,从而使交集运算变得更快.以下是详细步骤:
- 将一个数组转换为集合(Set),以利用集合的高效查找特性.
- 遍历另一个数组,检查元素是否在集合中.
这种方法的时间复杂度接近 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]
代码解释
-
转换为集合:
const set1 = new Set(arr1);
这一步将第一个数组转换为集合,以利用集合的高效查找特性.
-
遍历和查找:
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);
函数解释
flattenAndMerge
函数接受一个嵌套对象作为参数,并返回扁平化并合并相同项后的结果.flatten
函数是一个递归函数,用于遍历对象并将其扁平化.它接受两个参数:要扁平化的对象和当前属性的前缀.- 在
flatten
函数中,使用for...in
循环遍历对象的每个属性. - 如果属性值是一个对象,则递归调用
flatten
函数. - 如果属性值是一个基本类型,则将其添加到结果对象中,如果已经存在相同键的属性,则将其值合并为数组.
- 最后,将合并的数组拼接为单个值.
示例结果
对于给定的示例对象:
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)
事件循环是一个持续运行的循环,负责从任务队列中取出任务并执行.它的工作原理如下:
- 从宏任务队列中取出一个任务,将其推入调用栈中执行,直到调用栈为空.
- 当调用栈为空时,事件循环会检查微任务队列是否为空,如果不为空,则依次取出微任务执行,直到微任务队列为空.
- 如果宏任务队列和微任务队列都为空,则事件循环会等待新的任务被添加到任务队列中,然后继续执行.
示例
下面是一个简单的示例,演示了 JavaScript 中的事件循环机制:
console.log("Script Start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function () {
console.log("Promise");
});
console.log("Script End");
上述示例中,首先会打印出 "Script Start"
,然后执行 setTimeout
和 Promise
,最后打印出 "Script End"
.具体执行顺序如下:
- 执行同步代码,打印
"Script Start"
和"Script End"
. - 将
setTimeout
的回调函数推入宏任务队列. - 将 Promise 的回调函数推入微任务队列.
- 执行微任务队列中的所有任务,打印
"Promise"
. - 事件循环检查宏任务队列中是否有任务,发现有任务,取出
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
解释
-
暴力搜索方法:
isPalindrome
辅助函数检查一个字符串是否为回文.- 双重循环遍历所有子串,使用
isPalindrome
检查是否为回文. - 如果找到一个长度大于 1 的回文子串,返回
true
.
-
中心扩展方法:
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);
});
执行顺序和解释
-
同步代码执行:所有同步代码首先执行.
console.log(1);
输出1
console.log(7);
输出7
new Promise(...).then(...);
将then
回调console.log(8);
添加到微任务队列.
-
process.nextTick
回调:在当前事件循环结束前执行所有的process.nextTick
回调.process.nextTick(function() { console.log(6); });
输出6
-
Promise
微任务:在当前事件循环结束前,执行所有的Promise
微任务.Promise.resolve().then(function() { console.log(8); });
输出8
-
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);
添加到微任务队列.
-
第二次事件循环的微任务:在当前事件循环结束前,执行所有的
process.nextTick
和Promise
微任务.process.nextTick(function() { console.log(3); });
输出3
Promise.resolve().then(function() { console.log(5); });
输出5
最终输出顺序:
1
7
6
8
2
4
3
5
解释
-
同步代码首先执行:
console.log(1);
和console.log(7);
是同步代码,因此最先执行.
-
process.nextTick
回调:process.nextTick
回调在当前事件循环的尾部执行,优先于Promise
微任务.process.nextTick(function() { console.log(6); });
输出6
-
Promise
微任务:Promise
的then
回调作为微任务,在process.nextTick
回调之后执行.Promise.resolve().then(function() { console.log(8); });
输出8
-
setTimeout
宏任务:setTimeout
回调在下一次事件循环中执行.console.log(2);
输出2
console.log(4);
输出4
process.nextTick(function() { console.log(3); });
和Promise.resolve().then(function() { console.log(5); });
添加到微任务队列.
-
第二次事件循环的微任务:
- 在当前事件循环结束前,执行所有的
process.nextTick
和Promise
微任务. 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
call
、apply
和 bind
是 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!
区别总结
-
参数传递:
call
: 参数逐个传递.apply
: 参数以数组形式传递.bind
: 参数逐个传递,可以预先绑定部分参数.
-
调用时机:
call
和apply
: 立即调用函数.bind
: 返回一个新函数,不会立即调用,需要手动调用返回的新函数.
-
使用场景:
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
通过了解 call
、apply
和 bind
的用法和区别,可以在不同的场景下灵活地改变函数的 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, ! (空字符串不会触发默认值)
注意事项
-
默认值类型与参数类型一致:
- 默认值的类型必须与参数类型兼容.例如,如果函数参数
name
的类型为string
,那么默认值也必须是string
类型或者允许隐式转换为string
类型的值.
- 默认值的类型必须与参数类型兼容.例如,如果函数参数
-
默认值计算:
-
默认值可以是常量、表达式或函数调用的结果.例如:
function calculateTotal(price: number, taxRate: number = 0.2) { const total = price + price * taxRate; console.log(`Total: ${total}`); } calculateTotal(100); // Output: Total: 120
在这个例子中,默认的
taxRate
是0.2
,这是一个常量值.
-
-
默认值在可选参数中的应用:
-
如果参数声明为可选(即参数名后跟一个
?
),则它可以在未提供时是undefined
,而不会触发默认值.function greet(name?: string) { console.log(`Hello, ${name || "Guest"}!`); } greet(); // Output: Hello, Guest!
在这个例子中,参数
name
是可选的,如果没有传递,则默认为undefined
,这时会触发'Guest'
作为默认值.
-
-
注意
null
和undefined
的区别:-
默认值不会在参数显式传递
null
时触发,但会在参数未提供时触发.例如:function greet(name: string = "Guest") { console.log(`Hello, ${name}!`); } greet(null); // Output: Hello, null! (null 作为参数,不会触发默认值)
-
使用建议
- 在编写函数时,考虑哪些参数可以有合理的默认值,以简化函数的使用.
- 注意参数默认值的类型和逻辑,确保其行为符合预期.
参数默认值是 TypeScript 中提高函数可用性和灵活性的重要特性之一,合理使用可以提升代码的可读性和可维护性.