数据类型
约 2235 字大约 7 分钟
2025-11-04
数据类型
JavaScript 中的数据类型分为基本数据类型和引用数据类型
最新的 ECMAScript 2025 标准,JavaScript 中共定义了 8 种 数据类型。这些类型可以清晰地划分为两大类:原始类型(Primitive Types)和对象类型(Object Type)
| 类别 | 类型名称 | 简要说明 | 示例 |
|---|---|---|---|
| 原始类型 | String | 表示文本数据 | "hello" |
| Number | 表示整数和浮点数 | 42, 3.14 | |
| Boolean | 表示逻辑值,仅 true 或 false | true, false | |
| Undefined | 表示变量已声明但未赋值 | let a; // a 为 undefined | |
| Null | 表示一个空值或不存在的对象 | let a = null; | |
| Symbol | 表示唯一的、不可变的值,常用于对象属性的标识符(ES6新增) | Symbol('id') | |
| BigInt | 表示任意精度的整数(ES2020新增) | 123n | |
| 对象类型 | Object | 各种数据结构的集合,如普通对象、数组、函数、日期等 | {name: "Alice"}, [1,2,3] |
值有类型,变量无类型:JavaScript 是动态类型语言。变量本身没有类型,它只是容器;而变量中存储的值才有类型。同一个变量在不同时刻可以被赋予不同类型的值
typeof 操作符的“陷阱”:使用 typeof 检测类型时,有两个著名的特殊情况需要注意
typeof null的返回值是"object",这是语言遗留的 Bug。正确判断null需要组合条件:(!a && typeof a === "object")typeof function(){}的返回值是"function",尽管函数本质上是特殊的对象。
存储方式的差异:原始类型的值直接存储在栈内存中,而对象类型的内容存储在堆内存中,变量中保存的是其引用地址。这导致它们在赋值和比较时的行为完全不同。
| 特性 | 原始类型 | 对象类型 |
|---|---|---|
| 存储方式 | 栈内存,直接存值 | 堆内存,变量存引用地址 |
| 可变性 | 不可变,修改会创建新值 | 可变,可以直接修改其属性或内容 |
| 比较方式 | 比较值是否相等 | 比较引用地址是否指向同一个对象 |
| 传递方式 | 按值传递(传递值的副本) | 按引用传递(传递地址的副本) |
补充:在 JavaScript 中,“对象类型”和“引用类型”这两个术语是等价的,它们描述的是同一类数据类型,只是从不同角度进行命名:
- 对象类型:是从值的表现形式和构造方式角度命名的。
- 引用类型:是从内存存储和访问机制角度命名的。
注
- 基本类型仅保存原始值,不存在属性和方法
- 保存基本类型的变量是 按值访问 的,操作的就是存储在变量中的实际值
- 复制基本类型时会创建该值的第二个副本 (独立使用,互不干扰)
深入理解
let obj = { name: "Alice", age: 30 };这个对象在内存中的存储结构是这样的:
栈内存 (Stack)
┌───────────┬─────────────┐
│ 变量名 │ 存储的值 │
├───────────┼─────────────┤
│ obj │ 🔗0x1000 │ → 指向堆内存地址的引用
└───────────┴─────────────┘
堆内存 (Heap)
地址 0x1000
┌─────────────────────────┐
│ { │
│ name: 🔗0x2000 │ → 指向字符串"Alice"的地址
│ age: 30 │ ← 原始值直接存储
│ } │
└─────────────────────────┘
地址 0x2000
┌─────────────────────────┐
│ "Alice" (字符串原始值) │
└─────────────────────────┘关键点:
对象本身({name: "Alice", age: 30})存储在堆内存中
变量 obj 在栈内存中存储的是对象在堆内存中的地址引用
对象的属性值:
- 如果是原始值(如
age: 30),直接存储在对象内部 - 如果是另一个对象(如
name: "Alice"),存储的是指向那个对象的引用地址
当一个对象没有任何引用指向它时,它就变成了"垃圾",会被 JavaScript 引擎的垃圾回收器自动回收。
// 示例1:对象失去所有引用
let obj = { data: "important" };
obj = null; // 原来的 {data: "important"} 对象现在无引用,会被GC回收
// 示例2:更复杂的情况
function createUser() {
let user = { name: "Bob" };
return user;
}
let userRef = createUser();
// 函数执行完后,局部变量user被销毁,但对象{name: "Bob"}仍有userRef引用,不会被回收
userRef = null; // 现在 {name: "Bob"} 无引用,等待GC回收GC 的回收时机
垃圾回收不是立即发生的,而是在特定时机由引擎自动执行:
- 分配内存时:当需要分配新内存但内存不足时
- 周期性执行:引擎在空闲时自动运行GC
- 达到内存阈值:当内存使用达到某个上限时
现代JavaScript引擎主要使用两种GC算法:
- 引用计数法(早期,现在较少使用)
// 循环引用问题
let objA = { friend: null };
let objB = { friend: null };
objA.friend = objB; // objB 被引用次数: 2
objB.friend = objA; // objA 被引用次数: 2
objA = null;
objB = null;
// 虽然外部无引用,但彼此仍引用,引用计数不为0,导致内存泄漏- 标记-清除法(现代主流)
// GC从"根"对象(全局变量、当前执行上下文等)开始
// 标记所有可达的对象,然后清除不可达的对象
let obj1 = { data: "value1" };
let obj2 = { data: "value2" };
obj1.next = obj2;
obj2.prev = obj1;
obj1 = null;
obj2 = null;
// 标记-清除能识别这种循环引用,因为从根对象无法到达它们,都会被回收现代GC优化策略
- 分代收集:对象分为"新生代"(新创建)和"老生代"(存活时间长)
- 增量标记:将GC工作分成小片段执行,避免阻塞主线程
- 空闲时收集:在浏览器空闲时段执行GC
实践建议
// 及时释放不再需要的大对象引用
function processLargeData() {
let largeData = fetchHugeData(); // 获取大量数据
let result = processData(largeData);
// 处理完成后立即释放引用
largeData = null; // 帮助GC尽快回收
return result;
}
// 避免意外的全局变量引用
function createCache() {
// 错误:全局变量,永远不会被回收
cache = { /* 大量数据 */ };
// 正确:局部变量,函数执行完可被回收
let localCache = { /* 数据 */ };
return localCache;
}总结:对象类型通过引用访问,无引用时会被GC回收,只是具体的回收时机和算法由引擎智能管理。
浅拷贝和深拷贝
let original = { name: "Alice", hobbies: ["reading", "coding"] };
// 问题:直接赋值只是复制引用
let copy = original;
copy.name = "Bob";
console.log(original.name); // "Bob" - 原对象也被修改了!
// 这显然不是我们想要的效果!浅拷贝(Shallow Copy)
只拷贝第一层属性,如果属性值是对象,则拷贝的是引用地址。
// 浅拷贝方法
let original = {
name: "Alice",
hobbies: ["reading", "coding"], // 引用类型
age: 25 // 原始类型
};
// 方法1: Object.assign()
let shallow1 = Object.assign({}, original);
// 方法2: 展开运算符
let shallow2 = { ...original };
// 方法3: 数组的浅拷贝
let arr = [1, 2, { name: "test" }];
let arrShallow = [...arr]; // 或 arr.slice()
// 测试浅拷贝的效果
shallow1.name = "Bob"; // 修改原始类型 - 不影响原对象
shallow1.hobbies.push("gaming"); // 修改引用类型 - 原对象也被影响!
console.log(original.hobbies); // ["reading", "coding", "gaming"] - 被影响了!深拷贝(Deep Copy)
递归拷贝所有层级,创建完全独立的对象副本。
let original = {
name: "Alice",
hobbies: ["reading", "coding"],
address: {
city: "Beijing",
street: { no: 123, name: "Main St" }
}
};
// 方法1: JSON方法(最简单但有局限性)
let deep1 = JSON.parse(JSON.stringify(original));
// 方法2: 手动递归实现
function deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(item => deepClone(item));
let clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
let deep2 = deepClone(original);
// 方法3: 使用现成库(推荐用于生产环境)
// let deep3 = _.cloneDeep(original); // Lodash
// let deep4 = structuredClone(original); // 现代浏览器原生API
// 测试深拷贝的效果
deep1.hobbies.push("gaming");
deep1.address.city = "Shanghai";
console.log(original.hobbies); // ["reading", "coding"] - 不受影响!
console.log(original.address.city); // "Beijing" - 不受影响!注
总结
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝层级 | 仅第一层 | 所有层级(递归) |
| 对象属性 | 拷贝引用地址 | 创建新对象 |
| 性能 | 快 | 慢(尤其大对象) |
| 内存占用 | 少 | 多 |
| 循环引用 | 无问题 | 需要特殊处理 |
- 浅拷贝:对象结构简单,没有嵌套对象或数组
- 深拷贝:对象结构复杂,需要完全独立的副本
- 生产环境:推荐使用成熟的库(Lodash的
_.cloneDeep)或现代浏览器的structuredClone
// 现代浏览器的原生深拷贝API
if (typeof structuredClone === 'function') {
let deepCopy = structuredClone(original); // 处理大部分场景
}深拷贝的注意事项
// 1. JSON方法的局限性
let obj = {
date: new Date(),
func: function() { return "hello"; },
undefined: undefined,
infinity: Infinity,
regex: /pattern/gi
};
let jsonCopy = JSON.parse(JSON.stringify(obj));
console.log(jsonCopy);
// {
// date: "2023-10-01T12:00:00.000Z", // Date对象变字符串
// infinity: null, // Infinity变null
// regex: {} // 正则变空对象
// // func 和 undefined 属性丢失!
// }
// 2. 循环引用问题
let a = { name: "A" };
let b = { name: "B" };
a.child = b;
b.parent = a; // 循环引用
// JSON.stringify(a); // 报错: Converting circular structure to JSON
// deepClone(a); // 栈溢出: Maximum call stack size exceeded