JavaScript 数据类型
1. 基础类型与引用类型
- 基础类型 (Primitive):
string,number,boolean,null,undefined,symbol,bigint。- 存储在栈 (Stack) 中,占据空间小,大小固定,属于被频繁使用的数据。
- 引用类型 (Reference):
Object,Array,Function,Date,RegExp等。- 存储在堆 (Heap) 中,占据空间大,大小不固定。引用地址存储在栈中。
为什么基础类型存栈,引用类型存堆?
栈 (Stack) 的特点:
- 空间小且连续:操作系统自动分配内存空间。
- 存取速度快:遵循 LIFO(后进先出)原则,由系统自动释放。
- 数据大小固定:基础类型(如
number,boolean)占用空间固定,适合存储在栈中。
堆 (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 === NaN为false(使用Number.isNaN()判断)。- 对象比较的是引用地址。
3.2 == (宽松相等)
- 规则:如果类型不同,会先进行隐式类型转换,再比较。
- 转换顺序:
null==undefined(true)。- 如果一个是
string,一个是number,尝试将string转为number。 - 如果一个是
boolean,尝试将其转为number(true-> 1,false-> 0)。 - 如果一个是
object,一个是基础类型,尝试将object转为原始值 (调用valueOf或toString)。
经典面试题
javascript
[] == ![]; // true
// 解析:
// 1. ![] 转换为 boolean -> false
// 2. [] == false
// 3. false 转换为 number -> 0
// 4. [] 转换为原始值 -> "" (toString) -> 0 (Number)
// 5. 0 == 0 -> true4. 显式与隐式转换
4.1 显式转换
Number(): 整体转换 (Number('123a')->NaN)。parseInt(): 解析转换 (parseInt('123a')->123)。String(): 转字符串。Boolean(): 转布尔值 (0,'',null,undefined,NaN为 false,其余为 true)。
4.2 隐式转换
- 四则运算:
+号且有一边是字符串:进行字符串拼接。-,*,/,%:全部转为数字计算。
javascript1 + "1"; // '11' 1 - "1"; // 0 - 逻辑判断:
if(value)会自动调用Boolean(value)。
4.3 对象转原始值流程
当对象需要转为原始值时(如 obj + 1),JS 会按以下顺序调用:
Symbol.toPrimitive(如果有)。valueOf()。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)。 - 手写递归: 面试常见手写题。
