类型判断、深浅克隆
参考
- JavaScript 数据类型和数据结构 - MDN
- typeof - MDN
- instanceof - MDN
数据类型
- 基本数据类型(栈内存):String、Number、Boolean、Null、Undefined、Symbol、BigInt。
- 引用数据类型(堆内存):Object(包括 Array、Function、Date、RegExp 等)。
类型判断
typeof
结果:
- undefined、boolean、number、string、symbol、bigint
- object、function
const d = typeof new Date()
console.log(d)
执行结果
object
instanceof
结果: true false
object instanceof constructor
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 方法(返回对象的具体类型)。
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 1 : number
getType 1 : [object Number]
typeof new Number(1) : object
getType new Number(1) : [object Number]
function getType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase()
}
浅拷贝
只拷贝对象的第一层属性,如果属性是引用类型,拷贝的只是内存地址。
在 JavaScript 中,存在浅拷贝的现象有:
- Object.assign - MDN
- 使用 扩展运算符(…) 实现的复制
- Array.prototype.slice(), Array.prototype.concat()
深拷贝
层层拷贝,不仅拷贝值,还会在堆内存中开辟新的空间,修改新对象不会影响原对象。
常见的深拷贝方式有:
- JSON.parse(JSON.stringify(obj)) (会忽略 undefined、symbol、function、Map、Set,循环引用)
- lodash 的 _.cloneDeep
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() 可以顺带把原型链也继承过来。