Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Created July 17, 2019 19:34
Show Gist options
  • Save sebinsua/6d6f298a5a91eee37b9a507cce542b6a to your computer and use it in GitHub Desktop.
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.
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));
@sebinsua
Copy link
Author

Random note to self, it might be a better idea to use a WeakMap, to remember what we've seen already.

@sebinsua
Copy link
Author

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.

@sebinsua
Copy link
Author

sebinsua commented Aug 10, 2022

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.

@sebinsua
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment