刷面试题
刷题的重要性,不用多说。对于应届生或工作年限不长的人来说,刷面试题一方面能够尽可能地快速自己对某个技术点的理解,另一方面在面试时,有一定几率被问到相同或相似题,另外或多或少也能够为自己面试增加一些自信心,可见适当的刷题是很有必要的。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
-
前端字节跳动真题解析
-
【269页】前端大厂面试题宝典
最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。
this.ctx = ctx; // 弹幕功能类的执行上下文
}
// ********** 以下为新增代码 **********
init() {
this.opacity = this.item.opacity || this.ctx.opacity;
this.color = this.item.color || this.ctx.color;
this.fontSize = this.item.fontSize || this.ctx.fontSize;
this.speed = this.item.speed || this.ctx.speed;
// 求自己的宽度,目的是用来校验当前是否还要继续绘制(边界判断)
let span = document.createElement(“span”);
// 能决定宽度的只有弹幕的内容和文字的大小,和字体,字体默认为微软雅黑,我们就不做设置了
span.innerText = this.value;
span.style.font = this.fontSize + ‘px "Microsoft YaHei’;
// span 为行内元素,取不到宽度,所以我们通过定位给转换成块级元素
span.style.position = “absolute”;
document.body.appendChild(span); // 放入页面
this.width = span.clientWidth; // 记录弹幕的宽度
document.body.removeChild(span); // 从页面移除
// 存储弹幕出现的横纵坐标
this.x = this.ctx.canvas.width;
this.y = this.ctx.canvas.height;
// 处理弹幕纵向溢出的边界处理
if (this.y < this.fontSize) {
this.y = this.fontSize;
}
if (this.y > this.ctx.canvas.height - this.fontSize) {
this.y = this.ctx.canvas.height - this.fontSize;
}
}
// ********** 以上为新增代码 **********
}
在上面代码的 init
方法中我们其实可以看出,每条弹幕实例初始化的时候初始的信息除了之前说的弹幕的基本参数外,还获取了每条弹幕的宽度(用于后续做弹幕是否已经完全移出屏幕的边界判断)和每一条弹幕的 x
和 y
轴方向的坐标并为了防止弹幕在 y
轴显示不全做了边界处理。
6、实现每条弹幕的渲染和弹幕移除屏幕的处理
我们当时在 CanvasBarrage
类的 render
方法中的渲染每个弹幕的方法 renderBarrage
中(原谅这么啰嗦,因为到现在内容已经比较多,说的具体一点方便知道是哪个步骤,哈哈)只做了对每一条弹幕实例的初始化操作,并没有渲染在 Canvas 画布中,这时我们主要做两部操作,实现每条弹幕渲染在画布中和左侧移出屏幕不再渲染的边界处理。
// 文件:index.js
class CanvasBarrage {
constructor(canvas, video, options = {}) {
// 如果没有传入 canvas 或者 video 直接跳出
if (!canvas || !video) return;
this.canvas = canvas; // 当前的 canvas 元素
this.video = video; // 当前的 video 元素
// 设置 canvas 与 video 等高
this.canvas.width = video.clientWidth;
this.canvas.height = video.clientHeight;
// 默认暂停播放,表示不渲染弹幕
this.isPaused = true;
// 没传参数的默认值
let defaultOptions = {
fontSize: 20,
color: “gold”,
speed: 2,
opacity: 0.3,
data: []
};
// 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
Object.assign(this, defaultOptions, options);
// 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
this.barrages = this.data.map(item => new Barrage(item, this));
// Canvas 画布的内容
this.context = canvas.getContext(“2d”);
// 渲染所有的弹幕
this.render();
}
render() {
// 渲染整个弹幕
// 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染弹幕
this.renderBarrage();
if (this.isPaused == false) {
// 递归渲染
requestAnimationFrame(this.render.bind(this));
}
}
renderBarrage() {
// 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
let time = this.video.currentTime;
this.barrages.forEach(barrage => {
// ********** 以下为改动的代码 **********
// 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
if (!barrage.flag && time >= barrage.time) {
// ********** 以上为改动的代码 **********
// 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
// 如果没有初始化,先去初始化一下
if (!barrage.isInited) {
// 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
barrage.init();
barrage.isInited = true;
}
// ********** 以下为新增代码 **********
barrage.x -= barrage.speed;
barrage.render(); // 渲染该条弹幕
if (barrage.x < barrage.width * -1) {
barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
}
// ********** 以上为新增代码 **********
}
});
}
}
每个弹幕实例都有一个 speed
属性,该属性代表着弹幕移动的速度,换个说法其实就是每次减少的 x
轴的差值,所以我们其实是通过改变 x
轴的值再重新渲染而实现弹幕的左移,我们创建了一个标识 flag
挂在每个弹幕实例下,代表是否已经离开屏幕,如果离开则更改 flag
的值,使外层的 CanvasBarrage
类的 render
函数再次递归时不进入渲染程序。
每一条弹幕具体是怎么渲染的,通过代码可以看出每个弹幕实例在 x
坐标改变后都调用了实例方法 render
函数,注意此 render
非彼 render
,该 render
函数属于 Barrage
类,目的是为了渲染每一条弹幕,而 CanvasBarrage
类下的 render
,是为了在视频时间变化时清空并重新渲染整个 Canvas 画布。
7、Barrage 类下的 render 方法的实现
// 文件:index.js
class Barrage {
constructor(item, ctx) {
this.value = item.value; // 弹幕的内容
this.time = item.time; // 弹幕出现的时间
this.item = item; // 每一个弹幕的数据对象
this.ctx = ctx; // 弹幕功能类的执行上下文
}
init() {
this.opacity = this.item.opacity || this.ctx.opacity;
this.color = this.item.color || this.ctx.color;
this.fontSize = this.item.fontSize || this.ctx.fontSize;
this.speed = this.item.speed || this.ctx.speed;
// 求自己的宽度,目的是用来校验当前是否还要继续绘制(边界判断)
let span = document.createElement(“span”);
// 能决定宽度的只有弹幕的内容和文字的大小,和字体,字体默认为微软雅黑,我们就不做设置了
span.innerText = this.value;
span.style.font = this.fontSize + ‘px "Microsoft YaHei’;
// span 为行内元素,取不到宽度,所以我们通过定位给转换成块级元素
span.style.position = “absolute”;
document.body.appendChild(span); // 放入页面
this.width = span.clientWidth; // 记录弹幕的宽度
document.body.removeChild(span); // 从页面移除
// 存储弹幕出现的横纵坐标
this.x = this.ctx.canvas.width;
this.y = this.ctx.canvas.height;
// 处理弹幕纵向溢出的边界处理
if (this.y < this.fontSize) {
this.y = this.fontSize;
}
if (this.y > this.ctx.canvas.height - this.fontSize) {
this.y = this.ctx.canvas.height - this.fontSize;
}
}
// ********** 以下为新增代码 **********
render() {
this.ctx.context.font = this.fontSize + ‘px “Microsoft YaHei”’;
this.ctx.context.fillStyle = this.color;
this.ctx.context.fillText(this.value, this.x, this.y);
}
// ********** 以上为新增代码 **********
}
从上面新增代码我们可以看出,其实 Barrage
类的 render
方法只是将每一条弹幕的字号、颜色、内容、坐标等属性通过 Canvas 的 API 添加到了画布上。
8、实现播放、暂停事件
还记得我们的 CanvasBarrage
类里面有一个属性 isPaused
,属性值控制了我们是否递归渲染,这个属性与视频暂停的状态是一致的,我们在播放的时候,弹幕不断的清空并重新绘制,当暂停的时候弹幕也应该跟着暂停,说白了就是不在调用 CanvasBarrage
类的 render
方法,其实就是在暂停、播放的过程中不断的改变 isPaused
的值即可。
还记得我们之前构造的两条假数据 data
吧,接下来我们添加播放、暂停事件,来尝试使用一下我们的弹幕功能。
// 文件:index.js
// 实现一个简易选择器,方便获取元素,后面获取元素直接调用 $
const $ = document.querySelector.bind(document);
// 获取 Canvas 元素和 Video 元素
let canvas = $(“#canvas”);
let video = $(“#video”);
let canvasBarrage = new CanvasBarrage(canvas, video, {
data
});
// 添加播放事件
video.addEventListener(“play”, function() {
canvasBarrage.isPaused = false;
canvasBarrage.render();
});
// 添加暂停事件
video.addEventListener(“pause”, function() {
canvasBarrage.isPaused = true;
});
9、实现发送弹幕事件
// 文件:index.js
$(“#add”).addEventListener(“click”, function() {
let time = video.currentTime; // 发送弹幕的时间
let value = $(“#text”).value; // 发送弹幕的文字
let color = $(“#color”).value; // 发送弹幕文字的颜色
let fontSize = $(“#range”).value; // 发送弹幕的字体大小
let sendObj = { time, value, color, fontSize }; //发送弹幕的参数集合
canvasBarrage.add(sendObj); // 发送弹幕的方法
});
其实我们发送弹幕时,就是向 CanvasBarrage
类的 barrages
数组里添加了一条弹幕的实例,我们单独封装了一个 add
的实例方法。
// 文件:index.js
class CanvasBarrage {
constructor(canvas, video, options = {}) {
// 如果没有传入 canvas 或者 video 直接跳出
if (!canvas || !video) return;
this.canvas = canvas; // 当前的 canvas 元素
this.video = video; // 当前的 video 元素
// 设置 canvas 与 video 等高
this.canvas.width = video.clientWidth;
this.canvas.height = video.clientHeight;
// 默认暂停播放,表示不渲染弹幕
this.isPaused = true;
// 没传参数的默认值
let defaultOptions = {
fontSize: 20,
color: “gold”,
speed: 2,
opacity: 0.3,
data: []
};
// 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
Object.assign(this, defaultOptions, options);
// 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
this.barrages = this.data.map(item => new Barrage(item, this));
// Canvas 画布的内容
this.context = canvas.getContext(“2d”);
// 渲染所有的弹幕
this.render();
}
render() {
// 渲染整个弹幕
// 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染弹幕
this.renderBarrage();
if (this.isPaused == false) {
// 递归渲染
requestAnimationFrame(this.render.bind(this));
}
}
renderBarrage() {
// 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
let time = this.video.currentTime;
this.barrages.forEach(barrage => {
// 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
if (!barrage.flag && time >= barrage.time) {
// 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
// 如果没有初始化,先去初始化一下
if (!barrage.isInited) {
// 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
barrage.init();
barrage.isInited = true;
}
barrage.x -= barrage.speed;
barrage.render(); // 渲染该条弹幕
if (barrage.x < barrage.width * -1) {
barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
}
}
});
}
// ********** 以下为新增代码 **********
add(item) {
this.barrages.push(new Barrage(item, this));
}
// ********** 以上为新增代码 **********
}
10、拖动进度条实现弹幕的前进和后退
其实我们发现,弹幕虽然实现了正常的播放、暂停以及发送,但是当我们拖动进度条的时候弹幕应该是跟着视频时间同步播放的,现在的弹幕一旦播放过无论怎样拉动进度条弹幕都不会再出现,我们现在就来解决这个问题。
// 文件:index.js
// 拖动进度条事件
video.addEventListener(“seeked”, function() {
canvasBarrage.reset();
});
我们在事件内部其实只是调用了一下 CanvasBarrage
类的 reset
方法,这个方法就是在拖动进度条的时候来帮我们初始化弹幕的状态。
// 文件:index.js
class CanvasBarrage {
constructor(canvas, video, options = {}) {
// 如果没有传入 canvas 或者 video 直接跳出
if (!canvas || !video) return;
this.canvas = canvas; // 当前的 canvas 元素
this.video = video; // 当前的 video 元素
// 设置 canvas 与 video 等高
this.canvas.width = video.clientWidth;
this.canvas.height = video.clientHeight;
// 默认暂停播放,表示不渲染弹幕
this.isPaused = true;
// 没传参数的默认值
let defaultOptions = {
fontSize: 20,
color: “gold”,
speed: 2,
opacity: 0.3,
data: []
};
// 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
Object.assign(this, defaultOptions, options);
// 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
this.barrages = this.data.map(item => new Barrage(item, this));
// Canvas 画布的内容
this.context = canvas.getContext(“2d”);
// 渲染所有的弹幕
this.render();
}
render() {
// 渲染整个弹幕
// 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染弹幕
this.renderBarrage();
if (this.isPaused == false) {
// 递归渲染
requestAnimationFrame(this.render.bind(this));
}
}
renderBarrage() {
// 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
let time = this.video.currentTime;
this.barrages.forEach(barrage => {
// 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
if (!barrage.flag && time >= barrage.time) {
// 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
// 如果没有初始化,先去初始化一下
if (!barrage.isInited) {
// 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
barrage.init();
barrage.isInited = true;
}
barrage.x -= barrage.speed;
barrage.render(); // 渲染该条弹幕
if (barrage.x < barrage.width * -1) {
barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
}
}
});
}
add(item) {
this.barrages.push(new Barrage(item, this));
}
// ********** 以下为新增代码 **********
reset() {
// 先清空 Canvas 画布
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
let time = this.video.currentTime;
// 循环每一条弹幕实例
this.barrages.forEach(barrage => {
// 更改已经移出屏幕的弹幕状态
barrage.flag = false;
// 当拖动到的时间小于等于当前弹幕时间是,重新初始化弹幕的数据,实现渲染
if (time <= barrage.time) {
barrage.isInited = false;
} else {
barrage.flag = true; // 否则将弹幕的状态设置为以移出屏幕
}
});
}
// ********** 以上为新增代码 **********
}
其实 reset
方法中值做了几件事:
-
清空 Canvas 画布;
-
获取当前进度条拖动位置的时间;
-
循环存储弹幕实例的数组;
-
将所有弹幕更改为未移出屏幕;
-
判断拖动时间和每条弹幕的时间;
-
在当前时间以后的弹幕重新初始化数据;
-
以前的弹幕更改为已移出屏幕。
从而实现了拖动进度条弹幕的 “前进” 和 “后退” 功能。
使用 WebSocket 和 Redis 实现前后端通信及数据存储
1、服务器代码的实现
要使用 WebSocket 和 Redis 首先需要去安装 ws
、redis
依赖,在项目根目录执行下面命令:
npm install ws redis
我们创建一个 server.js
文件,用来写服务端的代码:
// 文件:index.js
const WebSocket = require(“ws”); // 引入 WebSocket
const redis = require(“redis”); // 引入 redis
// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
port: 3000
});
// 创建 redis 客户端
let client = redis.createClient(); // key value
// 原生的 websocket 就两个常用的方法 on(‘message’)、on(‘send’)
wss.on(“connection”, function(ws) {
// 监听连接
// 连接上需要立即把 redis 数据库的数据取出返回给前端
client.lrange(“barrages”, 0, -1, function(err, applies) {
// 由于 redis 的数据都是字符串,所以需要把数组中每一项转成对象
applies = applies.map(item => JSON.parse(item));
// 使用 websocket 服务器将 redis 数据库的数据发送给前端
// 构建一个对象,加入 type 属性告诉前端当前返回数据的行为,并将数据转换成字符串
ws.send(
JSON.stringify({
type: “INIT”,
data: applies
})
);
});
// 当服务器收到消息时,将数据存入 redis 数据库
ws.on(“message”, function(data) {
// 向数据库存储时存的是字符串,存入并打印数据,用来判断是否成功存入数据库
client.rpush(“barrages”, data, redis.print);
// 再将当前这条数据返回给前端,同样添加 type 字段告诉前端当前行为,并将数据转换成字符串
ws.send(
JSON.stringify({
type: “ADD”,
data: JSON.parse(data)
})
);
});
});
服务器的逻辑很清晰,在 WebSocket 连接上时,立即获取 Redis 数据库的所有弹幕数据返回给前端,当前端点击发送弹幕按钮发送数据时,接收数据存入 Redis 数据库中并打印验证数据是否成功存入,再通过 WebSocket 服务把当前这一条数返回给前端,需要注意一下几点:
-
从 Redis 数据库中取出全部弹幕数据的数组内部都存储的是字符串,需要使用
JSON.parse
方法进行解析; -
将数据发送前端时,最外层要使用
JSON.stringify
重新转换成字符串发送; -
在初始化阶段 WebSocket 发送所有数据和前端添加新弹幕 WebSocket 将弹幕的单条数据重新返回时,需要添加对应的
type
值告诉前端,当前的操作行为。
2、前端代码的修改
在没有实现后端代码之前,前端使用的是 data
的假数据,是在添加弹幕事件中,将获取的新增弹幕信息通过 CanvasBarrage
类的 add
方法直接创建 Barrage
类的实例,并加入到存放弹幕实例的 barrages
数组中。
现在我们需要更正一下交互逻辑,在发送弹幕事件触发时,我们应该先将获取的单条弹幕数据通过 WebSocket 发送给后端服务器,在服务器重新将消息返还给我们的时候,去将这条数据通过 CanvasBarrage
类的 add
方法加入到存放弹幕实例的 barrages
数组中。
还有在页面初始化时,我们之前在创建 CanvasBarrage
类实例的时候直接传入了 data
假数据,现在需要通过 WebSocket 的连接事件,在监听到连接 WebSocket 服务时,去创建 CanvasBarrage
类的实例,并直接把服务端返回 Redis 数据库真实的数据作为参数传入,前端代码修改如下:
// 文件:index.js
// ********** 下面代码被删掉了 **********
// let canvasBarrage = new CanvasBarrage(canvas, video, {
// data
// });
// ********** 上面代码被删掉了 **********
// ********** 以下为新增代码 **********
let canvasBarrage;
// 创建 WebSocket 连接
let socket = new WebSocket(“ws://localhost:3000”);
// 监听连接事件
socket.onopen = function() {
// 监听消息
socket.onmessage = function(e) {
// 将收到的消息从字符串转换成对象
let message = JSON.parse(e.data);
// 根据不同情况判断是初始化还是发送弹幕
if (message.type === “INIT”) {
// 创建 CanvasBarrage 的实例添加弹幕功能,传入真实的数据
canvasBarrage = new CanvasBarrage(canvas, video, {
data: message.data
});
} else if (message.type === “ADD”) {
// 如果是添加弹幕直接将 WebSocket 返回的单条弹幕存入 barrages 中
canvasBarrage.add(message.data);
}
};
};
// ********** 以上为新增代码 **********
$(“#add”).addEventListener(“click”, function() {
let time = video.currentTime; // 发送弹幕的时间
let value = $(“#text”).value; // 发送弹幕的文字
let color = $(“#color”).value; // 发送弹幕文字的颜色
let fontSize = $(“#range”).value; // 发送弹幕的字体大小
let sendObj = { time, value, color, fontSize }; //发送弹幕的参数集合
// ********** 以下为新增代码 **********
socket.send(JSON.stringify(sendObj));
// ********** 以上为新增代码 **********
// ********** 下面代码被删掉了 **********
// canvasBarrage.add(sendObj); // 发送弹幕的方法
// ********** 上面代码被删掉了 **********
});
现在我们可以打开 index.html
文件并启动 server.js
服务器,就可以实现真实的视频弹幕操作了,但是我们还是差了最后一步,当前的服务只能同时服务一个人,但真实的场景是同时看视频的有很多人,而且发送的弹幕是共享的。
3、实现多端通信、弹幕共享
我们需要处理两件事情:
-
第一件事情是实现多端通信共享数据库信息;
-
第二件事情是当有人离开的时候清除关闭的 WebSocket 对象。
// 文件:server.js
const WebSocket = require(“ws”); // 引入 WebSocket
const redis = require(“redis”); // 引入 redis
// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
port: 3000
});
// 创建 redis 客户端
let client = redis.createClient(); // key value
// ********** 以下为新增代码 **********
// 存储所有 WebSocket 用户
let clientsArr = [];
// ********** 以上为新增代码 **********
// 原生的 websocket 就两个常用的方法 on(‘message’)、on(‘send’)
wss.on(“connection”, function(ws) {
// ********** 以下为新增代码 **********
// 将所有通过 WebSocket 连接的用户存入数组中
clientsArr.push(ws);
// ********** 以上为新增代码 **********
// 监听连接
// 连接上需要立即把 redis 数据库的数据取出返回给前端
最后
面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
96道前端面试题:
常用算法面试题:
前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化
const WebSocket = require(“ws”); // 引入 WebSocket
const redis = require(“redis”); // 引入 redis
// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
port: 3000
});
// 创建 redis 客户端
let client = redis.createClient(); // key value
// ********** 以下为新增代码 **********
// 存储所有 WebSocket 用户
let clientsArr = [];
// ********** 以上为新增代码 **********
// 原生的 websocket 就两个常用的方法 on(‘message’)、on(‘send’)
wss.on(“connection”, function(ws) {
// ********** 以下为新增代码 **********
// 将所有通过 WebSocket 连接的用户存入数组中
clientsArr.push(ws);
// ********** 以上为新增代码 **********
// 监听连接
// 连接上需要立即把 redis 数据库的数据取出返回给前端
最后
面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
96道前端面试题:
- [外链图片转存中…(img-OY1VriH4-1714825786050)]
常用算法面试题:
- [外链图片转存中…(img-LNvVI44Z-1714825786051)]
前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化
- [外链图片转存中…(img-j2rtQqhu-1714825786052)]