4

I'm trying to create an updated object from an existing object. The sample object is:

// sample object
const testObj = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3,
      f: {
        g: 4
      }
    }
  }
};

I want to create a new object from the above object with some concatenation of each value:

// expected object
const expectedObject= {
  a: '1 a',
  b: {
    c: '2 a',
    d: {
      e: '3 a',
      f: {
        g: '4 a'
      }
    }
  }
};

here is my sample code:

let expectedObject = {};
const newObject = object => {
  Object.entries(object).forEach(([key, value]) => {
    if (typeof value === "object") {
      Object.keys(value).map(key => {
        value[key] = value[key] + " a";
        return value;
      });

      expectedObject[key] = value;
      //return newTest;
    } else {
      expectedObject[key] = value;
      return expectedObject;
    }
  });
  return expectedObject;
};
console.log(newObject(testObj));

the outcome in console is:

{a: 1, b: {…}}
a: 1
b:
c: "2 a"
d: "[object Object] a"
__proto__: Object
__proto__: Object

I wanted to use recursion here and also tried it but no luck. any help, thanks?

1
  • const a = o => Object.keys(o).reduce((p, c) => ((p[c] = typeof o[c] === "object" ? a(o[c]) : o[c] + " a"), p), {}); Commented Oct 1, 2019 at 12:34

5 Answers 5

7

You could get a new object my mapping changed values and creating new objects.

function map(object, fn) {
    return Object.fromEntries(Object
        .entries(object)
        .map(([k, v]) => [k, v && typeof v === 'object' ? map(v, fn) : fn(v)])
    );
}

var object = { a: 1, b: { c: 2, d: { e: 3, f: { g: 4 } } } },
    result = map(object, v => v + ' a');

console.log(result);

If you have arrays inside, you could add a check in advance and map the values.

const
    map = fn => {
        const iter = v => v && typeof v === 'object'
            ? Array.isArray(v)
                ? v.map(iter)
                : Object.fromEntries(Object.entries(v).map(([k, v]) => [k, iter(v, fn)]))
            : fn(v);
        return iter;
    };

var object = { a: 1, b: { c: 2, d: { e: 3, f: { g: 4, k: [5, 6] } } } },
    addA = map(v => v + ' a'),
    result = addA(object);

console.log(result);

Sign up to request clarification or add additional context in comments.

7 Comments

I jus copied your code to inex.js file and run with node index.js, got the following error: TypeError: Object.fromEntries is not a function
NB this will convert arrays [] to objects {} with string-based keys
@Yahya: But it's easy to write your own fromEntries, either by folding the results: (entries) => entries .reduce ((a, [k, v]) => ({...a, [k]: v}), {}) or with Object.assign: (entries) => Object.assign.apply (null, entries .map (([k, v]) => ({[k]: v}))). The latter is uglier, but likely more efficient.
@ScottSauyet thank you very much for your explanation. The answer from Nina Scholz is great, but I'm accepting the other answer because it meets the requirement without any error.
|
3

This is simply a refactoring of the answer from @user633183. I like that approach a lot, but think it can be simplified by extracting two more reusable functions. This started as a comment on that answer, but I thought it would be better to be explicit.

const map = (f) => (a) => 
  a.map(f)

const mapObj = (f) => (o) => 
  Object .entries (o) .reduce ( (a, [k, v] ) => ({ ...a, [k]: f(v) }), {})

const traverse = (f) => (t) =>
  Array.isArray(t)
    ? map (traverse (f)) (t)
  : Object(t) === t 
    ? mapObj (traverse (f)) (t)
  : f (t)

const input =
  { a: [ 1, 11, 111 ], b: { c: 2, d: { e: [ 3, { f: { g: 4 } } ] } } }

const output =
  traverse(x => `${x} a`) (input)

console.log(output)

mapObj can be written in many different ways. Here are two alternatives:

const mapObj = (f = identity) => (o = {}) => 
  Object .fromEntries (Object .entries (o) .map (([ k, v ]) => [ k, f (v) ]))


const mapObj = (f = identity) => (o = {}) => 
  Object .assign .apply (null, Object .entries (o) .map (([ k, v ]) => ({ [k]: f (v) 

Comments

1

Here's an approach using a modification of the original code to demonstrate what needed to be changed in order to make it work. You had some things switched up reading the value and setting the new one. Also I'm using the spread operator to clone the object before modifying it.

const testObj = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3,
      f: {
        g: 4
      }
    }
  }
};

const newObject = object => {
  const clonedObj = { ...object };
  const entries = Object.entries(clonedObj);

  entries.forEach(([key, value]) => {
    if (typeof value === "object") {
      clonedObj[key] = newObject(value);
    } else {
      clonedObj[key] = value + " a";
    }
  });
  return clonedObj;
};
console.log(newObject(testObj));
console.log(testObj); // prove that the original object hasn't changed

Comments

1

Here's a simple recursive technique. It is similar to Nina's but it preserves arrays, if present in the structure.

  1. If the input, t, is an array, create a new array by traversing each array value, v, with the traversing function, f
  2. (inductive) Otherwise t is not an array. If t is an object, create a new object from key value pairs, [ k, v ], by traversing each value, v, with the traversing function, f
  3. (inductive) Otherwise t is not an array and t is not an object. This means t is either a primitive value, such as string, number, or null

Numbered comments below correspond to the explanation above -

const identity = x =>
  x

const traverse = (f = identity, t = {}) =>
  Array.isArray(t)                         // 1
    ? Array.from(t, v => traverse(f, v))
: Object(t) === t                          // 2
    ? Object.fromEntries(Object.entries(t).map(([ k, v ]) => [ k, traverse(f, v) ]))
: f (t)                                    // 3

const input =
  { a: [ 1, 11, 111 ], b: { c: 2, d: { e: [ 3, { f: { g: 4 } } ] } } }

const output =
  traverse(x => `${x} a`, input)

console.log(output)

3 Comments

A suggested refactoring: pull out a mapObject function to use in part 2. It's a nicely reusable function and makes the main code quite clean
Note that 1 should be labeled as "inductive", not 3.
thanks Scott. Your answer is a nice decomposition of the problem.
0

Here is a solution using object-scan. It works by building the solution at the same time as the input is traversed.

// const objectScan = require('object-scan');

const testObj = { a: 1, b: { c: 2, d: { e: 3, f: { g: 4 } } } };

const cloneAndModify = (obj) => objectScan(['**'], {
  breakFn: ({ property, value, isLeaf, context }) => {
    if (property === undefined) {
      return;
    }
    const ref = context[context.length - 1];
    if (!(property in ref)) {
      ref[property] = isLeaf ? `${value} a` : {};
    }
    context.push(ref[property]);
  },
  filterFn: ({ context }) => {
    context.pop();
  }
})(obj, [{}])[0];

const r = cloneAndModify(testObj);
console.log(r);
// => { b: { d: { f: { g: '4 a' }, e: '3 a' }, c: '2 a' }, a: '1 a' }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.