11 BOM location

08 BOM location

在 JavaScript 中,location 对象是 window 对象的一个属性,它表示当前页面的 URL 地址,并提供了一系列方法来获取和设置 URL 的各个部分,以及实现页面的跳转等功能。
以下是 location 对象的一些常用属性和方法:

属性

  • href:完整的 URL 地址。
  • protocol:URL 的协议,例如 http:https:
  • hostname:主机名(域名)。
  • port:端口号,如果没有指定端口,则返回空字符串。
  • pathname:URL 的路径部分。
  • search:查询字符串部分,以 ? 开头。
  • hash:锚点部分,以 # 开头。

方法

  • assign(url):加载新的文档,等同于 location = url
  • reload():重新加载当前文档。可以传入一个参数 forceGet,如果为 true,则总是从服务器重新加载,而不是从缓存中加载。
  • replace(url):加载新的文档,并替换当前文档在会话历史中的位置,这意味着用户不能使用浏览器的后退按钮回到这个被替换的页面。
  • toString():返回 location 对象的 href 属性的值,即完整的 URL 地址。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取当前页面的完整URL
console.log(window.location.href);
// 获取当前页面的协议
console.log(window.location.protocol);
// 获取当前页面的域名
console.log(window.location.hostname);
// 获取当前页面的路径
console.log(window.location.pathname);
// 获取当前页面的查询字符串
console.log(window.location.search);
// 获取当前页面的锚点
console.log(window.location.hash);
// 跳转到新的页面
window.location.href = 'https://www.example.com';
// 重新加载当前页面
window.location.reload();
// 替换当前页面
window.location.replace('https://www.example.com/new-page');


//http://127.0.0.1:8848/JsWorkPlace/theme/javascript/js2.html?a=1&b=2#12321
//http:
//127.0.0.1
///JsWorkPlace/theme/javascript/js2.html
//?a=1&b=2
//#12321

location 对象在 Web 开发中非常有用,可以用于实现页面跳转、获取页面信息等功能。

12 其他BOM对象

09 其他BOM对象

在 JavaScript 中,BOM(Browser Object Model,浏览器对象模型)是浏览器提供的一个对象集合,用于操作浏览器窗口和其中的内容。除了 location 对象外,BOM 还包括以下一些重要的对象:

1. window 对象

  • 描述window 对象是浏览器的顶级对象,表示浏览器窗口本身。它是全局对象,在浏览器中,全局作用域中的变量和函数都属于 window 对象。
  • 常用属性和方法
    • document:获取当前窗口的文档对象。
    • navigator:获取浏览器信息。
    • screen:获取屏幕信息。
    • history:获取浏览器历史记录。
    • alert()confirm()prompt():弹出对话框的方法。

2. document 对象

  • 描述document 对象表示当前加载的 HTML 文档,是 window 对象的子对象。
  • 常用属性和方法
    • getElementById(id):通过元素的 ID 获取元素。
    • getElementsByClassName(className):通过类名获取元素集合。
    • querySelector(selector)querySelectorAll(selector):通过 CSS 选择器获取元素或元素集合。
    • createElement(tagName):创建一个新的元素。

3. navigator 对象

  • 描述navigator 对象提供了关于浏览器的信息。
  • 常用属性
    • userAgent:浏览器的用户代理字符串,包含浏览器的名称、版本等信息。
    • platform:浏览器所在的平台(如 Windows、Mac 等)。

4. screen 对象

  • 描述screen 对象提供了关于用户屏幕的信息。
  • 常用属性
    • widthheight:屏幕的宽度和高度。

5. history 对象

  • 描述history 对象提供了浏览器历史记录的操作方法。
  • 常用方法
    • back():后退到浏览器历史记录中的上一个页面。
    • forward():前进到浏览器历史记录中的下一个页面。
      这些对象共同构成了浏览器对象模型,使得开发者可以通过 JavaScript 对浏览器窗口和文档进行各种操作和交互。

13 本地存储

10 本地存储

几种储存方法

浏览器本地存储提供了几种不同的方式来在客户端存储数据,每种方式都有其特定的用途和特点。以下是几种常见的浏览器本地存储方法:

  • 存储量:每个域名下最多可存储 20 个 Cookie,每个 Cookie 最大 4KB。
  • 特点
    • 自动发送:Cookie 会随着 HTTP 请求自动发送到服务器,无需前端手动操作。
    • 跨会话存储:可以设置过期时间,即使关闭浏览器后数据依然存在。
    • 安全性:可以通过设置 HttpOnlySecure 标志来提高安全性,防止跨站脚本攻击(XSS)和非安全传输。
  • 使用场景:适用于存储用户会话信息、用户偏好设置等需要与服务器交互的数据。

2. localStorage

  • 存储量:通常为每个域名 5MB。
  • 特点
    • 持久存储:数据存储在本地,即使关闭浏览器后数据依然存在,直到被明确删除。
    • 同步操作:读写操作是同步的,可能会阻塞主线程。
    • 仅限同源:只能在同源的页面之间共享数据。
  • 使用场景:适用于存储不需要频繁与服务器交互的数据,如用户设置、缓存数据等。

3. sessionStorage

  • 存储量:通常为每个域名 5MB。
  • 特点
    • 会话存储:数据仅在当前浏览器会话中有效,关闭浏览器标签页后数据会被清除。
    • 同步操作:读写操作是同步的,可能会阻塞主线程。
    • 仅限同源:只能在同源的页面之间共享数据。
  • 使用场景:适用于存储临时数据,如表单数据、页面状态等,这些数据不需要在浏览器会话之间持久存在。

4. IndexedDB

  • 存储量:通常为每个域名 50MB 或更多,具体取决于浏览器和用户设置。
  • 特点
    • 非关系型数据库:提供类似于数据库的存储方式,可以存储结构化数据。
    • 异步操作:读写操作是异步的,不会阻塞主线程。
    • 事务支持:支持事务,可以保证数据的一致性和完整性。
  • 使用场景:适用于存储大量结构化数据,如离线应用、大型应用的数据缓存等。

5. Web SQL Database(已废弃)

  • 存储量:通常为每个域名 5MB 或更多。
  • 特点
    • 关系型数据库:提供 SQL 语法来操作数据。
    • 异步操作:读写操作是异步的,不会阻塞主线程。
  • 使用场景:由于已废弃,不推荐在新项目中使用。

选择合适的方法

  • 数据量大小:根据需要存储的数据量选择合适的方法,如小量数据可使用 Cookie 或 sessionStorage,大量数据可使用 IndexedDB。
  • 数据持久性:根据数据是否需要跨会话持久存在选择 localStorage 或 sessionStorage。
  • 数据结构:根据数据的结构选择 IndexedDB 进行结构化存储。
  • 与服务器交互:如果需要与服务器频繁交互,可以使用 Cookie。
    根据具体的应用需求和场景,选择合适的浏览器本地存储方法,可以有效地提高应用的性能和用户体验。

设置失效时间来清除本地存储的数据

在前端开发中,可以通过设置失效时间来清除本地存储的数据。以下是几种常用的方法:

使用 localStorage 和时间戳

  • 原理:在存储数据时,同时存储一个时间戳,表示数据的过期时间。当读取数据时,检查当前时间与存储的时间戳,如果超过了设定的失效时间,则清除数据。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function setDataWithExpiry(key, value, ttl) {
    const now = new Date();
    const item = {
    value: value,
    expiry: now.getTime() + ttl,
    };
    localStorage.setItem(key, JSON.stringify(item));
    }
    function getData(key) {
    const itemStr = localStorage.getItem(key);
    if (!itemStr) {
    return null;
    }
    const item = JSON.parse(itemStr);
    const now = new Date();
    if (now.getTime() > item.expiry) {
    localStorage.removeItem(key);
    return null;
    }
    return item.value;
    }

使用 sessionStorage 和定时器

  • 原理sessionStorage 存储的数据在浏览器会话结束时会自动清除。可以结合定时器来提前清除数据。
  • 示例代码
    1
    2
    3
    4
    5
    6
    function setDataWithTimer(key, value, ttl) {
    sessionStorage.setItem(key, value);
    setTimeout(() => {
    sessionStorage.removeItem(key);
    }, ttl);
    }

封装 localStorage 实现过期时间

  • 原理:封装 localStoragesetItemgetItem 方法,在存储数据时记录过期时间,并在读取数据时检查是否过期。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const customStorage = {
    setItem(key, value, ttl) {
    const now = new Date().getTime();
    const item = {
    value: value,
    expiry: now + ttl,
    };
    localStorage.setItem(key, JSON.stringify(item));
    },
    getItem(key) {
    const itemStr = localStorage.getItem(key);
    if (!itemStr) {
    return null;
    }
    const item = JSON.parse(itemStr);
    const now = new Date().getTime();
    if (now > item.expiry) {
    localStorage.removeItem(key);
    return null;
    }
    return item.value;
    },
    };
    通过这些方法,可以有效地管理本地存储的数据,确保数据在指定的时间后自动清除,从而提升应用的安全性和性能。

Cookie跨域问题是指在不同域之间进行请求时,Cookie无法被正确传递的问题。这主要是由于浏览器的同源策略和Cookie的SameSite属性所导致的。以下是一些常见的解决方案:

1. 使用CORS(跨域资源共享)

CORS是解决跨域问题的最常见和有效的方法之一。通过在服务器端设置特定的HTTP响应头,可以允许浏览器发送和接收跨域请求并携带Cookie。

服务器端配置

  • 设置Access-Control-Allow-Origin:指定允许跨域访问的源。可以是具体的域名,不推荐使用*,因为这会允许任何域访问。
  • 设置Access-Control-Allow-Credentials:设置为true,表示允许发送Cookie。
  • 设置Access-Control-Allow-Methods:指定允许的HTTP方法,如GETPOST等。
  • 设置Access-Control-Allow-Headers:指定允许的HTTP头。
    例如,在Node.js中可以这样配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: 'http://example.com',
methods: 'GET,POST',
allowedHeaders: 'Content-Type',
credentials: true // 允许携带Cookie
};
app.use(cors(corsOptions));
app.get('/data', (req, res) => {
res.json({ message: 'This is CORS-enabled for all origins!' });
});
app.listen(3000, () => {
console.log('CORS-enabled web server listening on port 3000');
});

客户端配置

在客户端发起请求时,需要设置credentials属性为include,以便浏览器发送Cookie。例如,使用fetch API时可以这样配置:

1
2
3
4
5
6
7
fetch('http://example.com/data', {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

2. 设置Cookie的SameSite属性

Cookie的SameSite属性可以控制Cookie在跨域请求中的行为。有三种值可以设置:StrictLaxNone

  • Strict:最为严格,完全禁止第三方Cookie,跨站点时,任何情况下都不会发送Cookie。
  • Lax:规则稍稍放宽,大多数情况也是不发送第三方Cookie,但是导航到目标网址的GET请求除外。
  • None:Cookie只能通过HTTPS协议发送,并且必须同时设置Secure属性。
    例如,在Node.js中可以这样设置Cookie:
1
2
3
4
res.cookie('name', 'value', {
sameSite: 'None',
secure: true
});

3. 使用JSONP

JSONP(JSON with Padding)是一种解决跨域问题的传统方法。它利用了<script>标签不受同源策略限制的特点,通过动态创建<script>标签来请求跨域资源,并在资源返回时执行一个回调函数。
例如,客户端代码可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP Example</title>
</head>
<body>
<script>
function handleResponse(data) {
console.log('Received data:', data);
}
var script = document.createElement('script');
script.src = 'http://yourserver.com/data?callback=handleResponse';
document.head.appendChild(script);
</script>
</body>
</html>

服务器端代码可以这样写:

1
2
3
4
5
app.get('/data', (req, res) => {
const callback = req.query.callback;
const data = { message: 'This is cross-origin data with JSONP!' };
res.send(`${callback}(${JSON.stringify(data)})`);
});

4. 使用代理服务器

通过设置代理服务器,可以将请求转发到目标服务器,从而绕过浏览器的同源策略。例如,使用Nginx配置反向代理:
nginx复制

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name yourdomain.com;
location /api {
proxy_pass http://targetdomain.com;
proxy_set_header Host targetdomain.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

5. 设置document.domain

如果一级域名相同,只是二级域名不同,可以通过设置document.domain共享Cookie。例如:

1
2
3
4
5
6
7
8
9
10
11
12
http://javascript.exam.cn/text.html:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
document.domain = 'exam.cn';
document.cookie = "name=value";
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
http://category.exam.cn/text.html:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
document.domain = 'exam.cn';
alert(document.cookie); // 输出:name=value
</script>
</body>
</html>

总结

解决Cookie跨域问题的方法有多种,具体选择哪种方法取决于具体的应用场景和需求。CORS是最常用和最灵活的方法,但需要服务器端和客户端的配合。设置Cookie的SameSite属性和使用代理服务器也是常见的解决方案。在选择方法时,需要综合考虑安全性、灵活性和开发成本。

14 canvas

11 canvas

canvas

canvas 是 HTML5 中的一个元素,用于在网页上绘制图形和动画。它提供了一个矩形区域,你可以使用 JavaScript 在这个区域内绘制各种图形,包括线条、形状、文本、图像等。canvas 元素本身没有绘图能力,所有的绘图操作都需要通过 JavaScript 来完成。

基本用法

  1. HTML 部分
    在 HTML 中添加一个 canvas 元素,并为其指定一个 id,以便在 JavaScript 中引用:
    1
    <canvas id="myCanvas" width="200" height="100"></canvas>
    • widthheight 属性定义了 canvas 的尺寸。如果省略这些属性,canvas 的默认尺寸是 300x150 像素。
  2. JavaScript 部分
    使用 JavaScript 获取 canvas 元素,并获取其绘图上下文(通常是 2D 上下文),然后进行绘图操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    // 绘制一个矩形
    ctx.fillStyle = 'green'; // 设置填充颜色
    ctx.fillRect(10, 10, 150, 100); // 绘制一个填充的矩形
    // 绘制一条线
    ctx.strokeStyle = 'blue'; // 设置线条颜色
    ctx.beginPath(); // 开始一条路径
    ctx.moveTo(50, 50); // 移动到起点
    ctx.lineTo(150, 50); // 绘制到终点
    ctx.stroke(); // 绘制路径
    // 绘制文本
    ctx.font = '20px Arial'; // 设置字体样式
    ctx.fillStyle = 'red'; // 设置文本颜色
    ctx.fillText('Hello, Canvas!', 50, 80); // 绘制文本

常用方法和属性

  • 绘图上下文getContext('2d') 返回一个 2D 渲染上下文对象,用于在 canvas 上进行绘图。
  • 绘制形状
    • fillRect(x, y, width, height):绘制一个填充的矩形。
    • strokeRect(x, y, width, height):绘制一个矩形边框。
    • beginPath():开始一条新的路径。
    • moveTo(x, y):移动到指定的点。
    • lineTo(x, y):绘制一条线到指定的点。
    • arc(x, y, radius, startAngle, endAngle, anticlockwise):绘制一个圆弧。
  • 绘制文本
    • fillText(text, x, y):绘制填充文本。
    • strokeText(text, x, y):绘制文本边框。
  • 样式和颜色
    • fillStyle:设置填充颜色或渐变。
    • strokeStyle:设置线条颜色或渐变。
  • 图像操作
    • drawImage(image, dx, dy):在指定位置绘制图像。
  • 保存和恢复状态
    • save():保存当前的绘图状态。
    • restore():恢复之前保存的绘图状态。

应用场景

  • 动画:通过不断清除和重绘 canvas 来创建动画效果。
  • 游戏开发:用于绘制游戏界面和动画。
  • 图表和数据可视化:绘制各种图表和图形以展示数据。
  • 图像处理:进行图像的裁剪、缩放、滤镜等操作。
    canvas 提供了强大的绘图功能,使得在网页上进行复杂的图形绘制和动画制作成为可能。

前端截图

前端实现截图需要使⽤ HTML5 的 Canvas 和相关 API,具体步骤如下:

  1. ⾸先在页面中创建⼀个 Canvas 元素,并设置其宽⾼和样式。
  2. 使⽤ Canvas API 在 Canvas 上绘制需要截图的内容,⽐如⻚⾯的某个区域、某个元素、图⽚等。
  3. 调⽤ Canvas API 中的 toDataURL () ⽅法将 Canvas 转化为 base64 编码的图⽚数据。
  4. 将 base64 编码的图⽚数据传递给后端进⾏处理或者直接在前端进⾏显⽰。
    以下是⼀个简单的例⼦,实现了对整个⻚⾯的截图:
1
2
3
4
5
6
7
8
9
10
const canvas = document.getElementById('canvas');
const ctx =canvas.getContext('2d');
const btn = document.getElementById('btn');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
btn.addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(document.documentElement, 0, 0);
const imgData = canvas.toDataURL();console.log(imgData);
});

这个例⼦中,在⻚⾯中创建了⼀个 canvas 元素,并设置其宽⾼和样式,将其放在⻚⾯最上⽅。在
点击“截图”按钮时,通过 toDataURL () ⽅法将整个⻚⾯的截图转换为 base64 编码的图⽚数据,
并打印到控制台上。

简单动画

下面是一个简单的 canvas 动画示例,它会在 canvas 上绘制一个不断移动的小球,实现一个简单的动画效果。

HTML 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas 动画示例</title>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="400" height="300"></canvas>
<script src="animation.js"></script>
</body>
</html>

JavaScript 部分 (animation.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 小球的初始位置和速度
let x = canvas.width / 2;
let y = canvas.height / 2;
let dx = 2; // 水平方向速度
let dy = -2; // 垂直方向速度
const ballRadius = 10;
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除之前的画面
drawBall(); // 绘制小球
// 更新小球的位置
x += dx;
y += dy;
// 碰到边界时反弹
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
dy = -dy;
}
requestAnimationFrame(draw); // 请求下一帧动画
}
// 开始动画
draw();

动画解释

  • 初始化:定义了小球的初始位置 xy,以及水平和垂直方向的速度 dxdy。还定义了小球的半径 ballRadius
  • 绘制小球drawBall 函数使用 arc 方法绘制一个圆形的小球,并用 fill 方法填充颜色。
  • 动画循环draw 函数首先清除 canvas 上的内容,然后调用 drawBall 绘制小球。接着更新小球的位置,并检查小球是否碰到 canvas 的边界。如果碰到边界,则反转速度,实现反弹效果。最后,使用 requestAnimationFrame(draw) 来请求浏览器在下一次重绘之前调用 draw 函数,从而实现动画效果。
  • 启动动画:在脚本的最后调用 draw 函数来启动动画。
    这个简单的示例展示了如何使用 canvas 和 JavaScript 创建一个基本的动画效果。你可以根据需要调整速度、颜色、形状等属性来创建更复杂的动画。

requestAnimationFrame

requestAnimationFrame 是 Web API 中的一个函数,用于在浏览器的下一次重绘之前执行动画函数。它提供了一种更高效的方式来创建平滑的动画效果,相比传统的 setTimeoutsetInterval 方法,requestAnimationFrame 有以下优点:

优点

  • 性能优化requestAnimationFrame 会在浏览器完成一次屏幕重绘之前执行动画函数,通常与浏览器的刷新率(通常是 60 次/秒)同步。这意味着它可以更高效地利用浏览器的重绘机制,避免不必要的渲染,从而提高性能,减少卡顿和撕裂现象。
  • 节能:当浏览器标签页不是处于活动状态时(例如用户切换到其他标签页),requestAnimationFrame 会暂停执行动画函数,从而节省资源和电量。
  • 更平滑的动画:由于与浏览器的重绘同步,requestAnimationFrame 可以创建更平滑的动画效果,避免出现抖动或不连贯的情况。

使用方法

1
2
3
4
5
6
7
8
function animate() {
// 执行动画相关的操作
// ...
// 请求下一次动画帧
requestAnimationFrame(animate);
}
// 启动动画
requestAnimationFrame(animate);

参数

  • callback:一个函数,它会在浏览器下一次重绘之前被调用。这个函数通常包含动画的更新和渲染逻辑。

返回值

  • 返回一个 ID 值,可以用于 cancelAnimationFrame 函数来取消动画请求。

取消动画

如果你需要在某些情况下停止动画,可以使用 cancelAnimationFrame 函数:

1
2
3
4
5
6
7
8
9
10
11
let animationId;
function animate() {
// 执行动画相关的操作
// ...
// 请求下一次动画帧
animationId = requestAnimationFrame(animate);
}
// 启动动画
animationId = requestAnimationFrame(animate);
// 在需要停止动画时调用
cancelAnimationFrame(animationId);

requestAnimationFrame 和 setInterval

requestAnimationFramesetInterval 都可以用来创建动画或定时执行代码,但它们在实现方式和性能表现上有一些关键区别:

requestAnimationFrame

  • 同步浏览器刷新率requestAnimationFrame 会在浏览器的下一次重绘之前执行回调函数,通常与浏览器的刷新率(通常是 60 次/秒)同步。这意味着它可以在每次屏幕刷新时更新动画,从而创建平滑的动画效果。
  • 性能优化:当浏览器标签页不是处于活动状态时(例如用户切换到其他标签页),requestAnimationFrame 会暂停执行回调函数,从而节省资源和电量。
  • 更平滑的动画:由于与浏览器的重绘同步,requestAnimationFrame 可以创建更平滑的动画效果,避免出现抖动或不连贯的情况。
  • 适用于动画:特别适合用于创建动画效果,因为它可以确保动画的更新与屏幕刷新同步,减少不必要的渲染,提高性能。

setInterval

  • 固定时间间隔setInterval 会按照指定的时间间隔(以毫秒为单位)重复执行回调函数,不考虑浏览器的刷新率。这意味着它可能会在浏览器的两次重绘之间执行多次,或者在浏览器的两次重绘之间不执行,导致动画效果不平滑。
  • 资源消耗:即使浏览器标签页不是处于活动状态,setInterval 仍然会继续执行回调函数,可能会消耗不必要的资源和电量。
  • 不考虑重绘setInterval 不会自动与浏览器的重绘同步,可能导致动画的更新与屏幕刷新不同步,出现抖动或不连贯的情况。
  • 适用于定时任务:更适合用于需要在固定时间间隔执行的定时任务,而不是动画效果。

总结

  • 动画效果:对于创建动画效果,requestAnimationFrame 是更好的选择,因为它可以与浏览器的重绘同步,创建平滑的动画,并且在非活动标签页时暂停执行,节省资源。
  • 定时任务:对于需要在固定时间间隔执行的定时任务,setInterval 可以满足需求,但需要注意它可能会导致动画效果不平滑,并且在非活动标签页时仍然消耗资源。
    通过理解 requestAnimationFramesetInterval 的区别,可以根据具体需求选择合适的函数来实现动画或定时任务。

15 WebSocket

12 WebSocket

WebSocket

WebSocket 是一种网络通信协议,它提供了一种在单个 TCP 连接上进行全双工通信的方式。WebSocket 允许服务器主动向客户端发送数据,而不需要客户端不断发送请求来轮询服务器。这种特性使得 WebSocket 非常适合需要实时数据传输的应用,如在线聊天、实时游戏、股票行情更新等。

WebSocket 的特点

  • 全双工通信:WebSocket 连接允许服务器和客户端之间同时进行数据的发送和接收,而不需要像 HTTP 请求那样等待对方的响应。
  • 持久连接:一旦 WebSocket 连接建立,它会保持打开状态,直到客户端或服务器显式地关闭连接。这避免了频繁地建立和关闭连接的开销。
  • 基于 HTTP 的握手:WebSocket 连接的建立是通过 HTTP 请求完成的,称为 WebSocket 握手。服务器在接收到 WebSocket 握手请求后,会将连接升级为 WebSocket 连接。
  • 二进制数据传输:WebSocket 支持传输二进制数据,如 Blob 或 ArrayBuffer,这使得 WebSocket 能够高效地传输图像、音频、视频等二进制文件。

WebSocket 的工作流程

  1. 握手:客户端通过发送一个特殊的 HTTP 请求来请求 WebSocket 连接。这个请求包含一个 Upgrade 头,表明客户端希望将连接升级为 WebSocket 连接。服务器如果同意升级,则会返回一个响应,将连接升级为 WebSocket 连接。
  2. 数据传输:一旦 WebSocket 连接建立,客户端和服务器就可以通过该连接自由地发送和接收数据。数据可以是文本数据,也可以是二进制数据。
  3. 关闭连接:WebSocket 连接可以由客户端或服务器关闭。关闭连接时,需要发送一个关闭帧,对方收到后也会关闭连接。

WebSocket API

在 JavaScript 中,可以通过 WebSocket 对象来使用 WebSocket 协议。以下是一些常用的属性和方法:

  • 属性
    • readyState:表示 WebSocket 连接的状态。可能的值有:
      • 0:连接尚未建立(CONNECTING)
      • 1:连接已建立(OPEN)
      • 2:正在关闭连接(CLOSING)
      • 3:连接已关闭(CLOSED)
    • bufferedAmount:表示已发送但尚未被浏览器传输的数据量(以字节为单位)。
    • extensions:表示服务器选择的 WebSocket 扩展。
    • protocol:表示服务器选择的子协议。
    • url:表示 WebSocket 连接的 URL。
  • 方法
    • constructor(url, [protocols]):创建一个新的 WebSocket 对象。url 是 WebSocket 服务器的地址,protocols 是可选的,可以指定一个或多个子协议。
    • send(data):向服务器发送数据。data 可以是字符串、Blob 或 ArrayBuffer。
    • close([code], [reason]):关闭 WebSocket 连接。code 是关闭连接的状态码,reason 是关闭连接的原因。
  • 事件
    • open:当 WebSocket 连接成功建立时触发。
    • error:当 WebSocket 连接发生错误时触发。
    • message:当从服务器接收到数据时触发。
    • close:当 WebSocket 连接关闭时触发。

示例代码

以下是一个简单的 WebSocket 客户端示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建WebSocket连接
const socket = new WebSocket('wss://example.com/socket');
// 连接成功时的处理
socket.addEventListener('open', function(event) {
console.log('WebSocket连接已建立');
socket.send('Hello, Server!'); // 向服务器发送数据
});
// 接收到服务器数据时的处理
socket.addEventListener('message', function(event) {
console.log('收到服务器数据:', event.data);
});
// 连接关闭时的处理
socket.addEventListener('close', function(event) {
console.log('WebSocket连接已关闭');
});
// 发生错误时的处理
socket.addEventListener('error', function(event) {
console.error('WebSocket连接发生错误');
});

WebSocket 协议的出现极大地简化了实时通信的开发,使得开发者可以更方便地实现各种实时交互功能。

WebSocket心跳机制

WebSocket心跳机制是用于维持客户端与服务器之间长连接的一种保活机制,通过定时发送心跳包来保持连接的活跃状态,防止因长时间无通讯而导致的自动断开。以下是心跳机制的详细说明:

一、心跳机制的实现方式

  1. 客户端定时向服务器发送心跳数据包
    • 客户端每隔一定时间(通常小于60秒)向服务器发送一个心跳数据包,服务器收到后返回一个响应。这种方式可以保持连接的活跃状态,防止连接因长时间无活动而被关闭。
    • 示例代码:
      1
      2
      3
      this.heartbeatTimer = setInterval(() => {
      ws.send('{"event":"ping","content":"ping heartbeat"}');
      }, originData.ping_interval); // originData.ping_interval 是心跳间隔时间
  2. 服务器定时向客户端发送心跳数据包
    • 服务器每隔一定时间向客户端发送一个心跳数据包,客户端收到后返回一个响应。这种方式可以检测客户端连接是否正常。
    • 示例代码(服务端配置):
      1
      2
      3
      4
      $gateway = new Gateway("websocket://0.0.0.0:8783");
      $gateway->pingInterval = 55; // 心跳检测时间间隔,单位:秒
      $gateway->pingNotResponseLimit = 0; // 客户端连续多少次不响应心跳时关闭连接
      $gateway->pingData = '{"type":"ping"}'; // 服务端发送的心跳数据
  3. 双向发送心跳数据包
    • 客户端和服务器都定时发送心跳数据包,这种方式可以更全面地检测连接状态。

二、心跳机制的原理

  1. 客户端建立WebSocket连接
    • 客户端通过new WebSocket(url)建立连接。
  2. 客户端向服务器发送心跳数据包
    • 客户端定时发送心跳数据包,服务器接收并返回响应。
  3. 服务器没有及时接收到心跳数据包
    • 如果服务器在设定时间内没有收到客户端的心跳数据包,会发送一个关闭连接的请求。
  4. 服务器定时向客户端发送心跳数据包
    • 服务器定时发送心跳数据包,客户端接收并返回响应。
  5. 客户端没有及时接收到心跳数据包
    • 如果客户端在设定时间内没有收到服务器的心跳数据包,会重新连接WebSocket。

三、心跳机制的作用

  1. 保持WebSocket连接不被断开
    • 通过定时发送心跳数据包,可以防止连接因长时间无活动而被关闭。
  2. 检测WebSocket连接状态
    • 可以及时发现连接是否异常断开,从而进行重连等操作。
  3. 减少资源消耗
    • 通过心跳机制,可以减少因连接异常断开而重新建立连接的次数,从而减少资源消耗。

四、心跳机制的必要性

WebSocket心跳机制是必要的,它可以使WebSocket连接保持长连接,避免断开连接的情况发生。同时,心跳机制也可以检查WebSocket连接的状态,及时处理异常情况。

五、心跳机制的实现示例

客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function(evt) {
console.log('连接成功');
startHeartBeat(30000); // 每30秒发送一次心跳
};
ws.onmessage = function(evt) {
console.log('收到消息: ' + evt.data);
// 重置心跳超时
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
console.log('心跳无响应,已断线');
ws.close();
// 重连操作
}, 30000);
};
ws.onclose = function(evt) {
console.log('连接关闭');
// 重连操作
setTimeout(() => {
ws = new WebSocket("ws://127.0.0.1:8783");
}, 1000);
};
function startHeartBeat(interval) {
this.heartbeatTimer = setInterval(() => {
ws.send('{"event":"ping","content":"ping heartbeat"}');
}, interval);
}

服务端实现(以Node.js为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8783 });
wss.on('connection', function connection(ws) {
console.log('客户端连接成功');
ws.on('message', function incoming(message) {
console.log('收到消息: %s', message);
if (message.includes('ping')) {
ws.send('{"event":"pong","content":"pong heartbeat"}');
}
});
// 服务端定时发送心跳数据包
setInterval(() => {
ws.send('{"type":"ping"}');
}, 55000); // 每55秒发送一次心跳
});

通过以上实现,可以有效地保持WebSocket连接的活跃状态,及时发现并处理连接异常。

16 多线程

13 多线程

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 console.log("Start of script");
// 宏任务:setTimeout
setTimeout(() => {
console.log("setTimeout 1");
}, 0);
// 微任务:Promise
Promise.resolve().then(() => {
console.log("Promise 1");
});
// 宏任务:setInterval
setInterval(() => {
console.log("setInterval 1");
}, 1000);
// 微任务:queueMicrotask
queueMicrotask(() => {
console.log("queueMicrotask 1");
});
//用于在浏览器重绘之前执行回调
requestAnimationFrame(() => {
console.log("requestAnimationFrame callback");
});
// 宏任务:setTimeout
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
// 微任务:Promise
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("End of script");
// 事件监听器
document.getElementById("myButton").addEventListener("click", () => {
console.log("Button clicked");
});

console:

1
2
3
4
5
6
7
8
9
10
11
Start of script
End of script
Promise 1
queueMicrotask 1
Promise 2
requestAnimationFrame callback
setTimeout 1
setTimeout 2
setInterval 1
Button clicked
setInterval 1

事件循环

JavaScript 的事件循环(Event Loop)是理解 JavaScript 异步编程和运行机制的关键概念。它解释了 JavaScript 如何在单线程环境下实现异步操作,如定时器、回调函数、Promise 等。以下是 JavaScript 事件循环的详细解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
A[事件循环开始] --> B{调用栈为空?}
B -->|是| C{微任务队列有任务?}
B -->|否| H[执行调用栈任务]
C -->|是| D[取出微任务执行]
D --> E{微任务队列为空?}
E -->|否| C
E -->|是| F{调用栈为空?}
F -->|是| G{微任务队列为空?}
G -->|是| I[从任务队列取出任务执行]
G -->|否| C
F -->|否| H
I --> A
H --> A

1. JavaScript 的运行机制

JavaScript 是单线程语言,这意味着它在同一时间只能执行一个任务。然而,JavaScript 通过事件循环机制实现了异步操作,使得代码可以“非阻塞”地运行。

核心概念:

  • 调用栈(Call Stack):记录当前正在执行的函数。
  • 任务队列(Task Queue):存放待执行的回调函数。
  • 微任务队列(Microtask Queue):存放由 Promise、MutationObserver 等产生的回调。
  • 事件循环(Event Loop):监控调用栈和任务队列,当调用栈为空时,从任务队列中取出任务执行。

2. 事件循环的工作原理

事件循环的主要工作是协调调用栈、任务队列和微任务队列之间的关系。

2.1 调用栈

调用栈是一个后进先出(LIFO)的栈结构,记录当前正在执行的函数。每当一个函数被调用时,它会被压入调用栈;当函数执行完成时,它会被弹出。

2.2 任务队列

任务队列存放由异步操作(如 setTimeoutsetIntervaladdEventListener 等)产生的回调函数。这些回调函数会在调用栈为空时被事件循环取出并执行。

2.3 微任务队列

微任务队列存放由 Promise、MutationObserver 等产生的回调函数。微任务的优先级高于任务队列中的任务,会在当前调用栈清空后立即执行。

2.4 事件循环

事件循环的主要工作是:

  1. 检查调用栈是否为空:如果调用栈为空,事件循环会从微任务队列中取出任务执行,直到微任务队列为空。
  2. 执行微任务:微任务执行完成后,事件循环会检查调用栈是否为空。
  3. 执行任务队列中的任务:如果调用栈为空且微任务队列为空,事件循环会从任务队列中取出任务执行。

3. 示例解析

示例 1:任务队列和调用栈

1
2
3
4
5
6
7
8
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');

执行顺序
4. console.log('Start') 执行,输出 Start
5. 两个 setTimeout 被添加到任务队列中。
6. console.log('End') 执行,输出 End
7. 调用栈为空,事件循环从任务队列中取出第一个回调函数执行,输出 Timeout 1
8. 事件循环从任务队列中取出第二个回调函数执行,输出 Timeout 2
输出结果

1
2
3
4
Start
End
Timeout 1
Timeout 2

示例 2:微任务队列和任务队列

1
2
3
4
5
6
7
8
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');

执行顺序
9. console.log('Start') 执行,输出 Start
10. setTimeout 被添加到任务队列中。
11. Promise.resolve().then() 被添加到微任务队列中。
12. console.log('End') 执行,输出 End
13. 调用栈为空,事件循环检查微任务队列,执行 Promise 回调,输出 Promise
14. 微任务队列为空,事件循环检查任务队列,执行 setTimeout 回调,输出 Timeout
输出结果

1
2
3
4
Start
End
Promise
Timeout

4. 事件循环的阶段

事件循环分为多个阶段,每个阶段都有特定的任务:
15. 宏任务(Macrotask)
- 包括 setTimeoutsetIntervalsetImmediate(Node.js)、I/O 操作等。
- 每次执行一个宏任务后,事件循环会检查微任务队列。
16. 微任务(Microtask)
- 包括 PromiseMutationObserver 等。
- 微任务的优先级高于宏任务,会在每次宏任务执行后立即执行。


5. Node.js 与浏览器中的事件循环

虽然浏览器和 Node.js 的事件循环机制类似,但它们在实现上有一些差异:

  • 浏览器:事件循环由浏览器引擎(如 V8)管理。
  • Node.js:事件循环由 libuv 库实现,支持更多类型的异步操作(如文件系统操作、网络请求等)。

6. 总结

JavaScript 的事件循环是理解异步编程的关键。它通过协调调用栈、任务队列和微任务队列,实现了高效的异步操作。事件循环的工作机制确保了 JavaScript 即使在单线程环境下也能高效地处理异步任务。
掌握事件循环的原理有助于更好地理解 JavaScript 中的异步编程,避免常见的错误(如回调地狱、Promise 陷阱等)。

settimeout能不能精准计时

setTimeout 是 JavaScript 中用于实现延迟执行的函数,但它并不能保证精准计时,主要有以下几方面原因:
一、事件循环机制的影响
17. JavaScript 是单线程运行的,采用事件驱动机制。当调用 setTimeout 设置延迟任务时,这个任务会被放入事件队列中。例如:

1
2
3
setTimeout(() => {
console.log('延迟任务');
}, 1000);

这段代码的意图是让“延迟任务”在1000毫秒后执行。但是,如果在 setTimeout 调用之后,主线程上还有其他耗时任务在执行,那么事件循环就需要等待这个耗时任务完成,才能从事件队列中取出 setTimeout 的回调函数来执行。比如:
1
2
3
4
5
6
setTimeout(() => {
console.log('延迟任务');
}, 1000);
// 假设这是一个耗时2000毫秒的同步任务
for (let i = 0; i < 1000000000; i++) {}
console.log('同步任务完成');

在这种情况下,“延迟任务”实际上会在2000毫秒之后的某个时刻才执行,因为事件循环需要等待同步任务完成。
二、浏览器或JavaScript引擎的限制

  1. 浏览器或 JavaScript 引擎对 setTimeout 的最小延迟时间有一定的限制。在大多数浏览器中,setTimeout 的最小延迟时间是4毫秒(在某些情况下,如页面处于后台标签页时,这个时间可能会更长)。即使你设置的延迟时间小于4毫秒,实际的延迟也会被调整为至少4毫秒。例如:
    1
    2
    3
    setTimeout(() => {
    console.log('快速延迟任务');
    }, 1); // 实际延迟至少为4毫秒
    这种限制是为了防止页面被过于频繁的定时任务所阻塞,从而影响用户体验。
    三、系统时间调整的影响(较少见)
  2. 如果在 setTimeout 执行期间,系统时间被人为调整(比如将系统时间向前拨动),也可能会对 setTimeout 的执行产生影响。不过这种情况在实际开发中比较少见,主要是在一些特殊场景下(如服务器时间校准等)可能会出现。
    虽然 setTimeout 不能保证精准计时,但在大多数情况下,它能够满足一般的延迟执行需求。如果需要更精准的计时,可以考虑使用 setInterval(虽然它也有类似的限制)或者 Web Workers(在后台线程中执行定时任务,减少主线程的影响)等其他方法,具体选择取决于应用场景和精度要求。

requestAnimationFrame

requestAnimationFrame 既不是传统的宏任务也不是微任务,它有自己独特的调度机制。不过,为了更好地理解它的执行时机,我们可以将其与宏任务和微任务进行对比。

宏任务(Macrotask)

宏任务包括整体的 script(整体代码)、setTimeout、setInterval、setImmediate(Node.js 环境)、I/O 操作、UI 交互事件等。宏任务的特点是它们会在当前任务执行完毕后,进入事件循环的下一个阶段进行处理。

微任务(Microtask)

微任务包括 Promise、MutationObserver(用于监听 DOM 变化)、process.nextTick(Node.js 环境)等。微任务的特点是它们会在当前任务执行完毕后,立即执行,不会进入事件循环的下一个阶段。

requestAnimationFrame

requestAnimationFrame 用于在下一次重绘之前执行回调函数。它的执行时机在宏任务和微任务之间。具体来说,requestAnimationFrame 的回调会在当前帧的所有宏任务执行完毕后,且在下一次重绘之前执行。如果在 requestAnimationFrame 的回调中再次调用 requestAnimationFrame,则新的回调会在下一次重绘之前执行。

执行顺序

  1. 宏任务:当前帧的宏任务执行完毕。
  2. 微任务:当前帧的微任务队列中的所有任务执行完毕。
  3. requestAnimationFrame:当前帧的 requestAnimationFrame 回调执行。
  4. 重绘:浏览器进行重绘。
  5. 下一个宏任务:进入事件循环的下一个宏任务。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log('1. Start');
setTimeout(() => {
console.log('2. Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
requestAnimationFrame(() => {
console.log('4. First rAF');
});
requestAnimationFrame(() => {
requestAnimationFrame(() => {
console.log('5. Second rAF');
});
});
console.log('6. End');

执行顺序将是:

1
2
3
4
5
6
8. Start
9. End
10. Promise
11. First rAF
12. Timeout
13. Second rAF

总结

  • requestAnimationFrame:在当前帧的所有宏任务和微任务执行完毕后,且在下一次重绘之前执行。
  • 微任务:在当前任务执行完毕后立即执行,不会进入事件循环的下一个阶段。
  • 宏任务:在当前任务执行完毕后,进入事件循环的下一个阶段执行。
    requestAnimationFrame 的这种独特的调度机制使其非常适合用于动画渲染,因为它能够确保在浏览器重绘之前完成必要的更新,从而提高动画的流畅性。

node

Node.js 和浏览器中的 JavaScript 都使用事件循环来处理异步任务,但它们在实现和具体行为上存在一些关键差异。这些差异主要源于它们运行环境的不同:浏览器侧重于用户交互和页面渲染,而 Node.js 侧重于服务器端的 I/O 操作和网络通信。
以下是 Node.js 事件循环和浏览器事件循环的主要区别:

1. 运行环境

  • 浏览器
    • 浏览器的事件循环由浏览器引擎(如 V8、SpiderMonkey 等)管理。
    • 主要处理用户交互(如点击、滚动)、页面渲染、定时器、网络请求等任务。
  • Node.js
    • Node.js 的事件循环由底层的 libuv 库实现。
    • 主要处理 I/O 操作(如文件读写、网络请求)、定时器、进程间通信等任务。

2. 事件循环的阶段

Node.js 和浏览器的事件循环都包含多个阶段,但具体阶段的划分和功能有所不同。

2.1 浏览器的事件循环阶段

浏览器的事件循环主要分为:
14. 宏任务(Macrotask)
- 包括 setTimeoutsetIntervalrequestAnimationFrameI/O 操作等。
- 每次执行一个宏任务后,事件循环会检查微任务队列。
15. 微任务(Microtask)
- 包括 PromiseMutationObserver 等。
- 微任务的优先级高于宏任务,会在每次宏任务执行后立即执行。

2.2 Node.js 的事件循环阶段

Node.js 的事件循环由 libuv 库实现,分为以下阶段:
16. Timers
- 执行到期的 setTimeoutsetInterval 回调。
17. Pending Callbacks
- 执行某些系统操作的回调(如 TCP 错误回调)。
18. Idle, Prepare
- 内部使用,通常与 Node.js 的 C++ 插件相关。
19. Poll
- 执行 I/O 回调(如文件读写、网络请求)。
- 如果没有待处理的 I/O 回调,事件循环可能会在此阶段阻塞,等待新的事件。
20. Check
- 执行 setImmediate 的回调。
21. Close Callbacks
- 执行某些资源关闭的回调(如 socket.close)。


3. 定时器的实现

  • 浏览器
    • setTimeoutsetInterval 是由浏览器的定时器机制实现的。
    • 它们依赖于浏览器的事件循环,但具体实现细节由浏览器引擎决定。
  • Node.js
    • setTimeoutsetInterval 是由 libuv 的定时器机制实现的。
    • 它们的行为与浏览器类似,但 Node.js 提供了更细粒度的控制(如 setImmediate)。

3.1 setImmediate

Node.js 提供了 setImmediate,它在事件循环的 Check 阶段执行回调。这与浏览器中的 setTimeout 有所不同:

1
2
3
4
5
6
setTimeout(() => {
console.log('Timeout');
}, 0);
setImmediate(() => {
console.log('Immediate');
});

在 Node.js 中,setImmediate 的回调会在 Check 阶段执行,而 setTimeout 的回调会在 Timers 阶段执行。因此,setImmediate 的回调通常会在 setTimeout 的回调之后执行。

4. I/O 操作

  • 浏览器
    • I/O 操作主要与网络请求(如 fetchXMLHttpRequest)和文件系统(通过 Web APIs)相关。
    • 浏览器的 I/O 操作通常由浏览器内核实现,与 JavaScript 运行时紧密集成。
  • Node.js
    • I/O 操作是 Node.js 的核心功能之一,由 libuv 库实现。
    • Node.js 提供了丰富的 I/O API(如 fs 模块、http 模块),支持非阻塞 I/O 操作。
    • Node.js 的 I/O 操作回调通常在 Poll 阶段执行。

5. 事件循环的控制

  • 浏览器
    • 事件循环由浏览器引擎管理,开发者无法直接控制事件循环的行为。
    • 事件循环的行为主要受 JavaScript 代码和浏览器内核的限制。
  • Node.js
    • 事件循环由 libuv 库实现,提供了更细粒度的控制。
    • 开发者可以通过 process.nextTicksetImmediate 等 API 控制事件循环的行为。

5.1 process.nextTick

Node.js 提供了 process.nextTick,它允许开发者在当前操作完成后立即执行回调:

1
2
3
4
5
console.log('Start');
process.nextTick(() => {
console.log('Next Tick');
});
console.log('End');

输出:
复制

1
2
3
Start
End
Next Tick

process.nextTick 的回调会在当前操作完成后立即执行,优先级高于微任务和宏任务。

6. 总结

Node.js 和浏览器中的事件循环虽然在核心机制上类似,但在实现和具体行为上存在以下主要区别:
22. 运行环境
- 浏览器侧重于用户交互和页面渲染。
- Node.js 侧重于 I/O 操作和网络通信。
23. 事件循环阶段
- 浏览器的事件循环分为宏任务和微任务。
- Node.js 的事件循环分为多个阶段(如 Timers、Poll、Check 等)。
24. 定时器实现
- 浏览器使用 setTimeoutsetInterval
- Node.js 提供了 setTimeoutsetIntervalsetImmediate
25. I/O 操作
- 浏览器的 I/O 操作主要与网络和文件系统相关。
- Node.js 的 I/O 操作由 libuv 库实现,支持丰富的异步操作。
26. 事件循环控制
- 浏览器的事件循环由浏览器引擎管理,开发者无法直接控制。
- Node.js 提供了 process.nextTick 等 API,允许开发者更细粒度地控制事件循环。
理解这些差异有助于更好地编写高效、可靠的代码,尤其是在处理异步操作和性能优化时。

17 typescript

TypeScript

TypeScript

  • 接口
  • 泛型
  • 枚举
  • 交叉类型
  • 联合类型

TypeScript是什么

TypeScript 是一种由微软开发的开源编程语言。它是 JavaScript 的一个超集,意味着任何有效的 JavaScript 代码也是有效的 TypeScript 代码。TypeScript 的设计目标是提供静态类型检查和对 ES6+(ECMAScript 2015 及更高版本)特性的支持,以帮助开发者更有效地开发大型应用程序。以下是 TypeScript 的一些关键特性:

  1. 静态类型系统
    • TypeScript 引入了静态类型系统,允许开发者为变量、函数参数和返回值指定类型。这有助于在编译时期就发现潜在的类型错误。
  2. 接口和类型别名
    • 提供了接口(Interfaces)和类型别名(Type Aliases)来定义对象的形状,增强代码的可读性和可维护性。
  3. 类和模块
    • 支持基于类的面向对象编程,包括类的继承、实现和泛型。同时,TypeScript 支持 ES6 模块系统,使得代码模块化更加方便。
  4. 高级类型
    • 提供了多种高级类型,如联合类型、交叉类型、泛型等,以表达复杂的类型关系。
  5. 编译到 JavaScript
    • TypeScript 最终会被编译成 JavaScript 代码,这意味着它可以在任何支持 JavaScript 的环境中运行。
  6. 工具和集成
    • 拥有强大的工具支持,包括自动补全、类型检查、重构等。它与现代的编辑器和 IDE(如 Visual Studio Code)集成良好。
  7. 可选链和空值合并
    • 支持可选链(Optional Chaining)和空值合并(Nullish Coalescing)等现代 JavaScript 特性。
  8. 装饰器
    • 提供了装饰器(Decorators)的实验性支持,这是一种特殊类型的声明性函数,可以被附加到类声明、方法、访问器、属性或参数上。
  9. 类型推断
    • TypeScript 的编译器非常智能,能够根据上下文推断出变量的类型,减少类型注解的需要。
  10. 编译配置
  • 通过 tsconfig.json 文件来配置编译选项,如目标 ECMAScript 版本、模块系统、源映射等。
    TypeScript 由于其类型系统和对现代 JavaScript 特性的支持,已经成为许多大型项目的首选语言,尤其是在 Angular、React 和 Vue 等前端框架的开发中。它帮助开发者编写更健壮、更易于维护的代码,并减少运行时错误。

在TypeScript中,类(Class)是一种基本类型,它提供了一种方式来创建和使用对象。类的主要作用包括:

  1. 封装(Encapsulation)
    • 类可以将数据(属性)和行为(方法)封装在一起,隐藏内部实现细节,只暴露必要的接口给外部使用。
  2. 继承(Inheritance)
    • 类可以实现继承,允许一个类(子类)继承另一个类(父类)的属性和方法,这有助于代码复用和组织结构。
  3. 多态(Polymorphism)
    • 通过类的继承和接口实现,可以在不同的上下文中以统一的方式使用不同的对象,这使得代码更加灵活和可扩展。
  4. 代码复用
    • 通过继承,子类可以复用父类的代码,减少重复代码的编写。
  5. 组织结构
    • 类提供了一种结构化的方式来组织代码,使得代码更加模块化,易于理解和维护。
  6. 类型安全
    • TypeScript中的类可以定义属性和方法的类型,这有助于编译时检查类型错误,提高代码的健壮性。
  7. 接口实现
    • 类可以实现一个或多个接口,确保类实现特定的结构和行为。
  8. 抽象
    • 类可以被声明为抽象类,这意味着它们不能被直接实例化,而是用来作为其他类的基类。
  9. 访问修饰符
    • 类可以使用访问修饰符(如public, private, protected)来控制成员的可见性和访问级别。
  10. 静态成员
    • 类可以包含静态成员(属性和方法),这些成员属于类本身而不是类的实例。
  11. 构造函数
    • 类可以有一个或多个构造函数,用于在创建类的实例时初始化对象的状态。
  12. 析构函数(TypeScript 4.0+):
    • 类可以有一个析构函数,用于在对象被销毁时执行清理工作。
  13. 装饰器(Decorators):
    • TypeScript支持装饰器,这是一种特殊类型的声明,它可以被附加到类声明、方法、属性、参数等,提供额外的元数据和功能。
      类是TypeScript中面向对象编程的核心概念之一,它们提供了一种强大的工具来构建复杂的应用程序和库。通过使用类,开发者可以创建可重用、可维护和可扩展的代码。

TypeScript 的交叉类型

TypeScript 的交叉类型(Intersection Types)是一种高级类型,它允许将多个类型合并成一个新类型,使用符号 & 表示。交叉类型的主要特点是,一个值必须同时满足所有合并类型的要求。

语法

交叉类型的语法是通过 & 符号将多个类型组合在一起,例如:

1
type NewType = Type1 & Type2 & Type3;

基本用法

以下是一个简单的交叉类型示例:

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
type EmployeePerson = Person & Employee;
const john: EmployeePerson = {
name: "John Doe",
employeeId: 1234
};

在这个例子中,EmployeePerson 类型结合了 PersonEmployee 接口,因此 john 对象必须同时包含 nameemployeeId 属性。

属性冲突

当合并的类型中存在同名属性时,TypeScript 会根据以下规则处理:

  • 如果同名属性的类型相同,则合并后的属性类型保持不变。
  • 如果同名属性的类型不同,则合并后的属性类型为 never,表示该属性不可用。

函数中的交叉类型

交叉类型也可以用于函数参数或返回值类型。例如:

1
2
3
4
function printEmployeeDetails(employee: Person & Employee): void {
console.log(`Name: ${employee.name}`);
console.log(`Employee ID: ${employee.employeeId}`);
}

在这个例子中,printEmployeeDetails 函数的参数类型是 Person & Employee,因此传入的对象必须同时包含 nameemployeeId 属性。

应用场景

交叉类型常用于以下场景:

  1. 混入(Mixins):通过合并多个类的实例来创建一个新的对象,使其具备多个类的属性和方法。
  2. 合并对象属性:将多个对象的属性合并到一个对象中。

注意事项

  • 原子类型(如 stringnumber)之间不能进行交叉类型合并,因为它们合并后的类型是 never
  • 如果合并的类型中存在方法冲突,TypeScript 会报错。
    总之,交叉类型是一种强大的类型操作符,可以帮助我们创建复杂且具有多种特性的类型,适用于需要合并多个类型属性和方法的场景。

联合类型

TypeScript 中的联合类型(Union Types)是一种非常强大的类型系统特性,它允许一个变量可以是多种类型中的任意一种。联合类型通过使用 | 符号来组合多个类型,表示一个值可以是这些类型中的任意一个。

语法

联合类型的语法是通过 | 符号将多个类型组合在一起,例如:

1
type StringOrNumber = string | number;

基本用法

以下是一个简单的联合类型示例:

1
2
3
4
let value: string | number;
value = "Hello"; // OK
value = 123; // OK
value = true; // Error: Type 'boolean' is not assignable to type 'string | number'

在这个例子中,value 可以是 stringnumber,但不能是其他类型。

联合类型的属性访问

当使用联合类型时,TypeScript 会限制你只能访问那些在所有联合类型中都存在的属性或方法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Bird {
fly: () => void;
layEggs: () => void;
}
interface Fish {
swim: () => void;
layEggs: () => void;
}
type BirdOrFish = Bird | Fish;
function getSmallPet(): BirdOrFish {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // OK,因为 Bird 和 Fish 都有 layEggs 方法
pet.swim(); // Error: Property 'swim' does not exist on type 'BirdOrFish'

在这个例子中,pet 的类型是 Bird | Fish。由于 BirdFish 都有 layEggs 方法,因此可以调用 pet.layEggs()。但是,swim 方法只存在于 Fish 中,因此不能直接调用 pet.swim()

类型守卫

为了处理联合类型中的具体类型,通常需要使用类型守卫(Type Guards)来区分具体的类型。TypeScript 提供了几种类型守卫的方式:

1. 类型谓词(Type Predicate)

通过 typeofinstanceof 来检查类型:

1
2
3
4
5
6
7
8
function isFish(pet: BirdOrFish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
if (isFish(pet)) {
pet.swim(); // OK,TypeScript 知道 pet 是 Fish 类型
} else {
pet.fly(); // OK,TypeScript 知道 pet 是 Bird 类型
}

2. 字面量类型守卫

如果联合类型中包含字面量类型,可以通过检查字面量值来区分类型:

1
2
3
4
5
6
7
8
type Shape = { kind: "circle"; radius: number } | { kind: "square"; sideLength: number };
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2; // OK,TypeScript 知道 shape 是圆
} else {
return shape.sideLength ** 2; // OK,TypeScript 知道 shape 是正方形
}
}

联合类型的赋值规则

联合类型遵循以下赋值规则:

  • 如果一个值的类型是联合类型中的任意一个,那么它可以赋值给联合类型。
  • 如果一个值的类型是联合类型的子类型,那么它也可以赋值给联合类型。

联合类型与交叉类型的区别

  • 联合类型(Union Types):表示一个值可以是多种类型中的任意一种,使用 | 符号。
  • 交叉类型(Intersection Types):表示一个值必须同时满足多种类型的要求,使用 & 符号。

应用场景

联合类型在以下场景中非常有用:

  1. 函数参数类型:当函数参数可以接受多种类型时,可以使用联合类型。
    1
    2
    3
    function printId(id: number | string) {
    console.log(`Your ID is: ${id}`);
    }
  2. 对象类型:当对象可以是多种形状时,可以使用联合类型。
    1
    2
    3
    4
    5
    type Point2D = { x: number; y: number };
    type Point3D = { x: number; y: number; z: number };
    type Point = Point2D | Point3D;
    let point: Point = { x: 1, y: 2 }; // OK
    let point3D: Point = { x: 1, y: 2, z: 3 }; // OK
  3. 类型守卫:通过类型守卫区分联合类型中的具体类型,从而实现类型安全的操作。
  4. 字面量类型:联合类型常用于字面量类型,例如字符串字面量或数字字面量。
    1
    type Direction = "up" | "down" | "left" | "right";

注意事项

  • 联合类型中如果有 nullundefined,则需要显式地包含它们,否则会导致类型错误。
  • 联合类型中如果有重叠的属性或方法,TypeScript 会限制你只能访问那些在所有联合类型中都存在的属性或方法。
    总之,联合类型是 TypeScript 中非常灵活且强大的类型系统特性,它可以帮助我们处理多种类型的值,同时保持类型安全。

泛型

TypeScript 中的泛型(Generics)是一种强大的类型系统特性,允许在定义函数、接口或类时,不预先指定具体的类型,而是在使用时再指定类型。泛型的主要优点是提供了类型重用、类型安全性和灵活性。

泛型的基本使用

泛型函数

泛型函数允许用户为函数的类型参数传递一个或多个类型参数。这些类型参数可用于函数参数、函数返回值或函数体中的其他任何位置。

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString"); // 明确指定T为string类型
let output2 = identity(5); // 编译器自动推断T为number类型

泛型接口

在定义接口时,也可以使用泛型来创建可重用的组件。

1
2
3
4
5
6
7
8
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
let output = myIdentity(5); // 输出5,且类型是number

泛型类

在 TypeScript 中,也可以创建泛型类。

1
2
3
4
5
6
7
8
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;
let result = myGenericNumber.add(5, 10); // 输出15,且类型是number

泛型的应用场景

1. 通用函数

泛型函数通常用于处理不同类型的数据,但又不希望在每次调用时都明确指定类型。通过泛型,我们可以为函数提供更强的类型安全性。

1
2
3
4
5
6
7
function reverseArray<T>(items: T[]): T[] {
return items.reverse();
}
const reversedStringArray = reverseArray(["one", "two", "three"]);
console.log(reversedStringArray); // Output: ["three", "two", "one"]
const reversedNumberArray = reverseArray([1, 2, 3]);
console.log(reversedNumberArray); // Output: [3, 2, 1]

2. 容器类(集合类型)

泛型在容器类中尤为重要。例如,可以定义一个数组类,允许数组中的元素是任何类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Inventory<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
}
interface Book {
title: string;
author: string;
}
let bookInventory = new Inventory<Book>();
bookInventory.add({ title: "The Catcher in the Rye", author: "J.D. Salinger" });
console.log(bookInventory.getAll()); // 输出书籍的数组

3. 类型约束

泛型可以配合类型约束,限制泛型的具体类型,使得代码在灵活性和安全性之间取得平衡。

1
2
3
4
5
6
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("Hello, TypeScript"); // Output: 16
logLength([1, 2, 3]); // Output: 3
// logLength(123); // Error: number does not have a length property

4. 接口和类

泛型不仅可以应用于函数,还可以应用于接口和类,进一步增强代码的复用性和类型安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ApiResponse<T> {
data: T;
status: string;
message: string;
}
function handleApiResponse<T>(response: ApiResponse<T>) {
console.log(`Status: ${response.status}`);
console.log(`Message: ${response.message}`);
console.log("Data:", response.data);
}
const userResponse: ApiResponse<{ id: number; name: string }> = {
data: { id: 1, name: "Alice" },
status: "success",
message: "User fetched successfully"
};
handleApiResponse(userResponse);

泛型的高级应用

1. 联合类型与泛型

泛型和联合类型结合使用,可以允许函数处理多种类型的输入。

1
2
3
4
5
function combine<T, U>(a: T, b: U): T | U {
return a || b;
}
let result = combine(5, "hello");
console.log(result); // 输出 "hello"

2. 条件类型

TypeScript 4.1 引入了条件类型,它允许根据条件来决定类型。

1
2
3
type IsString<T> = T extends string ? "Yes" : "No";
type A = IsString<string>; // "Yes"
type B = IsString<number>; // "No"

总结

泛型是 TypeScript 中的一个强大工具,它可以使得代码更加灵活、类型安全并且可重用。通过泛型,函数、类、接口等结构可以处理不同类型的数据,而不牺牲类型检查的安全性。常见的应用场景包括通用的数据结构或容器类、高度可重用的函数和 API 请求处理等。

98 es6新内容

es6 新内容

简述

  • 06 类
  • 模块 07 模块
  • 新增语法
    • letconst
    • 字符串模板
    • 函数参数默认值
    • 参数扩展…
    • 箭头函数=>
    • forof
  • 新增对象
  • 原有对象新增属性
    • symbol 的操作
    • 迭代器相关/forof
    • 其他

es6 新增语法

在编程语言中,“语法糖”(Syntactic Sugar)是指那些使代码更易于编写和阅读的语法特性,这些特性并不增加语言的功能,但可以使代码更加简洁和易懂。JavaScript 也引入了许多语法糖,以下是一些常见的示例:

1. 箭头函数(Arrow Functions)

箭头函数是 ES6 引入的一种更简洁的函数书写方式,它有以下几个特点:

  • 更简洁的语法:不需要使用 function 关键字,直接使用 => 表示。
  • 没有自己的 this:箭头函数不绑定自己的 this,它会捕获其所在上下文的 this 值作为自己的 this 值,所以在箭头函数内部使用 this 时,实际上来自于外围最近一层非箭头函数的上下文。
  • 没有 arguments 对象:箭头函数不绑定自己的 arguments 对象,只能使用 rest 参数替代。
  • 不能用作构造函数:箭头函数不能用作构造函数,即不能使用 new 关键字调用箭头函数。
1
2
3
4
5
6
// 普通函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;

2. 解构赋值(Destructuring Assignment)

解构赋值允许你从数组或对象中提取数据,并将其赋值给变量。这使得代码更加简洁和易于理解。

对象解构

1
2
3
4
5
6
// 普通方式
const person = { name: 'Alice', age: 30 };
const name = person.name;
const age = person.age;
// 解构赋值
const { name, age } = person;

数组解构

1
2
3
4
5
6
// 普通方式
const numbers = [1, 2, 3];
const first = numbers[0];
const second = numbers[1];
// 解构赋值
const [first, second] = numbers;

3. 模板字符串(Template Literals)

模板字符串使用反引号 ` 包围,可以包含嵌入的表达式,这些表达式用 ${} 包围。模板字符串支持多行字符串和字符串插值,使字符串操作更加方便。

1
2
3
4
5
6
// 普通字符串拼接
const name = 'Alice';
const age = 30;
const message = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';
// 模板字符串
const message = `Hello, my name is ${name} and I am ${age} years old.`;

4. 默认参数(Default Parameters)

ES6 允许在函数的参数定义中设置默认值,这使得函数调用时可以省略某些参数,而这些参数会自动使用默认值。

1
2
3
4
5
6
7
8
9
10
11
// 普通方式
function greet(name) {
if (!name) {
name = 'Guest';
}
console.log(`Hello, ${name}!`);
}
// 默认参数
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}

5. 扩展运算符(Spread Operator)

扩展运算符 ... 可以将一个数组或对象展开为多个元素或属性,常用于数组或对象的合并、函数参数的传递等。

数组扩展

1
2
3
4
5
6
7
8
9
10
// 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 传递函数参数
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

对象扩展

1
2
3
4
// 合并对象
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 }

6. 简化对象字面量

ES6 允许在对象字面量中省略属性名,如果属性名和变量名相同,可以直接使用变量名。

1
2
3
4
5
6
// 普通方式
const name = 'Alice';
const age = 30;
const person = { name: name, age: age };
// 简化对象字面量
const person = { name, age };

7. 类(Classes)

ES6 引入了类的语法,虽然 JavaScript 仍然是基于原型的,但类语法提供了一种更简洁的方式来定义构造函数和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 普通方式
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 类语法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}

8. Promise

Promise 是 ES6 引入的一种异步编程解决方案,它提供了一种更简洁的方式来处理异步操作,避免了回调地狱(Callback Hell)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 普通回调
function fetchData(callback) {
setTimeout(() => {
callback('Data');
}, 1000);
}
fetchData((data) => {
console.log(data); // 'Data'
});
// Promise
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data');
}, 1000);
});
}
fetchData().then(data => {
console.log(data); // 'Data'
});

9. async/await

asyncawait 是 ES2017 引入的语法糖,用于简化 Promise 的使用,使异步代码看起来更像同步代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Promise
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data');
}, 1000);
});
}
fetchData().then(data => {
console.log(data); // 'Data'
});
// async/await
async function fetchDataAsync() {
const data = await fetchData();
console.log(data); // 'Data'
}
fetchDataAsync();

这些语法糖使得 JavaScript 代码更加简洁、易读和易写,提高了开发效率和代码质量。

99 js实战

06 JS实战

在js中,如何解决递归导致栈溢出问题

在 JavaScript 中,递归函数如果调用层次过深,会导致调用栈溢出(Stack Overflow)。为了解决这个问题,可以采用以下几种方法:

1. 尾调用优化(Tail Call Optimization)

  • 原理:尾调用优化是一种优化技术,当递归函数的调用是函数体中的最后一个操作时,编译器或解释器可以优化掉当前的调用栈帧,从而避免栈溢出。
  • 实现条件:递归调用必须是函数体中的最后一个操作,不能有其他操作跟随。
  • 示例代码
    1
    2
    3
    4
    5
    function factorial(n, acc = 1) {
    if (n === 1) return acc;
    return factorial(n - 1, n * acc);
    }
    console.log(factorial(1000)); // 尾调用优化的递归函数
  • 注意:尾调用优化在严格模式下('use strict';)效果更好,且不是所有 JavaScript 引擎都支持尾调用优化。

2. 使用循环代替递归

  • 原理:将递归逻辑转换为循环逻辑,通过迭代的方式实现相同的功能,避免递归调用。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    function factorial(n) {
    let result = 1;
    for (let i = 2; i <= n; i++) {
    result *= i;
    }
    return result;
    }
    console.log(factorial(1000)); // 使用循环代替递归

3. 使用栈模拟递归

  • 原理:手动使用栈(数组)来模拟递归调用的过程,将递归参数和状态存储在栈中,然后通过循环来处理栈中的数据。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function factorial(n) {
    const stack = [];
    let result = 1;
    while (n > 1) {
    stack.push(n);
    n--;
    }
    while (stack.length > 0) {
    result *= stack.pop();
    }
    return result;
    }
    console.log(factorial(1000)); // 使用栈模拟递归

4. 使用 Trampoline 函数

  • 原理:Trampoline 函数是一种用于处理递归调用的技术,它通过将递归调用转换为迭代调用,避免了递归调用导致的栈溢出。
  • 实现方法:创建一个 Trampoline 函数,该函数接受一个递归函数作为参数,并返回一个迭代函数。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function trampoline(fn) {
    return function(...args) {
    let result = fn.apply(this, args);
    while (typeof result === 'function') {
    result = result();
    }
    return result;
    };
    }
    function factorial(n, acc = 1) {
    if (n === 1) return acc;
    return () => factorial(n - 1, n * acc);
    }
    const trampolinedFactorial = trampoline(factorial);
    console.log(trampolinedFactorial(1000)); // 使用 Trampoline 函数

5. 增加调用栈大小

  • 原理:通过增加 JavaScript 调用栈的大小来允许更深层次的递归调用。
  • 方法:在 Node.js 中,可以通过命令行参数 --stack-size 来增加调用栈大小,例如 node --stack-size=10000 script.js
  • 注意:这种方法仅适用于 Node.js 环境,且增加调用栈大小可能会导致其他问题,如内存溢出等。
    通过以上方法,可以有效地解决 JavaScript 中递归导致的栈溢出问题,选择合适的方法取决于具体的场景和需求。

script标签上有那些属性,分别什么作用

<script> 标签用于在 HTML 文档中嵌入或引用 JavaScript 代码。它有多个属性,每个属性都有特定的作用,以下是一些常见的 <script> 标签属性及其作用:

常用属性

  • src
    • 作用:指定外部 JavaScript 文件的路径。如果设置了 src 属性,浏览器会加载并执行该文件中的 JavaScript 代码。
    • 示例
      1
      <script src="script.js"></script>
  • type
    • 作用:指定脚本的 MIME 类型。对于 JavaScript,默认值为 text/javascript,通常可以省略。
    • 示例
      1
      <script type="text/javascript"></script>
  • async
    • 作用:指定脚本是否异步加载和执行。如果设置了 async 属性,脚本会在下载时不会阻塞页面的渲染,下载完成后立即执行。
    • 特点
      • 适用于不依赖其他脚本的独立模块。
      • 脚本执行顺序不确定,可能在 DOMContentLoaded 事件触发之前或之后执行。
    • 示例
      1
      <script src="script.js" async></script>
  • defer
    • 作用:指定脚本是否延迟执行。如果设置了 defer 属性,脚本会在文档解析完成后、DOMContentLoaded 事件触发前执行。
    • 特点
      • 脚本会按照在页面中出现的顺序执行。
      • 适用于依赖 DOM 的脚本。
    • 示例
      1
      <script src="script.js" defer></script>
  • charset
    • 作用:指定脚本文件的字符编码。如果未指定,默认使用页面的字符编码。
    • 示例
      1
      <script src="script.js" charset="utf-8"></script>
  • crossorigin
    • 作用:设置跨源资源共享(CORS)请求的模式。当加载外部脚本时,可以指定是否需要 CORS 验证。
      • anonymous:不发送凭据信息(如 cookies 和 HTTP 认证信息)。
      • use-credentials:发送凭据信息。
    • 示例
      1
      <script src="https://example.com/script.js" crossorigin="anonymous"></script>

其他属性

  • integrity
    • 作用:指定一个加密哈希值,用于验证加载的脚本文件是否被篡改。
    • 示例
      1
      <script src="script.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9UcV6wFojX8Io"></script>
  • nomodule
    • 作用:指定脚本是否在不支持 ES 模块的浏览器中执行。如果设置了 nomodule 属性,支持 ES 模块的浏览器会忽略该脚本。
    • 示例
      1
      <script src="legacy-script.js" nomodule></script>
  • referrerpolicy
    • 作用:指定引用者策略,控制在加载脚本时发送的 Referer 头信息。
      • no-referrer:不发送 Referer 头。
      • origin:只发送源信息。
      • unsafe-url:发送完整的 URL。
    • 示例
      1
      <script src="script.js" referrerpolicy="no-referrer"></script>
      通过合理使用这些属性,可以更好地控制脚本的加载和执行行为,提高页面的性能和安全性。

网页加载进度条

在网页中实现加载进度条可以提升用户体验,尤其是在加载大量资源或数据时。以下是几种常见的实现方法:

1. 使用 HTML5 的 <progress> 元素

HTML5 提供了 <progress> 元素,可以用来显示任务的进度。虽然它本身不直接显示网页加载进度,但可以结合 JavaScript 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Progress Bar Example</title>
<style>
progress {
width: 100%;
height: 20px;
}
</style>
</head>
<body>
<progress id="progressBar" value="0" max="100"></progress>
<script>
window.addEventListener('load', function() {
const progressBar = document.getElementById('progressBar');
progressBar.value = 100;
});
// 模拟加载过程
const totalResources = 5;
let loadedResources = 0;
function updateProgress() {
loadedResources++;
const progressBar = document.getElementById('progressBar');
progressBar.value = (loadedResources / totalResources) * 100;
if (loadedResources < totalResources) {
setTimeout(updateProgress, 1000);
}
}
updateProgress();
</script>
</body>
</html>

2. 使用 JavaScript 监听资源加载事件

可以通过监听各种资源加载事件(如图片、脚本等)来更新进度条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Progress Bar Example</title>
<style>
#progressBar {
width: 100%;
height: 20px;
background-color: #f3f3f3;
border: 1px solid #ccc;
}
#progressBar div {
height: 100%;
width: 0;
background-color: #4caf50;
}
</style>
</head>
<body>
<div id="progressBar"><div></div></div>
<img src="image1.jpg" alt="Image 1">
<img src="image2.jpg" alt="Image 2">
<img src="image3.jpg" alt="Image 3">
<script>
const progressBar = document.getElementById('progressBar').firstChild;
const images = document.getElementsByTagName('img');
const totalImages = images.length;
let loadedImages = 0;
function updateProgress() {
loadedImages++;
const percent = (loadedImages / totalImages) * 100;
progressBar.style.width = percent + '%';
}
for (let i = 0; i < images.length; i++) {
images[i].addEventListener('load', updateProgress);
}
</script>
</body>
</html>

3. 使用第三方库

有一些第三方库可以帮助实现更复杂的进度条效果,例如 nprogress

1
npm install nprogress

然后在你的项目中引入并使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Progress Bar Example</title>
<link rel="stylesheet" href="nprogress.css">
</head>
<body>
<script src="nprogress.js"></script>
<script>
NProgress.start();
// 模拟加载过程
setTimeout(() => {
NProgress.done();
}, 3000);
</script>
</body>
</html>

这些方法可以根据你的具体需求和项目结构来选择和调整。

4. window.performance

监听静态资源加载情况
可以通过10 BOM window对象对象来监听页面资源加载进度。该对象提供了各种⽅法来获取资源加载的详细信息。 可以使用performance.getEntries()⽅法获取页面上所有的资源加载信息。可以使用该⽅法来监测每个资源的加载状态,计算加载时间,并据此来实现⼀个资源加载进度条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const resources = window.performance.getEntriesByType('resource'); 
const totalResources = resources.length;
let loadedResources = 0;
resources.forEach((resource) => {
// 排除 AJAX 请求
if (resource.initiatorType !== 'xmlhttprequest') {
resource.onload = () => {
loadedResources++;
const progress = Math.round((loadedResources / totalResources) * 100);
updateProgress(progress);
};
}
});
// 更新进度条
function updateProgress(progress) {
}

该代码会遍历所有资源,并注册⼀个onload事件处理函数。当每个资源加载完成后,会更新loadedResources变量,并计算当前的进度百分比,然后调⽤updateProgress()函数来更新进度条。需要注意的是,这里排除了AJAX请求,因为它们不属于页面资源。当所有资源加载完成后,页面就会完全加载。

站点一键换肤

在Vue中实现站点一键换肤可以通过多种方式来实现,以下是几种常见的方法:

方法一:使用CSS变量

  1. 定义CSS变量:在全局样式文件中定义CSS变量,用于存储主题相关的颜色、字体等样式信息。
    1
    2
    3
    4
    :root {
    --background-color: #ffffff;
    --text-color: #333333;
    }
  2. 使用CSS变量:在CSS中使用这些变量来设置样式。
    1
    2
    3
    4
    body {
    background-color: var(--background-color);
    color: var(--text-color);
    }
  3. 动态切换主题:通过JavaScript动态修改CSS变量的值来切换主题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function changeTheme(themeName) {
    const root = document.documentElement;
    if (themeName === 'dark') {
    root.style.setProperty('--background-color', '#333333');
    root.style.setProperty('--text-color', '#ffffff');
    } else {
    root.style.setProperty('--background-color', '#ffffff');
    root.style.setProperty('--text-color', '#333333');
    }
    }
  4. 创建切换按钮:在Vue组件中创建一个按钮,用于触发主题切换.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <button @click="handleThemeChange">切换主题</button>
    </template>
    <script>
    export default {
    methods: {
    handleThemeChange() {
    const currentTheme = document.documentElement.style.getPropertyValue('--background-color') === '#333333' ? 'light' : 'dark';
    changeTheme(currentTheme);
    }
    }
    }
    </script>

方法二:切换CSS文件

  1. 准备多个CSS文件:为每个主题准备一个CSS文件,定义不同的样式.
  2. 动态加载CSS文件:通过JavaScript动态切换加载不同的CSS文件来实现主题切换.
    1
    2
    3
    4
    function changeTheme(theme) {
    const link = document.getElementById('theme');
    link.href = `./css/${theme}.css`;
    }

方法三:使用组件库的主题支持

如果你使用的是支持主题切换的UI组件库(如Element Plus),可以直接利用其提供的主题切换功能.

  1. 引入组件库的主题文件:在项目中引入组件库的主题文件.
    1
    2
    import 'element-plus/dist/index.css';
    import './styles/theme.scss';
  2. 切换主题:通过修改组件库提供的主题变量来切换主题.
    1
    2
    3
    4
    5
    6
    html {
    --v-bg-color: #cfcccc;
    }
    html.dark {
    --v-bg-color: #141414;
    }

方法四:使用Vuex管理主题状态

  1. 定义状态:在Vuex中定义一个状态来存储当前的主题.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const store = new Vuex.Store({
    state: {
    theme: 'light'
    },
    mutations: {
    changeTheme(state, theme) {
    state.theme = theme;
    }
    }
    });
  2. 动态应用主题:在组件中根据Vuex中的状态动态应用不同的主题样式.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div :class="theme">
    <!-- 组件内容 -->
    </div>
    </template>
    <script>
    import { mapState, mapMutations } from 'vuex';
    export default {
    computed: {
    ...mapState(['theme'])
    },
    methods: {
    ...mapMutations(['changeTheme'])
    }
    }
    </script>

方法五:使用Less/Sass

使用Less/Sass等CSS预处理器:通过预处理器提供的变量、函数等功能来实现主题切换。
这些方法各有优缺点,可以根据项目的具体需求和使用场景选择合适的方法来实现站点的一键换肤功能.

大文件上传

前端大文件上传是一个常见的需求,尤其是在需要处理视频、图片、文档等大型文件的应用中。以下是实现大文件上传的一些关键点和技术方案:

  1. 分片上传
    • 将大文件分割成多个小片段(chunk),然后逐一上传这些小片段。
    • 客户端可以并行上传多个片段,提高上传速度。
    • 服务端接收到所有片段后,再将它们重新组合成原始文件。
  2. 断点续传
    • 如果上传过程中断,可以从中断的地方继续上传,而不是从头开始。
    • 这通常通过记录每个片段的上传状态来实现。
  3. 进度反馈
    • 提供实时的上传进度反馈给用户,增强用户体验。
    • 可以通过JavaScript定时器或者事件监听来实现。
  4. 错误处理和重试机制
    • 对上传过程中可能出现的错误进行处理,比如网络问题导致的上传失败。
    • 实现自动重试机制,对于失败的片段进行重新上传。
  5. 安全性
    • 确保上传过程中的数据安全,使用HTTPS等加密传输协议。
    • 服务端验证上传的文件类型和大小,防止恶意文件上传。
  6. 服务端支持
    • 服务端需要能够处理分片上传的逻辑,包括接收片段、存储和重组文件。
    • 可以使用现有的云存储服务,如AWS S3、阿里云OSS等,它们通常支持分片上传。
  7. 前端技术栈
    • 使用HTML5的<input type="file">来选择文件。
    • 使用JavaScript(可能结合框架如React、Vue等)来处理文件的读取和上传逻辑。
    • 使用XMLHttpRequestFetch API来发送网络请求。
  8. 库和工具
    • 使用如Resumable.jsPluploadDropzone.js等第三方库来简化大文件上传的实现。
    • 这些库通常提供了分片上传、断点续传、进度条等功能。
  9. 用户体验
    • 提供取消上传的选项。
    • 在文件上传完成后提供反馈,比如上传成功或失败的通知。
  10. 测试
    • 在不同的网络条件下测试上传功能,确保在慢速或不稳定的网络环境下也能正常工作。
      实现大文件上传时,需要综合考虑上述因素,以确保上传过程既高效又稳定。

      太大的文件可以将文件分成2M左右的大小,blob表示原始数据,也就是二进制数据,同事提供了对数的截取方法slice,而File继承blob功能


分片上传的具体操作步骤如下:

  1. 分片上传整体流程
    • 开始上传:前端启动文件分片上传,后端返回唯一标识。
    • 分片上传:前端获取文件,设置固定分片大小,将文件切成多个小片,并计算每个分片的MD5值(32位)。将每个分片的内容和MD5标识符一同上传至服务器。服务端接收每个分片及相关信息后,通过对每个分片进行校验,来确保分片的完整性。
    • 结束上传:当分片上传完毕或者前端取消上传时,调用结束上传接口结束此次文件上传操作。服务端根据是正常结束或取消上传来决定后续操作。
  2. 前端具体流程
    • 开始上传:发送开始上传请求,向服务器传递文件名、文件总大小、分片总数和切片大小,获取并保存文件上传的唯一标识符。同时在发送请求前,对上传的文件名进行校验。
    • 分片上传:将文件进行切片,根据文件的大小决定每个分片的大小并切分成多个片段,同时计算出总切片数,并为每个切片添加从0开始的顺序索引。对每个切片计算出它们的MD5值,并将这些分片的MD5值和顺序索引保存在浏览器内存中。然后发送上传数据请求,向服务器发送唯一标识符、分片的顺序索引、分片数据,MD5值和当前分片的大小。在每个分片发送请求后,如果发送成功,则将其对应的信息从浏览器内存中删除,并计算出此时的上传进度,然后发送下一个片段直至最后。如果请求发生错误,则对该分片再次发送一次上传请求,如果仍然错误,不再上传,调用结束请求并提示错误原因。
    • 结束上传:如果文件分片全部成功上传,向服务器发送结束请求,传递正常结束状态码,清空浏览器内存。如果主动取消上传则传递取消请求状态码,同时清空浏览器内存,不再继续上传。
  3. 部分代码实现
    • 创建分片:createChunk函数用于将文件分割成多个分片。
      1
      2
      3
      4
      5
      6
      7
      function createChunk(file, chunkSize) {
      const result = [];
      for (let i = 0; i < file.size; i += chunkSize) {
      result.push(file.slice(i, i + chunkSize));
      }
      return result;
      }
    • 根据文件内容创建hash值:hash函数用于计算分片的MD5值。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      function hash(chunks) {
      return new Promise((resolve) => {
      var spark = new SparkMD5();
      function _read(i) {
      if (i >= chunks.length) {
      resolve(spark.end());
      return;
      }
      var blob = chunks[i];
      var reader = new FileReader();
      reader.onload = e => {
      var bytes = e.target.result;
      spark.append(bytes);
      _read(i + 1);
      };
      reader.readAsArrayBuffer(blob);
      }
      _read(0);
      });
      }
    • 分片上传:uploadChunk函数用于上传每个分片。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      function uploadChunk(chunks, hash, fileName) {
      var taskArr = [];
      chunks.forEach((chunk, index) => {
      var formdata = new FormData();
      formdata.append('chunk', chunk);
      formdata.append('chunkName', `${hash}-${index}-${fileName}`);
      formdata.append('fileName', fileName);
      var task = axios.post('http://127.0.0.1:3000/upload', formdata, {
      headers: {
      'Content-Type': 'multipart/form-data'
      }
      });
      taskArr.push(task);
      });
      Promise.all(taskArr).then(() => {
      console.log('通知后端');
      });
      }
    • 上传测试:可以看到最终上传的文件会存在target目录下,而chunkCache_开头的临时文件夹是用于存储切片(在合并完后会被删除)。
      通过上述步骤和代码示例,可以实现大文件的分片上传。

Js超过 Number 最⼤值的数怎么处理

在 JavaScript 中,超过 Number.MAX_VALUE 的数值被认为是 Infinity (正⽆穷⼤)。如果要处理超过 Number.MAX_VALUE 的数值,可以使⽤第三⽅的 JavaScript 库,如 big.js 或bignumber.js ,这些库可以处理任意精度的数值。
例如,使⽤ big.js 库可以将两个超过 Number.MAX_VALUE 的数相加:

1
2
3
4
5
const big = require('big.js');
const x = new big('9007199254740993');
const y = new big('100000000000000000');
const result = x.plus(y);
console.log(result.toString()); // 输出:100009007194925474093

这⾥创建了两个 big.js 对象 x 和 y ,分别存储超过 Number.MAX_VALUE 的数值。通过plus⽅法将它们相加,得到了正确的结果。最后,通过 toString ⽅法将结果转换为字符串。
如果不依赖外部库,JavaScript 中,数值超过了 Number 最⼤值时,可以使⽤ BigInt 类型来处理,它可以表⽰任意精度的整数。
使⽤ BigInt 类型时,需要在数值后⾯添加⼀个 n 后缀来表⽰ BigInt 类型。例如:

1
const bigNum = 9007199254740993n; // 注意:数字后⾯添加了 'n' 后缀

注意,BigInt 类型是 ECMAScript 2020 新增的特性,因此在某些浏览器中可能不被⽀持。如果需要在
不⽀持 BigInt 的环境中使⽤ BigInt,可以使⽤ polyfill 或者第三⽅库来实现。

js实现输入两个版本号,返回哪个版本号大,哪个版本号小

为了比较两个版本号的大小,我们可以将版本号按照点(.)分割成数组,然后逐个比较每个部分的大小。以下是一个JavaScript函数,用于比较两个版本号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function compareVersions(v1, v2) {
// 将版本号按照点分割成数组
var v1Parts = v1.split('.');
var v2Parts = v2.split('.');
// 比较每个部分的大小
for (var i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
// 如果v1的某个部分不存在,则认为该部分为0
var v1Part = v1Parts[i] || 0;
// 如果v2的某个部分不存在,则认为该部分为0
var v2Part = v2Parts[i] || 0;
// 将部分转换为数字进行比较
v1Part = parseInt(v1Part, 10);
v2Part = parseInt(v2Part, 10);
// 比较大小
if (v1Part > v2Part) {
return v1 + ' > ' + v2;
} else if (v1Part < v2Part) {
return v1 + ' < ' + v2;
}
}
// 如果所有部分都相等,则两个版本号相等
return v1 + ' = ' + v2;
}

使用这个函数,你可以传入两个版本号,它会返回哪个版本号大,哪个版本号小,或者它们是否相等。例如:

1
2
3
console.log(compareVersions('1.2.3', '1.2.4')); // 输出:1.2.3 < 1.2.4
console.log(compareVersions('1.2.3', '1.2.2')); // 输出:1.2.3 > 1.2.2
console.log(compareVersions('1.2.3', '1.2.3')); // 输出:1.2.3 = 1.2.3

这个函数假设版本号的每个部分都是数字,并且按照从左到右的顺序比较每个部分的大小。如果版本号的某个部分不存在,则认为该部分为0。

JS实现某一年的某一个月的最后一天是星期几

在JavaScript中,你可以使用Date对象来计算某年某月的最后一天是星期几。下面是一个简单的函数,它接受年份和月份作为参数,并返回该月最后一天是星期几:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getLastDayOfWeek(year, month) {
// 月份是从0开始的,所以需要减1
month = month - 1;
// 计算该月最后一天的日期
var lastDay = new Date(year, month + 1, 0);
// 获取星期几,getDay()返回0是周日,1是周一,以此类推,需要根据需要调整
var dayOfWeek = lastDay.getDay();
// 将数字转换为星期的名称
var days = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
// 返回结果
return days[dayOfWeek];
}
// 使用示例
var year = 2024; // 年份
var month = 12; // 月份
console.log(getLastDayOfWeek(year, month)); // 输出2024年12月的最后一天是星期几

请注意,getDay()方法返回的数字代表星期几,其中0代表星期日,1代表星期一,以此类推。如果你希望星期日为1,星期一为2,等等,你需要对返回的数字进行适当的调整。在上面的代码中,我直接使用了0-6的数字索引来从数组中获取对应的星期名称。

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

在JavaScript中,箭头函数(Arrow Function)和普通函数(也称为函数声明或函数表达式)是两种不同的函数定义方式,它们之间有几个关键的区别:

  1. 语法简洁性
    • 箭头函数:语法更简洁,不需要使用function关键字,也没有自己的arguments对象,没有superthis绑定,不能用作构造函数。
    • 普通函数:使用function关键字,可以有更复杂的函数体,有自己的arguments对象,可以作为构造函数使用。
  2. this
    • 箭头函数:不绑定自己的this值,它会捕获其所在上下文的this值作为自己的this值,这在回调函数中非常有用。
    • 普通函数:有自己的this值,这个值取决于函数的调用方式(例如,作为对象的方法调用、作为普通函数调用、使用callapply调用等)。
  3. arguments 对象
    • 箭头函数:没有自己的arguments对象,但可以通过剩余参数(...args)来访问其参数。
    • 普通函数:有自己的arguments对象,可以用来访问函数参数。
  4. 构造函数
    • 箭头函数:不能用作构造函数,尝试这样做会抛出错误。
    • 普通函数:可以用作构造函数,使用new关键字调用时会创建一个新对象。
  5. 原型链
    • 箭头函数:不创建自己的prototype属性。
    • 普通函数:创建自己的prototype属性,可以用来添加方法或属性,使得通过构造函数创建的对象可以共享这些方法和属性。
  6. 词法作用域
    • 箭头函数:不绑定自己的词法作用域,它们捕获其所在上下文的词法作用域。
    • 普通函数:创建自己的词法作用域。
  7. new.target
    • 箭头函数:没有new.target,因为它们不能作为构造函数使用。
    • 普通函数:作为构造函数调用时,new.target指向构造函数本身。
  8. yield 关键字
    • 箭头函数:不能直接包含yield关键字,因为它们不能用作Generator函数。
    • 普通函数:可以包含yield关键字,可以作为Generator函数。
  9. 函数体内变量
    • 箭头函数:不能在箭头函数内部使用var声明变量,只能使用letconst
    • 普通函数:可以在函数体内使用varletconst声明变量。
      箭头函数通常用于简短的回调函数,而普通函数则适用于需要更多控制和功能的场景。
      js箭头函数

浅拷贝和深拷贝

在JavaScript中,浅拷贝和深拷贝是两种常见的数据复制方法,它们的区别在于复制数据时是否递归复制对象的属性值。以下是一些实现浅拷贝和深拷贝的方法:

浅拷贝(Shallow Copy)

  1. Object.assign()
    • 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将源对象的属性复制到目标对象的顶层。
    1
    2
    3
    const obj1 = { a: 1 };
    const obj2 = { b: 2 };
    const shallowCopied = Object.assign({}, obj1, obj2);
  2. 扩展运算符(Spread Operator)
    • 可以用来复制数组和对象。
    1
    2
    3
    4
    const arr = [1, 2, 3];
    const shallowArrCopy = [...arr];
    const obj = { a: 1 };
    const shallowObjCopy = { ...obj };
  3. Array.prototype.slice()
    • 可以用于数组的浅拷贝。
    1
    2
    const arr = [1, 2, 3];
    const shallowArrCopy = arr.slice();

深拷贝(Deep Copy)

  1. JSON方法
    • 使用JSON.stringify()将对象转换成字符串,然后使用JSON.parse()将字符串转换回对象。
    1
    2
    const obj = { a: 1, b: { c: 2 } };
    const deepCopied = JSON.parse(JSON.stringify(obj));

    注意:这种方法不能复制函数、undefined、Symbol值、循环引用等。

  2. 递归复制
    • 手动编写函数递归地复制对象的属性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function deepCopy(obj) {
    if (obj === null || typeof obj !== 'object') {
    return obj;
    }
    let copiedObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    copiedObj[key] = deepCopy(obj[key]);
    }
    }
    return copiedObj;
    }
  3. 结构化克隆算法
    • 使用structuredClone()方法,这是一个浏览器API,可以深拷贝大多数类型的数据。
    1
    2
    const obj = { a: 1, b: { c: 2 } };
    const deepCopied = structuredClone(obj);
  4. 使用库
    • 使用第三方库,如Lodash的_.cloneDeep()方法。
    1
    2
    3
    4
    // 需要引入Lodash库
    const _ = require('lodash');
    const obj = { a: 1, b: { c: 2 } };
    const deepCopied = _.cloneDeep(obj);
  5. Map 和 Set 的深拷贝
    • 对于包含Map和Set的数据结构,需要特别处理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function deepCopy(obj) {
    if (obj === null || typeof obj !== 'object') {
    return obj;
    }
    if (obj instanceof Map) {
    return new Map(obj);
    }
    if (obj instanceof Set) {
    return new Set(obj);
    }
    let copiedObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    copiedObj[key] = deepCopy(obj[key]);
    }
    }
    return copiedObj;
    }

    每种方法都有其适用场景和限制,选择哪种方法取决于具体的需求和数据结构。

localstorage,sessionstorage和cookie

localStoragesessionStoragecookie 是客户端存储的三种主要方式,它们都用于在用户的浏览器上存储数据,但每种方式都有其特点和用途。

  1. 存储大小:每个cookie可以存储的数据量较小,一般不超过4KB。
  2. 有效期:可以设置过期时间,如果未设置,则为浏览器会话结束时。
  3. 作用域:默认情况下,cookie 对于同一域名下的多个路径都是可见的,但可以通过设置路径来限制其作用域。
  4. 发送到服务器:每次HTTP请求都会携带cookie,因此对于不需要在客户端存储的数据,使用cookie可能会增加服务器请求的开销。
  5. 安全:cookie可以设置为HttpOnly,这样JavaScript就无法访问cookie,增加了安全性。

localStorage

  1. 存储大小:可以存储大约5MB的数据。
  2. 有效期:数据没有过期时间,即使关闭浏览器也会保留。
  3. 作用域:数据存储在特定的协议和域名下,对所有页面都是可见的。
  4. 发送到服务器:数据仅存储在客户端,不会自动发送到服务器。
  5. API:提供了一组API,如setItemgetItemremoveItemclear等,用于操作存储的数据。

sessionStorage

  1. 存储大小:与localStorage相同,可以存储大约5MB的数据。
  2. 有效期:数据仅在浏览器会话期间有效,关闭标签页或浏览器后数据会被清除。
  3. 作用域:数据存储在特定的标签页或窗口中,同一窗口下的不同页面可以共享sessionStorage中的数据。
  4. 发送到服务器:与localStorage一样,数据仅存储在客户端,不会自动发送到服务器。
  5. API:同样提供了setItemgetItemremoveItemclear等API。

比较

  • 存储大小localStoragesessionStorage的存储容量远大于cookie
  • 有效期localStorage是永久存储,sessionStorage是会话存储,而cookie可以设置过期时间。
  • 作用域localStoragesessionStorage的作用域是域名级别的,而cookie可以设置为路径级别。
  • 安全性cookie可以设置为HttpOnly,增加安全性,而localStoragesessionStorage没有这种机制。
  • 性能:由于cookie每次HTTP请求都会发送,对于不需要频繁验证用户身份的大量数据,使用localStoragesessionStorage可能更合适。
    根据应用场景的不同,开发者可以选择最适合的数据存储方式。例如,对于需要跨会话持久化的数据,可以使用localStorage;对于会话级别的数据,可以使用sessionStorage;而对于需要在服务器端验证的用户身份信息,可以使用cookie

IndexedDB

IndexedDB 是一种在浏览器中存储大量结构化数据的方式,它具有以下几个特点:

  1. 存储空间大:IndexedDB 没有固定的存储上限,通常不小于 250MB,远超过 localStorage 的 5MB 限制。
  2. 存储格式多样:IndexedDB 支持字符串、二进制数据(ArrayBuffer 对象和 Blob 对象)以及 JSON 键值对存储。一个对象相当于关系型数据库中的数据表,称为 对象仓库 (object store)
  3. 异步操作:IndexedDB 的操作是异步的,这意味着在进行大量数据读写时,不会阻塞网页,提高性能。
  4. 同源限制:IndexedDB 只能访问自身域名下的数据库,不能跨域访问,与 localStoragesessionStorage 一样。
  5. 支持事务:IndexedDB 支持事务处理,保证了数据操作的原子性和一致性,避免数据不一致的情况。
  6. 复杂查询能力:IndexedDB 支持使用索引和游标进行复杂查询,能够高效地检索和操作数据。
  7. 结构化存储:IndexedDB 允许存储结构化数据,包括对象和文件,使数据管理更加灵活和方便。
    应用场景
  • IndexedDB 适用于存储大量结构化数据,例如离线应用的数据存储。
  • 对于简单的数据,可以使用 localStorage;而对于大量结构化数据,IndexedDB 更为适合。
  • IndexedDB 也适用于实现后退上一个页面不刷新页面的场景,通过将数据缓存到本地,下次打开列表后,如果 URL 中的 ID 和缓存的数据 ID 一致,可以直接使用缓存数据,不再进行请求。
    localStoragesessionStoragecookie 的比较
  • IndexedDB 与 localStorage 类似,都是通过 key-value 方式存储,但 IndexedDB 可以直接存储对象数组等,不需要像 localStorage 那样必须转为字符串。
  • IndexedDB 支持异步调用,不会因为写入数据慢而导致页面阻塞,而 localStorage 的操作是同步的。
  • IndexedDB 的存储空间远大于 localStoragesessionStorage,适合存储大量数据。
  • IndexedDB 支持二进制存储,而 localStoragesessionStorage 主要用于存储字符串。
    总的来说,IndexedDB 是一种强大的浏览器存储机制,特别适合于需要存储大量数据和复杂数据结构的Web应用。

Service Worker

Service Worker 是一种在浏览器后台运行的脚本,它作为网络代理,可以拦截和管理网络请求,实现资源的缓存、更新和推送等功能。以下是Service Worker的一些关键点:

定义

Service Worker 是一个后台运行的脚本,充当一个代理服务器,拦截用户发出的网络请求,比如加载脚本和图片。Service Worker 可以修改用户的请求,或者直接向用户发出回应,不用联系服务器,这使得用户可以在离线情况下使用网络应用。

工作原理

Service Worker 是事件驱动的,它独立于网页文档之外运行,不能直接操作DOM。当浏览器启动时,Service Worker 会自动加载并执行,之后它会一直运行在后台,等待处理来自页面的消息。Service Worker 的主要作用是对网络请求进行拦截和重定向,通过拦截请求,可以实现缓存资源、离线体验、请求拦截和修改以及推送通知等功能。

应用场景

Service Worker 适用于以下场景:

  1. 离线缓存:通过Service Worker,可以实现Web应用的离线访问。当用户首次访问网页时,Service Worker会拦截并缓存网络请求,使得用户在离线状态下依然可以访问这些资源。
  2. 推送通知:即使在用户没有打开网页的情况下,Service Worker也可以接收来自服务器的推送消息,并通过浏览器显示通知。这使得Web应用可以像原生应用一样,及时向用户传达重要信息。
  3. 背景数据处理:Service Worker可以在用户与网页交互的间隙,处理一些耗时的后台任务。例如,我们可以使用Service Worker来同步用户数据,或者构建网站内容的索引,从而提高后续访问的速度。

最新进展

Service Worker 技术仍在不断发展中,浏览器厂商持续对其进行优化和改进。例如,Webpack5引入了对Service Worker的支持,可以更好地处理Service Worker的更新和缓存策略。随着PWA(Progressive Web Apps)的兴起,Service Worker作为构建PWA的关键技术之一,其应用和优化也在不断扩展。

从输入 URL 到页面渲染的全过程

从输入URL到页面渲染的全过程涉及多个步骤,包括网络请求、浏览器处理、DOM构建、样式计算、布局、绘制等。以下是这个过程的详细步骤:

  1. 地址解析
    • 用户在浏览器地址栏输入URL,浏览器首先解析URL以确定要访问的资源。
  2. DNS解析
    • 浏览器通过DNS解析将域名转换为IP地址。
  3. 建立连接
    • 浏览器使用解析得到的IP地址,通过TCP协议与服务器建立连接(通常使用HTTP/HTTPS协议)。
  4. 发送HTTP请求
    • 浏览器构建HTTP请求,包括请求行(方法、URL、HTTP版本)、请求头(User-Agent、Accept等)、空行和请求体(POST请求时)。
  5. 服务器处理请求
    • 服务器接收到请求后,根据请求类型(GET、POST等)处理请求,查找资源。
  6. 服务器响应
    • 服务器将处理结果以HTTP响应的形式发送给浏览器,包括状态行、响应头(Content-Type、Content-Length等)和响应体。
  7. 浏览器接收响应
    • 浏览器接收到服务器的响应,解析状态码和响应头,根据状态码处理响应内容(200表示成功,404表示未找到等)。
  8. 内容解析
    • 浏览器开始解析响应内容,如果是HTML文档,则解析HTML构建DOM树。
  9. CSS解析与应用
    • 浏览器解析CSS样式表,构建CSSOM树(CSS Object Model Tree),并将CSSOM树与DOM树结合,生成渲染树。
  10. JavaScript解析与执行
    • 浏览器解析JavaScript代码,并执行。JavaScript可以操作DOM和CSSOM,可能会改变渲染树。
  11. 布局(Reflow)
    • 浏览器根据渲染树和CSS样式计算每个节点的几何信息(位置和尺寸),这个过程称为布局或重排。
  12. 绘制(Painting)
    • 浏览器使用布局信息对每个节点进行绘制,生成页面的像素数据。
  13. 合成(Compositing)
    • 浏览器将各个层的内容合并到屏幕上显示,这个过程称为合成。现代浏览器使用GPU加速合成,提高渲染效率。
  14. 页面渲染完成
    • 浏览器完成所有上述步骤后,用户可以看到完整的页面内容。
  15. Service Worker
    • 如果页面使用了Service Worker,它可以在上述过程中的多个点介入,如缓存资源、拦截请求等。
      这个过程可能会因为浏览器的优化和缓存机制而有所简化,例如,浏览器可能会缓存DNS解析结果、HTTP响应等,减少重复请求的处理时间。此外,现代浏览器会使用异步加载、预加载、预渲染等技术来提高性能和用户体验。

页面白屏问题

页面白屏问题是一个常见的前端问题,它严重影响用户体验。以下是一些可能导致页面白屏的原因以及相应的解决方案:

  1. 资源加载失败
    • 页面依赖的关键资源(CSS、JS、图片等)加载失败,导致页面无法正常渲染。例如,在React项目中引入了不存在的资源或者资源加载失败,就会造成页面无法正常渲染,导致白屏。解决方案包括检查资源文件的路径是否正确,确保资源文件的服务器响应状态码为200,以及使用网络监控工具来跟踪资源加载情况。
  2. 代码错误
    • 代码中存在语法错误、逻辑错误或运行时错误,导致页面无法正常加载。解决方案是使用浏览器的开发者工具(如Chrome DevTools)来检查控制台中的错误信息,并修复相应的代码问题。
  3. 兼容性问题
    • 页面在某些浏览器或设备上无法正常显示,导致白屏。解决方案是进行跨浏览器测试,并使用polyfills或者特定的CSS/JS代码来解决兼容性问题。
  4. 网络问题
    • 用户的网络连接出现问题,导致页面无法正常加载。解决方案是优化网络请求,使用CDN、压缩技术和缓存策略优化网络请求,减少页面加载时间。
  5. 服务器故障
    • 服务器出现故障,导致页面无法正常加载。解决方案是检查服务器状态,确保服务器正常运行,并且资源文件可以被正确访问。
  6. Service Worker缓存问题
    • 如果使用了Service Worker进行缓存管理,错误的缓存策略可能导致页面白屏。解决方案是检查Service Worker的缓存策略,确保正确地缓存了资源,并且在需要时更新缓存。
  7. 优化页面大小和性能
    • 压缩图像、移除不必要的脚本和样式,减小页面大小。合理使用脚本,避免在页面加载时执行大量脚本,并考虑使用延迟加载或异步加载策略。
  8. 浏览器渲染优化
    • 简化HTML和CSS代码,减少HTML和CSS代码中的复杂度,提高浏览器对这些代码的解析效率。减少页面上加载的资源数量,尤其是图片和视频等资源。优化JavaScript代码的执行效率,减少JavaScript代码对CPU资源的占用。
      通过上述解决方案,可以有效地排查和解决页面白屏问题,提升网页性能和用户体验。

判断两个域名是否一样

判断两个域名是否一样,通常需要考虑以下几个因素:

  1. 域名本身
    • 直接比较两个域名的字符串是否完全相同。例如,example.comexample.com 是相同的,而 example.comwww.example.com 是不同的。
  2. 子域名
    • 子域名会影响域名的判断。例如,sub.example.comexample.com 是不同的,因为它们指向不同的主机。
  3. 顶级域名(TLD)
    • 顶级域名(如.com、.org、.net)也会影响域名的判断。
  4. 端口号
    • 如果域名包含端口号,那么端口号的不同也会导致域名不同。例如,example.com:80example.com:443 是不同的。
  5. 协议
    • 协议(如HTTP和HTTPS)的不同也会影响域名的判断。http://example.comhttps://example.com 是不同的。

在JavaScript中判断域名是否相同

在JavaScript中,你可以使用URL对象来解析和比较域名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function areDomainsTheSame(url1, url2) {
const parsedUrl1 = new URL(url1);
const parsedUrl2 = new URL(url2);
// 比较协议、域名和端口
return (
parsedUrl1.protocol === parsedUrl2.protocol &&
parsedUrl1.hostname === parsedUrl2.hostname &&
parsedUrl1.port === parsedUrl2.port
);
}
// 示例
const url1 = 'https://example.com';
const url2 = 'http://example.com';
console.log(areDomainsTheSame(url1, url2)); // 输出:false
const url3 = 'https://example.com';
const url4 = 'https://example.com';
console.log(areDomainsTheSame(url3, url4)); // 输出:true

注意事项

  • 子域名:如果你需要判断两个域名是否属于同一个顶级域名,但不考虑子域名的差异,你可能需要手动去除子域名的部分,只比较顶级域名和二级域名。
  • 国际化域名:对于包含非ASCII字符的国际化域名,比较之前可能需要进行Punycode转换。
  • 大小写:域名本身是不区分大小写的,但在实际比较时,需要确保两个域名的大小写完全一致。
    通过这些方法,你可以准确地判断两个域名是否相同。

cookie

cookie

JavaScript 的 Cookie 是存储在用户浏览器中的小型文本数据,用于跟踪用户会话、存储用户偏好或其他需要在浏览器和服务器之间传递的信息。Cookie 由服务器通过 HTTP 头(Set-Cookie)发送到浏览器,浏览器会保存这些数据,并在后续请求中自动附加到 HTTP 头(Cookie)中返回给服务器。

  1. 存储大小:单个 Cookie 大小通常限制为 4KB,每个域名下的 Cookie 数量有限(通常 50 个左右)。
  2. 生命周期
    • 会话 Cookie:浏览器关闭后自动删除(未设置 ExpiresMax-Age)。
    • 持久 Cookie:通过 ExpiresMax-Age 设置过期时间。
  3. 作用域
    • Domain:指定哪些域名可以访问 Cookie(默认为当前域名)。
    • Path:指定 URL 路径下才能访问 Cookie(默认为当前路径)。

通过 document.cookie API 可以读写 Cookie,但需手动处理字符串格式。

1
document.cookie = "username=John Doe; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
  • 键值对格式:key=value
  • 可选属性用分号分隔:expires, max-age, path, domain, secure, samesite
1
2
3
4
5
6
7
8
const cookies = document.cookie; // 返回所有 Cookie,如 "username=John; theme=dark"
// 解析 Cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
console.log(getCookie("username")); // 输出 "John Doe"

将 Cookie 的过期时间设为过去的时间:

1
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";

  1. ExpiresMax-Age
    • Expires:指定过期时间(UTC 格式)。
    • Max-Age:指定存活秒数(优先级高于 Expires)。
  2. Domain
    • 默认仅当前域名可用,设置 Domain=example.com 允许子域名(如 sub.example.com)访问。
  3. Path
    • 限制 Cookie 仅在特定路径下有效(如 Path=/admin)。
  4. Secure
    • 仅通过 HTTPS 协议传输 Cookie。
  5. HttpOnly
    • 阻止 JavaScript 访问 Cookie(防 XSS 攻击),只能由服务器设置。
  6. SameSite
    • Strict:仅同站请求发送 Cookie。
    • Lax:允许部分跨站请求(如导航链接)。
    • None:允许跨站请求(需配合 Secure)。

安全性注意事项

  • 敏感信息:避免在 Cookie 中存储密码等敏感数据。
  • HttpOnly:对身份验证 Cookie 启用此属性,防止 XSS 攻击窃取。
  • Secure:确保通过 HTTPS 传输敏感 Cookie。
  • SameSite:设置 LaxStrict 防止 CSRF 攻击。

实际应用场景

  1. 用户认证:服务器发送 Session ID 作为 Cookie,用于识别用户登录状态。
  2. 个性化设置:存储用户主题、语言偏好。
  3. 购物车:临时保存用户选择的商品。
  4. 跟踪与分析:记录用户行为(需符合隐私政策如 GDPR)。

  • localStoragesessionStorage
    • 存储容量更大(约 5MB)。
    • 数据仅存于客户端,不自动发送到服务器。
    • 无过期时间(localStorage)或会话级(sessionStorage)。
  • JWT(JSON Web Token):用于无状态身份验证,替代 Session Cookie。

总结

Cookie 是 Web 开发中实现状态管理的基础工具,但需谨慎处理安全性和隐私问题。现代开发中,可结合 Cookie 与其他存储技术(如 localStorage)以满足不同需求。对于敏感操作,务必遵循安全最佳实践。