Skip to content

引用

注:本文中提到的“引用”是广义概念,不仅仅指 JavaScript 里的“引用类型”。

在程序里,我们有两种常见的“东西”:

  • (Value):比如数字、布尔值、字符串。
  • 引用(Reference):指向某块数据的“地址”。

内存里发生了什么(极简版)

可以把内存想成一个大仓库:

  • 小而简单的数据(例如数字)可以直接放在变量里。
  • 复杂对象通常放在“仓库里”,变量里只放一个“门牌号”(引用)。

在 JavaScript 里,基本类型是“值语义”,对象/数组/函数是“引用语义”。

值语义 vs. 引用语义

看一个最简单的例子:

javascript
let a = 1;
let b = a;

b = 2;
a; // 1

ab 是两个独立的值,互不影响。

再看对象:

javascript
let obj1 = { x: 1 };
let obj2 = obj1;

obj2.x = 2;
obj1.x; // 2

obj1obj2 实际上指向同一份数据,这就叫“引用语义”。当两条变量指向同一块数据时,它们就成了别名(alias)。

引用的作用

引用让我们可以共享大型结构,避免频繁复制。例如一个很大的数组:

javascript
let big = new Array(1000000).fill(0);
let shared = big; // 共享引用,几乎不占额外内存

如果每次都复制一份,性能和内存都会受影响。

引用与数据结构

链表、树、图等复杂结构都离不开引用。

以链表为例:每个节点保存“下一个节点”的引用:

javascript
function node(value, next = null) {
    return { value, next };
}

let list = node(1, node(2, node(3)));

树和图也是一样:通过引用把节点连接起来。

更进一步:同一个节点也可以被多个父节点引用,这样就形成了“有向无环图”(DAG)。这在编译器、缓存结构里很常见。

函数参数里的引用

JavaScript 函数传参时,传递的是“值”,但对于对象来说,这个“值”是引用:

javascript
function addOne(point) {
    point.x += 1;
}

let p = { x: 0 };
addOne(p);
p.x; // 1

这也是很多“状态被意外修改”的来源。

相等与同一

两个对象长得一样,不代表它们是“同一个”:

javascript
let a = { x: 1 };
let b = { x: 1 };

(a === b); // false

=== 对对象比较的是引用是否相同。如果你需要比较内容是否相同,就要写深度比较(或用库)。

拷贝:浅拷贝与深拷贝

避免“别名问题”的一种方式是复制:

javascript
let user = { name: 'Kimmy', age: 18 };
let copy = { ...user }; // 浅拷贝

浅拷贝只复制“第一层”。如果有嵌套对象,还是共享引用:

javascript
let u1 = { profile: { city: 'SH' } };
let u2 = { ...u1 };

u2.profile.city = 'BJ';
u1.profile.city; // 'BJ'

深拷贝更彻底,但实现更复杂。一个常见(但有局限)的方法是:

javascript
let deep = JSON.parse(JSON.stringify(u1));

提醒:这种方式会丢失函数、DateMap 等复杂类型。

引用与对象生命周期(内存管理)

在某些语言里(如 C/C++),你需要手动申请和释放内存。释放不当就会造成“内存泄漏”或“野指针”。

JavaScript 有自动垃圾回收(GC),但这并不等于“不会泄漏”。

引用计数(概念)

一种常见的内存管理方式叫“引用计数”:

  • 每个对象有一个计数。
  • 有一个新的引用指向它,计数 +1。
  • 引用消失,计数 -1。
  • 计数为 0 时释放。

问题是:循环引用会让计数永远不为 0:

javascript
let a = {};
let b = {};
a.other = b;
b.other = a;

引用计数在这种情况下可能无法回收。现代 JS 引擎通常使用“可达性分析”而不是纯引用计数,所以可以处理循环引用。

常见的内存泄漏场景(JS)

  • 全局变量:全局对象引用一直存在。
  • 事件监听未移除:DOM 结点被移除,但监听还在。
  • 缓存无限增长:数组/Map 只增不减。
  • 闭包长时间持有大对象:函数引用了一个很大的结构。

例如:

javascript
let cache = [];
function remember(data) {
    cache.push(data); // cache 不断变大
}

如何避免

  • 及时清理不用的引用(设为 null)。
  • 给缓存设置上限。
  • 事件监听记得 removeEventListener
  • 临时映射用 WeakMap / WeakSet

小结

引用让我们能够构建复杂结构、提高效率,但也带来共享状态、别名、生命周期等问题。理解引用是理解“程序行为”的关键一步。

练习

  • 解释下面代码的输出:
javascript
let a = [1, 2];
let b = a;
let c = a.slice();

b.push(3);

console.log(a, b, c);
  • 写一个 cloneUser(user),返回一个“只做浅拷贝”的函数。
  • 写一个 freeze 的例子,说明不可变对象能减少引用带来的副作用。
  • 设计一个链表结构,让两个链表共享同一个尾部节点,并解释这样做的好处与风险。

延伸阅读

CC-BY 4.0 Licensed