深浅拷贝和extend
前言
深浅拷贝和 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 分为两部分:
- 浅合并,
Object.assign
就是浅拷贝; - 深合并,没有相关的的 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
,对作者也是一种鼓励。