深浅拷贝和extend实现

610 阅读4分钟

前言

深浅拷贝和 extend 是项目中常用的工具类函数,今天就动手实现一下

下面会重复这几个基础函数,为了简洁下面不会出现相关定义

function isObject(obj) {
  return obj && typeof obj === "object";
}
function type(obj) {
  return Object.prototype.toString
    .call(obj)
    .replace(/\[object.(.+?)\]/, "$1")
    .toLowerCase();
}
function array(par) {
  return type(par) === "array";
}

浅拷贝用法

var obj = { name: "test", args: [{ name: 1 }] };
var test = { ...obj };
// 修改下原来属性
obj.name = "foo";
// test.name test
obj.args.push("456");
// args: (2) [{…}, "456"]

从上面例子可以看到,当两个对象出现相同字段的时候,后者会覆盖前者,而不会进行深层次的覆盖。

由此可以得到一个结论:浅拷贝可以简单理解为只拷贝对象的一层属性,如果拷贝的属性还是对象,那么修改它则会影响到拥有相同属性的对象。

浅拷贝实现

结合上面的结论,我们动手实现一个函数

export function copy(obj) {
  if (!isObject(obj)) {
    return;
  }
  const src = array(obj) ? [] : {};
  for (const key in obj) {
    const value = obj[key];
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      src[key] = value;
    }
  }
  return src;
}

测试用例

test("浅拷贝", () => {
  const value = { a: 123, b: 456, c: [1] };
  const test = copy(value);
  expect(test).toEqual({ a: 123, b: 456, c: [1] });
  expect(value === test).toBeFalsy();
  expect(value.c === test.c).toBeTruthy();
  expect(value.a === test.a).toBeTruthy();
});

深拷贝

上面我们已经实现了浅拷贝,浅拷贝只是简单的拷贝了一层属性,如果是深拷贝呢?

其实就是对属性为对象进行重复的调用,在实现这个之前先看一个比较简单的做法。

json

function deepAssign(par) {
  return JSON.parse(JSON.stringify(par));
}
// 测试用例
const value = {a: {name: 456}};
const test = deepAssign({value);
test.a === value.a;
//false
deepAssign({ a() {}, b: /abc/, c: Math.floor, d: null, e: new Set() });
// {b: {}, d: null, e: {}}

需要特别注意用这个方法处理函数、正则之类的对象不会出现预期结果,不过在处理接口返回的数据不失为一种方法。

递归

相比JSON的方式实现,可以让我们自由定制一些类型,例如上面的正则就可以通过hack方法创建,为了简化源码,这里只处理对象和数组,对于其他类型的处理可以参考一下第三方库

function deepCopy(obj, has = new WeakMap()) {
  if (!isObject(obj)) {
    return;
  }
  // 避免循环引用
  if (has.has(obj)) {
    return has.get(obj);
  }
  const src = array(obj) ? [] : {};
  has.set(obj, src);
  for (const key in obj) {
    const value = obj[key];
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      // 注意这一行
      src[key] = isObject(value) ? copy(value, has) : value;
    }
  }
  return src;
}

测试用例

test("深拷贝", () => {
  const value = { a: 123, b: 456, c: [1] };
  const test = deepCopy(value);
  expect(test).toEqual({ a: 123, b: 456, c: [1] });
  expect(value === test).toBeFalsy();
  expect(value.c === test.c).toBeFalsy();
  expect(value.a === test.a).toBeTruthy();
});

注意上面用了一个WeakMap,这个是为了避免循环引入,你可以把上面的WeakMap相关代码去掉,然后在控制台输入下面这个例子,看看会出现什么结果。

var a = {};
a.a = a;
var test = deepCopy(a);

extend

extend 分为两部分:

  1. 浅合并,Object.assign就是浅拷贝;
  2. 深合并,没有相关的的 api,需要手动实现

浅合并使用方法

在实现这个方法之前,我们约定一下格式

assign( target [, object1 ] [, objectN ] )

第一个参数为目标对象必须是对象,之后的参数是目标对象,可以不为对象

var obj1 = {
  a: 1,
  b: { b1: 1, b2: 2 },
};

var obj2 = {
  b: { b1: 3, b3: 4 },
  c: 3,
};

var obj3 = {
  d: 4,
};

console.log(assign(obj1, obj2, obj3));

// {
//    a: 1,
//    b: { b1: 3, b3: 4 },
//    c: 3,
//    d: 4
// }

浅合并实现

根据上面的约束,动手实现一下

function assign() {
  let target = arguments[0];
  let i = 1,
    leg = arguments.length;
  if (!isObject(target)) {
    target = {};
  }
  for (; i < leg; i++) {
    const value = arguments[i];
    if (value == null) {
      continue;
    }
    for (const name in value) {
      if (Object.prototype.hasOwnProperty.call(value, name)) {
        const copy = value[name];
        target[name] = copy;
      }
    }
  }
  return target;
}

测试用例

test("浅拷贝", () => {
  const value1 = { name: "test", age: 17 };
  const value2 = { name: "app", age: [1, 2, 3] };
  const test = assign(value1, value2, null, undefined, "abc");
  expect(assign(test)).toEqual({
    name: "app",
    age: [1, 2, 3],
    0: "a",
    1: "b",
    2: "c",
  });
  expect(test.age === value2.age).toBeTruthy();
  var obj1 = {
    a: 1,
    b: { b1: 1, b2: 2 },
  };

  var obj2 = {
    b: { b1: 3, b3: 4 },
    c: 3,
  };

  var obj3 = {
    d: 4,
  };
  expect(assign(obj1, obj2, obj3)).toEqual({
    a: 1,
    b: { b1: 3, b3: 4 },
    c: 3,
    d: 4,
  });
});

深合并

function deepAssign(...args) {
  let obj = {};
  for (const value of args) {
    if (array(value)) {
      if (!array(obj)) {
        obj = [];
      }
      // 对数组内容进行深拷贝
      const arr = value.reduce((x, y) => {
        x = x.concat(isObject(y) ? deepAssign([], y) : y);
        return x;
      }, []);
      obj = [...obj, ...arr];
    } else if (isObject(value)) {
      for (let [name, val] of Object.entries(value)) {
        if (isObject(val) && name in obj) {
          val = deepAssign(obj[name], val);
        }
        obj = {
          ...obj,
          [name]: val,
        };
      }
    }
  }
  return obj;
}

if (isObject(val) && name in obj)注意这行的判断,只有当 target 属性上有值的时候才会进行递归调用。

测试用例

test("深合并", () => {
  const value1 = [1, 2, { name: { age: [1, 2, 3, 4, 5] } }];
  const value2 = [1, 2, { age: 18 }];
  const test = deepAssign(value1, value2);
  expect(test[2] === value1[2]).toBe(false);
  expect(test).toEqual([
    1,
    2,
    { name: { age: [1, 2, 3, 4, 5] } },
    1,
    2,
    { age: 18 },
  ]);
  const obj1 = { name: [1, 2, 3] };
  const obj2 = { age: 17, name: [4, 5, 6] };
  const obj = deepAssign(obj1, obj2);
  expect(obj).toEqual({ name: [1, 2, 3, 4, 5, 6], age: 17 });
  const a = {};
  a.a = a;
  expect(deepAssign(a)).toEqual({ a: { a } });
});

最后

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。