user3425506
user3425506

Reputation: 1437

Iterating Properties in Object Oriented JavaScript

I am trying to develop a more object oriented approach to my programming but have run into a problem. I thought it would be useful to group related properties in an object together with methods that operate on the properties.

An example of this would be an object holding some properties with strings as values together with a method that can iterate over all of the (non-method) properties changing them to uppercase.

const myObj = {
  prop1: "one",
  prop2: "two",
  myMethod: function() {
    Object.keys(this).forEach(prop => {
      if (typeof this[prop] !== "function"){
        this[prop] = this[prop].toUpperCase();
      }
    });
  },
}

console.log(myObj.prop1, myObj.prop2);
myObj.myMethod();
console.log(myObj.prop1, myObj.prop2);

I would be happy with this if it was not for the need to have a conditional clause, if (typeof this[prop] !== "function"){...}, in order to prevent the method working on methods as well as properties. Although the conditional clause does work in this case you might have a case where you have some methods which you want other methods to operate on and there is a distinction (for you) between the methods you want to be operated on and those that do the operating.

One way around using the conditional would be to put the properties you want to be acted on inside another object but then that means that when you use them you have a more complicated way of referencing them:

const myObj = {
  props: {
    prop1: "one",
    prop2: "two",
  }
  myMethod: function() { ... }
    });
  },
}

Now I would have to reference the properties as myObj.props.prop1 instead of myObj.prop1 which looks much better to me.

I was wondering whether there is some common satisfactory way of addressing this issue and what other people think about it.

Upvotes: 0

Views: 114

Answers (3)

Peter Seliger
Peter Seliger

Reputation: 13432

Taking into account the OP's example code ...

const myObj = {
  prop1: "one",
  prop2: "two",
  myMethod: function() {
    Object.keys(this).forEach(prop => {
      if (typeof this[prop] !== "function"){
        this[prop] = this[prop].toUpperCase();
      }
    });
  },
}
console.log('Object.entries(myObj) ...', Object.entries(myObj));

console.log('myObj.myMethod()', myObj.myMethod() ?? '');

console.log('Object.entries(myObj) ...', Object.entries(myObj));
console.log(
  'Object.entries(Object.getPrototypeOf(myObj)) ...',
  Object.entries(Object.getPrototypeOf(myObj)),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

... and looking into the base approach of the other two, so far, provide answers (utilizing Object.defineProperty), a more flexible/generic implementation of the OP's OO core-design is much recommended because from there one can iterate towards various approaches that are all offered/supported by the language's core.

All of the following code examples will use this simple factory pattern, which implements glue-code in order to assemble various solutions, based on different core-features, but all achieving the same goal, as requested by the OP.

Starting with what already has been proposed ...

function myMethod() {
  Object
    .entries(this)
    .forEach(([key, value]) => {

      this[key] = value.toUpperCase();
    });
}

const myObj = createObjectWithNonEnumerableMethods({
  prop1: "one",
  prop2: "two",
  //anotherProp,
  //andYetAnotherProp,
}, {
  myMethod,
  //anotherMethod,
  //andYetAnotherMethod
});
console.log('Object.entries(myObj) ...', Object.entries(myObj));

console.log('myObj.myMethod()', myObj.myMethod() ?? '');

console.log('Object.entries(myObj) ...', Object.entries(myObj));
console.log(
  'Object.entries(Object.getPrototypeOf(myObj)) ...',
  Object.entries(Object.getPrototypeOf(myObj)),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
function createObjectWithNonEnumerableMethods(props, methods) {
  return Object
    .entries(methods ?? {})
    .reduce((obj, [key, value]) => {
      if (typeof value === 'function') {

        Object.defineProperty(obj, key, {
          enumerable: false,
          value,
        });
      }
      return obj;

    }, structuredClone(props ?? {}));
}
</script>

... the above code provides a generic factory function which implements glue-code that first clones a plain object of key-value pairs, where the values are not supposed to be functions and second assigns non-enumerable methods to this newly created object.

From there one could go on and discover what the language's prototype core feature is for by utilizing Object.create ...

function myMethod() {
  Object
    .entries(this)
    .forEach(([key, value]) => {

      this[key] = value.toUpperCase();
    });
}

const myObj = createObjectWithPrototypeMethods({
  prop1: "one",
  prop2: "two",
  //anotherProp,
  //andYetAnotherProp,
}, {
  myMethod,
  //anotherMethod,
  //andYetAnotherMethod
});
console.log('Object.entries(myObj) ...', Object.entries(myObj));

console.log('myObj.myMethod()', myObj.myMethod() ?? '');

console.log('Object.entries(myObj) ...', Object.entries(myObj));
console.log(
  'Object.entries(Object.getPrototypeOf(myObj)) ...',
  Object.entries(Object.getPrototypeOf(myObj)),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
function createObjectWithPrototypeMethods(props, protoObject) {
  return Object.create(
  
    // - the prototype of the to be created object.
    protoObject ?? {},

    // - the property descriptors of the to be created object
    //   have to be created from the provided `props` argument.
    Object
      .keys(props ?? {})
      .reduce((descriptor, key) => {

        descriptor[key] = Object.getOwnPropertyDescriptor(props, key);
        return descriptor;

      }, {}),
  );
}
</script>

Whereas the first provided factory creates objects that each own every property themself the enumerable as well as the non-enumerable ones, the second, above provided, factory creates objects where each object does feature a prototype-object with the latter being the real owner of the former object's methods. Thus each of the latter factory's objects makes use of prototypal delegation.

From there the OP could further explore object creation by constructor functions, either convenient class-based or pretty old-school.

Either way, the underlying mechanism is prototype based as demonstrated with the last example code.

Upvotes: 0

Mister Jojo
Mister Jojo

Reputation: 22365

You can simply set the enumerable property to false:

const myObj = {
  prop1: "one",
  prop2: "two",
  myMethod: function() {
    Object.keys(this).forEach(prop => {
      console.log (prop,  this[prop], typeof this[prop] );
    });
  },
}

// change enumerable property :
Object.defineProperty(myObj, 'myMethod', {enumerable: false} );

 
myObj.myMethod();
 

Upvotes: 0

user27425627
user27425627

Reputation: 1293

You could always define your method as non-enumerable. Then, methods that act on properties through the Object.keys method will not affect it.

const myObj = {
  prop1: "one",
  prop2: "two"
}

Object.defineProperty(myObj, 'myMethod', {
  enumerable: false,
  value: () => Object.keys(myObj).forEach(prop => myObj[prop] = myObj[prop].toUpperCase())
});

console.log(Object.keys(myObj));

console.log(myObj.prop1, myObj.prop2);
myObj.myMethod();
console.log(myObj.prop1, myObj.prop2);

References

Upvotes: 3

Related Questions