099 Primitives vs. Objects (Primitive vs. Reference Types) 笔记

一、核心问题引入:原始类型与对象的行为差异

1.1 原始类型的 “预期行为”(代码示例)

1
2
3
4
5
6
7
8
9
// 1. 声明原始类型变量(数字)
let age = 30;
// 2. 复制变量值
let oldAge = age;
// 3. 修改原变量
age = 31;
// 4. 输出结果
console.log(age); // 输出:31(原变量修改后的值)
console.log(oldAge); // 输出:30(复制时的初始值,不受原变量修改影响)

结论:原始类型复制后,修改原变量不会影响复制变量,符合直觉。

1.2 对象类型的 “意外行为”(代码示例)

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 声明对象类型变量
const me = {
name: "Jonas",
age: 30,
};
// 2. 复制对象(看似复制,实则引用)
const friend = me;
// 3. 修改复制对象的属性
friend.age = 27;
// 4. 输出结果
console.log(me.age); // 输出:27(原对象属性被修改)
console.log(friend.age); // 输出:27(复制对象的修改同步到原对象)

问题:为何修改friend会影响me?需从 “内存存储机制” 解释。

二、基础知识回顾:JS 数据类型分类

2.1 原始类型(Primitive Types,又称 “值类型”)

  • 包含 7 种:Number(数字)、String(字符串)、Boolean(布尔值)、Undefined(未定义)、Null(空值)、Symbol(唯一标识)、BigInt(大整数)。
  • 特点:存储 “具体值”,占用内存小且固定。

2.2 引用类型(Reference Types,核心是对象)

  • 包含:Object(普通对象)、Array(数组,本质是特殊对象)、Function(函数,本质是可执行对象)、Date(日期对象)等。
  • 特点:存储 “复杂结构”,占用内存大小不固定。

三、关键原理:JS 引擎的内存存储机制

3.1 内存分区:调用堆栈(Call Stack)与堆(Heap)

内存区域 存储内容 特点
调用堆栈 原始类型、函数执行上下文 速度快,内存大小有限,自动释放
引用类型(对象) 速度较慢,内存空间大,手动回收(GC)

3.2 原始类型的内存存储流程(对应 1.1 示例)

  1. 声明

    1
    age = 30

    时:

    • 调用堆栈中分配一块内存(地址如0001),存储值30
    • 变量age指向内存地址0001(而非直接指向值30)。
  2. 复制

    1
    oldAge = age

    时:

    • oldAge直接指向age的内存地址0001,与age共享同一值。
  3. 修改

    1
    age = 31

    时:

    • 原始类型值不可变(无法直接修改0001地址的值);
    • 新分配一块内存(地址如0002),存储31
    • age转而指向0002oldAge仍指向0001(值30)。

3.3 引用类型的内存存储流程(对应 1.2 示例)

  1. 声明

    1
    const me = {name: "Jonas", age: 30}

    时:

    • 堆中分配一块内存(地址如D30F),存储对象的键值对;
    • 调用堆栈中分配一块内存(地址如0003),存储堆地址D30F
    • 变量me指向调用堆栈的0003(间接指向堆中的对象)。
  2. 复制

    1
    const friend = me

    时:

    • friend指向调用堆栈中与me相同的地址0003
    • 二者通过0003间接指向堆中同一对象(D30F),未创建新对象。
  3. 修改

    1
    friend.age = 27

    时:

    • 直接修改堆中D30F地址的对象属性(age30改为27);
    • mefriend仍共享同一堆地址D30F,因此二者访问到的age均为27

四、重要延伸:const声明的可变性差异

4.1 const与原始类型:完全不可变

1
2
const num = 10;
num = 20; // 报错:Assignment to constant variable(const禁止修改原始类型的引用)

原因const声明的原始类型变量,其指向的内存地址不可变,且原始类型值本身不可变,因此变量完全无法修改。

4.2 const与引用类型:引用不可变,属性可变

1
2
3
4
5
6
7
const person = { name: "Alice", age: 25 };
// 1. 修改对象属性:允许
person.age = 26;
console.log(person.age); // 输出:26(属性修改成功)

// 2. 修改引用(指向新对象):禁止
person = { name: "Bob", age: 30 }; // 报错:Assignment to constant variable

原因const仅禁止修改 “调用堆栈中的引用地址”(如person不能从指向0003改为指向0004),但不限制 “堆中对象属性的修改”。

五、实践影响与后续学习提示

5.1 常见坑点:“复制对象” 的本质

  • 误区:const newObj = oldObj不是 “复制对象”,而是 “复制引用”,二者指向同一对象。

  • 示例(坑点):

    1
    2
    3
    4
    const user1 = { score: 80 };
    const user2 = user1;
    user2.score = 90;
    console.log(user1.score); // 输出:90(意外修改了原对象)

5.2 后续学习方向

  1. 解决 “对象深复制” 问题:后续课程会讲解Object.assign()、扩展运算符(...)、JSON.parse(JSON.stringify())等深复制方法,避免引用共享导致的意外修改。
  2. 关联核心概念:
    • 原型继承(OOP 章节):对象的原型链依赖引用类型的内存机制;
    • 事件循环(异步 JS 章节):堆中对象的回收时机与事件循环相关;
    • DOM 操作(高级 DOM 章节):DOM 元素本质是引用类型,修改 DOM 属性的原理与对象一致。

六、总结:原始类型与引用类型核心差异表

对比维度 原始类型(值类型) 引用类型(对象)
存储位置 调用堆栈 堆(调用堆栈存引用地址)
复制行为 复制值,修改原变量不影响副本 复制引用,修改副本影响原对象
const可变性 完全不可变(地址和值均不可改) 引用不可变,对象属性可改
典型示例 let a = 10const str = "abc" const obj = {}let arr = []