完整的实现一个javascript深拷贝

2019 / 11 / 14

获取类型

常见的获取类型可以通过inctanceof和typeof来判断,但是他们都有缺陷,比如typeof判断null会返回object,instanceof相对准确一些,但是面对它主要是用来判断实例的归属关系。

最好的方法是Object.prototype.toString.call,这个方法拿到一个字符串。比如

数组 [object Array]

对象 [object Object]

函数 [object Function]

正则 [object RegExp]

等等,这个方法用来判断js的内置对象最好不过了,slice一下之后就可以拿到类型的字符串了。

function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1)
}

拷贝基本类型

function deepClone(source) {
const simpleType = ['String', 'Number', 'Boolean', 'Null', 'Undefined']

if (simpleType.includes(getType(source))) {
return source
}

// 非简单类型
}

简单类型拷贝直接返回原始值就行,不需要额外处理

拷贝引用类型

拷贝引用类型时需要针对不同的类型做不同的处理,最常见的Js的内置类型是Object, Array,但是其他的类型Function, Map, Set, RegExp, Date, Error, Symbol的拷贝方法和Object, Array完全不同,所以都需要额外处理。

另外为了解决循环引用,我们引入了WeakMap。

const memo = new WeakMap()

function deepClone(source) {
const simpleType = ['String', 'Number', 'Boolean', 'Null', 'Undefined']

if (simpleType.includes(getType(source))) {
return source
}

if (memo.get(source)) {
return memo.get(source)
}

switch (getType(source)) {
case 'Array':
return cloneArray(source)

case 'Object':
return cloneObject(source)

case 'RegExp':
return cloneRegExp(source)

case 'Function':
return cloneFunction(source)

case 'Set':
return cloneSet(source)

case 'Map':
return cloneMap(source)

case 'Symbol':
return cloneSymbol(source)

case 'Error':
case 'Date':
return new source.constructor(source)

default:
return null
}
}

数组和对象的拷贝方式很简单,遍历就好了。注意,必须在遍历之前就把数据放入weakMap中。

拷贝对象的时候,需要用Object.create通过source的原型创建,否则原型可能就丢失了。

function cloneArray(array) {
let result = []
memo.set(array, result)
for (const key in array) {
result[key] = deepClone(array[key])
}
return result
}

function cloneObject(object) {
let result = Object.create(Object.getPrototypeOf(object))
memo.set(object, result)
for (const key in object) {
result[key] = deepClone(object[key])
}
return result
}

拷贝函数,函数需要拿到函数的字符串然后静态解析,拿到参数名,函数名和函数体,最后再通过new Function的方式生成一个新的函数。

function cloneFunction(fn) {
const fnString = fn.toString()
const matchedParams = fnString.match(/(?<=\()(.+)?(?=\) \{)/)
const matchedBody = fnString.match(/(?<=\{)(.|\n)+?(?=})/m)
const body = matchedBody ? matchedBody[0] : ''
const params = matchedParams ? matchedParams[0].split(',').map(item => item.trim()) : ''
const result = new Function(...[...params, body])

memo.set(fn, result)

for (const key in fn) {
result[key] = deepClone(fn[key])
}

return result
}

其他的数据类型的拷贝方法

function cloneRegExp(reg: RegExp) {
const result = new RegExp(reg.source, reg.flags)
result.lastIndex = reg.lastIndex
return result
}

function cloneSet(set) {
const result = new Set()
memo.set(set, result)
set.forEach(item => result.add(deepClone(item)))
return result
}

function cloneMap(map) {
const result = new Map()
memo.set(map, result)
map.forEach((value, key) => result.set(key, deepClone(value)))
return result
}

function cloneSymbol(symbol) {
return Object(Symbol.prototype.valueOf.call(symbol));
}

Date和Error这样的数据类型可以通过constructor重新构造的方法生产一个新的对象。

new source.constructor(source)

javascript的内置对象很多,每个拷贝方法都不同,上边这个是lodash的实现方式,然后进行了一些代码的调整,lodash会比这个更全面一些,除了这些类型之外,还有Buffer等其他的数据类型,这里不做展开。

如果能按照上边的代码实现一遍,一定会收获非常多的,能很清楚的理解js的各种不同的数据结构的特质,也能对原型,函数等进行一定的理解,是一个非常基础而且实用的知识点。

嗨,请先 登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟