JavaScript高级程序设计笔记 4
第4章 变量、作用域与内存
1. 原始值与引用值
1.1 核心区别
| 特性 | 原始值 | 引用值 |
|---|---|---|
| 类型 | Undefined, Null, Boolean, Number, String, Symbol, BigInt | Object (Array, Function, Date, RegExp 等) |
| 存储位置 | 栈内存 | 堆内存 |
| 访问方式 | 按值访问 | 按引用访问 |
| 动态属性 | 不可添加 | 可随时添加、修改、删除 |
| 复制方式 | 复制值副本 | 复制指针(引用) |
| 比较方式 | 值比较 | 引用比较(比较是否为同一对象) |
1.2 复制值
- 原始值:两个变量独立,互不影响。
let num1 = 5; let num2 = num1; // 独立副本 num2 = 10; // num1 仍为 5 - 引用值:两个变量指向同一对象,修改会相互影响。
let obj1 = {}; let obj2 = obj1; // 复制的是指针 obj1.name = 'Nicholas'; console.log(obj2.name); // 'Nicholas'
1.3 传递参数(关键考点!)
JavaScript 中所有函数参数都是按值传递的。
-
传递原始值:将值复制给局部变量,函数内修改不影响外部。
function addTen(num) { num += 10; return num; } let count = 20; let result = addTen(count); console.log(count); // 20(未变) -
传递引用值:复制的是地址副本,仍指向同一对象。但重新赋值不会影响原变量。
function setName(obj) { obj.name = 'Nicholas'; // 修改原对象 obj = new Object(); // 重新赋值,切断关联 obj.name = 'Greg'; // 修改新对象 } let person = {}; setName(person); console.log(person.name); // 'Nicholas'(不是'Greg')
1.4 确定类型
typeof:最适合判断原始值。null返回"object"(历史遗留 Bug)。instanceof:判断引用值的具体类型。console.log(person instanceof Object); // true console.log([] instanceof Array); // true console.log('hello' instanceof String); // false
2. 执行上下文与作用域
2.1 执行上下文(Execution Context)
JavaScript 代码解析和执行时的环境。
| 类型 | 数量 | 生命周期 |
|---|---|---|
| 全局执行上下文 | 唯一(浏览器中为 window) |
页面关闭时销毁 |
| 函数执行上下文 | 多个 | 函数调用时创建,执行完销毁 |
| eval 执行上下文 | 按需 | 不推荐使用 |
2.2 执行栈(Call Stack)
后进先出的栈结构,用于管理执行上下文。调用函数时入栈,执行完后出栈。
2.3 执行上下文的创建过程
- 生成变量对象(Variable Object)
- 建立作用域链
- 确定
this的指向
2.4 变量对象与活动对象
| 概念 | 说明 |
|---|---|
| 全局对象(GO) | 存放内置属性和方法 |
| 变量对象(VO) | 存放执行上下文中创建的变量和值 |
| 活动对象(AO) | 函数私有上下文中的变量对象 |
2.5 作用域(静态)与上下文(动态)
- 作用域是静态的:函数定义时就确定了访问范围。
- 执行上下文是动态的:函数调用时才创建,执行完销毁。
2.6 作用域链
当前执行环境有权访问的变量和函数的有序层级结构。
- 查找规则:从自身活动对象开始,逐级向外层查找,直到全局作用域。
- 作用域链增强:
try/catch的catch块和with语句会临时增加作用域链长度。
3. 垃圾回收与内存管理
3.1 主要垃圾回收策略
引用计数(已淘汰)
- 基本原理:记录每个对象的引用次数。
- 致命缺陷:循环引用导致内存泄漏。
let objA = {}; let objB = {}; objA.ref = objB; objB.ref = objA; // 相互引用,引用计数永不为 0
标记清除(主流)
- 基本原理:标记所有可达对象,清除未被标记的对象。
- 可达性:从根(
window、活动栈、闭包环境、DOM 树)出发,能被访问到的对象。
3.2 性能优化
- V8 分代回收:新生代使用 Scavenge,老生代使用 Mark-Sweep/Mark-Compact。
- 增量/并发标记:降低垃圾回收造成的页面卡顿。
3.3 常见内存泄漏场景
| 场景 | 说明 | 解决方案 |
|---|---|---|
| 意外的全局变量 | function foo() { bar = [] } 中 bar 隐式挂到 window |
启用严格模式 + ESLint |
| 定时器未清理 | setInterval 闭包持有大对象 |
组件销毁时 clearInterval |
| 事件监听未解绑 | 闭包持有已移除的 DOM 元素 | 使用 AbortController 或事件委托 |
| 闭包滥用 | 外部函数变量持续被引用 | 及时置为 null,用完后解除引用 |
4. 常见面试题速查
基础概念
-
原始值和引用值的区别?
→ 原始值存栈、不可变、复制值;引用值存堆、可变、复制指针。 -
typeof null为什么返回'object'?
→ JavaScript 的 Bug,在语言实现早期就存在,为保持兼容性至今未修改。 -
如何准确判断一个值是数组?
→Array.isArray(value)或value instanceof Array。 -
null和undefined的区别?
→undefined是声明未初始化,null是空对象指针。
执行上下文与作用域
-
什么是执行上下文?有哪几种类型?
→ 代码执行时的环境。有全局、函数、eval 三种。 -
什么是作用域链?如何查找变量?
→ 从当前作用域开始,逐级向外层查找,直到全局。找不到则报ReferenceError。 -
作用域和执行上下文有什么区别?
→ 作用域是静态的(定义时确定),执行上下文是动态的(调用时创建)。 -
函数执行上下文何时创建和销毁?
→ 函数调用时创建,执行完毕后销毁(闭包可使变量保留)。
参数传递
-
JavaScript 是值传递还是引用传递?
→ 按值传递。原始值传值副本,引用值传递地址的副本(共享传递)。 -
为什么函数内
obj = new Object()不会影响外部变量?
→ 因为重新赋值改变的是形参的指向,外部变量仍指向原对象。 -
如何实现函数参数的“按引用传递”效果?
→ 将参数包装为对象属性,或通过返回值赋值。
垃圾回收
-
引用计数有何缺陷?为什么被淘汰?
→ 循环引用导致对象无法回收。如对象 A 和 B 相互引用,引用计数永不为 0。 -
标记清除的基本原理是什么?
→ 标记所有可达对象,清除未被标记的对象。 -
什么情况下会造成内存泄漏?如何避免?
→ 意外全局变量、未清理定时器、未解绑事件监听、滥用闭包。使用严格模式、组件销毁时清理。 -
闭包为什么可能导致内存泄漏?
→ 闭包会持有外部函数的变量,如果这些变量长期不被释放(如存到全局数组),就会占用内存。

浙公网安备 33010602011771号