对象的浅合并与深合并、浅拷贝与深拷贝

2,095 阅读5分钟

在学习对js的array、number、objects、string的操作时,我们可以对比loadash工具库的实现,在实际开发过程中进行使用。

在开始之前我们先明确一个概念,js的栈与堆。栈与堆都是js的数据结构,栈的空间较小,用来存储基础类型的数据,例如string,number;堆空间较大,用来存储大的数据,例如function、object等。

在执行js的时候,会先创建一个上下文,这里就是一个栈结构

    var a = 1
    var b = {}
    // 上下文环境存储的是 
    // 键 a : 值 1
    // 键 b : 值 对象的引用地址

一、浅合并

浅合并通俗来理解就是只合并第一层,不对相同属性做递归合并处理,而是直接替换,比如以下示例

let obj1 = {
    a: 'a1',
    b: 'b1',
    c: {
        d: 'c1',
        e: 'c2'
    }
}
let obj2 = {
    a: 'a2',
    c: {
        d: 'c3'
    }
}
console.log(shallowMerge(obj1, obj2))
// 输出
{
    a: 'a2',
    b: 'b1',
    c: {
        d: 'c3'
    }
}

实现浅合并思想如下:

  • 边界值判定,如果object1不是对象,使用object2替换object1
  • 如果object1是对象,object2不是对象,返回object1
  • 如果object1是对象且object2是对象,遍历object2进行合并,遇到相同属性直接进行替换

js已经替我们实现了Object.assign,但是会有一些边界值问题,我们可以进行自定义实现

// 是否是引用类型,具体指使用typeof类型为object,并且不是null的值
function isObjectLike(value) {
 return typeof value === 'object' && value !== null
}

// 使用Object.prototype.toString获取表示该对象的字符串,例如[object Array]
// 在es5之后,toString方法已经可以返回正确的类型,null对应[object Null]
const toString = Object.prototype.toString
function getTag(value) {
 return toString.call(value)
}
// 判断是否是普通对象,typeof性能会更好
function isPlainObject(value) {
 if (!isObjectLike(value) || getTag(value) != '[object Object]') {
   return false
 }
 // 例如:Object.create(null)
 if (Object.getPrototypeOf(value) === null) {
   return true
 }
 // 循环遍历对象,如果是自定义构造器实例化的object则返回false
 let proto = value
 while (Object.getPrototypeOf(proto) !== null) {
   proto = Object.getPrototypeOf(proto)
 }
 return Object.getPrototypeOf(value) === proto
}
// 浅合并
 shallowMerge (obj1, obj2) {
     let isPlain1 = isPlainObject(obj1)
     let isPlain2 = isPlainObject(obj2)
    // 1.边界值判定,如果object1不是对象,使用object2替换object1
    if(!isPlain1){
        return obj2
    }
    // 2.如果object1是对象,object2不是对象,返回object1
    if(!isPlain2){
        return obj1
    }
    // 3.如果object1是对象且object2是对象,遍历object2进行合并
    // Object.keys 获取可枚举普通键
    // Object.getOwnPropertyNames 获取除symbol以外的所有键
    // Object.getOwnPropertySymbols 获取symbol键
    // 根据Object.assign定义,它会合并Object.keys与Object.getOwnPropertySymbols的所有值
    [...Object.keys(obj2),...Object.getOwnPropertySymbols(obj2)].forEach((key)=>{
        obj1[key] = obj2[key]
    })
    return obj1
}

二、深合并

深合并也就是要进行递归合并,将对象的所有子属性也进行合并,代码演示如下:

 let obj1 = {
     a: 'a1',
     b: 'b1',
     c: {
         d: 'c1',
         e: 'c2'
     }
 }
 let obj2 = {
     a: 'a2',
     c: {
         d: 'c3'
     }
 }
 console.log(deepMerge(obj1, obj2))
 // 输出
 {
     a: 'a2',
     b: 'b1',
     c: {
         d: 'c3',
         e: 'c2'
     }
 }

实现深合并思想如下:

  • 边界值判定,如果object1不是对象,使用object2替换object1
  • 如果object1是对象,object2不是对象,返回object1
  • 如果object1是对象且object2是对象,遍历object2进行合并,遇到键相同的普通对象值,递归合并,其他直接进行替换 与浅拷贝唯一的区别就是赋值的时候判断一下值类型,进行递归调用
deepMerge (obj1, obj2) {
    let isPlain1 = isPlainObject(obj1)
    let isPlain2 = isPlainObject(obj2)
    if(!isPlain1){
        return obj2
    }
    if(!isPlain2){
        return obj1
    }
    [...Object.keys(obj2),...Object.getOwnPropertySymbols(obj2)].forEach((key)=>{
            //与浅拷贝区别之处
            obj1[key] = deepMerge(obj1[key],obj2[key])
        })
        return obj1
    }

三、浅拷贝

浅拷贝也就是只拷贝一层,对于非基础类型直接引用,其中任一对象属性的改变会影响到另一对象,示例如下:

let obj1 = {
    a: 'a1',
    c: {
        d: 'c1'
    }
}
let obj2 = shallowClone(obj1)
obj1.a = 'a2'
obj1.c.d = 'c2'
console.log(obj2)
// 输出
{
    a: 'a1',
    c: {
        d: 'c2'
    }
}

浅拷贝实现思想

  • 对obj1进行遍历,然后将基础类型值直接赋值给obj2,将引用类型的引用地址赋值给obj2
shallowClone(obj){
    let result = {}
    // for...in以任意顺序遍历一个对象的除[Symbol]以外的[可枚举]属性,包括继承的可枚举属性
    for (const key in obj){
        // hasOwnProperty会返回一个布尔值,指示对象自身属性中是否具有指定的属性
        if(obj.hasOwnProperty(key)){
           result[key] = obj[key] 
        }
    }
    return result
}
    

三、深拷贝

深拷贝就是对对象所有属性进行递归遍历,且拷贝对象之间的值不会有相互影响。 示例如下

let obj1 = {
     a: 'a1',
     c: {
         d: 'c1'
     }
 }
 let obj2 = deepClone(obj1)
 obj1.a = 'a2'
 obj1.c.d = 'c2'
 console.log(obj2)
 // 输出
 {
     a: 'a1',
     c: {
         d: 'c1'
     }
 }

最简单的深拷贝使用Object.parse(Object.stringfy()),缺点是不能处理函数与正则,拷贝出来的值会变成null与空对象。

深拷贝实现思想

  • 要对子元素的类型先进行分类
  • 如果是基础类型则直接赋值
  • 如果是引用类型则根据不同情况做处理
  • 对于普通对象做递归拷贝
// 将所有类型罗列
// 基础类型
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const numberTag = '[object Number]'
const boolTag = '[object Boolean]'
const nullTag = '[object Null]'
const undefinedTag = '[object Undefined]'

// 内置对象
const argsTag = '[object Arguments]'
const arrayTag = '[object Array]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const weakMapTag = '[object WeakMap]'
...

function deepClone(obj, hash = new WeakMap()) { 
    // 防止循环引用,缓存引用地址,如果出现过则直接返回结果
    if (hash.get(obj)) return hash.get(obj);
    
    // 基础类型与function直接返回值,function的拷贝没有考虑this指向问题
    if(typeof obj !== 'object' || obj === null){
       return obj 
    }
    // 对日期和正则做特殊处理,如果数据复杂,可以针对需要支持的内置对象做响应处理
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    
    // 对其他引用对象做处理
    let cloneObj = new obj.constructor() 
    hash.set(obj, cloneObj);
    for (let key in obj) { 
        if (obj.hasOwnProperty(key)) { 
            // 实现一个递归拷贝 
            cloneObj[key] = deepClone(obj[key])
        }
    } 
    return cloneObj; 
 }

没有尽善尽美的深拷贝,知晓其支持程度及相应优缺点用来支持业务功能。