isEqual(o1, o2, depth = 0) {
    // Protect against infinite loops.
    if (depth > 100) return false;

    // If they don't share the same type they're not equal.
    if (typeof o1 !== typeof o2) return false;
    // If they're not objects, use referential equality with NaN handling.
    if (Number.isNaN(o1) && Number.isNaN(o2)) return true;
    if (typeof o1 !== 'object') return o1 === o2;
    // Try quick referential equality before perfoming deep comparisons.
    if (o1 === o2) return true;
    // Check for null.
    if (!o1 || !o2) return false;
    // At this point we know they're both truthy objects.
    // Perform deep inspection on the object's properties.

    if (Array.isArray(o1)) {
      if (!Array.isArray(o2)) return false;
      return o1.every((v, i) => this.isEqual(v, o2[i])) && o1.length === o2.length;
    }

    const o1Keys = Object.keys(o1).sort();
    const o2Keys = Object.keys(o2).sort();
    const allChildrenEqual = o1Keys.every(k => this.isEqual(o1[k], o2[k], depth + 1));
    return allChildrenEqual && this.isEqual(o1Keys, o2Keys);
  }
@param {any} o1
@param {any} o2
@param {number} [depth]
@return {boolean}