循序渐进深拷贝

js对象拷贝是面试官喜欢问的点,细细探究下来,深拷贝涉及了很多细节。

# 浅拷贝

拷贝一个对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,
所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

我们可以很容易实现一个浅copy:

function clone(obj) {
  let res = {}
  Object.keys(obj).forEach(k=>res[k]=obj[k])
  return res
}

或者使用

  function clone(obj) {
   return Object.assign({}, obj)
  }

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

# 深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

简单实现:

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj))
}

下面这个例子可以直观看出区别

let obj = {b: {c: {}};

let obj1 = clone(obj); // 浅拷贝
obj1.b.c === obj.b.c; // true

let obj2 = deepClone(obj); // 深拷贝
obj2.b.c === obj.b.c; // false

上面讨巧的JSON序列化的方法有这几个问题:

  1. date对象clone回来会变成字符串
  2. RegExp、Error对象clone回来会变成空对象
  3. 函数,undefined类型会丢失
  4. NaN、Infinity和-Infinity会变为null
  5. 循环引用会抛出错误
let obj = {
	num:1,
	date:new Date(),
	regex:new RegExp('\d'),
	err:new Error('err'),
	fn:new Function(),
	undf:undefined,
	nan:NaN,
	inf:Infinity,
	map:new Map(),
	wmap:new WeakMap(),
	arr:Array.from({length:3}).map(o=>1),
	obArr:[{a:1,b:{c:{}}},{b:[1,23]}],
	deep:{a:{b:{c:{d:{e:{f:{g:{}}}}}}}}
}
let res = JSON.parse(JSON.stringify(obj))
console.log(res)
/*
   res: {
        arr: (3) [1, 1, 1]
        date: "2021-04-08T07:32:10.996Z"
        err: {}
        inf: null
        map: {}
        nan: null
        num: 1
        regex: {}
        wmap: {}
        __proto__: Object
    }
 */
 // 循环引用
 obj.obj = obj
 JSON.parse(JSON.stringify(obj)) // Uncaught TypeError: Converting circular structure to JSON

对于复杂场景,我们需要一个更完善等deepClone;

# 循序渐进

我们按最简单浅copy版本一步一步完善。

# 基础版本

function deepClone(obj) {
    let res = {};
    Object.keys(obj).forEach(k=>res[k]=obj[k])
    return res;
}

这个版本实现了浅拷贝,对象多层嵌套会有问题。

# 考虑对象引用

const isType = type => terget=> `[object ${type}]`===Object.prototype.toString.call(terget)
const isObject = isType('Object')

function deepClone(obj) {
    if (isObject(obj)) {
        let res = {};
        Object.keys(obj).forEach(k=>{
            res[k] = deepClone(obj[k])
        })
        return res
    } else {
        return obj;
    }
}

let res = deepClone(obj)

/*
   res: {
        arr: (3) [1, 1, 1]
        date: Thu Apr 08 2021 16:54:36 GMT+0800 (China Standard Time) {}
        deep: {a: {…}}
        err: Error: err at snippet:///deepClone:5:10
        fn: ƒ anonymous( )
        inf: Infinity
        map: Map(0) {}
        nan: NaN
        num: 1
        regex: /d/
        obArr: (2) [{…}, {…}]
        undf: undefined
        wmap: WeakMap {}
    }
 */
// obj.obArr[0].b === res.obArr[0].b // true

使用递归实现了层级深入遍历,接下来我们加入数组判断

# 考虑数组

const isType = type => terget=> `[object ${type}]`===Object.prototype.toString.call(terget)
const isObject = isType('Object')
const isArray = isType('Array')

function deepClone(obj) {
    if (isObject(obj)||isArray(obj)) {
        let res = isArray?[]:{};
        Object.keys(obj).forEach(k=>{
            res[k] = deepClone(obj[k])
        })
        return res
    } else {
        return obj;
    }
}

let res = deepClone(obj)

/*
   res: {
        arr: (3) [1, 1, 1]
        date: Thu Apr 08 2021 16:54:36 GMT+0800 (China Standard Time) {}
        deep: {a: {…}}
        err: Error: err at snippet:///deepClone:5:10
        fn: ƒ anonymous( )
        inf: Infinity
        map: Map(0) {}
        nan: NaN
        num: 1
        regex: /d/
        obArr: (2) [{…}, {…}]
        undf: undefined
        wmap: WeakMap {}
    }
 */
// obj.obArr[0].b === res.obArr[0].b // false

// 循环引用会造成无限循环
obj.obj=obj
let res = deepClone(obj)
// Uncaught RangeError: Maximum call stack size exceeded

# 考虑循环引用

假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象。这叫做引用丢失。原对象引用关系与复制后不一致,这当然也是不对的。

我们这样解决,将需要复制的对象开辟一片空间存起来,复制时判断是否已经复制过,复制过直接取用,否则继续。循环引用也如此解决。我们使用map存储,map的key可以是任意类型,而且key的比较是基于 sameValueZero (opens new window) 算法。 存取性能也都非常好。

const isType = type => terget=> `[object ${type}]`===Object.prototype.toString.call(terget)
const isObject = isType('Object')
const isArray = isType('Array')

function deepClone(obj,map=new Map()) {
    if (isObject(obj)||isArray(obj)) {
        let res = isArray?[]:{};
        if(map.get(obj)){
        	return map.get(obj)
        }
        map.set(obj,res)
        Object.keys(obj).forEach(k=>{
            res[k] = deepClone(obj[k],map)
        })
        return res
    } else {
        return obj;
    }
}
obj.obj=obj
let res = deepClone(obj)
// res.obj.obj.obj === obj // true

# 考虑其他类型

以上我们仅仅处理了object和array两种引用类型,实际上还有其他很多类型需要考虑。

# null 和 function

null直接返回,function也可以直接返回,因为克隆函数是没有实际应用场景。如果非要clone函数,还得区分普通函数与箭头函数。可以prototype来区分下箭头函数和普通函数,箭头函数是没有prototype

let x = {a:()=>{},b:function(){}}
console.log(x)

/*
   x: {
        a: ()=>{}
            arguments: (...)
            caller: (...)
            length: 0
            name: "a"
            __proto__: ƒ ()
            [[FunctionLocation]]: VM2310:1
            [[Scopes]]: Scopes[2]
        b: ƒ ()
            arguments: null
            caller: null
            length: 0
            name: "b"
            prototype: {constructor: ƒ}
            __proto__: ƒ ()
            [[FunctionLocation]]: VM2310:1
            [[Scopes]]: Scopes[2]
            __proto__: Object
     }
* 
* 
* 
* */

函数复制方法


let fun = {
    a: (v)=>{
        console.log(v)
    }
    ,
    b: function(v) {
        console.log(v + v)
    }
}

//  很多函数库都是用这个方法
let cloneFn = (fn) =>{
	return new Function('return ' + fn.toString())()
}

let a1 = cloneFn(fun.a)
let b1 = cloneFn(fun.b)

// 这样好像不需要区分普通函数和剪头函数了

# Bool、Number、String、Date、Error

这几种类型我们都可以直接用构造函数和原始数据创建一个新对象

克隆Symbol类型

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

// 克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

综上已实现一个较为完备的深拷贝,还有其他很多类型处理都可以按部就班if分支😄。

# 相关资料

lodash baseClone (opens new window)

最近更新
01
echarts扇形模拟镜头焦距与可视化角度示意图
03-10
02
vite插件钩子
03-02
03
vite的依赖预构建
02-13
更多文章>