zhangdizhangdi

类型判断、深浅克隆

参考

数据类型

  • 基本数据类型(栈内存):String、Number、Boolean、Null、Undefined、Symbol、BigInt。
  • 引用数据类型(堆内存):Object(包括 Array、Function、Date、RegExp 等)。

类型判断

typeof

结果:

  • undefined、boolean、number、string、symbol、bigint
  • object、function
js
const d = typeof new Date()
console.log(d)
执行结果
object

instanceof

结果: true false

js
object instanceof constructor
js
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car("Honda", "Accord", 1998);

console.log(auto instanceof Car);
console.log(auto instanceof Object);
执行结果
true
true

Object.prototype.toString

toString是 Object 的原型方法,而 Array、Function 等类型作为 Object 的实例,都重写了 toString 方法。

不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(Function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型)。

js
const str = 'test'
const date = new Date()
const array = [1, 2, 3]

const getType = target => Object.prototype.toString.call(target)

console.log(getType(str))
console.log(getType(date))
console.log(getType(array))

const num = 1
const nnum = new Number(1)
console.log('typeof 1 :', typeof num)
console.log('getType 1 :', getType(num))
console.log('typeof new Number(1) :', typeof nnum)
console.log('getType new Number(1) :', getType(nnum))
执行结果
[object String]
[object Date]
[object Array]
typeof 1number
getType 1 : [object Number]
typeof new Number(1) : object
getType new Number(1) : [object Number]
js
function getType(target) {
  return Object.prototype.toString.call(target).slice(8, -1).toLowerCase()
}

浅拷贝

只拷贝对象的第一层属性,如果属性是引用类型,拷贝的只是内存地址。

在 JavaScript 中,存在浅拷贝的现象有:

  • Object.assign - MDN
  • 使用 扩展运算符(…) 实现的复制
  • Array.prototype.slice(), Array.prototype.concat()

深拷贝

层层拷贝,不仅拷贝值,还会在堆内存中开辟新的空间,修改新对象不会影响原对象。

常见的深拷贝方式有:

  1. JSON.parse(JSON.stringify(obj)) (会忽略 undefined、symbol、function、Map、Set,循环引用)
  2. lodash 的 _.cloneDeep
js
function deepClone(target, map = new WeakMap()) {
  // 基本数据类型、Function,直接返回
  if (typeof target !== 'object' || target === null) return target

  // map标记每一个出现过的属性,避免循环引用
  if (map.get(target)) return map.get(target)

  // 获取对象类型
  const type = Object.prototype.toString.call(target).slice(8, -1).toLowerCase()

  // RegExp、Date,返回新的对象
  if (type === 'date') return new Date(target)
  if (type === 'regexp]') return new RegExp(target.source, target.flags)

  const cloneTarget = new target.constructor()
  map.set(target, cloneTarget)

  switch (type) {
    // Map
    case 'map':
      target.forEach((item, key) => {
        cloneTarget.set(deepClone(key, map), deepClone(item, map))
      })
      break
    // Set
    case 'set':
      target.forEach(item => {
        cloneTarget.add(deepClone(item, map))
      })
      break
    // Object、Array
    default:
      // 使用 Reflect.ownKeys() 可以获取普通键名以及 Symbol 类型的键名
      Reflect.ownKeys(target).forEach(key => {
        cloneTarget[key] = deepClone(target[key], map)
      })
      break
  }

  return cloneTarget
}

const sym = Symbol('key')

const obj = {
  a: 1,
  b: 'string',
  c: true,
  d: null,
  e: undefined,
  f: function () {
    console.log('I am function')
  },
  g: new Date(),
  h: new RegExp('\\w+', 'g'),
  i: new Map([['mapKey', 'mapValue']]),
  j: new Set([1, 2, 3]),
  [sym]: 'symbol value',
}

// 制造循环引用
obj.loop = obj

const clonedObj = deepClone(obj)

console.log('原对象:', obj)
console.log('克隆对象:', clonedObj)
console.log('Map 是否被深拷贝:', clonedObj.i === obj.i) // false,说明 Map 被深拷贝了
console.log('循环引用是否成功:', clonedObj.loop === clonedObj) // true,完美解决循环引用
console.log('Symbol 键是否被克隆:', clonedObj[sym] === obj[sym]) // true,说明 Symbol 键也被克隆了

console.log('-----修改原对象的 Symbol 键值-----')
obj[sym] = 'new symbol value'
console.log('obj[sym] 📌', obj[sym])
console.log('clonedObj[sym] 📌', clonedObj[sym])
执行结果
原对象: [object Object]
克隆对象: [object Object]
Map 是否被深拷贝false
循环引用是否成功true
Symbol 键是否被克隆true
-----修改原对象的 Symbol 键值-----
obj[sym] 📌 new symbol value
clonedObj[sym] 📌 symbol value
1. 为什么要用 WeakMap 而不是 Map 解决循环引用?

✅ 主要是为了防止内存泄漏。
WeakMap 的键是弱引用(只接受对象作为键)。
当原本的对象(即 target)被销毁时,WeakMap 中对应的键值对会自动被垃圾回收机制(GC)清理掉;
如果用 Map,即使外界不再引用该对象,因为 Map 里还存着强引用,导致它永远无法被回收,久而久之造成内存泄漏。

2. 为什么用 Reflect.ownKeys() 而不是 for…in 或 Object.keys()?

✅ for…in 会遍历原型链上的属性,通常我们深拷贝只拷自身属性即可;
Object.keys() 只能获取可枚举的普通字符串属性,无法获取 Symbol 类型的键。
Reflect.ownKeys() 相当于 Object.getOwnPropertyNames() + Object.getOwnPropertySymbols(),是最全面的。

3. 为什么代码里保留了函数 (typeof target !== ‘object’) 直接返回,而不对函数进行克隆?

✅ 在日常开发中,克隆函数是没有意义的(两个对象共享同一个函数引用完全没问题)。
如果要强行克隆函数,通常会用到 new Function(target.toString()) 或 eval,但这样做有两个致命缺点:

  • 会丢失函数闭包的作用域(Lexical Scope)。
  • 会引发类似 XSS 的安全问题。所以业界(包括 lodash 的 cloneDeep)默认不对函数做深拷贝。
4. new target.constructor() 这句话精妙在哪里?

✅ 如果单纯用 target instanceof Array ? [] : {},虽然能区分数组和对象,但如果源对象是一个自定义类的实例(比如 class Person {}),克隆出来的对象就会丢失它身上的原型,变成一个普通的 {}。使用 target.constructor() 可以顺带把原型链也继承过来。