深浅拷贝,以及JS如何判断数据类型

1. 最基础的方法:typeof

特点:简单,适合判断基础数据类型。

  • 适用:Number, String, Boolean, Symbol, Undefined, Function。
  • 局限性(面试必考点):
    • null 的陷阱:typeof null 会返回 'object'(这是 JS 的历史遗留 Bug)。
    • 引用类型无法区分:除了 Function 外,其他的引用类型(Array, Date, RegExp, Error)都会统统返回 'object',无法具体区分。
js
12345678
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object' (注意!)
typeof [] // 'object'
typeof {} // 'object'

2. 基于原型链的方法:instanceof

特点:适合判断引用数据类型(Object),检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

  • 适用:Array, Date, RegExp(正则) 等对象类型。
  • 局限性:
    • 不能判断基础类型:比如 1 instanceof Number 为 false。
    • 原型链可被修改:如果手动修改了原型指向,结果可能不准确。
    • 多窗口/iframe 问题:在不同 iframe 中,全局对象不同,[] instanceof Array 可能会失效。
js
123
[] instanceof Array // true
{} instanceof Object // true
new Date() instanceof Date // true

3. 构造函数属性:constructor

特点:查看对象的构造函数。

  • 适用:大部分类型。
  • 局限性:非常不安全。因为 constructor 属性是可以被随意修改的,而且 null和 undefined 没有 constructor。一般不推荐作为首选方案。
JavaScript
1
[].constructor === Array // true

4. 终极全能方案:Object.prototype.toString.call()

特点:这是最准确、最通用的方法,也是面试中的“标准答案”。

  • 原理:所有对象都继承自 Object,调用 Object 原型上的 toString 方法,会返回格式为 [object type] 的字符串。
  • 适用:所有类型(包括 null 和 undefined)。
js
12345678
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({}) // "[object Object]"

手写通用类型判断函数

js
123456789101112131415
function getType(value) {
  // 1. 处理 null 和 undefined (虽然 toString 也能处理,但这样更直观)
  if (value === null) return 'null';
  if (typeof value === 'undefined') return 'undefined';

  // 2. 使用 typeof 处理基础类型 (性能稍好)
  // 注意:Function 也是 'function',这里也可以直接返回
  if (typeof value !== 'object') {
      return typeof value;
  }

  // 3. 使用 Object.prototype.toString.call 处理复杂类型
  // slice(8, -1) 是为了去掉 "[object " 和 "]",只保留类型名称
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

深浅拷贝

第一,从原理上讲: 浅拷贝 (Shallow Copy) 就像是‘复制了钥匙’。对于引用类型,它只复制了指向堆内存中对象的地址(指针)。所以如果修改了新对象里的属性,原对象也会受到影响,因为它们指向的是同一个房间。 深拷贝 (Deep Copy) 则是‘建造了一个一模一样的新房子’。它会在堆内存中开辟新的空间,完整复制原对象的所有属性。新旧对象互不干扰。

第二,在日常开发中: 如果只是简单的对象,我会用 Object.assign() 或者 ES6 的展开运算符 ... 来做浅拷贝。 如果要深拷贝,最简单粗暴的方法是 JSON.parse(JSON.stringify()),但这只适用于纯数据对象。

第三,关于特殊情况: JSON 方法有局限性,比如它无法处理函数、正则、Date 对象,遇到循环引用还会报错。 所以在复杂的场景或者面试手写题中,我会采用递归的方式来实现深拷贝,并且需要通过 WeakMap 来解决循环引用的问题。”

手写实现深拷贝

js
1234567891011121314151617181920212223242526272829303132
function deepClone(obj, hash = new WeakMap()) {
    // 如果是null或者不是对象,则直接返回
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    // 如果是正则表达式,则直接返回
    if (obj instanceof RegExp) {
        return new RegExp(obj);
    }
    // 如果是日期对象,则直接返回
    if (obj instanceof Date) {
        return new Date(obj);
    }
    // 如果是函数,则直接返回
    if (typeof obj === 'function') {
        return obj;
    }
    //如果已经存在于hash中,直接返回克隆后的对象
    if (map.hash(obj)) {
        return hash.get(obj);
    }
    // 创建一个新的对象或数组
    const cloneObj = Array.isArray(obj) ? [] : {};
    // 将新对象存入hash中
    hash.set(obj, cloneObj);
    // 递归克隆
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
}