Day18 (前端:JavaScript基础阶段)
接续上文:Day17(前端:JavaScript基础阶段)-CSDN博客
点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!
主页:一位搞嵌入式的 genius-CSDN博客
系列文章专栏:
https://blog.csdn.net/m0_73589512/category_13011829.html
目录
JavaScript 核心概念深度解析:内存、执行机制与作用域
一、内存空间:JavaScript 的 "存储仓库"
1.1 三种核心数据结构
1.2 数据类型的存储差异
1.3 内存生命周期
1.4 垃圾回收机制
1.5 内存泄漏:隐形的性能杀手
二、执行上下文:代码运行的 "环境容器"
2.1 执行上下文的类型
2.2 执行栈:上下文的 "管理队列"
2.3 执行上下文的生命周期
三、变量对象:执行上下文的 "数据仓库"
3.1 变量对象的创建步骤
3.2 变量提升:创建阶段的 "提前登记"
四、作用域链与闭包:变量访问的 "规则与陷阱"
4.1 作用域链:变量查找的 "路径地图"
4.2 闭包:突破作用域的 "变量保鲜盒"
闭包的形成条件:
闭包的应用场景:
闭包的注意事项:
总结
JavaScript 核心概念深度解析:内存、执行机制与作用域
JavaScript 作为前端开发的核心语言,其底层机制对理解代码运行逻辑、排查问题及性能优化至关重要。本文将系统解析内存空间、执行上下文、变量对象、作用域链与闭包等核心概念,帮助开发者构建完整的 JavaScript 知识体系。
一、内存空间:JavaScript 的 "存储仓库"
JavaScript 具有自动垃圾回收机制,但这并不意味着开发者可以忽视内存管理。理解内存空间的工作原理,是掌握闭包、原型、深浅复制等高级概念的基础。
1.1 三种核心数据结构
JavaScript 的内存管理依赖于三种基础数据结构:
-
堆(Heap):树状结构,用于存储引用类型数据(如对象、数组)。堆内存中的数据无需关注顺序,访问时通过 "变量名" 定位,就像图书馆按书名查找书籍。
-
栈(Stack):遵循 "先进后出,后进先出" 原则,主要存储基本数据类型(如数字、布尔值)和引用类型的地址。栈的操作速度极快,就像叠盘子 —— 最后放的盘子最先被拿走。
-
队列(Queue):遵循 "先进先出" 原则,是理解事件循环(Event Loop)的关键。想象排队买票,先到的人先处理。
1.2 数据类型的存储差异
JavaScript 中的变量按存储方式分为两类:
-
基本数据类型:包括
Undefined
、Null
、Boolean
、Number
、String
(ES6 新增Symbol
)。这类数据直接存储在栈内存中,按值访问,操作的是变量的实际值。 -
引用数据类型:如对象(
Object
)、数组(Array
)等。这类数据的实际值存储在堆内存中,栈内存仅保存指向堆内存的地址(引用)。操作时,实际修改的是堆内存中的数据。
举例说明:
let a = 10; // 基本类型,栈内存直接存储值10 let b = { name: "张三" }; // 引用类型,栈存储地址,堆存储{name: "张三"} let c = b; // 复制的是地址,c和b指向同一个堆内存对象 c.name = "李四"; // 修改堆内存中的数据,b.name也会变为"李四"
1.3 内存生命周期
JavaScript 内存管理遵循三个阶段:
-
分配:当声明变量或创建对象时,自动分配内存空间。
let str = "hello"; // 分配存储字符串的内存 let arr = [1, 2, 3]; // 分配存储数组的内存
-
使用:通过读写变量使用已分配的内存。
console.log(str.toUpperCase()); // 读取并使用str的内存 arr.push(4); // 修改arr的内存数据
-
回收:当变量不再使用时,垃圾回收机制自动释放内存。
1.4 垃圾回收机制
JavaScript 通过标记清除算法实现自动垃圾回收:
-
当变量进入执行环境(如函数内部)时,标记为 "正在使用"。
-
当变量离开执行环境(如函数执行结束)时,标记为 "可回收"。
-
垃圾回收器定期扫描内存,释放所有 "可回收" 的变量内存。
注意:a = null
并非直接释放内存,而是解除变量与值的引用关系,让值成为 "可回收" 状态,等待垃圾回收器处理。
1.5 内存泄漏:隐形的性能杀手
内存泄漏指无用的变量仍占用内存且无法被回收的现象,常见场景及解决方案如下:
泄漏场景 | 原因 | 解决方案 |
---|---|---|
意外全局变量 | 未声明的变量自动成为全局变量,始终存在于内存中 | 严格使用 var /let /const 声明变量,避免隐式全局变量 |
未清理的定时器 | setInterval /setTimeout 引用的变量不会被回收 | 不再需要时用 clearInterval /clearTimeout 清除 |
未移除的事件监听器 | 已移除的 DOM 元素仍被事件监听器引用 | 组件卸载时用 removeEventListener 移除监听器 |
闭包长期引用 | 闭包保留对外部变量的引用,导致变量无法回收 | 避免不必要的闭包,手动解除引用(如赋值 null ) |
未清理的 DOM 引用 | 缓存的 DOM 元素已从页面移除,但仍被变量引用 | 不再需要时将 DOM 引用设为 null |
排查工具:Chrome 开发者工具的 "Memory" 面板可通过以下步骤检测内存泄漏:
-
录制内存快照(Heap Snapshot)。
-
对比多次快照,查看持续增长的变量。
-
通过 "Retainment" 面板分析变量的引用链,定位泄漏源头。
二、执行上下文:代码运行的 "环境容器"
执行上下文是 JavaScript 代码执行的环境抽象,决定了变量的可访问性、this
指向等关键信息。
2.1 执行上下文的类型
JavaScript 有三种执行上下文:
-
全局执行上下文:整个脚本运行的基础环境,唯一且在页面关闭时销毁。浏览器中全局对象为
window
,this
指向window
。 -
函数执行上下文:每次函数被调用时创建,函数执行结束后出栈销毁。
-
Eval 执行上下文:
eval
函数内部的执行环境,现代开发中极少使用(因安全性和性能问题)。
2.2 执行栈:上下文的 "管理队列"
执行栈(调用栈)按 "先进后出" 原则管理执行上下文:
-
脚本运行时,首先创建全局执行上下文并压入栈中。
-
每当函数被调用,创建新的函数执行上下文并压入栈顶(成为当前执行环境)。
-
函数执行结束后,其执行上下文从栈中弹出,继续执行栈顶的上下文。
举例:
function fn1() {fn2(); // 调用fn2,创建fn2执行上下文并压栈 } function fn2() {console.log("fn2执行"); } fn1(); // 调用fn1,创建fn1执行上下文并压栈
执行栈过程:全局上下文入栈 → fn1
上下文入栈 → fn2
上下文入栈 → fn2
执行完出栈 → fn1
执行完出栈 → 全局上下文保持到页面关闭。
2.3 执行上下文的生命周期
每个执行上下文的生命周期分为三个阶段:
-
创建阶段:
-
创建变量对象(存储变量、函数声明等)。
-
建立作用域链(确定变量查找规则)。
-
确定
this
指向(全局 / 函数 / 绑定对象)。
-
-
执行阶段:
-
执行代码,给变量赋值、执行函数等。
-
变量对象变为活动对象(可直接访问其属性)。
-
-
回收阶段:
-
执行上下文出栈,等待垃圾回收。
-
三、变量对象:执行上下文的 "数据仓库"
变量对象(VO)是执行上下文中存储变量、函数声明和参数的容器,其创建过程决定了 "变量提升" 等关键特性。
3.1 变量对象的创建步骤
变量对象在执行上下文的创建阶段生成,遵循以下顺序:
-
处理函数参数:创建
arguments
对象,存储函数参数(仅函数执行上下文有)。 -
处理函数声明:在变量对象中创建函数名属性,值为函数引用。若同名函数已存在,覆盖原引用。
-
处理变量声明:在变量对象中创建变量名属性,值为
undefined
。若同名属性已存在(如函数声明),不覆盖(避免函数被变量覆盖为undefined
)。
举例:
function fn(a) {console.log(b); // 函数声明提升,输出函数体console.log(c); // 变量提升,输出undefinedfunction b() {}var c = 10; } fn(5);
变量对象创建过程:
-
处理参数:
arguments = [5]
,a = 5
。 -
处理函数声明:
b = 函数引用
。 -
处理变量声明:
c = undefined
(因b
已存在,不重复创建)。
3.2 变量提升:创建阶段的 "提前登记"
变量提升指 var
声明的变量和函数声明在代码执行前被 "提前处理" 的现象,本质是变量对象创建阶段的特性:
-
函数声明被完整提升(可在声明前调用)。
-
var
变量仅声明被提升(声明前访问为undefined
)。
对比var
、let
、const
:
-
var
:存在变量提升,作用域为函数级。 -
let
/const
:无变量提升,作用域为块级({}
内),声明前访问会报错(暂时性死区)。
console.log(varVar); // undefined(var提升) var varVar = 10; console.log(letVar); // 报错(let无提升,处于暂时性死区) let letVar = 20;
四、作用域链与闭包:变量访问的 "规则与陷阱"
作用域链和闭包是 JavaScript 中控制变量访问的核心机制,直接影响代码的灵活性和性能。
4.1 作用域链:变量查找的 "路径地图"
作用域链是由当前执行上下文的变量对象和所有父级执行上下文的变量对象组成的链表,用于查找变量:
-
查找变量时,从当前变量对象开始,沿作用域链向上级查找。
-
若找到则使用该变量;若到达全局对象仍未找到,返回
undefined
。
作用域链的形成:函数的作用域链在定义时确定(词法作用域),而非调用时。例如:
var globalVar = "全局变量"; function outer() {var outerVar = "外部变量";function inner() {console.log(outerVar); // 沿作用域链找到outer的变量对象console.log(globalVar); // 沿作用域链找到全局变量对象}inner(); } outer();
4.2 闭包:突破作用域的 "变量保鲜盒"
闭包指函数(B)在其外部函数(A)执行结束后,仍能访问 A 中的变量的现象。本质是 B 的作用域链保留了对 A 的变量对象的引用。
闭包的形成条件:
-
函数嵌套(内部函数 B 定义在外部函数 A 中)。
-
内部函数 B 引用了外部函数 A 中的变量。
-
内部函数 B 被外部函数 A 之外的作用域引用(如返回 B 或赋值给全局变量)。
举例:
function createCounter() {let count = 0; // 被闭包保留的变量return function() {count++; // 访问外部函数的变量return count;}; } const counter = createCounter(); console.log(counter()); // 1(count被闭包保留) console.log(counter()); // 2
闭包的应用场景:
-
数据封装:隐藏变量,仅通过特定函数访问(模拟私有变量)。
function createPerson(name) {return {getName: () => name, // 闭包访问namesetName: (newName) => { name = newName; }}; } const person = createPerson("张三"); console.log(person.getName()); // 张三(无法直接访问name)
-
函数柯里化:将多参数函数转为单参数函数序列,便于复用。
function add(a) {return function(b) { return a + b; }; } const add5 = add(5); // 闭包保留a=5 console.log(add5(3)); // 8
-
缓存计算结果:存储耗时计算的结果,避免重复计算。
function createCache() {const cache = {};return (key, fn) => {if (cache[key]) return cache[key];cache[key] = fn(); // 闭包访问cachereturn cache[key];}; } const calculate = createCache();
闭包的注意事项:
-
闭包会延长外部变量的生命周期,可能导致内存占用增加,需避免不必要的闭包。
-
若闭包引用的变量是循环变量(如
for
循环中的i
),需通过立即执行函数等方式固定变量值(见前文代码 2 示例)。
总结
JavaScript 的内存管理、执行上下文、变量对象、作用域链与闭包等概念相互关联,共同构成了代码运行的底层逻辑:
-
内存空间决定了数据的存储方式,垃圾回收机制保障内存高效利用。
-
执行上下文和执行栈控制代码的执行顺序,变量对象则管理上下文中的数据。
-
作用域链定义变量的访问规则,闭包则是作用域链特性的延伸,既带来灵活性也可能引发内存问题。
深入理解这些概念,不仅能帮助开发者写出更高效的代码,更能在面对复杂问题时快速定位根源,是从 "会用"JavaScript 到 "精通"JavaScript 的关键一步。