JavaScript 的数据类型分为两大类:
-
原始类型 (Primitive Types) :
- string
- number (包括 NaN, Infinity, -Infinity)
- boolean
- null
- undefined
- symbol (ES6+)
- bigint (ES2020+)
-
对象类型 (Object Type) :
- object (这包括普通对象 {}、数组 []、函数 function(){}、日期 Date、正则表达式 RegExp、错误 Error 等等)
以下是常用的判断方法及其优缺点:
1. typeof 操作符
这是最基础的类型判断方式,但有其局限性。
返回值:
- "string"
- "number"
- "boolean"
- "undefined"
- "symbol"
- "bigint"
- "object" (用于 null、Array、Date、RegExp、普通对象等)
- "function" (用于函数)
优点:
- 简单易用。
- 对于大多数原始类型(除了 null)和 function 类型判断准确。
缺点:
- typeof null 返回 "object" :这是一个历史遗留的 bug,无法区分 null 和其他对象。
- 无法区分具体的对象类型(如 Array, Date, RegExp, 普通对象都返回 "object")。
示例:
console.log(typeof "hello"); // "string"
console.log(typeof 123); // "number"
console.log(typeof NaN); // "number" (注意!)
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol("id")); // "symbol"
console.log(typeof 123n); // "bigint"
console.log(typeof function() {});// "function"
console.log(typeof null); // "object" (!!!)
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof /regex/); // "object" (在一些老旧浏览器可能返回 "function")
2. instanceof 操作符
instanceof 用于检查一个对象是否是某个构造函数的实例,它会检查对象的原型链。
优点:
- 可以区分具体的对象类型(如 Array, Date, RegExp)。
- 可以判断自定义类的实例。
缺点:
- 不能用于判断原始类型(它会返回 false,除非是使用构造函数创建的包装对象,如 new String("abc"),但这不推荐)。
- 对 null 和 undefined 无效(会报错或返回 false)。
- 在跨窗口/iframe 环境下可能失效:如果对象是在一个 iframe 中创建的,它的原型链与当前窗口的原型链不同,instanceof Array 可能会返回 false,即使它确实是个数组。
示例:
const arr = [];
const date = new Date();
const obj = {};
function Person() {}
const person = new Person();
console.log(arr instanceof Array); // true
console.log(date instanceof Date); // true
console.log(obj instanceof Object); // true
console.log(arr instanceof Object); // true (数组也是对象)
console.log(date instanceof Object); // true (日期也是对象)
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true (自定义对象也是对象)
console.log("hello" instanceof String); // false (原始类型)
console.log(new String("hello") instanceof String); // true (包装对象)
console.log(123 instanceof Number); // false (原始类型)
// console.log(null instanceof Object); // Uncaught TypeError (在某些环境下可能不报错但返回 false)
// console.log(undefined instanceof Object); // Uncaught TypeError (同上)
3. Object.prototype.toString.call()
这是最推荐、最通用、最准确的判断方法,尤其是在需要区分各种内置对象类型时。它利用了 Object.prototype.toString 方法,该方法能返回一个表示对象类型的内部 [[Class]] 属性的字符串。
返回值格式: "[object Type]",其中 Type 是具体的类型名(如 String, Number, Boolean, Null, Undefined, Symbol, BigInt, Object, Array, Function, Date, RegExp, Error 等)。
优点:
- 非常准确:可以精确判断所有内置类型,包括 null 和 undefined。
- 不受跨窗口/iframe 问题影响。
- 适用于原始类型和对象类型。
缺点:
- 写法稍微复杂一些。
- 对于自定义类的实例,通常返回 "[object Object]",无法直接判断出自定义的类名(此时 instanceof 更合适)。
示例:
function getType(value) {
// 获取 [object Type] 字符串,并提取 Type 部分
return Object.prototype.toString.call(value).slice(8, -1);
}
console.log(getType("hello")); // "String"
console.log(getType(123)); // "Number"
console.log(getType(NaN)); // "Number"
console.log(getType(true)); // "Boolean"
console.log(getType(undefined)); // "Undefined"
console.log(getType(null)); // "Null" (!!!)
console.log(getType(Symbol("id"))); // "Symbol"
console.log(getType(123n)); // "BigInt"
console.log(getType(function() {})); // "Function"
console.log(getType({})); // "Object"
console.log(getType([])); // "Array"
console.log(getType(new Date())); // "Date"
console.log(getType(/regex/)); // "RegExp"
console.log(getType(new Error())); // "Error"
console.log(getType(new Map())); // "Map"
console.log(getType(new Set())); // "Set"
class MyClass {}
const myInstance = new MyClass();
console.log(getType(myInstance)); // "Object" (无法识别自定义类)
4. 特定类型的检查方法
对于某些常用类型,JavaScript 提供了更直接、更语义化的检查方法。
-
Array.isArray() : 判断数组的最佳方法。它不受 instanceof 的跨窗口限制。
console.log(Array.isArray([])); // true console.log(Array.isArray({})); // false console.log(Array.isArray("hello")); // false console.log(Array.isArray(null)); // false -
Number.isNaN() : 判断一个值是否为 NaN 的最佳方法。它不会像全局 isNaN() 那样进行类型转换。
console.log(Number.isNaN(NaN)); // true console.log(Number.isNaN(123)); // false console.log(Number.isNaN("hello")); // false (不会转换) console.log(isNaN(NaN)); // true console.log(isNaN("hello")); // true (会先尝试将 "hello" 转为 Number,得到 NaN) -
直接比较 === null 和 === undefined: 判断 null 和 undefined 最简单、最直接的方法。
let x = null; let y; console.log(x === null); // true console.log(y === undefined); // true console.log(x === undefined); // false console.log(y === null); // false
总结与推荐
| 类型 | 推荐方法 | 备选/说明 |
|---|---|---|
| string | typeof value === 'string' | getType(value) === 'String' |
| number | typeof value === 'number' | getType(value) === 'Number' (包含 NaN, Infinity) |
| NaN | Number.isNaN(value) | value !== value (只有 NaN 不等于自身) |
| boolean | typeof value === 'boolean' | getType(value) === 'Boolean' |
| undefined | value === undefined | typeof value === 'undefined', getType(value) === 'Undefined' |
| null | value === null | getType(value) === 'Null' (注意 typeof null 是 'object') |
| symbol | typeof value === 'symbol' | getType(value) === 'Symbol' |
| bigint | typeof value === 'bigint' | getType(value) === 'BigInt' |
| function | typeof value === 'function' | getType(value) === 'Function', value instanceof Function |
| Array | Array.isArray(value) | getType(value) === 'Array', value instanceof Array (注意跨窗口问题) |
| 普通对象 ({}) | getType(value) === 'Object' | typeof value === 'object' && value !== null && !Array.isArray(value) 等组合判断 |
| Date | value instanceof Date && !isNaN(value) | getType(value) === 'Date' (注意 new Date('invalid') 也是 Date 对象) |
| RegExp | value instanceof RegExp | getType(value) === 'RegExp' |
| Error | value instanceof Error | getType(value) === 'Error' |
| 其他内置对象 (Map, Set) | value instanceof Map / Set | getType(value) === 'Map' / 'Set' |
| 自定义类实例 | value instanceof MyClass | - |
| 通用/精确判断 | Object.prototype.toString.call(value) | (封装成 getType 函数使用) |
总原则:
- 优先使用语义化的专用方法:Array.isArray(), Number.isNaN(), value === null, value === undefined。
- 判断原始类型和 function:typeof 通常足够且方便(但要记住 typeof null === 'object')。
- 判断内置对象类型(Array, Date, RegExp 等)且需要跨窗口兼容:Object.prototype.toString.call() 是最可靠的选择。
- 判断某个对象是否是特定(自定义)类的实例:instanceof 是最佳选择。
- 需要一个能处理所有类型的通用函数:基于 Object.prototype.toString.call() 实现。
深浅拷贝
好的,我们来详细探讨 JavaScript 中的浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。
理解这两者的区别对于避免意外修改数据至关重要,尤其是在处理对象和数组时。
核心概念
JavaScript 的数据类型分为原始类型和对象类型。
- 原始类型 (Primitives) : string, number, boolean, null, undefined, symbol, bigint. 赋值操作是按值传递 (pass by value) ,拷贝的是值的副本。
- 对象类型 (Objects) : object (包括普通对象 {}、数组 []、函数 function(){}、Date, RegExp 等)。赋值操作是按引用传递 (pass by reference) ,拷贝的是指向内存地址的指针。
拷贝操作的目标是创建一个新的对象或数组,而不是仅仅复制引用。
浅拷贝 (Shallow Copy)
定义:
浅拷贝创建一个新对象或数组,并将原始对象/数组的顶层属性/元素复制到新对象/数组中。
- 如果属性值是原始类型,则拷贝这个值。
- 如果属性值是对象类型(如嵌套的对象或数组),则拷贝这个引用(内存地址) 。
结果:
新对象/数组的顶层是独立的,但其内部嵌套的对象/数组仍然与原始对象/数组共享同一份数据。修改其中一个的嵌套对象/数组会影响到另一个。
实现方法:
-
Object.assign(target, ...sources) :
- 用于拷贝对象的可枚举自有属性。
- target 是目标对象,sources 是一个或多个源对象。
- 通常用法:const shallowCopy = Object.assign({}, originalObject);
-
展开语法 (Spread Syntax) ... :
- ES6+ 的现代语法,更简洁。
- 对象:const shallowCopy = { ...originalObject };
- 数组:const shallowCopy = [ ...originalArray ];
-
Array.prototype.slice() :
- 用于数组。slice() 不带参数时会返回原数组的浅拷贝。
- const shallowCopy = originalArray.slice();
-
Array.prototype.concat() :
- 用于数组。concat() 不带参数或与空数组合并时返回原数组的浅拷贝。
- const shallowCopy = originalArray.concat();
- const shallowCopy = [].concat(originalArray);
-
Array.from() :
- 用于类数组对象或可迭代对象(包括数组)创建新数组。
- const shallowCopy = Array.from(originalArray);
示例:
const original = {
a: 1,
b: 'hello',
c: {
d: 2
},
e: [10, 20]
};
// 使用展开语法进行浅拷贝
const shallowCopy = { ...original };
console.log(original === shallowCopy); // false (顶层是新对象)
console.log(original.c === shallowCopy.c); // true (嵌套对象引用相同)
console.log(original.e === shallowCopy.e); // true (嵌套数组引用相同)
// 修改浅拷贝的顶层原始类型属性
shallowCopy.a = 100;
console.log(original.a); // 1 (原始对象不受影响)
// 修改浅拷贝的嵌套对象属性
shallowCopy.c.d = 200;
console.log(original.c.d); // 200 (原始对象受到影响!)
// 修改浅拷贝的嵌套数组元素
shallowCopy.e.push(30);
console.log(original.e); // [10, 20, 30] (原始对象受到影响!)
适用场景:
- 当对象/数组只有一层结构,或者你明确知道不需要独立的嵌套结构时。
- 在某些状态管理(如 React/Vue)中,只修改顶层属性以创建新状态对象,触发更新。
深拷贝 (Deep Copy)
定义:
深拷贝创建一个完全独立的新对象或数组。它不仅复制顶层属性/元素,还会递归地复制所有嵌套的对象和数组,确保新对象/数组及其所有内部结构都与原始对象/数组完全分离,不共享任何引用(除了像函数这种通常不拷贝或共享也可以的情况)。
结果:
修改新对象/数组(包括其嵌套结构)不会影响原始对象/数组,反之亦然。
实现方法:
-
JSON.parse(JSON.stringify(object)) :
-
原理: 将对象序列化成 JSON 字符串,然后再将字符串解析回新的 JavaScript 对象。
-
优点: 实现简单,浏览器原生支持。
-
缺点 (非常重要):
- 会忽略 undefined、Symbol 属性。
- 不能序列化函数 (function),会直接忽略。
- 不能正确处理 Date 对象(会转换成 ISO 格式的字符串)。
- 不能处理 RegExp、Error 对象(会转换成空对象 {})。
- 不能处理 Map, Set 等 ES6+ 的数据结构。
- 不能处理循环引用(对象内部属性直接或间接引用自身),会抛出 TypeError。
- 会丢失对象的原型链。
- 只拷贝对象自身的可枚举属性。
-
示例:
const original = { a: 1, b: new Date(), c: function() { console.log('hello'); }, d: undefined, e: Symbol('id'), f: /regex/i, g: new Map([['key', 'value']]), h: { nested: 1 } }; // let circular = { x: 1 }; // circular.y = circular; // 循环引用 try { const deepCopyJSON = JSON.parse(JSON.stringify(original)); console.log(deepCopyJSON); // 输出: { a: 1, b: "2023-10-27T...", f: {}, h: { nested: 1 }, g: {} } // 丢失了 c, d, e, g被转为空对象, f被转为空对象, b变成字符串 console.log(original.h === deepCopyJSON.h); // false (嵌套对象是独立的) deepCopyJSON.h.nested = 2; console.log(original.h.nested); // 1 (原始对象不受影响) // const deepCopyCircular = JSON.parse(JSON.stringify(circular)); // 这里会报错 TypeError } catch (error) { console.error("JSON deep copy failed:", error); }
-
-
structuredClone() (现代方法) :
-
原理: 使用 HTML 规范定义的“结构化克隆算法”,与 postMessage, IndexedDB, History API 等使用的算法相同。
-
优点:
- 浏览器和 Node.js v17+ 内置 API,无需库。
- 支持多种 JavaScript 类型,包括 Date, RegExp, Map, Set, ArrayBuffer, Blob, File, ImageData 等等。
- 支持循环引用。
- 比 JSON.parse(JSON.stringify()) 更强大、更可靠。
- 保留原型链(部分情况)。
-
缺点:
- 不能克隆函数 (function),会抛出 DataCloneError。
- 不能克隆 DOM 节点,会抛出 DataCloneError。
- 不能克隆 Error 对象。
- 不保留属性描述符(writable, enumerable, configurable)、getter 和 setter。
- 相对较新,需要注意环境兼容性(查看 MDN)。
-
示例:
const original = { a: 1, b: new Date(), // c: function() { console.log('hello'); }, // 会报错 d: undefined, // structuredClone 可以处理 undefined e: Symbol('id'), // structuredClone 可以处理 Symbol f: /regex/i, g: new Map([['key', 'value']]), h: { nested: 1 } }; let circular = { x: 1 }; circular.y = circular; // 循环引用 original.i = circular; try { const deepCopyStructured = structuredClone(original); console.log(deepCopyStructured); // 输出类似: { a: 1, b: Date{...}, d: undefined, e: Symbol(id), f: /regex/i, g: Map(1){'key' => 'value'}, h: { nested: 1 }, i: { x: 1, y: [Circular] } } console.log(original.h === deepCopyStructured.h); // false console.log(original.g === deepCopyStructured.g); // false console.log(original.i === deepCopyStructured.i); // false console.log(deepCopyStructured.i.y === deepCopyStructured.i); // true (循环引用被正确复制) deepCopyStructured.h.nested = 2; console.log(original.h.nested); // 1 (原始对象不受影响) deepCopyStructured.g.set('key', 'newValue'); console.log(original.g.get('key')); // 'value' (原始对象不受影响) } catch (error) { console.error("structuredClone failed:", error); // 如果包含函数会在这里捕获 }
-
-
递归手动实现:
-
原理: 编写一个递归函数,遍历对象/数组的每个属性/元素。如果是原始类型,直接返回值;如果是对象/数组,创建一个新的空对象/数组,并递归调用深拷贝函数来复制其内部属性/元素。
-
优点: 完全可控,可以根据需要处理特定类型(如函数、DOM 节点等),可以处理循环引用(需要使用 Map 或 WeakMap 来记录已拷贝的对象,防止无限递归)。
-
缺点: 实现复杂,容易出错,需要考虑各种边界情况(null、原型链、特殊对象类型、循环引用等)。
-
简化示例 (未处理循环引用和所有类型):
function simpleDeepClone(obj) { if (typeof obj !== 'object' || obj === null) { return obj; // 基本类型或 null 直接返回 } let clone; if (Array.isArray(obj)) { clone = []; } else { clone = {}; } for (const key in obj) { // 只拷贝对象自身的属性 if (Object.prototype.hasOwnProperty.call(obj, key)) { clone[key] = simpleDeepClone(obj[key]); // 递归调用 } } return clone; } const original = { a: 1, b: { c: 2 } }; const deepCopyManual = simpleDeepClone(original); console.log(original.b === deepCopyManual.b); // false deepCopyManual.b.c = 3; console.log(original.b.c); // 2注意: 这是一个非常基础的实现,生产环境需要更完善的版本来处理循环引用、Date、RegExp、Map、Set 等。
-
-
使用第三方库 (如 Lodash) :
-
原理: 这些库提供了经过充分测试、功能完善的深拷贝函数。
-
优点: 功能强大,稳定可靠,处理各种边缘情况,通常性能较好。
-
缺点: 需要引入额外的库依赖。
-
示例 (Lodash):
// 需要先安装 lodash: npm install lodash 或 yarn add lodash // 或者在 HTML 中引入 lodash CDN // import _ from 'lodash'; // 在模块环境中使用 const original = { a: 1, b: new Date(), c: function() { console.log('hello'); }, d: undefined, e: Symbol('id'), f: /regex/i, g: new Map([['key', 'value']]), h: { nested: 1 } }; let circular = { x: 1 }; circular.y = circular; original.i = circular; const deepCopyLodash = _.cloneDeep(original); console.log(deepCopyLodash); // Lodash 会正确处理大部分类型和循环引用 console.log(original.h === deepCopyLodash.h); // false console.log(original.g === deepCopyLodash.g); // false console.log(original.i === deepCopyLodash.i); // false console.log(deepCopyLodash.i.y === deepCopyLodash.i); // true // Lodash 默认会复制函数引用,而不是创建新函数 console.log(original.c === deepCopyLodash.c); // true
-
总结与选择
| 特性 | 浅拷贝 | 深拷贝 (JSON) | 深拷贝 (structuredClone) | 深拷贝 (Lodash _.cloneDeep) | 深拷贝 (手动递归) |
|---|---|---|---|---|---|
| 目的 | 复制顶层,共享嵌套引用 | 创建完全独立的副本 (有限类型) | 创建完全独立的副本 (多数类型) | 创建完全独立的副本 (类型广泛) | 创建完全独立的副本 (自定义) |
| 嵌套对象/数组 | 共享引用 | 独立副本 | 独立副本 | 独立副本 | 独立副本 (需正确实现) |
| 性能 | 快 | 较快 (但序列化/反序列化有开销) | 较快 (原生优化) | 可能较慢 (功能全面) | 取决于实现复杂度 |
| 函数 | 复制引用 | 丢失 | 报错 (DataCloneError) | 复制引用 (默认) | 可自定义处理 |
| undefined | 复制值 | 丢失 | 保留 | 保留 | 保留 (需正确实现) |
| Symbol | 复制引用 | 丢失 | 保留 (作为 key 或 value) | 保留 | 可自定义处理 |
| Date | 复制引用 | 转为字符串 | 正确克隆 | 正确克隆 | 正确克隆 (需正确实现) |
| RegExp | 复制引用 | 转为 {} | 正确克隆 | 正确克隆 | 正确克隆 (需正确实现) |
| Map/Set | 复制引用 | 转为 {} | 正确克隆 | 正确克隆 | 正确克隆 (需正确实现) |
| 循环引用 | 复制引用 (可能导致问题) | 报错 (TypeError) | 支持 | 支持 | 支持 (需正确实现) |
| 原型链 | 丢失 (assign/spread 创建新对象) | 丢失 | 部分保留 | 保留 | 可自定义处理 |
| DOM 节点 | 复制引用 | 报错 | 报错 (DataCloneError) | 通常不处理或报错 | 可自定义处理 (复杂) |
| 实现复杂度 | 低 | 低 | 低 | 低 (使用库) | 高 |
| 依赖 | 无 | 无 | 无 (需环境支持) | 第三方库 | 无 |
选择建议:
-
需要完全独立、无副作用的副本,且数据结构复杂或包含特殊类型 (Date, RegExp, Map, Set, 循环引用)?
- 首选 structuredClone() (如果环境支持且不需克隆函数/DOM节点)。
- 其次使用 Lodash _.cloneDeep() (如果可以接受引入库)。
- 避免使用 JSON.parse(JSON.stringify()),除非你非常确定其限制对你的数据没有影响。
- 仅在有特殊需求且其他方法不满足时考虑手动实现。
-
只需要复制对象/数组顶层,或者嵌套部分可以共享?
- 使用浅拷贝方法 (Object.assign, ... 展开语法, slice, concat, Array.from)。这通常更快、更简单。