前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • HTTP

    • 基础篇

      • HTTP的前世今生
      • HTTP是什么
      • HTTP世界全览
      • HTTP分层
      • 键入网址到回车发生什么
      • HTTP报文是什么样子的
      • 理解请求方法
      • URI
      • 响应状态码
      • HTTP有哪些特点
      • HTTP优缺点
      • HTTP的实体数据
      • HTTP传输大文件
      • HTTP的连接管理
      • HTTP的重定向
      • HTTP的Cookie机制
      • HTTP的缓存控制
      • HTTP的代理服务
      • HTTP的缓存代理
    • 高级篇

    • 扩展篇

  • 浏览器相关

  • 计算机基础

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

栈空间和堆空间:数据是如何存储的|浏览器篇

# 栈空间和堆空间:数据是如何存储的

30 秒速记

  • JavaScript 会自动管理内存,但开发者仍需理解数据在内存中的存放方式。
  • 内存机制直接影响对赋值行为、垃圾回收和代码执行过程的判断。
  • 缺少内存模型认知,容易在实际项目中误判数据变化,并埋下性能问题。
  • 当前主题只讨论数据如何存储;垃圾回收和 V8 执行代码属于后续层次。

工程上可以把执行上下文和局部变量理解为由栈管理,而对象、数组等大小动态且可能共享的数据通常分配在堆中。 基本类型赋值会得到独立的值,对象赋值复制的则是引用,所以通过任一引用修改属性,其他引用也能看到变化。比如展开语法只能完成浅拷贝,新对象与原对象不再相等,但嵌套对象仍可能共享。需要注意,栈和堆只是便于推理的模型,实际布局会受闭包、逃逸分析和引擎优化影响,不能用它断言某个值的精确内存位置。

对于前端开发者来说,JavaScript 的内存机制是一个不被经常提及的概念 ,因此很容易被忽视。特别是一些非计算机专业的同学,对内存机制可能没有非常清晰的认识,甚至有些同学根本就不知道 JavaScript 的内存机制是什么

但是如果你想成为行业专家,并打造高性能前端应用,那么你就必须要搞清楚JavaScript 的内存机制了。

其实,要搞清楚 JavaScript 的内存机制并不是一件很困难的事,在接下来的三篇文章(数据在内存中的存放、JavaScript 处理垃圾回收以及 V8 执行代码)中,我们将通过内存机制的介绍,循序渐进带你走进 JavaScript 内存的世界。

今天我们讲述第一部分的内容——JavaScript 中的数据是如何存储在内存中的。虽然 JavaScript 并不需要直接去管理内存,但是在实际项目中为了能避开一些不必要的坑,你还是需要了解数据在内存中的存储方式的

原理拆解: JavaScript 语言规范定义的是值、对象和赋值语义,并未强制引擎把某类数据物理存入栈或堆。工程上通常把执行上下文、局部变量等短生命周期信息概括为栈空间,把对象、数组、函数等可共享且大小动态变化的数据概括为堆空间。变量保存基本类型时,赋值获得独立的值;变量指向对象时,赋值复制的是同一对象的引用,因此通过任一引用修改属性,其他引用都能观察到变化。对象变量本身与对象内容应分开理解:前者可随调用帧管理,后者通常由引擎在堆中分配。

最小验证: 输入一个数字和一个对象,分别执行赋值与修改,用来验证基本类型的值复制和对象的引用共享。

let scoreA = 10;
let scoreB = scoreA;
scoreB = 20;

const userA = { name: "Ada", skills: ["JavaScript"] };
const userB = userA;
userB.name = "Grace";
userB.skills.push("TypeScript");

const userC = { ...userA };
userC.name = "Lin";
userC.skills.push("CSS");

console.log(scoreA, scoreB);
console.log(userA.name, userB.name, userA === userB);
console.log(userA.skills);
console.log(userC.name, userC === userA);
@前端进阶之旅: 代码已经复制到剪贴板

数字部分预期输出 10 20,说明重新赋值 scoreB 不会改变 scoreA。userA === userB 为 true,且两者名称都变为 Grace。展开语法只创建浅拷贝,所以 userC !== userA,但嵌套的 skills 仍被共享,最终 userA.skills 也包含 CSS。

边界与排查: “基本类型一定在栈、对象一定在堆”只是便于推理的模型,字符串驻留、闭包捕获、逃逸分析和引擎优化都可能改变实际布局,不能据此判断精确内存位置。const 只禁止变量重新绑定,并不冻结对象;需要阻止顶层属性修改可使用 Object.freeze,但它同样不是递归冻结。排查意外联动时,应先检查 ===、嵌套引用和浅拷贝边界;排查占用增长可使用浏览器开发者工具的 Heap Snapshot 对比分配与保留路径,但快照只能观察堆对象,不能直接证明某个值位于调用栈。

面试官追问

追问 1代码评审中有人坚持“基本类型一定在栈、对象一定在堆”,并据此断言闭包里的字符串绝不可能受引擎优化影响,你会认可吗?
参考回答

不会认可,这只是便于工程推理的常用模型,不是 ECMAScript 对物理布局的强制规定。字符串驻留、闭包捕获和逃逸分析都可能影响实际实现;分析代码行为应依据值语义与对象身份,不能由模型反推精确地址。

追问 2用户资料页执行 const draft = source 后修改 draft.name,预览区里的 source.name 也同步变化,你会怎样定位并修正设计?
参考回答

应先用 draft === source 确认二者是否指向同一对象,再检查嵌套字段是否仍共享引用。若业务要求独立草稿,需要按数据结构选择浅复制或满足条件时使用 structuredClone;函数、DOM 节点等不能被当作普通结构化数据直接克隆。

追问 3表单原来只有一层标量字段,使用 {...source} 一直正常;新增嵌套的 skills 数组后,编辑副本却污染原数据,复制方案为什么失效?
参考回答

展开语法只复制顶层属性,新增的 skills 数组仍由原对象和副本共同引用。应明确需要隔离的层级并复制相应嵌套结构,或在数据满足结构化克隆条件时采用 structuredClone;盲目深复制也会增加处理和内存成本。

追问 4单页应用运行数小时后占用持续增长,开发者拿一张 Heap Snapshot 断言某个局部数字必定位于调用栈且无法回收,你会如何排查?
参考回答

这张快照只能帮助观察堆对象、分配情况和保留路径,不能直接证明某个值的精确物理位置。应对比多次快照,沿引用链寻找持续保留的对象,并结合闭包和共享引用检查;不要用“栈或堆”的简化模型替代证据。

追问 5状态库团队在“每次深复制整个大对象”和“直接共享并修改对象”之间争论,你会基于什么做选型?
参考回答

直接共享修改成本较低,但容易产生跨模块联动;完整深复制能加强隔离,却会增加时间与空间开销。应按对象规模、嵌套深度和独立编辑需求确定复制边界,并以对象身份和实际变更路径验证,不能把任何一端当作通用答案。

# 让人疑惑的代码

30 秒速记

  • 给变量赋原始值后再复制,a 与 b 的后续修改互不影响;示例最终分别输出 2 和 1。
  • 把对象变量 a 赋给 b 后,二者关联的是同一个对象;通过 a.name 修改属性,两个变量观察到的内容都会变化。
  • b = a 对对象执行的不是独立内容复制,因此不能把变量赋值等同于对象克隆。
  • 判断赋值后的联动行为,关键是区分被赋的值属于原始类型还是引用类型。

第一段最终输出 2 和 1,第二段中的 a、b 则都会看到 {name: "极客邦"}。 原始值赋值复制的是数据本身,所以修改 a 不影响 b;对象赋值复制的是引用,两者仍指向同一个对象。修改 a.name 改的是共享对象,而执行 a = {...} 是让 a 指向新对象,不会影响 b。工程中可用 === 判断是否共享对象;展开语法和 Object.assign 只做浅复制,嵌套对象仍可能共用引用。

首先,我们先看下面这两段代码:

function foo(){
    var a = 1
    var b = a
    a = 2
    console.log(a)
    console.log(b)
}
foo()
@前端进阶之旅: 代码已经复制到剪贴板
function foo(){
    var a = {name:" 极客时间 "}
    var b = a
    a.name = " 极客邦 " 
    console.log(a)
    console.log(b)
}
foo()
@前端进阶之旅: 代码已经复制到剪贴板

若执行上述这两段代码,你知道它们输出的结果是什么吗?下面我们就来一个一个分析下。

执行第一段代码,打印出来 a 的值是 2,b 的值是 1,这没什么难以理解的。

接着,再执行第二段代码,你会发现,仅仅改变了 a 中 name 的属性值,但是最终 a 和 b 打印出来的值都是{name:“极客邦”}。这就和我们预期的不一致了,因为我们想改变的仅仅是 a 的内容,但 b 的内容也同时被改变了。

要彻底弄清楚这个问题,我们就得先从“JavaScript 是什么类型的语言”讲起。

原理拆解: 变量赋值复制的是“值”。原始类型的值就是数据本身,因此 b = a 后,a 与 b 各自保存独立的数值;随后执行 a = 2,只是让 a 绑定新值。对象变量中保存的值可理解为指向对象的引用,b = a 复制了该引用,所以两者能访问同一对象。a.name = "极客邦" 修改的是共享对象的属性,并没有改变 a 自身保存的引用。所谓“栈存原始值、堆存对象”是常用的实现模型,但 ECMAScript 规范并不要求引擎采用固定的栈、堆布局;判断代码行为应以值语义和对象身份为准。

最小验证: 输入包含原始值赋值、对象赋值和对象重新赋值,用来验证“修改共享对象”与“替换变量引用”是两种不同操作。

function verify() {
  let a = 1;
  let b = a;
  a = 2;
  console.log(a, b); // 2 1

  let userA = { name: "极客时间" };
  let userB = userA;
  console.log(userA === userB); // true

  userA.name = "极客邦";
  console.log(userA.name, userB.name); // 极客邦 极客邦

  userA = { name: "新对象" };
  console.log(userA.name, userB.name); // 新对象 极客邦
  console.log(userA === userB); // false
}

verify();
@前端进阶之旅: 代码已经复制到剪贴板

关键行 userA.name = ... 沿引用修改原对象,因此 userB 可观察到变化;userA = {...} 则创建新对象并替换 userA 的引用,不会反向修改 userB。若第二组身份比较仍为 true,通常说明测试代码没有真正执行重新赋值。

边界与排查: 展开语法 {...userA}、Object.assign({}, userA) 只能完成浅复制:顶层属性可以独立修改,但嵌套对象仍可能共享。需要深复制且数据满足结构化克隆条件时,可使用现代浏览器提供的 structuredClone;函数、DOM 节点等值不能按普通结构化数据直接克隆。const 只禁止变量重新绑定,并不冻结对象属性;若要阻止顶层属性修改,可用 Object.freeze,但它同样不是递归深冻结。工程排查时可用 === 验证两个变量是否指向同一对象,并分别测试属性修改与变量重新赋值,避免把引用共享误判为赋值异常。

← this:从JavaScript执行上下文视角讲this垃圾回收:垃圾数据如何自动回收 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • HTTP

    • 基础篇

      • HTTP的前世今生
      • HTTP是什么
      • HTTP世界全览
      • HTTP分层
      • 键入网址到回车发生什么
      • HTTP报文是什么样子的
      • 理解请求方法
      • URI
      • 响应状态码
      • HTTP有哪些特点
      • HTTP优缺点
      • HTTP的实体数据
      • HTTP传输大文件
      • HTTP的连接管理
      • HTTP的重定向
      • HTTP的Cookie机制
      • HTTP的缓存控制
      • HTTP的代理服务
      • HTTP的缓存代理
    • 高级篇

    • 扩展篇

  • 浏览器相关

  • 计算机基础