Created
July 17, 2019 19:34
-
-
Save sebinsua/6d6f298a5a91eee37b9a507cce542b6a to your computer and use it in GitHub Desktop.
Trying to write `deepClone`, `deepEquals`, `deepDiff`, etc. There are zero tests and I'm not paying attention to browser oddities so this is probably quite flawed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const deepClone = (value, seen = new Map()) => { | |
| if (seen.has(value)) { | |
| return value; | |
| } else { | |
| seen.set(value, true); | |
| } | |
| if (Array.isArray(value)) { | |
| const newArr = []; | |
| for (let el of value) { | |
| newArr.push(deepClone(el, seen)); | |
| } | |
| return newArr; | |
| } else if (value instanceof Date) { | |
| const newDate = new Date(); | |
| newDate.setTime(value.getTime()); | |
| return newDate; | |
| } else if (typeof value === "object" && value !== null) { | |
| // note: there may be edge-cases on objects/primitives i've not considered... | |
| const newObj = {}; | |
| for (let [key, innerValue] of Object.entries(value)) { | |
| newObj[key] = deepClone(innerValue, seen); | |
| } | |
| return newObj; | |
| } else if (typeof value === "function") { | |
| // dragons: https://stackoverflow.com/a/6772648 | |
| return value.bind(); | |
| } else { | |
| return value; | |
| } | |
| }; | |
| const deepEqual = (a, b, seen = new Map()) => { | |
| if (seen.has(a) || seen.has(b)) { | |
| // If we've seen these before and not bailed, it implies that they were okay. | |
| return true; | |
| } else { | |
| seen.set(a, true); | |
| seen.set(b, true); | |
| } | |
| if (a !== b) { | |
| return false; | |
| } else { | |
| // a and b must be the same type if they are equal... | |
| if (Array.isArray(a)) { | |
| // all good so far. let's take a look inside if necessary. | |
| const length = a.length; | |
| for (let idx = 0; idx < length; idx++) { | |
| if (deepEqual(a[idx], b[idx], seen) === false) { | |
| return false; | |
| } | |
| } | |
| } else if (typeof a === "object" && a !== null) { | |
| // note: there may be edge-cases on objects/primitives i've not considered... | |
| for (let key of Object.keys(a)) { | |
| if (deepEqual(a[key], b[key], seen) === false) { | |
| return false; | |
| } | |
| } | |
| } | |
| } | |
| return true; | |
| }; | |
| const deepDiff = (a, b, seen = new Map()) => { | |
| if (seen.has(a) && seen.has(b)) { | |
| return a !== b; | |
| } else { | |
| seen.set(a, true); | |
| seen.set(b, true); | |
| } | |
| if (typeof a !== typeof b || Array.isArray(a) !== Array.isArray(b)) { | |
| return true; | |
| // This should imply that everything after this has the same type... | |
| } else if (Array.isArray(a)) { | |
| // all good so far. let's take a look inside if necessary. | |
| const newArr = []; | |
| const length = Math.max(a.length, b.length); | |
| for (let idx = 0; idx < length; idx++) { | |
| newArr.push(deepDiff(a[idx], b[idx], seen)); | |
| } | |
| return newArr; | |
| } else if (typeof a === "object" && a !== null) { | |
| // note: there may be edge-cases on objects/primitives i've not considered... | |
| const newObj = {}; | |
| const allKeys = Object.keys({ ...a, ...b }); | |
| for (let key of allKeys) { | |
| newObj[key] = deepDiff(a[key], b[key], seen); | |
| } | |
| return newObj; | |
| } | |
| return a !== b; | |
| }; | |
| const oldObject = { | |
| a: 5, | |
| b: 6, | |
| c: function() {}, | |
| d: [ | |
| { | |
| hey: "there", | |
| notThis: null, | |
| metadata: { | |
| unexpected: "property" | |
| } | |
| } | |
| ], | |
| details: { | |
| parent: { | |
| id: 5 | |
| } | |
| } | |
| }; | |
| const innerObj = { | |
| hey: "there" | |
| }; | |
| const middleObj = { | |
| contains: innerObj | |
| }; | |
| const oldRecursiveObject = { | |
| middleObj, | |
| innerObj | |
| }; | |
| innerObj.loop = oldRecursiveObject; | |
| const newObject = deepClone(oldObject); | |
| const newRecursiveObject = deepClone(oldRecursiveObject); | |
| console.log(newObject); | |
| console.log(deepEqual(oldObject, oldObject)); | |
| console.log(deepEqual(oldObject, newObject)); | |
| console.log(newRecursiveObject); | |
| console.log(deepEqual(oldRecursiveObject, oldRecursiveObject)); | |
| console.log(deepEqual(oldRecursiveObject, newRecursiveObject)); | |
| const base = { type: "person", details: {} }; | |
| const seb = { | |
| ...base, | |
| name: "Seb", | |
| pocket: ["wallet"], | |
| details: { age: 32, address: ["17", "Clapton Terrace"] } | |
| }; | |
| const gilly = { | |
| ...base, | |
| name: "Gilly", | |
| pocket: [], | |
| details: { age: 31, address: ["17", "Clapton Terrace"] } | |
| }; | |
| const malformed = { | |
| ...base, | |
| name: "Mr. Malformed", | |
| pocket: "my hands", | |
| details: { age: 32, address: null } | |
| }; | |
| console.log("diff", deepDiff(seb, gilly)); | |
| console.log("diff", deepDiff(seb, malformed)); | |
| // @todo: Cyclic structures in diffs is still buggy. Or rather, I need to work out what I want to do here... | |
| console.log(deepDiff(oldRecursiveObject, oldRecursiveObject)); | |
| console.log(deepDiff(oldRecursiveObject, newRecursiveObject)); |
Very simplistic (and probably incorrect) deepMerge:
const isObject = (value) =>
typeof value === "object" && value !== null && Array.isArray(value) === false;
function deepMerge(a, b) {
if (Array.isArray(a) && Array.isArray(b)) {
for (const value of b) {
a.push(value);
}
return a;
} else if (isObject(a) && isObject(b)) {
for (const key of Object.keys(b)) {
a[key] = deepMerge(a[key], b[key]);
}
} else {
return b;
}
return a;
}
const a = {
well: {
this: {
works: true
}
},
thing: {
property1: "Hello",
items: [1, 2]
}
};
const b = {
well: {
this: {
works: false
}
},
thing: {
property2: "World!",
items: [3, 4, 5]
}
};
console.log(deepMerge(a, b));See: https://codesandbox.io/s/bold-sky-03bmo?file=/src/index.js:0-712
In order to be more compliant, I should handle multiple arguments, and check the spec to see what the merge behaviours should be.
Another deepClone: https://codesandbox.io/s/elegant-murdock-ktbkyn?file=%2Fsrc%2Findex.js
const isPrimitive = (a) => {
return (
typeof a === "string" ||
typeof a === "number" ||
typeof a === "boolean" ||
typeof a === "function" ||
typeof a === "symbol" ||
// CodeSandbox dies if I do this...
// typeof a === "bigint" ||
a === null ||
a === undefined
);
};
const isDate = (a) => {
return a instanceof Date;
};
const isMap = (a) => {
return a instanceof Map;
};
const isSet = (a) => {
return a instanceof Set;
};
const isObject = (a) => {
return typeof a === "object" && Array.isArray(a) === false && a !== null;
};
function deepClone(value) {
const seen = new WeakMap();
function clone(a) {
if (seen.has(a)) {
// I hope to have cloned it previously.
return seen.get(a);
}
if (Array.isArray(a)) {
seen.set(a, a);
const newArray = a.map((v) => clone(v));
seen.set(a, newArray);
return newArray;
}
if (isDate(a)) {
const time = a.getTime();
const newDate = new Date();
newDate.setTime(time);
seen.set(a, newDate);
return newDate;
}
if (isMap(a)) {
const newMap = new Map(
Array.from(a.entries()).map(([key, value]) => [key, clone(value)])
);
seen.set(a, newMap);
return newMap;
}
if (isSet(a)) {
const newSet = new Set(
Array.from(a.values()).map((value) => clone(value))
);
seen.set(a, newSet);
return newSet;
}
if (isObject(a)) {
seen.set(a, a);
const newObject = Object.fromEntries(
Object.entries(a).map(([key, value]) => [key, clone(value)])
);
seen.set(a, newObject);
return newObject;
}
if (typeof a === "function") {
function fn(...args) {
return a(...args);
}
for (let [key, value] of Object.entries(a)) {
fn[key] = clone(value);
}
Object.defineProperty(fn, "name", {
value: a.name
});
seen.set(a, fn);
return fn;
}
if (isPrimitive(a)) {
return a;
}
console.assert(
true,
`A value type not handled by our deep clone has been encountered. It is: ${value} (${typeof value})`
);
}
return clone(value);
}
const recursiveObject = {
hey: {
there: "person",
array: [
1,
2,
3,
4,
5,
234234.12345,
{
another: {
deep: {
object: {
with: "properties",
inside: "it",
number: 100,
fn: function helloWorld() {
console.log("hello world!");
}
}
}
}
},
["really", "works"]
]
},
set: new Set(["very", "funny"]),
map: new Map([
["yes", "yes"],
["no", "no"],
["no?", "yes?"],
["yes?", "no?"]
])
};
recursiveObject.hey.array.push(recursiveObject);
const deepClonedObject = deepClone(recursiveObject);
console.log(deepClonedObject);
console.log(
recursiveObject.hey.array === recursiveObject.hey.array,
recursiveObject.hey.array === deepClonedObject.hey.array
);
console.log(
recursiveObject.hey.array[6].another.deep.object ===
recursiveObject.hey.array[6].another.deep.object,
recursiveObject.hey.array[6].another.deep.object ===
deepClonedObject.hey.array[6].another.deep.object
);
console.log(
recursiveObject.hey.array[6].another.deep.object.fn ===
recursiveObject.hey.array[6].another.deep.object.fn,
recursiveObject.hey.array[6].another.deep.object.fn ===
deepClonedObject.hey.array[6].another.deep.object.fn
);
console.log(
recursiveObject.hey.array[7] === recursiveObject.hey.array[7],
recursiveObject.hey.array[7] === deepClonedObject.hey.array[7]
);
console.log(
recursiveObject.map === recursiveObject.map,
recursiveObject.map === deepClonedObject.maps
);
console.log(
recursiveObject.set === recursiveObject.set,
recursiveObject.set === deepClonedObject.set
);
console.log(deepClonedObject.map.get("yes?"));
console.log(deepClonedObject.set.has("funny"));Can't be bothered to implement anymore, as at a certain point I'll start to need proper unit tests and actual clear-cut intentions.
Trying again to deepMerge: https://codesandbox.io/s/holy-forest-fhikp8?file=%2Fsrc%2Findex.js
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Random note to self, it might be a better idea to use a
WeakMap, to remember what we've seen already.