Skip to content

JavaScript 数据类型

MDN JavaScript 文档

1. 基础类型与引用类型

  • 基础类型 (Primitive): string, number, boolean, null, undefined, symbol, bigint
    • 存储在栈 (Stack) 中,占据空间小,大小固定,属于被频繁使用的数据。
  • 引用类型 (Reference): Object, Array, Function, Date, RegExp 等。
    • 存储在堆 (Heap) 中,占据空间大,大小不固定。引用地址存储在栈中。

为什么基础类型存栈,引用类型存堆?

  1. 栈 (Stack) 的特点

    • 空间小且连续:操作系统自动分配内存空间。
    • 存取速度快:遵循 LIFO(后进先出)原则,由系统自动释放。
    • 数据大小固定:基础类型(如 number, boolean)占用空间固定,适合存储在栈中。
  2. 堆 (Heap) 的特点

    • 空间大且杂乱:内存分配灵活,由开发者分配释放(JS 中由垃圾回收机制 GC 处理)。
    • 数据大小不固定:对象可以动态添加属性,大小不确定,如果不放在堆中,容易导致栈溢出。
    • 性能考量:如果在栈中存储复杂对象,会影响程序的运行性能(栈的查找速度)。因此,将对象的 地址(引用) 存在栈中,实体存在堆中。

举个栗子 🌰

  • 栈 (Stack) 就像是你的口袋

    • 口袋空间小,拿东西非常快。
    • 适合放一些小的、固定的东西,比如硬币(数字)、写着“是/否”的小纸条(布尔值)。
    • 这些东西一旦放进去,大小就不会变了。
  • 堆 (Heap) 就像是一个巨大的仓库

    • 仓库空间非常大,但找东西需要时间。
    • 适合放那些可能随时变大、结构复杂的东西,比如一整箱书(对象)、一个随时可能增加页数的笔记本(数组)。
    • 因为口袋装不下仓库里的东西,所以我们把 仓库的钥匙(引用地址) 放在口袋里。
    • 当你需要用这些东西时,你从口袋掏出钥匙,去仓库里找到对应的东西。
javascript
let age = 18; // 基础类型:直接把 18 放在口袋(栈)里
let user = { name: "AC", age: 18 };
// 引用类型:
// 1. 在仓库(堆)里开辟一块空间,存放 { name: "AC", age: 18 }
// 2. 把这块空间的地址(比如 0x001)写在纸条上,放在口袋(栈)里

这也解释了为什么:

  • 修改 age 时,是直接换了口袋里的东西。
  • 修改 user.name 时,是拿着口袋里的地址,去仓库里修改了内容。
  • user 赋值给另一个变量 (let user2 = user),其实只是复印了一张地址纸条给 user2,他们指向的还是同一个仓库空间(浅拷贝)。

2. 判断数据类型

2.1 typeof

  • 特点:简单方便,适合判断基础类型。
  • 弊端
    • typeof null === 'object' (历史遗留 bug)。
    • 无法区分对象类型([], {}, Date 都会返回 'object')。
    • 函数会返回 'function'
javascript
typeof "str"; // 'string'
typeof 123; // 'number'
typeof null; // 'object' ❌
typeof []; // 'object' ❌
typeof {}; // 'object'
typeof (() => {}); // 'function'

2.2 instanceof

  • 特点:检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 弊端详解

1. 跨 iframe 环境失效

如果页面有多个 iframe,每个 iframe 都有自己的全局对象(window)和内置构造函数(如 Array)。

  • 父页面的 Array 和 iframe 中的 Array 是两个不同的对象(内存地址不同)。
  • 当你把 iframe 中的数组传给父页面时,父页面的 Array 构造函数并不在那个数组的原型链上。
javascript
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const xArray = window.frames[0].Array; // iframe 中的 Array 构造函数
const arr = new xArray(1, 2, 3); // 在 iframe 环境创建数组

console.log(arr instanceof Array); // false ❌
console.log(arr instanceof xArray); // true ✅
// 推荐使用 Array.isArray(arr) 来解决此问题

2. 原型链被篡改

instanceof 的判断逻辑是看 RightHandSide.prototype 是否在 LeftHandSide 的原型链上。如果我们手动修改了原型链,结果就会不准确。

javascript
// 示例 1:让普通对象 "伪装" 成数组
const obj = {};
obj.__proto__ = Array.prototype; // 修改原型链指向
console.log(obj instanceof Array); // true ❌ (实际上它只是个对象)

// 示例 2:切断原型链
const arr = [];
arr.__proto__ = null;
console.log(arr instanceof Array); // false ❌ (虽然它本质是数组)
javascript
[] instanceof Array; // true
[] instanceof Object; // true (因为 Array 原型链最终指向 Object)

2.3 Object.prototype.toString.call() (推荐)

  • 特点:最准确的方式,返回 [object Type]
  • 原理:借用 Object 原型上的 toString 方法,打印其内部属性 [[Class]]
javascript
const getType = (val) => Object.prototype.toString.call(val).slice(8, -1);

getType(1); // "Number"
getType(null); // "Null"
getType([]); // "Array"
getType(new Date()); // "Date"

3. "==" 和 "===" 的区别

3.1 === (严格相等)

  • 规则
    • 不进行类型转换。
    • 类型相同且值相同才为 true
    • NaN === NaNfalse (使用 Number.isNaN() 判断)。
    • 对象比较的是引用地址

3.2 == (宽松相等)

  • 规则:如果类型不同,会先进行隐式类型转换,再比较。
  • 转换顺序
    1. null == undefined (true)。
    2. 如果一个是 string,一个是 number,尝试将 string 转为 number
    3. 如果一个是 boolean,尝试将其转为 number (true -> 1, false -> 0)。
    4. 如果一个是 object,一个是基础类型,尝试将 object 转为原始值 (调用 valueOftoString)。

经典面试题

javascript
[] == ![]; // true
// 解析:
// 1. ![] 转换为 boolean -> false
// 2. [] == false
// 3. false 转换为 number -> 0
// 4. [] 转换为原始值 -> "" (toString) -> 0 (Number)
// 5. 0 == 0 -> true

4. 显式与隐式转换

4.1 显式转换

  • Number(): 整体转换 (Number('123a') -> NaN)。
  • parseInt(): 解析转换 (parseInt('123a') -> 123)。
  • String(): 转字符串。
  • Boolean(): 转布尔值 (0, '', null, undefined, NaN 为 false,其余为 true)。

4.2 隐式转换

  • 四则运算
    • + 号且有一边是字符串:进行字符串拼接。
    • -, *, /, %:全部转为数字计算。
    javascript
    1 + "1"; // '11'
    1 - "1"; // 0
  • 逻辑判断if(value) 会自动调用 Boolean(value)

4.3 对象转原始值流程

当对象需要转为原始值时(如 obj + 1),JS 会按以下顺序调用:

  1. Symbol.toPrimitive (如果有)。
  2. valueOf()
  3. toString()
javascript
const obj = {
  valueOf() {
    return 100;
  },
  toString() {
    return "200";
  },
};
console.log(obj + 1); // 101 (使用了 valueOf)

5. JSON.stringify 的弊端 (深拷贝陷阱)

JSON.parse(JSON.stringify(obj)) 常用于深拷贝,但有以下丢失数据的风险:

1. 无法处理的情况

类型结果示例
Function丢失{ fn: () => {} } -> {}
undefined丢失{ val: undefined } -> {}
Symbol丢失{ [Symbol()]: 1 } -> {}
Date变字符串{ date: new Date() } -> { date: "2023-..." }
RegExp变空对象{ reg: /abc/ } -> { reg: {} }
NaN / Infinity变 null{ num: NaN } -> { num: null }
循环引用报错obj.a = obj -> Uncaught TypeError

2. 代码示例

javascript
const obj = {
  a: undefined,
  b: function () {},
  c: /abc/,
  d: new Date(),
};

const clone = JSON.parse(JSON.stringify(obj));
console.log(clone);
// {
//   c: {},
//   d: "2023-10-27T08:00:00.000Z"
// }
// a, b 丢失

3. 替代方案

  • structuredClone(): 现代浏览器原生支持的深拷贝 API,支持 Date, RegExp, Map, Set 等。
  • Lodash: _.cloneDeep(obj)
  • 手写递归: 面试常见手写题。

Power by VitePress