关于JavaScript深拷贝的一些理解
JavaScript 中的深拷贝
什么是深拷贝?
JavaScript中存在三种可执行代码,global,function,eval。可执行代码的执行依赖于执行上下文。我们可以抽象的将执行上下文理解为一个对象。这个对象上会包含一些属性,如variable object(变量对象),this value(this指针),scope chain(作用域链)。
JavaScript中的基础数据类型都是存储在变量对象中。JavaScript*有5种基础数据类型,Undefined、Null、Boolean、Number、String。我们可以直接操作存储在变量对象中的,基础数据类型。
JavaScript中的引用数据类型的值,是保存在堆内存中的。我们不能直接操作堆内存空间。我们通过操作variable object中的数据的引用,访问修改对象。这里的应用是堆内存空间中的地址。
当我们使用 var a = b赋值的时候,如果b是引用类型,我们赋于a的只是内存空间的地址。
这就是产生了一个问题,我们该如何复制引用类型的对象。
常见的深拷贝的方式
JSON.stringify 和 JSON.parse
最简单的方式将对象使用JSON.stringify将对象序列化为JSON格式的字符串,然后使用JSON.parse将JSON字符串反序列化为对象。
for in 配合 递归
另外一种方式是使用for in循环,配合递归
深拷贝中遇到的问题
到这里,问题解决了吗?并没有,上面方式有以下的问题
- JSON以及for…in无法对Date,正则,Set,Map等数据结构做出正确的处理
- 没有对Symbol的属性做出正确的处理。
- for…in没有对对象中可能存在的循环引用做出正确的处理。
如何解决这些问题?
对于上面问题的解决, 我参考了lodash的源码,以及ramda的源码。lodash的源码中涉及到非常多的边界条件的处理,可读性相对差一些。我在此基础上做出了一定的简化,具体代码在文末。
Date的的处理
当我们检测到value的为[object Date]类型的时候,我们通过Date实例的constructor属性,获取实例的构造函数。Date的构造函数可以接收一个Date的实例作为参数。从而创建一个新的Date实例。
Map,Set的处理
和Date对象同理,使用Map或者Set实例的constructor属性,创建新的实例后,通过forEach对Map和Set进行遍历,重新设置Map以及Set的内容。
正则的处理
- source,正则对象的源模式文本
- global, global属性表明正则表达式是否使用了"g"标志
- ignoreCase, ignoreCase属性表明正则表达式是否使用了"i"标志
- multiline, multiline属性表明正则表达式是否使用了"m"标志
- sticky, sticky属性表明正则表达式是否使用了"y"标志
- unicode, unicode属性表明正则表达式带有"u"标
循环引用的处理
当出现循环引用的时候,如果不进行判断,使用递归。会爆栈
WeakMap支持使用引用类型作为key,我们可以利用这个特性,将对象的每一个引用类型的属性作为key,存储在WeakMap中。当同样的key再次出现时,可以证明发生了循环引用。直接返回结果。
Symbol属性的处理
Symbols类型的属性无法使用Object.keys获取,可以通过Object.getOwnPropertySymbols方法获取对象上的Symbol类型的属性。
源码:
// 使用WeakMap判断是否形成了环,避免爆栈
const hash = new WeakMap()
function hasHash (value) {
return hash.has(value)
}
function setHash (value, result) {
hash.set(value, result)
}
function getHash (value) {
return hash.get(value)
}
function getSymbols (value) {
let symKeys = []
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
const nativeGetSymbols = Object.getOwnPropertySymbols
symKeys = nativeGetSymbols(value)
// 判断是否是可枚举的属性
symKeys = symKeys.filter(symkey => propertyIsEnumerable.call(value, symkey))
return symKeys
}
function getAllKeys (value) {
let keys = Object.keys(value)
keys = [...keys, ...getSymbols(value)]
return keys
}
const types = {
'[object Array]': true,
'[object Boolean]': true,
'[object Date]': true,
'[object Map]': true,
'[object Set]': true,
'[object Number]': true,
'[object Object]': true,
'[object RegExp]': true,
'[object Symbol]': true,
'[object String]': true,
'[object ArrayBuffer]': true,
'[object Function]': true,
'[object WeakMap]': false,
'[object Error]': false
}
function isObject (value) {
const type = typeof value;
return value != null && (type === 'object' || type === 'function')
}
function getType (value) {
return Object.prototype.toString.call(value);
}
function initCloneArray (value) {
const { length } = value
return new value.constructor(length)
}
function initCloneArrayBuffer (value) {
const result = new value.constructor(value.byteLength)
new Uint8Array(result).set(new Uint8Array(value))
return result
}
function initCloneObject (value) {
return Object.create(Object.getPrototypeOf(value))
}
function initCloneRegExp (value) {
return new RegExp(value.source,
(value.global ? 'g' : '') +
(value.ignoreCase ? 'i' : '') +
(value.multiline ? 'm' : '') +
(value.sticky ? 'y' : '') +
(value.unicode ? 'u' : '')
)
}
function initCloneFunction (value) {
return function (...args) {
return value.apply(null, ...args)
}
}
function initClone (value, type) {
const Ctor = value.constructor
switch (type) {
case '[object ArrayBuffer]':
return initCloneArrayBuffer(value)
case '[object Date]':
case '[object Map]':
case '[object Set]':
return new Ctor(value)
case '[object RegExp]':
return initCloneRegExp(value)
case '[object Array]':
return initCloneArray(value)
case '[object Object]':
return initCloneObject(value)
case '[object Function]':
return initCloneFunction(value)
}
}
export default function deepClone (value) {
let result;
const type = getType(value)
// 如果不是引用类型直接返回
if (!isObject(value)) {
return value
}
// 如果是weakMap,Error类型直接返回
if (!types[type]) {
return value
}
let isArr = Array.isArray(value)
result = initClone(value, type)
// 判断是否产生了循环引用, 避免爆栈
if (hasHash(value)) {
// 如果存在直接返回
return getHash(value)
}
// 在weakMap添加标记
setHash(value, result)
if (type === '[object Map]') {
value.forEach((val, key) => {
result.set(key, deepClone(val))
})
return result
}
if (type === '[object Set]') {
value.forEach((val, key) => {
result.add(deepClone(val))
})
return result
}
const props = getAllKeys(value)
for (let i = 0; i < props.length; i++) {
const key = props[i]
const val = value[key]
result[key] = deepClone(val)
}
return result
}
上一篇: Java-通过URL来调用WebAPI