JavaScript值和引用详解:从栈堆内存到面试实战

JavaScript值和引用详解:从栈堆内存到面试实战

引言

在JavaScript开发中,理解值和引用的区别是掌握语言核心的关键。本文将深入探讨JavaScript中的简单值和复杂值,以及它们在内存中的存储机制。

1. 数据类型分类

1.1 简单值(原始类型)

JavaScript中的简单值包括:

javascript 复制代码
// 基本数据类型
let num = 42;           // Number
let str = "Hello";      // String
let bool = true;        // Boolean
let undef = undefined;  // Undefined
let nul = null;         // Null (特殊的原始值)
let sym = Symbol('id'); // Symbol (ES6)
let big = 123n;         // BigInt (ES2020)

1.2 复杂值(引用类型)

javascript 复制代码
// 引用数据类型
let obj = { name: "张三", age: 25 };    // Object
let arr = [1, 2, 3, 4, 5];             // Array
let func = function() { return "函数"; }; // Function
let date = new Date();                  // Date
let reg = /pattern/g;                   // RegExp

2. 内存存储机制

2.1 null和undefined的特殊性

在深入了解内存存储之前,我们需要特别理解null和undefined这两个特殊值:

2.1.1 null的历史包袱

typeof null的"著名bug":

javascript 复制代码
console.log(typeof null);      // "object" ❌ 这是JavaScript的历史bug
console.log(typeof undefined); // "undefined" ✅ 正确的

// null实际上是原始值,不是对象
console.log(null instanceof Object);    // false
console.log(null === null);             // true
console.log(Object.prototype.toString.call(null)); // "[object Null]"

历史原因解释:

在JavaScript的最初实现中(1995年),Brendan Eich在设计类型检测时采用了一个基于标签位的系统:

  • 对象的类型标签是000
  • null在底层被表示为0x00(全零)
  • typeof操作符检查类型标签,发现是000就认为是对象

这个设计缺陷一直保留至今,因为修复它会破坏大量现有代码。

javascript 复制代码
// 演示null的特殊性
let obj = {};
let nullValue = null;

console.log(typeof obj);        // "object" ✅
console.log(typeof nullValue);  // "object" ❌ 历史bug

// 正确检测null的方法
console.log(nullValue === null);           // true ✅
console.log(Object.is(nullValue, null));   // true ✅
console.log(String(nullValue));            // "null" ✅
2.1.2 undefined的后来补充

undefined的设计初衷:

undefined是后来添加的概念,用来表示"未定义"的状态:

javascript 复制代码
// undefined的各种出现场景
let a;                          // 声明但未赋值
console.log(a);                 // undefined

function func(param) {
  console.log(param);           // 未传参时为undefined
}
func(); // undefined

let obj = { name: "张三" };
console.log(obj.age);           // 访问不存在的属性:undefined

function noReturn() {
  // 没有return语句
}
console.log(noReturn());        // undefined

// 数组的稀疏元素
let arr = [1, , , 4];
console.log(arr[1]);            // undefined
console.log(arr[2]);            // undefined

null vs undefined的设计哲学:

javascript 复制代码
// null:有意的空值,表示"无对象值"
let user = null;  // 明确表示用户对象为空

// undefined:系统层面的"未定义"
let config;       // 配置还未初始化

// 比较行为
console.log(null == undefined);   // true (抽象相等)
console.log(null === undefined);  // false (严格相等)

// 类型转换差异
console.log(Number(null));        // 0
console.log(Number(undefined));   // NaN

console.log(String(null));        // "null"
console.log(String(undefined));   // "undefined"

console.log(Boolean(null));       // false
console.log(Boolean(undefined));  // false

实际开发建议:

javascript 复制代码
// 推荐的使用方式
let user = null;           // 主动设置为空
let config = undefined;    // 或者不设置,让其保持undefined

// 检测方法
function checkValue(value) {
  if (value === null) {
    console.log("值被主动设置为null");
  } else if (value === undefined) {
    console.log("值未定义或未初始化");
  } else if (value == null) {  // 同时检测null和undefined
    console.log("值为空(null或undefined)");
  }
}

// 安全的属性访问
let user = { profile: null };
console.log(user.profile?.name);     // undefined (可选链)
console.log(user.settings?.theme);   // undefined

2.2 为什么需要了解栈和堆?

理解栈和堆的存储机制,能帮我们解释以下现象:

javascript 复制代码
// 为什么这样会发生?
let num1 = 10;
let num2 = num1;
num1 = 20;
console.log(num2); // 10,为什么num2没有变?

// 为什么这样会发生?
let obj1 = { name: "张三" };
let obj2 = obj1;
obj1.name = "李四";
console.log(obj2.name); // "李四",为什么obj2也变了?

答案就在于它们的存储方式不同!

2.3 栈内存(Stack)- 简单值的家

栈的工作原理:先进后出(LIFO - Last In First Out)

想象栈就像一个弹夹,子弹只能从顶部装入和取出:

javascript 复制代码
// 让我们看看这些变量是如何进栈的
let a = 10;        // 第一个进栈
let b = "hello";   // 第二个进栈
let c = true;      // 第三个进栈
let d = null;      // 第四个进栈
let e = undefined; // 最后进栈

// 栈内存的实际存储(从底部到顶部)
┌─────────────┐ ← 栈顶(最后进入)
│ e: undefined│   第5个进栈
├─────────────┤
│ d: null     │   第4个进栈
├─────────────┤
│ c: true     │   第3个进栈
├─────────────┤
│ b: "hello"  │   第2个进栈
├─────────────┤
│ a: 10       │   第1个进栈
└─────────────┘ ← 栈底(最先进入)

// 当函数执行完毕,变量会按相反顺序出栈
// 出栈顺序:e → d → c → b → a

为什么简单值的赋值不会相互影响?

javascript 复制代码
let original = 100;
let copy = original; // 这里发生了什么?

// 栈内存中的实际情况:
┌─────────────┐
│ copy: 100   │ ← 完全独立的内存空间
├─────────────┤
│original: 100│ ← 另一个独立的内存空间
└─────────────┘

original = 200; // 只修改original的内存空间

// 修改后的栈内存:
┌─────────────┐
│ copy: 100   │ ← 不受影响,仍然是100
├─────────────┤
│original: 200│ ← 被修改了
└─────────────┘

2.4 堆内存(Heap)- 复杂值的家

为什么复杂值不能直接存在栈中?

javascript 复制代码
// 想象一下,如果对象直接存在栈中会怎样?
let hugeObject = {
  users: [/* 假设有10000个用户 */],
  settings: {/* 复杂的配置对象 */},
  cache: {/* 大量缓存数据 */}
  // ... 更多数据
};

// 栈的空间有限,无法存储这么大的对象!
// 而且栈需要快速的压入和弹出操作

堆栈配合的工作方式:

javascript 复制代码
let person = { name: "张三", age: 25 };
let student = { grade: "A", school: "清华" };

// 实际的内存分布:
栈内存(存储变量名和地址)    堆内存(存储实际数据)
┌─────────────────┐        ┌──────────────────────┐
│student: 0x002   │───────→│0x002: {              │
├─────────────────┤        │  grade: "A",         │
│person: 0x001    │───────→│  school: "清华"       │
└─────────────────┘        │}                     │
                           ├──────────────────────┤
                           │0x001: {              │
                           │  name: "张三",        │
                           │  age: 25             │
                           │}                     │
                           └──────────────────────┘

现在我们来解释开头的谜题:

javascript 复制代码
// 谜题1:为什么简单值互不影响?
let num1 = 10;     // 栈中分配空间存储10
let num2 = num1;   // 栈中分配新空间,复制10的值
num1 = 20;         // 只修改num1的栈空间

// 栈内存状态:
┌─────────────┐
│ num2: 10    │ ← 独立空间,不受影响
├─────────────┤
│ num1: 20    │ ← 被修改
└─────────────┘

// 谜题2:为什么复杂值会相互影响?
let obj1 = { name: "张三" };  // 栈中存储地址0x001,堆中存储实际对象
let obj2 = obj1;              // 栈中存储相同的地址0x001
obj1.name = "李四";           // 通过地址0x001修改堆中的对象

// 内存状态:
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj2: 0x001  │────→│0x001: {         │
├─────────────┤  ┌─→│  name: "李四"    │ ← 被修改
│obj1: 0x001  │──┘  │}                │
└─────────────┘     └─────────────────┘
// 两个变量指向同一个堆内存地址,所以obj2.name也变成了"李四"

3. 基于内存机制理解访问方式

现在我们已经理解了栈和堆的工作原理,让我们深入分析不同的访问方式:

3.1 简单值访问:值拷贝的本质

javascript 复制代码
// 每次赋值都是完整的值复制
let a = 10;
let b = a;    // 在栈中为b分配新空间,复制a的值
a = 20;       // 修改a的栈空间,不影响b

console.log(a); // 20
console.log(b); // 10 (不受影响)

// 内存变化过程:
// 步骤1: let a = 10
┌─────────────┐
│ a: 10       │
└─────────────┘

// 步骤2: let b = a
┌─────────────┐
│ b: 10       │ ← 新分配的栈空间,复制了a的值
├─────────────┤
│ a: 10       │
└─────────────┘

// 步骤3: a = 20
┌─────────────┐
│ b: 10       │ ← 不受影响
├─────────────┤
│ a: 20       │ ← 只修改a的栈空间
└─────────────┘

null和undefined的值拷贝:

javascript 复制代码
// null和undefined也是值拷贝
let value1 = null;
let value2 = value1;  // 复制null值到新的栈空间
value1 = "changed";   // 只修改value1的栈空间

console.log(value1); // "changed"
console.log(value2); // null (不受影响)

// undefined的赋值演示
let x;              // x在栈中被分配空间,值为undefined
let y = x;          // y在栈中被分配新空间,复制undefined
x = 100;            // 只修改x的栈空间

console.log(x);     // 100
console.log(y);     // undefined (不受影响)

3.2 复杂值访问:引用拷贝的真相

javascript 复制代码
// 引用拷贝:拷贝的是堆内存的地址
let obj1 = { name: "张三" };  // obj1存储堆地址
let obj2 = obj1;              // obj2复制相同的堆地址
obj1.name = "李四";           // 通过地址修改堆中的对象

console.log(obj1.name); // "李四"
console.log(obj2.name); // "李四" (受影响,因为指向同一个对象)

// 详细的内存变化过程:
// 步骤1: let obj1 = { name: "张三" }
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj1: 0x001  │────→│0x001: {         │
└─────────────┘     │  name: "张三"    │
                    │}                │
                    └─────────────────┘

// 步骤2: let obj2 = obj1
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj2: 0x001  │────→│0x001: {         │
├─────────────┤  ┌─→│  name: "张三"    │
│obj1: 0x001  │──┘  │}                │
└─────────────┘     └─────────────────┘

// 步骤3: obj1.name = "李四"
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj2: 0x001  │────→│0x001: {         │
├─────────────┤  ┌─→│  name: "李四"    │ ← 堆中对象被修改
│obj1: 0x001  │──┘  │}                │
└─────────────┘     └─────────────────┘
// obj1和obj2都指向同一个堆地址,所以都能看到修改

函数参数传递的内存机制:

javascript 复制代码
// 简单值传递
function changeNumber(num) {
  num = 999;  // 修改的是参数num的栈空间
  return num;
}

let original = 10;
let result = changeNumber(original); // 传递original的值的副本

// 内存状态:
┌─────────────┐
│result: 999  │ ← 函数返回值
├─────────────┤
│original: 10 │ ← 原值不变
└─────────────┘

console.log(original); // 10 (不变)
console.log(result);   // 999

// 复杂值传递
function changeObject(obj) {
  obj.count = 999;  // 通过传递的地址修改堆中的对象
  return obj;       // 返回相同的地址
}

let myObj = { count: 10 };
let result2 = changeObject(myObj); // 传递myObj存储的地址

// 内存状态:
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│result2:0x001│────→│0x001: {         │
├─────────────┤  ┌─→│  count: 999     │ ← 被修改
│myObj: 0x001 │──┘  │}                │
└─────────────┘     └─────────────────┘

console.log(myObj.count);  // 999 (被修改)
console.log(result2.count); // 999 (指向同一对象)

4. 基于内存机制理解比较方式

理解了栈和堆的存储机制后,我们就能深刻理解为什么比较会有不同的结果:

4.1 简单值比较:比较栈中的实际值

javascript 复制代码
// 值比较:直接比较栈内存中存储的值
let a = 10;
let b = 10;
console.log(a === b); // true,因为栈中都存储着相同的值10

let str1 = "hello";
let str2 = "hello";
console.log(str1 === str2); // true,栈中都存储着相同的字符串

// 内存中的实际情况:
┌─────────────┐
│ str2:"hello"│ ← 值相同
├─────────────┤
│ str1:"hello"│ ← 值相同
├─────────────┤
│ b: 10       │ ← 值相同
├─────────────┤
│ a: 10       │ ← 值相同
└─────────────┘
// 比较时直接比较栈中的值,所以结果为true

null和undefined的比较特殊性:

javascript 复制代码
// 严格比较:比较值和类型
console.log(null === null);         // true,值和类型都相同
console.log(undefined === undefined); // true,值和类型都相同
console.log(null === undefined);    // false,类型不同

// 抽象比较:有特殊的转换规则
console.log(null == undefined);     // true,ES规范规定它们抽象相等

// 内存中的存储:
┌─────────────┐
│var4:undefined│ ← 类型:undefined
├─────────────┤
│var3:undefined│ ← 类型:undefined
├─────────────┤
│var2: null   │ ← 类型:object(历史bug)
├─────────────┤
│var1: null   │ ← 类型:object(历史bug)
└─────────────┘

// 特殊值的比较
console.log(NaN === NaN);           // false (NaN是唯一不等于自身的值)
console.log(Object.is(NaN, NaN));   // true (推荐的比较方式)

console.log(+0 === -0);             // true (IEEE 754标准认为相等)
console.log(Object.is(+0, -0));     // false (Object.is能区分)

4.2 复杂值比较:比较栈中的地址

javascript 复制代码
// 引用比较:比较的是栈中存储的堆内存地址
let obj1 = { name: "张三" };
let obj2 = { name: "张三" };  // 内容相同但是不同对象
let obj3 = obj1;              // 指向同一个对象

console.log(obj1 === obj2); // false (不同的堆内存地址)
console.log(obj1 === obj3); // true (相同的堆内存地址)

// 内存中的实际情况:
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj3: 0x001  │────→│0x001: {         │
├─────────────┤  ┌─→│  name: "张三"    │
│obj2: 0x002  │──┼─→│}                │
├─────────────┤  │  ├─────────────────┤
│obj1: 0x001  │──┘  │0x002: {         │
└─────────────┘     │  name: "张三"    │
                    │}                │
                    └─────────────────┘

// 比较过程:
// obj1 === obj2: 比较 0x001 === 0x002,结果false
// obj1 === obj3: 比较 0x001 === 0x001,结果true

如何进行内容比较?

javascript 复制代码
// 简单的内容比较(有局限性)
function simpleEqual(obj1, obj2) {
  return JSON.stringify(obj1) === JSON.stringify(obj2);
}

let person1 = { name: "张三", age: 25 };
let person2 = { name: "张三", age: 25 };
console.log(simpleEqual(person1, person2)); // true

// 但这种方法有问题:
let obj1 = { a: 1, b: 2 };
let obj2 = { b: 2, a: 1 };  // 属性顺序不同
console.log(simpleEqual(obj1, obj2)); // 可能是false(取决于属性顺序)

// 更可靠的深度比较函数
function deepEqual(a, b) {
  if (a === b) return true;
  
  if (a == null || b == null) return false;
  
  if (typeof a !== typeof b) return false;
  
  if (typeof a !== 'object') return false;
  
  let keysA = Object.keys(a);
  let keysB = Object.keys(b);
  
  if (keysA.length !== keysB.length) return false;
  
  for (let key of keysA) {
    if (!keysB.includes(key)) return false;
    if (!deepEqual(a[key], b[key])) return false;
  }
  
  return true;
}

console.log(deepEqual(obj1, obj2)); // true,正确的内容比较

5. 基于内存机制理解动态属性

栈和堆的不同存储方式,也解释了为什么简单值无法添加属性,而复杂值可以:

5.1 简单值无法添加属性的内存原因

javascript 复制代码
let num = 42;
num.property = "test";
console.log(num.property); // undefined

// 为什么会这样?让我们看看内存层面发生了什么:

// 步骤1: let num = 42
┌─────────────┐
│ num: 42     │ ← 栈中存储原始值
└─────────────┘

// 步骤2: num.property = "test"
// JavaScript引擎尝试:
// 1. 临时将42装箱为Number对象
// 2. 在临时对象上设置property
// 3. 临时对象被销毁
// 4. num仍然是栈中的原始值42

// 步骤3: console.log(num.property)
// JavaScript引擎再次:
// 1. 临时将42装箱为一个新的Number对象
// 2. 查找property属性
// 3. 新对象没有property属性,返回undefined

字符串的只读特性:

javascript 复制代码
let str = "hello";
str.length = 10;            // 尝试修改length属性
console.log(str.length);    // 5 (仍然是原来的长度)

// 内存解释:
┌─────────────┐
│ str:"hello" │ ← 栈中的字符串是不可变的
└─────────────┘
// length是字符串的内置只读属性,无法修改

null和undefined访问属性的错误:

javascript 复制代码
let nullValue = null;
let undefinedValue = undefined;

try {
  nullValue.property = "test";    // TypeError!
} catch (e) {
  console.log("null无法添加属性:", e.message);
  // 原因:null在栈中就是null,无法装箱为对象
}

try {
  undefinedValue.property = "test"; // TypeError!
} catch (e) {
  console.log("undefined无法添加属性:", e.message);
  // 原因:undefined在栈中就是undefined,无法装箱为对象
}

// 但数字和字符串可以临时装箱访问方法:
console.log((42).toString());      // "42" ← 临时装箱为Number对象
console.log("hello".toUpperCase()); // "HELLO" ← 临时装箱为String对象

// 装箱过程的内存示意:
// (42).toString() 执行时:
// 1. 创建临时Number对象
// 2. 调用toString方法
// 3. 返回结果
// 4. 销毁临时对象
// 5. 原始值42保持不变

5.2 复杂值可以动态添加属性的内存原理

javascript 复制代码
let obj = {};  // 在堆中创建空对象
obj.name = "张三";        // 在堆对象中添加属性
obj.sayHello = function() { // 在堆对象中添加方法
  return `你好,我是${this.name}`;
};

// 内存变化过程:
// 步骤1: let obj = {}
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│ obj: 0x001  │────→│0x001: {         │
└─────────────┘     │}                │
                    └─────────────────┘

// 步骤2: obj.name = "张三"
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│ obj: 0x001  │────→│0x001: {         │
└─────────────┘     │  name: "张三"    │
                    │}                │
                    └─────────────────┘

// 步骤3: obj.sayHello = function...
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│ obj: 0x001  │────→│0x001: {         │
└─────────────┘     │  name: "张三",   │
                    │  sayHello: fn   │
                    │}                │
                    └─────────────────┘

console.log(obj.name);     // "张三" ← 从堆中读取
console.log(obj.sayHello()); // "你好,我是张三" ← 执行堆中的函数

数组也是对象,同样可以添加属性:

javascript 复制代码
let arr = [1, 2, 3];
arr.customProperty = "自定义属性";
arr.customMethod = function() {
  return this.length;
};

// 内存结构:
栈内存               堆内存
┌─────────────┐     ┌─────────────────────┐
│ arr: 0x001  │────→│0x001: {             │
└─────────────┘     │  0: 1,              │ ← 数组索引
                    │  1: 2,              │
                    │  2: 3,              │
                    │  length: 3,         │ ← 内置属性
                    │  customProperty:    │ ← 自定义属性
                    │    "自定义属性",      │
                    │  customMethod: fn   │ ← 自定义方法
                    │}                    │
                    └─────────────────────┘

console.log(arr.customProperty); // "自定义属性"
console.log(arr.customMethod());  // 3
console.log(arr[0]);              // 1 ← 正常的数组访问

为什么复杂值可以动态添加属性?

  1. 堆内存的可扩展性:堆内存空间相对灵活,可以动态分配和扩展
  2. 对象的哈希表结构:JavaScript对象本质上是属性名到值的映射(哈希表)
  3. 引用访问:通过栈中的地址,可以直接修改堆中的对象结构
javascript 复制代码
// 对象属性的本质:哈希表
let person = {};

// 每次添加属性,就是在堆中的哈希表添加键值对
person["name"] = "张三";     // 等价于 person.name = "张三"
person["age"] = 25;         // 等价于 person.age = 25
person[Symbol("id")] = 123; // 甚至可以用Symbol作为键

// 堆中的结构类似于:
// {
//   "name" -> "张三",
//   "age" -> 25,
//   Symbol(id) -> 123
// }

6. 基于内存机制深入理解变量赋值

通过前面对栈和堆内存的详细了解,我们现在可以更深入地理解各种赋值操作:

6.1 简单值赋值:栈中的值复制

javascript 复制代码
// 值拷贝的完整过程
let original = 100;
let copy = original;
original = 200;

// 内存变化的完整过程:

// 步骤1: let original = 100
┌─────────────┐
│original: 100│ ← 在栈中分配空间存储100
└─────────────┘

// 步骤2: let copy = original
┌─────────────┐
│ copy: 100   │ ← 新分配栈空间,复制original的值
├─────────────┤
│original: 100│
└─────────────┘

// 步骤3: original = 200
┌─────────────┐
│ copy: 100   │ ← 不受影响,仍然是独立的值
├─────────────┤
│original: 200│ ← 修改original的栈空间
└─────────────┘

console.log(original); // 200
console.log(copy);     // 100

函数参数传递中的值拷贝:

javascript 复制代码
function changeValue(val) {
  val = 999;  // 修改参数val的栈空间
  return val;
}

let num = 10;
let result = changeValue(num);

// 函数调用的内存过程:
// 调用前:
┌─────────────┐
│ num: 10     │
└─────────────┘

// 调用changeValue(num)时:
// 1. 为参数val分配新的栈空间
// 2. 复制num的值到val的栈空间
┌─────────────┐
│ val: 10     │ ← 函数内部的参数空间
├─────────────┤
│ num: 10     │ ← 外部变量空间
└─────────────┘

// 3. 执行val = 999
┌─────────────┐
│ val: 999    │ ← 只修改函数内部的参数
├─────────────┤
│ num: 10     │ ← 外部变量不受影响
└─────────────┘

// 4. 函数返回,val的栈空间被释放
┌─────────────┐
│result: 999  │ ← 返回值被存储到新变量
├─────────────┤
│ num: 10     │ ← 原值保持不变
└─────────────┘

console.log(num);    // 10 (原值不变)
console.log(result); // 999

6.2 复杂值赋值:栈中的地址复制

javascript 复制代码
// 引用拷贝的完整过程
let originalObj = { count: 1 };
let copyObj = originalObj;
originalObj.count = 2;

// 内存变化的详细过程:

// 步骤1: let originalObj = { count: 1 }
栈内存                    堆内存
┌──────────────────┐     ┌─────────────────┐
│originalObj: 0x001│────→│0x001: {         │
└──────────────────┘     │  count: 1       │
                         │}                │
                         └─────────────────┘

// 步骤2: let copyObj = originalObj
栈内存                    堆内存
┌──────────────────┐     ┌─────────────────┐
│copyObj: 0x001    │────→│0x001: {         │
├──────────────────┤  ┌─→│  count: 1       │
│originalObj: 0x001│──┘  │}                │
└──────────────────┘     └─────────────────┘
// 注意:复制的是地址0x001,不是对象本身

// 步骤3: originalObj.count = 2
栈内存                    堆内存
┌──────────────────┐     ┌─────────────────┐
│copyObj: 0x001    │────→│0x001: {         │
├──────────────────┤  ┌─→│  count: 2       │ ← 堆中对象被修改
│originalObj: 0x001│──┘  │}                │
└──────────────────┘     └─────────────────┘
// 两个变量都指向同一个堆地址,所以都看到了修改

console.log(originalObj.count); // 2
console.log(copyObj.count);     // 2 (受影响)

函数参数传递中的引用拷贝:

javascript 复制代码
function changeObject(obj) {
  obj.count = 999;  // 通过地址修改堆中的对象
  return obj;       // 返回相同的地址
}

let myObj = { count: 10 };
let result = changeObject(myObj);

// 函数调用的内存过程:
// 调用前:
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│myObj: 0x001 │────→│0x001: {         │
└─────────────┘     │  count: 10      │
                    │}                │
                    └─────────────────┘

// 调用changeObject(myObj)时:
// 1. 为参数obj分配栈空间
// 2. 复制myObj的地址到obj的栈空间
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj: 0x001   │────→│0x001: {         │
├─────────────┤  ┌─→│  count: 10      │
│myObj: 0x001 │──┘  │}                │
└─────────────┘     └─────────────────┘

// 3. 执行obj.count = 999
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│obj: 0x001   │────→│0x001: {         │
├─────────────┤  ┌─→│  count: 999     │ ← 堆中对象被修改
│myObj: 0x001 │──┘  │}                │
└─────────────┘     └─────────────────┘

// 4. 函数返回,obj的栈空间被释放,但返回地址
栈内存               堆内存
┌─────────────┐     ┌─────────────────┐
│result:0x001 │────→│0x001: {         │
├─────────────┤  ┌─→│  count: 999     │
│myObj: 0x001 │──┘  │}                │
└─────────────┘     └─────────────────┘

console.log(myObj.count);    // 999 (原对象被修改)
console.log(result.count);   // 999 (指向同一对象)
console.log(myObj === result); // true (相同的地址)

6.3 深拷贝 vs 浅拷贝的内存机制

浅拷贝:只复制第一层的地址

javascript 复制代码
let original = {
  name: "张三",
  hobbies: ["游戏", "电影"]
};

let shallowCopy = { ...original };

// 浅拷贝的内存结构:
栈内存                    堆内存
┌──────────────────┐     ┌─────────────────────┐
│shallowCopy: 0x002│────→│0x002: {             │
├──────────────────┤     │  name: "张三",       │
│original: 0x001   │──┐  │  hobbies: 0x003 ────┼─┐
└──────────────────┘  │  │}                    │ │
                      │  ├─────────────────────┤ │
                      └─→│0x001: {             │ │
                         │  name: "张三",       │ │
                         │  hobbies: 0x003 ────┼─┘
                         │}                    │
                         ├─────────────────────┤
                         │0x003: ["游戏","电影"] │ ← 共享的数组
                         └─────────────────────┘

shallowCopy.name = "李四";        // 修改浅拷贝对象的name
shallowCopy.hobbies.push("读书"); // 修改共享的数组

console.log(original.name);       // "张三" (不受影响)
console.log(original.hobbies);    // ["游戏", "电影", "读书"] (受影响)
console.log(shallowCopy.name);    // "李四"
console.log(shallowCopy.hobbies); // ["游戏", "电影", "读书"]

深拷贝:递归复制所有层级

javascript 复制代码
function deepClone(obj) {
  // 处理原始值
  if (obj === null || typeof obj !== "object") {
    return obj;
  }
  
  // 处理日期
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }
  
  // 处理数组
  if (obj instanceof Array) {
    return obj.map(item => deepClone(item));
  }
  
  // 处理对象
  if (typeof obj === "object") {
    const clonedObj = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        clonedObj[key] = deepClone(obj[key]); // 递归复制
      }
    }
    return clonedObj;
  }
}

let deepCopy = deepClone(original);

// 深拷贝的内存结构:
栈内存                    堆内存
┌──────────────────┐     ┌─────────────────────┐
│deepCopy: 0x004   │────→│0x004: {             │
├──────────────────┤     │  name: "张三",       │
│original: 0x001   │──┐  │  hobbies: 0x005 ────┼─┐
└──────────────────┘  │  │}                    │ │
                      │  ├─────────────────────┤ │
                      └─→│0x001: {             │ │
                         │  name: "张三",       │ │
                         │  hobbies: 0x003 ────┼─┼─┐
                         │}                    │ │ │
                         ├─────────────────────┤ │ │
                         │0x005: ["游戏","电影"] │←┘ │ ← 独立的数组副本
                         ├─────────────────────┤   │
                         │0x003: ["游戏","电影"] │←──┘ ← 原始数组
                         └─────────────────────┘

deepCopy.hobbies.push("旅行");
console.log(original.hobbies); // ["游戏", "电影"] (不受影响)
console.log(deepCopy.hobbies); // ["游戏", "电影", "旅行"]

这样的解释让读者能够清楚地看到每种操作背后的内存机制,理解为什么会产生不同的结果。

7. 经典面试题

面试题:分析下面代码的输出结果

javascript 复制代码
// 题目
function test() {
  let a = 1;
  let b = { value: 1 };
  let c = [1, 2, 3];
  
  function changeValues(x, y, z) {
    x = 10;
    y.value = 10;
    z.push(4);
    z = [5, 6, 7];
    z.push(8);
  }
  
  changeValues(a, b, c);
  
  console.log("a:", a);
  console.log("b:", b);
  console.log("c:", c);
}

test();

分析过程:

  1. 初始状态:

    css 复制代码
    栈内存               堆内存
    ┌─────────────┐     ┌──────────────────┐
    │a: 1         │     │0x001: {value: 1} │
    │b: 0x001     │────→├──────────────────┤
    │c: 0x002     │────→│0x002: [1, 2, 3]  │
    └─────────────┘     └──────────────────┘
  2. 函数调用 changeValues(a, b, c):

    • x = 10:创建新的局部变量x,不影响外部的a
    • y.value = 10:通过引用修改堆内存中的对象
    • z.push(4):通过引用修改堆内存中的数组
    • z = [5, 6, 7]:z指向新的数组对象
    • z.push(8):向新数组添加元素,不影响原数组
  3. 执行后内存状态:

    css 复制代码
    栈内存               堆内存
    ┌─────────────┐     ┌─────────────────────┐
    │a: 1         │     │0x001: {value: 10}   │
    │b: 0x001     │────→├─────────────────────┤
    │c: 0x002     │────→│0x002: [1, 2, 3, 4]  │
    └─────────────┘     ├─────────────────────┤
                        │0x003: [5, 6, 7, 8]  │ ← 局部变量z指向的新数组
                        └─────────────────────┘

答案:

yaml 复制代码
a: 1
b: { value: 10 }
c: [1, 2, 3, 4]

进阶面试题

javascript 复制代码
// 更复杂的题目
let obj1 = {
  name: "原始对象",
  nested: { count: 1 }
};

let obj2 = obj1;
let obj3 = { ...obj1 };
let obj4 = JSON.parse(JSON.stringify(obj1));

obj1.name = "修改后的对象";
obj1.nested.count = 999;

console.log("obj1:", obj1);
console.log("obj2:", obj2);
console.log("obj3:", obj3);
console.log("obj4:", obj4);

// 请分析输出结果

答案:

javascript 复制代码
obj1: { name: "修改后的对象", nested: { count: 999 } }
obj2: { name: "修改后的对象", nested: { count: 999 } }  // 引用拷贝
obj3: { name: "原始对象", nested: { count: 999 } }      // 浅拷贝
obj4: { name: "原始对象", nested: { count: 1 } }        // 深拷贝

null和undefined的终极面试题

javascript 复制代码
// 经典陷阱题
function mysteryFunction() {
  console.log("1.", typeof null);
  console.log("2.", null == undefined);
  console.log("3.", null === undefined);
  console.log("4.", !null);
  console.log("5.", !undefined);
  console.log("6.", null + 1);
  console.log("7.", undefined + 1);
  console.log("8.", String(null));
  console.log("9.", String(undefined));
  
  let obj = { a: null, b: undefined };
  console.log("10.", JSON.stringify(obj));
  
  let arr = [null, undefined, , null];
  console.log("11.", arr.length);
  console.log("12.", arr[2]);
  console.log("13.", JSON.stringify(arr));
}

mysteryFunction();

参考答案:

javascript 复制代码
1. object        // typeof null的历史bug
2. true          // 抽象相等,null和undefined被认为相等
3. false         // 严格相等,类型不同
4. true          // null是falsy值
5. true          // undefined是falsy值
6. 1             // null转换为0
7. NaN           // undefined转换为NaN
8. null          // null的字符串表示
9. undefined     // undefined的字符串表示
10. {"a":null}   // JSON.stringify会省略undefined属性
11. 4            // 数组长度包括稀疏元素
12. undefined    // 稀疏数组的空位
13. [null,null,null,null] // JSON.stringify将undefined和稀疏位都转为null

8. 总结

关键要点

  1. 简单值:存储在栈内存,赋值时进行值拷贝
  2. 复杂值:存储在堆内存,栈中存储引用地址,赋值时进行引用拷贝
  3. 栈内存:先进后出,存储简单值和引用地址
  4. 堆内存:存储复杂值的实际数据
  5. 比较方式:简单值比较值,复杂值比较引用地址
  6. null特殊性:虽然typeof返回"object",但它是原始值
  7. undefined设计:表示未定义状态,与null在语义上有区别

实际开发建议

  1. 理解引用拷贝,避免意外修改原对象
  2. 合理使用深拷贝和浅拷贝
  3. 注意函数参数传递的特性
  4. 使用不可变数据结构减少副作用
  5. 正确处理null和undefined
    • 使用value === null检测null
    • 使用value === undefined检测undefined
    • 使用value == null同时检测两者
    • 避免依赖typeof null的结果
  6. JSON序列化注意事项
    • undefined属性会被忽略
    • null会被保留
    • 函数会被忽略

掌握这些概念对于编写高质量的JavaScript代码至关重要,也是面试中的常考点。

相关推荐
yes or ok15 分钟前
前端工程师面试题-vue
前端·javascript·vue.js
我要成为前端高手29 分钟前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
牧野星辰1 小时前
让el-table长个小脑袋,记住我的滚动位置
前端·javascript·element
_Congratulate1 小时前
vue3高德地图api整合封装(自定义撒点、轨迹等)
前端·javascript·vue.js
JohnYan1 小时前
Bun技术评估 - 23 Glob
javascript·后端·bun
富婆苗子1 小时前
关于wangeditor的自定义组件和元素
前端·javascript
前端老鹰2 小时前
JavaScript Intl.RelativeTimeFormat:自动生成 “3 分钟前” 的国际化工具
前端·javascript
梦想CAD控件2 小时前
(在线CAD插件)网页CAD实现图纸表格智能提取
前端·javascript·全栈
sorryhc2 小时前
【AI解读源码系列】ant design mobile——Space间距
前端·javascript·react.js
uhakadotcom2 小时前
NPM与NPX的区别是什么?
前端·面试·github