大家好,我是前端架构师,关注微信公众号【程序员大卫】:
- 回复 [面试] :免费领取“前端面试大全2025(Vue,React等)”
- 回复 [架构师] :免费领取“前端精品架构师资料”
- 回复 [书] :免费领取“前端精品电子书”
- 回复 [软件] :免费领取“Window和Mac精品安装软件”
如何实现深拷贝
JSON.parse(JSON.stringify()) 实现深拷贝的缺点是:
- 无法拷贝函数
- 无法处理 undefined、Symbol、BigInt
- 会丢失原型链
- Date 会变成字符串
- 正则表达式会变成空对象
- 循环引用会报错
创建一个自定义的 deepClone 函数:
type AnyObject = Record<PropertyKey, any>;
function deepClone<T>(obj: T, hash = new WeakMap()): T {
// 处理原始类型和函数
if (Object(obj) !== obj || obj instanceof Function) {
return obj;
}
// 循环引用处理
if (hash.has(obj)) {
return hash.get(obj);
}
let result: any;
if (obj instanceof Date) { // Date 处理
result = new Date(obj);
} else if (obj instanceof RegExp) { // RegExp 处理
result = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) { // Map 处理
result = new Map();
hash.set(obj, result);
obj.forEach((value, key) => {
result.set(deepClone(key, hash), deepClone(value, hash));
});
return result;
} else if (obj instanceof Set) { // Set 处理
result = new Set();
hash.set(obj, result);
obj.forEach((value) => {
result.add(deepClone(value, hash));
});
return result;
} else if (Array.isArray(obj)) {
result = [];
} else {
// 保留原型链
result = Object.create(Object.getPrototypeOf(obj));
}
hash.set(obj, result);
// 拷贝所有属性,包括 Symbol 和不可枚举属性
Reflect.ownKeys(obj).forEach((key) => {
const val = (obj as AnyObject)[key];
(result as AnyObject)[key] = deepClone(val, hash);
});
return result;
}
使用示例:
const a = {
num: 1,
str: 'hello',
bool: true,
date: new Date(),
regex: /abc/gi,
arr: [1, 2, { nested: 'value' }],
func: () => console.log('hi'),
[Symbol('id')]: 123,
};
a.self = a; // 循环引用
const b = deepClone(a);
console.log(b);
对象的属性遍历顺序
-
JavaScript 中对象属性的遍历顺序确实不是单纯按照书写顺序。
-
数字类属性(array index-like keys)会被提前,并按照升序排列。
-
其他字符串键则按照书写顺序排列(即插入顺序)。
var obj = {
q2: '',
2: '',
1: '',
q1: ''
}
for(let i in obj){
console.log(i);
}
/*
1
2
q2
q1
*/
定义一个函数:只会合并source中与target类型匹配的属性
function assignExisting<T extends object>(target: T, source: Partial<T>) {
Object.entries(source).forEach(([key, value]) => {
if (target.hasOwnProperty(key)) {
target[key as keyof T] = value as T[keyof T];
}
});
return target;
}
const target = assignExisting({ a: 1, b: 2 }, { a: 1000 });
console.log("target: ", target); // { a: 100, b: 2 }
自定义网页的默认字体
@font-face {
font-family: "SourceHanSans";
src: url("@/assets/fonts/SourceHanSansCN-Regular.otf");
}
@font-face {
font-family: "SourceHanSans";
src: url("@/assets/fonts/SourceHanSansCN-Bold.otf");
font-weight: bold;
}
html {
font-family: "SourceHanSans", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
}
项目中的性能优化
实践
- Performance 性能监测和查看内存泄漏。
- 渲染的任务分片,控制元素渲染的优先级。
代码层面
- 骨架屏
- 使用路由懒加载、异步组件
- 防抖,截流
- 虚拟滚动
- 图片懒加载
网络层面
- cdn 加载
- gzip 压缩
- 资源缓存
SSE(Server-Sent Events) 和 websocket 的区别
SSE 单向走 HTTP,简单文本推;WebSocket 双向 TCP,功能更强大。
git fetch 和 git pull 的区别
在使用 Git 时,可以理解为存在三个目录:
- 本地工作目录:你正在编辑和修改文件的地方。
- 本地仓库(Repository):通过
git commit将修改保存后生成的提交记录所存储的地方。 - 远程仓库:托管在远程服务器(如 GitHub、GitLab 等)上的仓库。
git fetch
git fetch 的作用是从远程仓库获取最新的提交记录,并更新到本地仓库中的远程分支(如 origin/main),不会影响当前工作目录或本地分支。
git pull
git pull 则是 git fetch 和 git merge 的组合命令。它会先执行 fetch 获取远程更新,然后立即将这些更新合并到你当前所在的本地分支中(可能是自动合并,也可能触发冲突)。
强缓存 和 协商缓存
强缓存
从本地资源获取,不再请求服务端资源。
- 第 1 次请求服务端资源,服务端返回
cache-control: max-age=200(注意单位是秒) - 第 2 次请求服务端资源,如果时间还没过期, 则直接从本地缓存里面读取,返回
200 from disk cache
协商缓存
和服务器协商,服务器根据 Etag 返回 304 或者 200。Etag 是资源的唯一标识。
- 如果
max-age的秒已经失效了, 那么浏览器请求服务端会携带If-None-Match:W/"10-18b811fd103" - 如果资源没有发生变化,服务端返回 304 和 Etag,否则服务端返回 200 和 新的 Etag。
- 浏览器从本地缓存里读取资源。
示例代码
const express = require("express");
const app = express();
var options = {
setHeaders: (res) => {
res.set({
"Cache-Control": "max-age=10"
});
}
};
app.use(express.static(__dirname, options));
app.listen(3000);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>index</title>
</head>
<body>
<script src="1.js"></script>
</body>
</html>
HTTPS 的通俗的理解
当你访问一个 HTTPS 网站时,比如淘宝,其实背后发生了这样一套流程:
-
你打开一个 HTTPS 网站,浏览器尝试与服务器建立加密连接。
-
服务器返回数字证书,这个证书是淘宝事先向权威机构(CA,证书颁发机构)申请的,包含以下信息:
- 淘宝的公钥
- 网站的域名信息
- 有效期
- 以及最重要的:由 CA 使用私钥生成的数字签名(用于验证证书是否可信、是否被篡改)
-
浏览器验证数字证书的有效性:
- 使用浏览器内置的 CA 根证书中的公钥,对数字签名进行验证;
- 如果验证通过,说明证书确实由可信 CA 颁发,且内容没有被篡改。
-
建立安全通道(协商对称密钥):
- 浏览器和服务器通过一种安全的密钥交换算法(比如 ECDHE)协商出一个对称加密密钥 R;
- 在某些简化场景中,可以理解为:浏览器生成 R,用证书中的公钥加密后发送给服务器,服务器用私钥解密得到 R。
-
建立加密通信:
- 双方都拿到了这个对称密钥 R;
- 接下来的所有数据(如账号、密码、交易内容)都通过 R 加密后再传输,确保内容保密、完整、不被篡改。
一句话总结:
HTTPS 用 数字证书 验证身份,用 非对称加密安全交换密钥,再用 对称加密传输数据。
密码登录和存储
在进行密码登录时,通常密码会经过加密传输,而用户名则是明文传输。那么,密码是如何加密传输的呢?
加密传输的步骤:
-
客户端请求:用户向服务端发送请求,服务端返回公钥给客户端。
-
客户端加密:前端使用 RSA(非对称加密)加密密码后,将加密后的数据发送给服务端。
import JSEncrypt from "jsencrypt";
const encrypt = new JSEncrypt();
encrypt.setPublicKey("公钥");
const encryptedPassword = encrypt.encrypt("登录密码");
// 发送 POST 请求,携带用户名和加密后的密码给服务端
-
服务端解密:服务端接收到加密的密码后,使用私钥进行解密。如果能成功解密,说明密码在传输过程中没有被篡改。
-
密码存储:服务端将解密后的密码与一个随机字符串(盐)拼接在一起,使用哈希算法进行加密,最终将加密后的值存储在数据库中。
注意:每个用户的盐值是唯一的,盐值和哈希后的密码会一同存储,以便后续验证用户的密码。
Session 和 Cookie
-
Cookie:
sessionId通常以 Cookie 的形式存储在客户端浏览器中。这个 Cookie 会在每次请求时自动发送给服务端,用于标识用户的会话。 -
Session:
Session是存储在 服务端 的数据。服务端通过sessionId(从 Cookie 中获取)查找到与之对应的用户信息。服务端的存储通常是以键值对的形式保存,其中键是sessionId,值是与用户相关的各种信息(如登录状态、权限等)。
IM 即时通讯的原理
即时通讯(IM)系统的工作原理通常是“先存储,后同步”。具体过程如下:
-
消息存储库:所有会话的消息会在云端进行全量存储,确保消息的持久性和安全性。
-
消息同步库:对于离线的接收方,系统会拉取并同步这些离线消息。当接收方重新在线时,会获取所有未读消息。
-
在线推送:对于在线的接收方,系统会直接推送消息,确保即时性和实时性。
Http 协议与 TCP 协议简单理解
-
TCP 协议: 工作在传输层,是一种面向连接的协议,确保数据可靠传输。
-
HTTP 协议: 工作在应用层,建立在 TCP 协议之上。HTTP 协议通过 TCP 建立一个到服务器的连接通道。当本次请求需要的数据传输完毕后,HTTP 会立即断开 TCP 连接,这个过程非常短暂,因此 HTTP 被称为短连接和无状态连接。
为什么要三次握手?
三次握手的目的是为了确保客户端和服务器双方的发送和接收都正常,建立可靠的连接。
三次握手过程:
第一次握手:客户端向服务器发送 SYN(同步)报文,服务器接收后确认对方发送正常,自己也能接收。
第二次握手:服务器向客户端发送 SYN-ACK(同步确认)报文,客户端接收后确认自己发送正常,并确认对方发送和接收正常。
第三次握手:客户端向服务器发送 ACK(确认)报文,确认自己和对方都能正常发送和接收数据。此时,双方的连接已建立,可以开始数据传输。
HTTP 状态码
| 分类 | 状态码 | 说明 | 备注 |
|---|---|---|---|
| 表示消息 | 101 | WebSocket 或 HTTP/2 升级 | 用于协议升级 |
| 表示成功 | 200 | 请求成功 | 表示请求成功 |
| 206 | 断点续传 | 用于分块传输,支持断点续传 | |
| 表示重定向 | 301 | 永久重定向 | 新域名替代旧域名 |
| 302 | 临时重定向 | 如用户访问用户中心时重定向到登录页面 | |
| 304 | 资源未修改 | 客户端可以使用本地缓存 | |
| 301 与 302 的区别 | 301 表示资源被永久挪到新 URL,302 表示资源临时挪动 | 301 会缓存重定向地址,302 不会缓存 | |
| 表示请求错误 | 404 | 服务器找不到资源 | 请求的资源不存在 |
| 表示服务器错误 | 500 | 服务器内部错误 | 服务器发生了意外错误 |
| 503 | 服务器停机维护 | 服务器暂时无法提供服务 | |
| 504 | 网关超时 | 网关或代理服务器无法在规定时间内获得响应 |
前端安全
在前端开发中,确保应用安全是至关重要的。以下是常见的前端安全风险以及相应的防护措施。
1. XSS(跨站脚本攻击)
XSS 攻击指的是攻击者通过注入恶意脚本到网页中,从而窃取用户数据、执行恶意操作,或者劫持用户会话。
防护措施:
- 输入过滤与输出编码:对所有用户输入进行严格验证,避免恶意代码注入。在渲染动态内容时,确保对 HTML、JavaScript 等进行编码或转义。
- 使用 CSP(内容安全策略):设置 CSP 可以有效阻止恶意脚本的执行。
script-src限制加载的脚本来源。
Content-Security-Policy: script-src 'self' https://trusted-cdn.com;
2. CSRF(跨站请求伪造)
CSRF 攻击通过诱导已登录用户发起恶意请求,通常利用用户已认证的会话进行操作。
防护措施:
- 使用 CSRF Token:每个敏感请求都携带一个随机生成的 Token,服务器验证该 Token 是否正确。
- SameSite Cookie:设置 Cookie 的
SameSite属性为Strict或Lax,限制第三方站点访问 Cookie。
Set-Cookie: sessionId=123456; SameSite=Strict;
3. 点击劫持(Clickjacking)
攻击者通过将目标网页嵌入到一个透明的 iframe 中,欺骗用户点击隐藏的按钮或链接,执行恶意操作。
防护措施:
- X-Frame-Options:防止网页被嵌入到 iframe 中,确保页面不会被劫持。
X-Frame-Options: DENY;
- CSP(frame-ancestors):另一种方法是通过 CSP 指定页面能被嵌入的源。
Content-Security-Policy: frame-ancestors 'none';
4. 内容安全策略(CSP)
CSP 可以帮助控制网页中哪些资源是可以加载的,极大地减少 XSS 攻击和资源注入的风险。
防护措施:
- 启用 CSP:强制浏览器只允许从指定的来源加载脚本、图片、样式等资源。
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com;
5. 文件上传安全
上传文件时,攻击者可能上传恶意代码或执行脚本,可能导致远程代码执行等安全问题。
防护措施:
- 限制文件类型:仅允许上传特定类型的文件(例如,图片文件)。
- 文件重命名:上传文件时重命名文件,避免文件名被攻击者利用。
- 文件内容校验:检查上传文件的内容类型(如图片、PDF 等)是否符合预期,而不仅仅检查文件扩展名。
- 服务器端校验:在服务器端验证上传的文件,确保其合法性,避免执行权限的上传。
6. 防止信息泄露
前端应用可能通过错误的配置或不当的处理方式泄露敏感信息,如 API 密钥、用户信息等。
防护措施:
- 避免暴露敏感信息:不要在前端代码中暴露敏感数据,如数据库密码、API 密钥等。使用环境变量存储敏感信息。
- HTTP-only Cookies:敏感信息应存储在 HTTP-only 和 Secure 属性的 Cookies 中,防止通过 JavaScript 访问。
Set-Cookie: sessionId=123456; HttpOnly; Secure;
7. HTTP 严格传输安全(HSTS)
HSTS(HTTP Strict Transport Security)是一种防止中间人攻击(MITM)的机制,通过强制浏览器只通过 HTTPS 访问站点。
防护措施:
- 启用 HSTS:通过设置 HTTP 头部告知浏览器在未来一段时间内只能通过 HTTPS 访问站点。
Strict-Transport-Security: max-age=31536000; includeSubDomains;
8. 使用 HTTPS 强制加密
所有与服务器的通信都应该使用 HTTPS 来保证数据的安全传输。通过加密保护传输中的数据,防止窃听和数据篡改。
防护措施:
- 强制 HTTPS:通过 HTTP 跳转到 HTTPS 页面,确保数据传输过程中的安全。
- 证书更新和管理:定期更新 SSL/TLS 证书,确保使用强加密算法(如 AES、RSA)。
9. 浏览器缓存控制
缓存敏感信息可能导致信息泄露,尤其是在用户登录后。要控制哪些内容可以缓存,哪些内容需要实时加载。
防护措施:
- 禁止缓存敏感页面:对于登录页面、用户信息等敏感数据页面,使用适当的缓存控制头部。
Cache-Control: no-store, no-cache, must-revalidate;
10. 保护 HTTP Headers
某些 HTTP headers 可以增强 Web 安全性,避免攻击者利用弱点。
防护措施:
- X-XSS-Protection:启用浏览器的内建 XSS 保护功能。
X-XSS-Protection: 1; mode=block;
- X-Content-Type-Options:防止浏览器自动猜测内容类型,避免某些攻击。
X-Content-Type-Options: nosniff;
前端如何实现鉴权?
前端常通过 Axios 拦截器在请求前添加 token,并在响应后处理鉴权失败的情况。
请求拦截:添加 Token 到请求头
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = token;
}
return config;
},
(error) => Promise.reject(error)
);
响应拦截:处理未登录或 Token 失效
service.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// Token 无效或未登录,跳转到登录页
window.location.href = "/login";
}
return Promise.reject(error);
}
);
闭包是什么?优缺点有哪些?
闭包就是函数+函数能访问的外部变量。
function outer() {
let count = 0;
return function inner() {
count++; // 内部函数访问外部的局部变量
console.log(count);
};
}
const fn = outer();
fn(); // 输出 1
fn(); // 输出 2
✅ 优点:
- 数据保留: 外部函数变量不会被销毁,记住状态。
- 私有变量: 可以模拟 私有作用域(外部访问不到内部变量)。
✅ 缺点:
- 内存占用高: 外部变量常驻内存,可能导致 内存泄漏。
- 调试复杂: 变量作用域混乱,不易排查。
canvas 如何优化
核心在于减少重绘次数、分担渲染负担、合理使用缓存与图层,从而提升整体帧率与响应性能。
一、减少重绘次数
原理:每次 ctx.clearRect + ctx.drawXXX 都是一次重绘,频繁执行会拖慢性能。
优化方式:
- 使用
requestAnimationFrame替代setInterval,更平滑、更节能。 - 判断是否真的需要重绘,比如只有在物体位置改变时才重绘。
二、离屏渲染(Offscreen Canvas)
原理:在内存中的 canvas 上渲染,然后一次性绘制到主 canvas。
优化方式:
const offscreen = document.createElement('canvas')
const offCtx = offscreen.getContext('2d')
// 先画在 offscreen 上,再 ctx.drawImage(offscreen, 0, 0)
- 适合背景、静态图层等不用频繁更新的场景。
三、图层分离
原理:将不同逻辑的内容用多个 <canvas> 叠加绘制。
举例:
- 背景图层一个 canvas,角色动画一个 canvas,UI 一个 canvas。
- 背景不动就不用重绘,只更新动画层。
四、使用精灵图(Sprite)
原理:将多个小图拼成一张图,用 drawImage 裁剪渲染。
好处:
- 减少图片加载次数(节省网络资源)。
- 提升
drawImage性能(浏览器更容易优化)。
五、批量绘制 vs 单独绘制
尽量合并绘制操作:
- 避免频繁调用
beginPath/fill/stroke。 - 一个路径里画多个元素。
六、降低像素密度(有需要时)
原理:Canvas 默认按 devicePixelRatio 渲染,高分屏会让像素数暴增。
优化方法:
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
- 有时候也可以故意降低像素密度换取更高性能,特别是动画频繁的场景。
🔹 七、简化绘制逻辑
- 减少不必要的阴影、渐变、透明度变化。
- 避免复杂路径(例如贝塞尔曲线)或用缓存代替。
- 图像处理尽量用
ImageBitmap替代Image。
八、避免内存泄漏
- 使用完的图片及时释放引用。
- 大型 canvas 页面销毁时清理 DOM 和事件监听器。
九、WebGL(高级)
如果需要更高性能、3D 支持,或者想用 GPU 加速。
Cookie,LocalStorage 和 SessionStorage 的区别
| 特性 | Cookie | LocalStorage | SessionStorage |
|---|---|---|---|
| 生命周期 | 默认随会话结束而失效,可设置过期时间 | 永久保存,除非主动删除 | 仅在当前会话/标签页中有效 |
| 存储位置 | 浏览器 + 可发送至服务器 | 浏览器本地,属于当前域名 | 浏览器本地,仅当前窗口可访问 |
| 与服务器通信 | 每次请求都会自动携带 | 不与服务器通信 | 不与服务器通信 |
| 最大存储容量 | 约 4KB | 约 5MB | 约 5MB |
GET 和 POST 对比
✅ GET vs POST 最核心 3 点区别:
参数位置:
- GET:参数在 URL 后面(?key=value)
- POST:参数在请求体里
安全性:
- GET:不安全,参数暴露在地址栏
- POST:相对安全,参数藏在 body 中
数据大小:
- GET:有长度限制(URL 限制)
- POST:理论无限制(由服务器限制)
HTTP/1.0 vs HTTP/1.1 vs HTTP/2 区别
| 特性 | HTTP/1.0 | HTTP/1.1 | HTTP/2 |
|---|---|---|---|
| 连接复用 | ❌ 每次请求都新建连接 | ✅ 默认启用 Keep-Alive 复用连接 | ✅ 多路复用,共用一条连接 |
| 请求并发 | ❌ 不支持 | ❌ 排队(队头阻塞) | ✅ 并发(多路复用无阻塞) |
| 头部压缩 | ❌ 无 | ❌ 无 | ✅ 有(HPACK 压缩) |
var, let 和 const 区别
var是老时代的,提升严重容易出 bug。let用得最多,安全又灵活。const用来声明常量,不可重新赋值。
浏览器地址栏输入 URL 后发生了什么?
1.DNS 解析
浏览器先查询域名对应的 IP 地址(通过本地缓存 → 系统缓存 → 路由器 → DNS 服务器)。
2.建立连接
使用 TCP(三次握手)连接服务器,如果是 HTTPS,还会进行 TLS 握手加密。
3.发送请求
浏览器构造 HTTP 请求报文,发送给服务器。
4.服务器响应
服务器处理请求,返回响应报文(状态码、Header、Body 等)。
5.浏览器解析响应内容
- 根据 MIME 类型渲染页面
- 解析 HTML,构建 DOM 树
- 下载 CSS/JS/图片,构建 CSSOM、执行 JS
- 生成 Render Tree,布局和绘制页面
6.页面渲染完成
页面展示给用户,开始交互。
一句话总结 :
DNS 解析 → 建立连接 → 发送请求 → 获取响应 → 渲染页面
前端的加密方法
1. 哈希函数
将任意长度的消息映射为固定长度的输出的算法。 特点: 不可逆,抗碰撞性,固定长度。 类型有:
- SHA(Secure Hash Algorithm)
- MD5(Message Digest Algorithm 5)
SHA/MD5 对比:SHA 在安全性方面优于 MD5,并且可以选择多种不同的密钥长度。 但是,由于内存需求更高,运行速度可能会更慢。 不过,MD5 因其速度而得到广泛使用,但是由于存在碰撞攻击风险,因此不再推荐使用。
2. 对称加密
加密和解密的密钥是用同一个。 类型有:
- AES(Advanced Encryption Standard):高级加密标准算法,速度快,安全级别高,目前已被广泛应用,适用于加密大量数据,如文件加密、网络通信加密等。
3.非对称加密
加密和解密使用不同的密钥的算法。 类型有:
- RSA。RSA 是由 3 个人的姓氏开头字母拼接起来。是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的。RSA 是一种非对称加密算法,即加密和解密使用一对不同的密钥,分别称为公钥和私钥。
上传文件的 content-type
multipart/form-data; boundary=----WebKitFormBoundaryxkIYCvQLm0nswkRJ
- multipart/form-data 是文件传输的 content-type 格式
- boundary 是分隔符,分隔多个文件、表单项。如果不自己设置,默认由浏览器自动产生
微任务和宏任务有哪些
- 宏任务: script 代码, setTimeout, setInterval, MessageChannel
- 微任务: Promise.then, MutationObserver
浏览器每一帧执行的时候,事件队列先执行微任务再执行宏任务。
注意: 微任务放的事件队列和宏任务放的事件队列不一样。
webpack
原理
@babel/parser 把入口文件转换成 AST 抽象语法树,然后获得入口文件的依赖,再递归入口文件的依赖,最后再生成可执行的 js。
什么是 loader?
由于 webpack 本身只支持处理 js,json 文件,为了处理其它类型的文件,所以需要 loader。 列举一些 loader:
- css-loader: 解析 css 文件
- style-loader: css 样式插入到 dom
- @babel/preset-env: es6 转 es5
- @babel/preset-react: jsx 语法转换
- @babel/preset-typescript: tsx 语法转换
什么是 plugins?
plugin 则可以用于执行范围更广的任务。 如:压缩代码(new TerserWebpackPlugin()),资源管理(new HtmlWebPackPlugin()),注入环境变量(new webpack.DefinePlugin({...}))等。 列举一些:
- terser-webpack-plugin: 压缩代码, webpack5 内置, 当 mode 为 production 时会自动启用。
- html-webpack-plugin: 根据指定的模版生成 html 文件
- mini-css-extract-plugin: 将 js 中的 css 提取到单独的 css 文件中
作用
-
模块打包 Webpack 可以将应用程序的各种模块(JavaScript、CSS、图片等)打包成一个或多个文件,以减少加载时间和提高性能。这使得模块化开发更加方便。
-
依赖管理 Webpack 可以分析应用程序的模块之间的依赖关系,并自动加载所需的模块。它支持 CommonJS、ES6 模块、AMD 等不同的模块系统。
-
自动刷新 Webpack 提供开发服务器,支持热模块替换(Hot Module Replacement,HMR),在开发过程中,可以自动刷新浏览器并保留应用程序的状态,而无需手动刷新。
-
代码分割 Webpack 支持代码分割,允许将应用程序代码拆分为多个文件,从而实现按需加载,减少初始加载时间。
-
资源加载 Webpack 可以处理各种资源文件,包括图片、字体、样式表等,将它们打包到应用程序中,并自动生成文件名和路径。
-
插件系统 具有丰富的插件系统,允许开发者使用各种插件来扩展其功能。例如,通过插件可以进行代码压缩、性能优化、代码分析等操作。
-
预处理支持 支持使用预处理器(如 Sass、Less、TypeScript)来编写样式和代码,可以通过加载器(loader)来处理这些预处理器。
-
多环境配置 Webpack 支持多环境配置,允许开发者为开发、测试和生产环境配置不同的构建选项,以满足不同环境的需求。
-
源映射 Webpack 生成源映射文件,可以帮助开发者在调试时跟踪到原始源代码,以便更容易定位和修复问题。
-
社区支持 Webpack 有一个庞大的社区和生态系统,提供了丰富的文档、插件和工具,使其更易于学习和使用。
Echarts 内核
echarts 是基于 ZRender 框架写的。 ZRender 是二维绘图引擎,它提供 Canvas、SVG、VML 等多种渲染方式。ZRender 也是 ECharts 的渲染器。
浏览器每一帧做了什么?
浏览器的一帧为 16.7ms。
-
用户事件, 比如 click
-
宏任务和微任务。把微任务队列执行完成。宏任务会被浏览器自动调控。比如浏览器如果觉得宏任务执行时间太久,它会将下一个宏任务分配到下一帧中,避免掉帧。
-
在渲染前执行 scroll/resize 等事件回调。
-
在渲染前执行 requestAnimationFrame 回调。
-
渲染界面:面试中经常提到的浏览器渲染时 html、css 的计算布局绘制等都是在这里完成。
-
requestIdleCallback 执行回调:如果前面的那些任务执行完成了,一帧还剩余时间,那么会调用该函数。
宏任务主要包含:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI 交互事件。 微任务主要包含:Promise、MutationObserver 等。
requestAnimationFrame 和 setTimeout, setInterval 的区别?
requestAnimationFrame在浏览器的下一帧渲染之前执行代码,更适合创建平滑的动画效果,因为它与浏览器的渲染循环同步,可以准确控制动画的每一帧。setTimeout,setInterval在指定的时间间隔执行代码,执行时间不固定,可能受到浏览器的负载和性能影响。
requestAnimationFrame 的优点:
- 经过浏览器优化,动画更流畅
- 窗口没激活时,动画将停止,节省计算资源
- 更省电,尤其是对移动终端
Object.freeze()
冻结一个对象,冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。
const obj = {
prop: 42
};
Object.freeze(obj);
obj.prop = 33;
// Throws an error in strict mode 严格模式下会抛出错误
console.log(obj.prop);
// Expected output: 42
WeakMap 和 Map 的区别
WeakMap 只接受对象(null 除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。 WeakMap键名所引用的对象都是弱引用,只要所引用的对象被清除,垃圾回收机制就会释放该对象所占用的内存。 应用场景,Dom 元素当作 WeakMap 键名。当该 DOM 元素被清除,其所对应的 WeakMap 记录就会自动被移除。
const wm = new WeakMap();
const element = document.getElementById("example");
wm.set(element, "some information");
wm.get(element); // "some information"
Koa 洋葱模型的优点
每个请求进入的时候传递给下一个中间件,能够精确的控制请求处理流程。 异步处理更好,避免回调地狱。 下一个中间件对数据进行处理之后,上一个中间件可以获取到这个数据再进行处理。
如何使用 while (true) 模拟 setInterval
直接上代码,注意函数不能直接返回 t, 因为 t 其实是一直变化的。
const setIntervalSimulate = (cb, time) => {
let t;
const loop = async () => {
while (true) {
await new Promise((r) => (t = setTimeout(r, time)));
cb();
}
};
loop();
return () => {
clearTimeout(t);
};
};
const clear = setIntervalSimulate(() => {
console.log(1);
}, 200);
CSS3
CSS3 是使用 GPU 加速,如果用 css 来做动画,可以监听下面的事件:
animationstart: 动画开始
animationiteration: 动画执行的次数,和css 里面 ```animation-iteration-count``` 有关。
animationend: 动画结束
animationcancel: 动画取消, 比如把下面的 css 里的 .active 样式移除了。
.animation.active {
animation-duration: 2s;
animation-name: slidein;
animation-iteration-count: 2;
}
@keyframes slidein {
from {
margin-left: 100%;
width: 300%;
}
to {
margin-left: 0%;
width: 100%;
}
}
回流(Reflow) 和 重绘 (Repaint)
回流比重绘的代价要更高。
回流:元素位置变化就会发生 回流,比如 窗口大小改变,字体改变。 重绘:元素颜色或者 visibility 发生变化。
如何避免
- 避免频繁操作 DOM 和 频繁操作 CSS,最好一次性重写和更改。
- 对于动画使用绝对定位,让它脱离文档流。
快速排序
选择一个基准的数字(通常是中间的),小于它的都放在左边数据集,大于它的都放在右边数据集,然后对于左边的数据集和右边的数据集重复上面的步骤。
const arr = [2, 3, 1, 5, 6, 4];
const quickSort = (arr) => {
const len = arr.length;
if (len <= 1) return arr;
const pos = Math.floor(len / 2);
const middle = arr[pos];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (i === pos) continue;
if (arr[i] <= middle) left.push(arr[i]);
else right.push(arr[i]);
}
return quickSort(left).concat([middle]).concat(quickSort(right));
};
const newArr = quickSort(arr);
console.log(newArr);
for...of 和 for...in 区别
数据只要部署了 Iterator['itəreitə] 接口,那么它就可以使用 for...of 来遍历。
Iterator 接口部署在数据结构的 Symbol.Iterator 属性上。
举例下面的数组:
var arr = [1, 2, 3];
var iter = arr[Symbol.Iterator]();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value:3, done: false}
iter.next(); // {value:undefined, done: true}
具有 iterator 的数据结构如下:
Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象
for...of 不能遍历对象,因为对象没有部署 Symbol.Iterator 数据结构:
console.log(Symbol.iterator); // Symbol(Symbol.iterator)
var obj = { a: 1, b: 2 };
for (let i of obj) {
console.log(i);
}
/*
VM201:1 Uncaught TypeError: obj is not iterable
at <anonymous>:1:14
*/
如何给对象部署 Iterator 数据结构:
Object.prototype[Symbol.iterator] = function () {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
const done = i > keys.length - 1;
const value = done ? undefined : this[keys[i]];
i++;
return {
value,
done
};
}
};
};
var obj = { a: 1, b: 2 };
for (let i of obj) {
console.log(i); // 1, 2
}
小程序的架构
在渲染流程中,WebView H5 方案类似于传统的 Web 应用,先由 Native 打开一个 WebView 容器,WebView 就像浏览器一样,打开 WebView 对应的 URL 地址,然后进行请求资源、加载数据、绘制页面,最终页面呈现在我们眼前。
小程序采用双线程架构,分为逻辑层和渲染层。首先也是 Native 打开一个 WebView 页面,渲染层加载 WXML 和 WXSS 编译后的文件,同时逻辑层用于逻辑处理,比如触发网络请求、setData 更新等等。接下来是请求资源,请求到数据之后,数据先通过逻辑层传递给 Native,然后通过 Native 把数据传递给渲染层 WebView,再进行渲染。
拦截 XMLHttpRequest 和 fetch
(() => {
/** 拦截 XMLHttpRequest **/
const OriginalXHR = window.XMLHttpRequest;
class XHRInterceptor extends OriginalXHR {
constructor() {
super();
const originalOpen = this.open;
this.open = (method, url, ...args) => {
console.log('[XHR] 请求方法:', method);
console.log('[XHR] 请求地址:', url);
return originalOpen.call(this, method, url, ...args);
};
}
}
// 保持原型一致性(重要)
XHRInterceptor.prototype = OriginalXHR.prototype;
window.XMLHttpRequest = XHRInterceptor;
/** 拦截 fetch **/
const originalFetch = window.fetch;
window.fetch = async (input, init = {}) => {
const method = init.method || 'GET';
const url = typeof input === 'string' ? input : input.url;
console.log('[Fetch] 请求方法:', method);
console.log('[Fetch] 请求地址:', url);
return originalFetch(input, init);
};
})();