DEV Community

Mitchell
Mitchell

Posted on • Edited on

Object Traverse - JavaScript Challenges

You can find all the code in this post in the repo Github.


Object traverse related challenges

The following challenges are essentially about object key&value traversal.


Chaining

It's like object key&value depth-first traversal.

/**
 * @param {object} obj
 * @param {string} start
 * @return
 */

function format(obj, start) {
  let result = "";
  let current = start;

  while (current) {
    const entry = obj.find((item) => item.source === current);

    if (entry) {
      result += current;
      current = entry.target;
    } else {
      break;
    }
  }

  result += current;
  return result;
}

// Usage example
const origin = [
  { source: "b", target: "c" },
  { source: "a", target: "b" },
  { source: "c", target: "d" },
];

console.log(format(origin, "a")); // => "abcd"
Enter fullscreen mode Exit fullscreen mode

Compact

Filtering the falsy values of the object.

/**
 * @param {Array} array: The array to compact.
 * @return {Array} Returns the new array of filtered values.
 */

function compact(arr) {
  const newArray = [];

  for (const item of arr) {
    if (item) {
      newArray.push(item);
    }
  }

  return newArray;
}

// Usage example
console.log(compact([0, 1, false, 2, "", 3, null])); // => [1, 2, 3]
console.log(compact(["hello", 123, [], {}, function () {}])); // => ['hello', 123, [], {}, function() {}]

// handle circular reference

/**
 * @param {Object|Array} obj
 * @return {Object|Array}
 */
function compactObject(obj) {
  if (typeof obj !== "object" || obj == null) {
    return obj;
  }

  if (Array.isArray(obj)) {
    const compactArr = [];

    obj.forEach((item) => {
      if (item) {
        compactArr.push(compactObject(item));
      }
    });

    return compactArr;
  }

  const compactObj = Object.create(null);
  Object.entries(obj).forEach(([key, value]) => {
    if (value) {
      compactObj[key] = compactObject(value);
    }
  });

  return compactObj;
}

// Usage example
console.log(compact([0, 1, false, 2, "", 3, null])); // => [1, 2, 3]
console.log(compact({ foo: true, bar: null })); // => { foo: true }
Enter fullscreen mode Exit fullscreen mode

Count by

A common pattern to count the result of the function call.

/**
 * @param {Array} array The array to iterate over.
 * @param {Function} iteratee The function invoked per iteration.
 * @returns {Object} Returns the composed aggregate object.
 */

function countBy(arr, iteratee) {
  const result = Object.create(null);

  for (const item of arr) {
    const key = String(iteratee(item));
    result[key] ??= 0;
    result[key] += 1;
  }

  return result;
}

// Usage example
console.log(countBy([6.1, 4.2, 6.3], Math.floor)); // => { '4': 1, '6': 2 }
console.log(countBy([{ n: 3 }, { n: 5 }, { n: 3 }], (o) => o.n)); // => { '3': 2, '5': 1 }
console.log(countBy([], (o) => o)); // => {}
console.log(countBy([{ n: 1 }, { n: 2 }], (o) => o.m)); // => { undefined: 2 }
Enter fullscreen mode Exit fullscreen mode

Deep clone

Deep clone means the modifications to one object doesn't affect the other since they reference the different addresses.

Four ways:

  1. structuredClone() Web API
  2. JSON.parse(JSON.stringify()), can't not handle circular references
  3. lodash._cloneDeep()
  4. Implement by your self. Notes:
    1. use a Map() or Set() to handle circular refernences
    2. use Reflect.ownKeys() to get all the properties of an object
/**
 * @template T
 * @param {T} value
 * @return {T}
 */

// I: `use structuredClone API`

// II: use `JSON.parse(JSON.stringify(value))`

// III: use `lodash._cloneDeep()`

// IV:
// Handle primitive types and functions
function isPrimitiveTypeOrFunction(value) {
  return (
    typeof value !== "object" || value === null || typeof value === "function"
  );
}

// Check types
function getType(value) {
  const type = typeof value;

  // primitive
  if (type !== "object") {
    return type;
  }

  // non-primitive
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

// deep clone
function deepClone(value) {
  // check circular reference
  return deepCloneImpl(value, new Map());
}

function deepCloneImpl(value, cache) {
  // primitive case
  if (isPrimitiveTypeOrFunction(value)) {
    return value;
  }

  // get type
  const type = getType(value);

  // set
  if (type === "set") {
    const cloned = new Set();
    value.forEach((item) => {
      cloned.add(deepCloneImpl(item, cache));
    });

    return cloned;
  }

  // map
  if (type === "map") {
    const cloned = new Map();
    value.forEach((value_, key) => {
      cloned.set(key, deepCloneImpl(value_, cache));
    });

    return cloned;
  }

  // date
  if (type === "date") {
    return new Date(value);
  }

  // function
  if (type === "function") {
    return value;
  }

  // regexp
  if (type === "regexp") {
    return new RegExp(value);
  }

  // array
  if (Array.isArray(value)) {
    return value.map((item) => deepCloneImpl(item, cache));
  }

  // circular reference
  if (cache.has(value)) {
    return cache.get(value);
  }

  // object
  const cloned = Object.create(Object.getPrototypeOf(value));
  cache.set(value, cloned);

  for (const key of Reflect.ownKeys(value)) {
    const item = value[key];
    cloned[key] = isPrimitiveTypeOrFunction(item)
      ? item
      : deepCloneImpl(item, cache);
  }

  return cloned;
}

// Usage example
const obj1 = {
  num: 0,
  str: "",
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: "foo", id: 1 },
  arr: [0, 1, 2],
  date: new Date(),
  reg: new RegExp("/bar/ig"),
  [Symbol("s")]: "baz",
};

const clonedObj1 = deepClone(obj1);
clonedObj1.arr.push(3);
console.log(obj1.arr); // => [0, 1, 2]

const obj2 = { a: {} };
obj2.a.b = obj2; // Circular reference

const clonedObj2 = deepClone(obj2); // Should not cause a stack overflow by recursing into an infinite loop.

clonedObj2.a.b = "something new";

console.log(obj2.a.b === obj2); // => true
Enter fullscreen mode Exit fullscreen mode

Deep equal

function shouldDeepCompare(type) {
  return type === "[object Object]" || type === "[object Array]";
}

function getType(value) {
  return Object.prototype.toString.call(value);
}

/**
 * @param {*} valueA
 * @param {*} valueB
 * @returns
 */
function deepEqual(valueA, valueB) {
  const typeA = getType(valueA);
  const typeB = getType(valueB);

  if (typeA === typeB && shouldDeepCompare(typeA) && shouldDeepCompare(typeB)) {
    const entriesA = Object.entries(valueA);
    const entriesB = Object.entries(valueB);

    if (entriesA.length !== entriesB.length) {
      return false;
    }

    return entriesA.every(
      ([key, value]) =>
        Object.hasOwn(valueB, key) && deepEqual(value, valueB[key])
    );
  }

  return Object.is(valueA, valueB);
}

// Usage example
console.log(deepEqual("foo", "foo")); // true
console.log(deepEqual({ id: 1 }, { id: 1 })); // true
console.log(deepEqual([1, 2, 3], [1, 2, 3])); // true
console.log(deepEqual([{ id: "1" }], [{ id: "2" }])); // false
Enter fullscreen mode Exit fullscreen mode

Deep merge

function isPlainObject(value) {
  if (value === null) {
    return false;
  }

  const prototype = Object.getPrototypeOf(value);
  return prototype === null || prototype === Object.prototype;
}

/**
 * @param {Object|Array} valueA
 * @param {Object|Array} valueB
 * @returns Object|Array
 */
function deepMerge(valueA, valueB) {
  if (Array.isArray(valueA) && Array.isArray(valueB)) {
    return [...valueA, ...valueB];
  }

  if (isPlainObject(valueA) && isPlainObject(valueB)) {
    const newObj = { ...valueA };

    for (const key in valueB) {
      if (Object.prototype.hasOwnProperty.call(valueA, key)) {
        newObj[key] = deepMerge(valueA[key], valueB[key]);
      } else {
        newObj[key] = valueB[key];
      }
    }

    return newObj;
  }

  return valueB;
}

// Usage example
console.log(deepMerge({ a: 1 }, { b: 2 })); // { a: 1, b: 2 }
console.log(deepMerge({ a: 1 }, { a: 2 })); // { a: 2 }
console.log(deepMerge({ a: 1, b: [2] }, { b: [3, 4] })); // { a: 1, b: [2, 3, 4] }
Enter fullscreen mode Exit fullscreen mode

Deep omit && emitBy

/**
 * @param {object} obj
 * @param {array} []
 * @return {object}
 */

function omit(obj, keys) {
  const keysSet = new Set(keys);

  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => !keysSet.has(key))
  );
}

/**
 * @param {object} obj
 * @param {function} callbackFn
 * @return {object}
 */

function omitBy(obj, callbackFn) {
  return Object.fromEntries(
    Object.entries(obj).filter(([key, value]) => !callbackFn(value, key))
  );
}

// Usage example
const object = {
  a: 3,
  b: 4,
  c: 5,
};

console.log(omit(object, ["a", "b"])); // => { c: 5 }

console.log(omitBy(object, (value) => value === 3)); // => { b: 4, c: 5 }
Enter fullscreen mode Exit fullscreen mode

Is object empty

/**
 * @param {Object | Array} obj
 * @return {boolean}
 */

function isObjectEmpty(obj) {
  for (const _ in obj) {
    return false;
  }

  return true;
}

// Usage example
const emptyObj = Object.create(null);
const emptyObjLiteral = {};
const nonEmptyObj = {
  name: "Jack",
};
console.log(isObjectEmpty(emptyObj)); // true
console.log(isObjectEmpty(emptyObjLiteral)); // true
console.log(isObjectEmpty(nonEmptyObj)); // false
Enter fullscreen mode Exit fullscreen mode

Get

/**
 * @param {Object} objectParam
 * @param {string|Array<string>} pathParam
 * @param {*} [defaultValue]
 * @return {*}
 */

function get(objectParam, pathParam, defaultValue) {
  // Convert pathParam to array
  const path = Array.isArray(pathParam)
    ? pathParam
    : pathParam.replaceAll("[", ".").replaceAll("]", "").split(".");

  if (path.length === 0) {
    return defaultValue;
  }

  let obj = objectParam;

  for (const key of path) {
    if (obj === null || obj[key] === undefined) {
      return defaultValue;
    }

    obj = obj[key];
  }

  return obj;
}

// Usage example
const john = {
  profile: {
    name: { firstName: "John", lastName: "Doe" },
    age: 20,
    gender: "Male",
  },
};

const jane = {
  profile: {
    age: 19,
    gender: "Female",
  },
};

console.log(get(john, "profile.name.firstName")); // => 'John'
console.log(get(john, "profile.gender")); // => 'Male'
console.log(get(jane, "profile.name.firstName")); // => undefined

console.log(get({ a: [{ b: { c: 3 } }] }, "a.0.b.c")); // => 3
Enter fullscreen mode Exit fullscreen mode

Does object has any properties

/**
 * @param {Object | Array} obj
 * @return {boolean}
 */

function isObjectEmpty(obj) {
  for (const _ in obj) {
    return false;
  }

  return true;
}

// Usage example
const emptyObj = Object.create(null);
const emptyObjLiteral = {};
const nonEmptyObj = {
  name: "Jack",
};
console.log(isObjectEmpty(emptyObj)); // true
console.log(isObjectEmpty(emptyObjLiteral)); // true
console.log(isObjectEmpty(nonEmptyObj)); // false
Enter fullscreen mode Exit fullscreen mode

Key By

/**
 * @param {Array} collection
 * @param {Function} iteratee
 * @return {Object}
 */

function keyBy(collection, iteratee) {
  return collection.reduce((result, item) => {
    const key = iteratee(item);
    result[key] = item;

    return result;
  }, {});
}

// Example usage
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" },
];

// Using keyBy to group users by their names
const groupedByName = keyBy(users, (user) => user.name);
console.log(groupedByName);
/*
Output:
{
  Alice: { id: 1, name: 'Alice' },
  Bob: { id: 2, name: 'Bob' },
  Charlie: { id: 3, name: 'Charlie' }
}
*/

// Using keyBy to group users by their IDs
const groupedById = keyBy(users, (user) => user.id);
console.log(groupedById);
/*
Output:
{
  1: { id: 1, name: 'Alice' },
  2: { id: 2, name: 'Bob' },
  3: { id: 3, name: 'Charlie' }
}
*/
Enter fullscreen mode Exit fullscreen mode

Max By

/**
 * @param {Array} arr
 * @param {Function} callbackFn
 * @return {Array}
 */

function maxBy(arr, callbackFn) {
  if (arr.length === 0) {
    return [];
  }

  const values = arr.map(callbackFn);
  const maxValue = Math.max(...values);
  return arr.filter((_, index) => values[index] === maxValue);
}

// Example usage:
const data = [{ a: 3 }, { a: 4 }, { a: 5 }, { a: 5 }];

const result = maxBy(data, (item) => item.a);
console.log(result); // Output: [{ a: 5 }, { a: 5 }]
Enter fullscreen mode Exit fullscreen mode

Object map

/**
 * @param {Object} obj
 * @param {Function} fn
 * @returns Object
 */
function objectMap(obj, fn) {
  const result = {};

  for (const key in obj) {
    if (Object.hasOwn(obj, key)) {
      result[key] = fn.call(obj, obj[key]);
    }
  }

  return result;
}

// Usage example
const double = (x) => x * 2;
console.log(objectMap({ foo: 1, bar: 2 }, double)); // => { foo: 2, bar: 4}
Enter fullscreen mode Exit fullscreen mode

Object to array

/**
 * @param {object} obj
 * @return {Array}
 */

function objToArr(obj) {
  return Object.keys(obj).reduce((value, key) => {
    const op = Object.keys(obj[key])[0];
    value.push({
      key: key,
      op: op,
      value: obj[key][op],
    });

    return value;
  }, []);
}

// Usage example
const obj = {
  key1: {
    op1: "value1",
  },
  key2: {
    op2: "value2",
  },
};

console.log(objToArr(obj));
/*
[
  { key: 'key1', op: 'op1', value: 'value1' },
  { key: 'key2', op: 'op2', value: 'value2' }
]
*/
Enter fullscreen mode Exit fullscreen mode

Set

/**
 * @param {object} obj
 * @param {string | string[]} path
 * @param {any} value
 */

function set(obj, path, value) {
  if (!Array.isArray(path)) {
    path = path.replaceAll("[", ".").replaceAll("]", "").split(".");
  }

  for (let i = 0; i < path.length - 1; i += 1) {
    let nextPath = path[i + 1];
    const newObj = "" + +nextPath === nextPath ? [] : {};

    if (!obj[path[i]]) {
      obj[path[i]] = newObj;
    }
    obj = obj[path[i]];
  }

  obj[path.at(-1)] = value;
}

// Usage example
const obj = {
  a: {
    b: {
      c: [1, 2, 3],
    },
  },
};

set(obj, "a.b.c", "BFE");
console.log(obj.a.b.c); // => "BFE"
set(obj, "a.b.c.0", "BFE");
console.log(obj.a.b.c[0]); // => "B"
Enter fullscreen mode Exit fullscreen mode

Set object value

/**
 * @param {Object} obj
 * @param {Array} keys
 * @param {any} value
 * @returns
 */

function setObjectValue(obj, keys, value) {
  let currentObj = obj;

  for (let i = 0; i < keys.length; i += 1) {
    const key = keys[i];

    // Check if the current object is null or undefined
    if (currentObj === null || currentObj === undefined) {
      return obj; // Return the original object if any part of the path is null or undefined
    }

    // If it's the last key, set the value
    if (i === keys.length - 1) {
      currentObj[key] = value;
    } else {
      // Create a new object if the current key doesn't exist or is not an object
      if (
        typeof currentObj[key] !== "object" ||
        Array.isArray(currentObj[key])
      ) {
        currentObj[key] = {};
      }

      // Move to the next part of the path
      currentObj = currentObj[key];
    }
  }

  return obj;
}

// Usage example
const obj = {
  a: {
    b: {
      c: 42,
    },
  },
};

const updatedObj = setObjectValue(obj, ["a", "b", "c"], 100);
console.log(updatedObj); // => { a: { b: { c: 100 } } }

const anotherObj = {
  x: {
    y: null,
  },
};

const updatedAnotherObj = setObjectValue(anotherObj, ["x", "y", "z"], "value");
console.log(updatedAnotherObj); // => { x: { y: null } } (original object returned because 'y' is null);
Enter fullscreen mode Exit fullscreen mode

Squash object

/**
 * @param {Object} obj
 * @return {Object}
 */
function squashObject(obj) {
  const outObj = {};

  function squashImpl(obj_, path, output) {
    for (const [key, value] of Object.entries(obj_)) {
      if (typeof value !== "object" || value === null) {
        output[path.concat(key).filter(Boolean).join(".")] = value;
      } else {
        squashImpl(value, path.concat(key), output);
      }
    }
  }

  squashImpl(obj, [], outObj);

  return outObj;
}

// Usage example
const object = {
  foo: {
    "": { "": 1, bar: 2 },
  },
};
console.log(squashObject(object)); // { foo: 1, 'foo.bar': 2 }
Enter fullscreen mode Exit fullscreen mode

Shallow copy

/**
 * @param {Object} obj
 * @return {Object}
 */
function shallowClone(obj) {
  const copyObj = {};

  for (const key in obj) {
    if (Object.hasOwn(obj, key)) {
      copyObj[key] = obj[key];
    }
  }

  return copyObj;
}

// Usage example
const obj = {
  name: "Mike",
  age: 25,
};

const nestedObj = {
  name: "Mike",
  address: {
    state: "NY",
    city: "NYC",
  },
};

console.log(shallowClone(obj)); // => { name: 'Mike', age: 25 }
console.log(shallowClone(nestedObj)); // => { name: 'Mike', address: { state: 'NY', city: 'NYC' } }
Enter fullscreen mode Exit fullscreen mode

Reference

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more