普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月14日首页

从值拷贝到深拷贝:彻底弄懂 JavaScript 的堆与栈

作者 ouma_syu
2025年11月13日 23:57

彻底搞懂 JS 的堆内存与栈内存:从值拷贝到深拷贝

在学习 JavaScript 的过程中,经常会遇到一些令人困惑的问题:

  • 为什么我改了一个对象,另一个变量也变了?
  • 为什么有的赋值是“独立”的,有的却会互相影响?
  • 深拷贝和浅拷贝,到底区别在哪?

这些现象的根源,其实都来自 —— 堆内存(Heap)与栈内存(Stack) 的不同存储机制。
本文将带你从内存模型出发,搞懂数据的“居住位置”和“传递方式”,彻底弄清值拷贝与引用拷贝的差异。


一、栈内存与堆内存:存储机制的区别

JavaScript 会根据变量类型,选择不同的存储方式:

数据类型 存储位置 特点
基本类型(Number、String、Boolean、null、undefined、Symbol、BigInt) 栈内存(Stack) 连续存储、读写高效、空间固定
引用类型(Object、Array、Function) 堆内存(Heap) 动态分配、可扩展、访问间接

栈内存:简单变量的“快递柜”

let a = 1;
let b = 2;
let c = 3;
let d = a; // 值拷贝

栈内存中存储的是值本身,且空间连续。
d = a 实际上是对 1 这个值的复制。
因此,无论之后如何修改 a,都不会影响到 d

关键特征:
每个变量都有自己的小空间,互不干扰、读取极快。


堆内存:对象与数组的“仓库区”

const users = [
  { id: 1, name: "oumasyu", hometown: "赣州" },
  { id: 2, name: "inx177", hometown: "南昌" },
  { id: 3, name: "gustt_", hometown: "赣州" }
];

数组和对象属于引用类型
它们的实际数据存放在堆内存中,而变量 users 只是保存了一个引用地址

当你执行:

const data = users;
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);

结果会发现 —— users 也被改动了!

这是因为:

  • usersdata 在栈中存放的地址相同
  • 它们同时指向堆内存中同一块数据区域
  • 改变任何一方,其实都在修改那块共享的堆空间。

二、引用式拷贝:看似复制,实则共用

可以把这种情况理解为:

users ──► [ { id:1, name:"oumasyu" } ]
   ▲
   │(共用同一地址)
data ┘

datausers 并没有创建两份数据,只是共用一个引用。
所以,修改 data[0] 的属性,等同于修改 users[0]


三、想要真正“分家”?你需要深拷贝

如果希望两个对象互不影响,就必须让它们在堆内存中拥有各自的空间

方法一:JSON.parse(JSON.stringify())

const data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);

执行后你会发现:
修改 data 不会再影响 users —— 它们终于“分家”了。

原理
通过序列化和反序列化,将对象转成字符串再重新生成,从而创建一份全新的数据结构。

优点:简单直接、常用于深拷贝。
缺点:无法拷贝函数、undefinedSymbol、循环引用等。


方法二:structuredClone()(更现代)

const data = structuredClone(users);

structuredClone() 是浏览器原生的深拷贝 API。
相比 JSON 方法,它支持更多数据类型(如 DateRegExpMapSet、循环引用等),
是未来更推荐的写法。


四、图解内存变化:从共享到独立

# 引用式拷贝
users ──► [ { id:1, name:"oumasyu" } ]
   ▲
   │
data ┘  (共用同一堆空间)

# 深拷贝后
users ──► [ { id:1, name:"oumasyu" } ]
data  ──► [ { id:1, name:"oumasyu", hobbies:["篮球"] } ]
(独立的两份堆内存数据)

五、核心对比总结

拷贝类型 是否新建堆内存 是否共享数据 常见实现方式
值拷贝(基本类型) =
引用拷贝(对象/数组) =
深拷贝 JSON.parse(JSON.stringify()) / structuredClone()

总结

理解堆内存与栈内存的本质,是写好 JS 的关键一步。
当你清楚变量“指的是什么”,你就能轻松判断:

  • 哪些修改会相互影响;
  • 何时该用深拷贝;
  • 如何优化内存和性能。

一句话总结:

基本类型复制的是值,引用类型复制的是地址。
想要真正“断开关系”,就得创建新的堆内存。

从零掌握 Ajax:一次请求带你读懂异步数据加载原理

作者 ouma_syu
2025年11月13日 23:45

一、精炼理解:Ajax 到底是啥?

Ajax = 在不刷新页面的情况下,和服务器交换数据并动态更新页面
它让 Web 页面拥有“应用级”的体验——局部更新、响应式交互、更少等待。核心实现长期基于浏览器内建对象 XMLHttpRequest(XHR) ,现代开发则多用 fetch / Axios,但底层思想相同:异步请求 + 数据驱动视图


二、核心流程

  1. 创建 XHR 实例

    const xhr = new XMLHttpRequest();
    
  2. 打开连接

    xhr.open('GET', '/api/members', true); // true => 异步
    
  3. 可选:设置请求头 / 超时 / 响应类型

    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.responseType = 'json'; // 直接得到对象(现代浏览器)
    xhr.timeout = 5000; // 毫秒
    
  4. 发送请求

    xhr.send();
    
  5. 监听状态 / 处理响应

    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          const data = xhr.responseType === 'json' ? xhr.response : JSON.parse(xhr.responseText);
          // 更新 DOM
        } else {
          // 处理非 200 的情况
        }
      }
    };
    
  6. 清理/取消

    xhr.abort(); // 主动取消请求
    

三、readyState 一眼看懂

  • 0 UNSENT:未调用 open()
  • 1 OPENED:调用了 open(),可 setRequestHeadersend
  • 2 HEADERS_RECEIVED:已接收响应头(可读取 status)
  • 3 LOADING:接收响应体中(可处理流式数据)
  • 4 DONE:响应完成,数据就绪

调试建议:打印 readyStatestatus,快速定位在哪个阶段失败。


四、常见坑与解决方法

1. 同步请求会卡死 UI

xhr.open(..., false) 是同步,会阻塞主线程。不要在浏览器中使用同步请求,除非你非常清楚场景(几乎没有)。

2. 跨域(CORS)错误

浏览器会阻止不满足 CORS 策略的请求。后端需设置 Access-Control-Allow-Origin,或通过代理绕过开发时限制。

3. JSON 解析报错

JSON.parse 报错说明服务器没返回合法 JSON。排查响应 Content-Type、响应体首尾空白和服务器异常栈。

4. 超时与重试

设置合理 xhr.timeout,并在 ontimeout 中进行用户提示或重试逻辑(避免无限重试)。

5. 状态码不是 200

处理 4xx/5xx、304 等不同类型响应,给用户明确提示而非只在控制台打印错误。

6. 网络抖动与幂等

POST 请求若可重试需保证幂等(或在服务端做去重),避免重复提交造成的数据问题。


五、XHR 与 fetch/Axios 对比(实用速览)

特性 XHR fetch Axios
语法风格 回调/事件 Promise(更现代) Promise + 自动 JSON 处理
支持流 有(onprogress) 有(ReadableStream) 基于 fetch/XHR,封装更好
自动 JSON 解析 .json() 是(响应自动转换)
超时设置 有(timeout 需 AbortController 内置超时配置
更灵活的进度 是(上传/下载 progress) 比较复杂 封装上传/下载进度支持

建议:新项目优先用 fetch 或 Axios;但遇到上传进度/低层控制时,XHR 的事件模型仍然非常有用。


六、实用示例:可靠的异步请求

<ul id="members"></ul>
<script>
  const ul = document.getElementById('members');

  function fetchMembers() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);
    xhr.responseType = 'json'; // 现代浏览器直接得到对象
    xhr.timeout = 7000;

    xhr.onreadystatechange = () => {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200) {
        const data = xhr.response;
        ul.innerHTML = data.map(u => `<li>${u.login}</li>`).join('');
      } else {
        ul.innerHTML = `<li>请求失败:${xhr.status} ${xhr.statusText}</li>`;
      }
    };

    xhr.ontimeout = () => {
      ul.innerHTML = '<li>请求超时,请重试</li>';
    };

    xhr.onerror = () => {
      ul.innerHTML = '<li>网络错误,请检查连接</li>';
    };

    xhr.send();
    return xhr; // 若需要外部取消请求,可保留此引用并调用 xhr.abort()
  }

  fetchMembers();
</script>

七、总结

掌握 Ajax,不只是知道如何发请求,而是理解请求生命周期、错误处理与用户体验折衷——把网络的不确定性,优雅地交给代码去处理。

昨天以前首页

JavaScript Promise 机制解析

作者 ouma_syu
2025年11月10日 23:13

深入理解 JavaScript Promise:运行机制、状态流转与执行顺序解析

在现代 JavaScript 开发中,Promise 已经成为异步编程的核心抽象。相比传统回调,它带来了更清晰的流程控制、更可靠的错误链路和更安全的异步表达方式。

然而,要真正用好 Promise,不仅要知道 .then().catch() 的写法,更要理解:

  • Promise 为什么能“让异步看起来更同步”?
  • .then() 为什么是微任务,它什么时候执行?
  • resolve 到底做了什么?
  • Promise 与定时器、I/O 的执行顺序是什么?

本文将以 Promise 为中心,从执行机制、状态模型到微任务队列进行深度解析。


一、Promise 是什么?为何出现?

Promise 的根本目的:

用可控、可链式的方式管理异步任务,让异步逻辑更接近同步结构。

在 Promise 之前,回调模式会导致:

  • 回调地狱
  • 错误无法统一捕获
  • 执行顺序难以推断
  • 流程控制能力弱

Promise 解决了这些问题,通过:

  • 明确的 状态模型(pending → fulfilled / rejected)
  • 链式调用
  • 微任务调度机制
  • 捕获一致性(then/catch/finally)

使异步流程变得更可预测。


二、Promise 的构造与执行阶段

来看基础示例:

const p = new Promise((resolve, reject) => {
    console.log(1); // 立即执行
    setTimeout(() => {
        console.log(2);
        resolve();
    }, 1000);
});

p.then(() => console.log(3));
console.log(4);

输出顺序为:

1
4
2
3

核心原因:Promise 构造函数 同步执行

  • new Promise(...) 内部代码立即执行(同步)
  • .then() 的回调不会立即执行,而是进入 微任务队列
  • 定时器是 宏任务

执行优先级:

同步任务 > 微任务 > 宏任务

执行过程:

  1. 输出 1
  2. 注册定时器(异步)
  3. 注册 .then() 回调(微任务)
  4. 输出 4
  5. 定时器回调执行,输出 2
  6. resolve → 将 .then() 放入微任务队列
  7. 输出 3

这一机制奠定了 Promise 的核心价值:流程清晰且可控


三、Promise 的状态流转与行为规则

Promise 的状态只有三种:

  • pending
  • fulfilled
  • rejected

状态特点:

  • 一旦从 pending 转为 fulfilled 或 rejected,就不可逆。(immutable)
  • resolve 或 reject 只能触发状态变化一次
  • then/catch 只会在状态稳定后异步执行(微任务)

示例:

resolve(1);
resolve(2); // 无效
reject('error'); // 无效

Promise 设计成只执行一次,是为了避免异步任务重复触发造成混乱。


四、为什么 Promise 属于“异步微任务”?

Promise 回调不属于普通异步,而是:

属于微任务(Microtask),优先级高于定时器、I/O 等宏任务。

微任务来源:

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver

宏任务来源:

  • setTimeout / setInterval
  • I/O(如 fs.readFile)
  • setImmediate
  • UI 渲染事件

顺序:

同步 → 所有微任务 → 一个宏任务 → 微任务 → 宏任务 → 循环...

这也是为什么下面代码:

Promise.resolve().then(() => console.log('micro'));
setTimeout(() => console.log('macro'));
console.log('sync');

输出:

sync
micro
macro

五、Promise 与异步 I/O

在 Node 中,I/O 操作(如 fs.readFile)属于宏任务,因此即使放在 Promise 中,也不会立即触发 .then()

示例:

import fs from 'fs';

const p = new Promise((resolve, reject) => {
    console.log(1);
    fs.readFile('./b.txt', (err, data) => {
        if (err) return reject(err);
        resolve(data.toString());
    });
});

p.then(data => console.log(data))
 .catch(err => console.log(err));

console.log(2);

执行顺序:

1
2
(文件内容)

流程:

  1. Promise 执行器同步执行 → 输出 1
  2. I/O 操作异步 → 注册回调
  3. 输出 2
  4. 读取完成 → resolve → 微任务 → then 执行

Promise 和 Node I/O 的组合能形成非常清晰、链式的文件读取逻辑。


六、Promise 为什么能“让异步看起来更同步”?

核心原因:

1. 异步结果以链式 .then() 形式呈现

结构像同步流程:

task()
  .then(step2)
  .then(step3)
  .catch(handleError)

比回调嵌套清晰太多。

2. 异步调度由微任务队列保证

顺序确定、可预测,不受浏览器或 Node 背后调度干扰。

3. 错误管理统一

try/catch 不适用于异步,Promise 则能把错误向下传递到 catch。


七、总结:Promise 的核心要点

  • 构造函数立即执行
  • resolve/reject 会改变状态,并触发微任务
  • then/catch 属于微任务,优先级高
  • Promise 只会被解决一次,状态不可逆
  • 与异步 I/O、定时器配合时,Promise 决定回调进入微任务队列,而不是宏任务

Promise 是现代 JS 异步的基础,也是 async/await 的底层支撑。

这些 CSS 小细节没处理好,你的页面就会“闪、抖、卡”——渲染机制深度拆解

作者 ouma_syu
2025年11月7日 23:10

前端开发中最容易忽略的性能细节:页面为何会“卡顿、闪动、抖”?从渲染机制深度拆解

在前端开发中,性能问题往往不是来自你写了多少 JS、用了多少 DOM 操作,而是来自更隐蔽的点——渲染细节

你可能遇到过:

  • 页面加载时闪一下
  • 滑动列表总感觉不够流畅
  • 图片突然出现导致其他内容被“挤走”
  • 某些 UI 样式导致明显掉帧
  • CSS 文件加载顺序干扰页面首屏呈现

这些现象的根源,常常不是“写法不规范”,而是对浏览器渲染机制的误解或忽视

本文将围绕三大高频细节深入讲解:

  • 图片尺寸缺失导致的 CLS
  • 复杂视觉效果导致的掉帧
  • @import 的阻塞机制

一、图片不写 width/height:为什么会引发 CLS(布局抖动)?

许多开发者以为只要 CSS 里设置了宽高就够了,但实际上:

浏览器必须提前知道图片占多大空间,才能正确分配页面布局。

如果你在 HTML 中不写尺寸:

<img src="/banner.jpg">

浏览器在下载图片之前不知道它真实大小,因此只能先渲染一个“未知高度”的框架。

等图片加载完成后,它又会根据实际尺寸重新计算布局 → 于是页面出现跳动(Cumulative Layout Shift)

什么是 CLS?

CLS(累计布局偏移)是 Web Vitals 重要指标,衡量页面因元素变化而产生的视觉位移。

表现为:

  • 文字突然被图片挤开
  • 按钮被推走导致用户点错
  • 页面加载时上下跳动

尤其严重影响用户体验,也影响 SEO。

为什么 HTML 属性比 CSS 更重要?

CSS 是在渲染树生成过程中才参与布局。
HTML width/height 属性能在图片下载之前就提供布局信息,浏览器能立即给出占位。

这就是为什么即使业务用 CSS 控制大小,HTML 中仍建议写:

<img src="/banner.jpg" width="800" height="400">

现代浏览器会自动按比例缩放,不会被固定死。

工程化最佳实践

  • 全部图片必须写 w/h(包括组件库内部 image)
  • next/image、uniapp、webp loader 等框架本质都帮你做了这件事
  • 设计稿已给尺寸,就直接写入
  • 若为响应式布局,可用 CSS max-width 调整,而不是删除 HTML 尺寸

二、慎用 box-shadowfilterbackdrop-filter:它们为何会让页面掉帧?

在视觉效果上,这些属性很常见:

box-shadow: 0 4px 20px rgba(0,0,0,0.2);
filter: blur(20px);
backdrop-filter: blur(10px);

但它们有一个共同特征:

可能触发独立合成层或高成本绘制 → 导致 GPU/CPU 压力增大。

尤其是在移动端或低端设备上,掉帧极其明显。

1. 为什么 box-shadow 会让页面卡顿?

因为阴影计算需要:

  • 多次模糊运算
  • 扩散边缘处理
  • GPU 合成层的额外绘制

当一个列表有几十个卡片,每个都带 box-shadow,性能会直接下降。

2. filter: blur() 的成本更高

滤镜需要像素级处理(per-pixel),属于渲染链路的重任务。

大面积模糊相当于“实时在浏览器中跑 Photoshop”,不慢才怪。

3. backdrop-filter 成本更高

它需要:

  • 获取元素背后的像素
  • 动态模糊背景
  • 不断重绘(尤其在滚动时)

Safari、Chrome 都曾因此出现性能问题。

可视化效果不等于不能用,而要“合理用”

  • 不要对列表项、滚动内容、频繁变化元素使用滤镜
  • 阴影尽量轻、浅、简短,减少模糊半径
  • 界面需要大模糊时,应使用位图模糊背景图模拟
  • 避免多层滤镜叠加

工程化优化思路

  • 超过 15px 的 blur 几乎一定掉帧,尽量避免
  • 重度阴影可用伪元素 + 轻量图片替代
  • 避免嵌套阴影
  • 根据分辨率用媒体查询开关效果

三、为什么生产环境必须避免 @import

许多初学者喜欢这样写:

/* main.css */
@import url("reset.css");
@import url("color.css");
@import url("layout.css");

看似简洁,但它是加载阻塞的噩梦

原因 1:@import 会阻塞 CSSOM 构建

浏览器加载 main.css → 发现 @import → 停下来去下载子 CSS → 再继续解析
而 CSS 阻塞渲染,这意味着:

首屏渲染推迟,白屏时长增加。

原因 2:嵌套导入会指数级拖慢加载

像:

@import "a.css";
/* a.css 中又有 */
@import "b.css";

每一层都是阻塞链。

原因 3:HTTP/2 并不能完全解决

即使多路复用存在,浏览器仍然按“解析顺序”等待 CSSOM,这不是网络问题,而是渲染机制决定的

正确做法

  • 用构建工具将 CSS 打包成一个文件
  • 使用 <link> 替代 @import
<link rel="stylesheet" href="/css/main.css">

浏览器可并行加载 CSS,且不阻塞解析链。


性能问题从来不“写太多”,而是“写错了”

页面闪动、卡顿、迟滞、掉帧,往往有一个共同根源:

开发者忽略了浏览器渲染机制下的细节行为。

当这些关键细节被妥善处理后,页面将具备 更稳健的布局结构、更顺滑的动画与滚动体验、更快速的首屏呈现、更友好的用户交互感受,以及更健康的 SEO 指标。这些并不是微不足道的优化项,而是直接决定产品品质的工程能力体现。关注细节,持续打磨,正是前端工程真正的价值所在。

❌
❌