0
\$\begingroup\$

i attempted to create a way (not sure if already implemented by someone) by which when i update a property of an object. it returns a new reference of the object with the new value. i have a usecase in my mind for that purpose you can say i want to use it.

for some example reference you can consider redux reducer where you have to return a new reference everytime store is updated.

here is my small code which attempts to do it.

kindly ignore the setIn implementation its the first attempt.

function NewRef(jsObj) {
  Object.keys(jsObj).forEach((key) => {
    this[key] = jsObj[key];
  });
}

NewRef.prototype.set = function set(key, value) {
  let copyValue = null;
  if (Array.isArray(value)) copyValue = [...value];
  else if (typeof value === 'object') copyValue = { ...value };
  else copyValue = value;
  if (this[key] || ['object', 'boolean', 'number'].includes(typeof this[key])) {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), {
      ...this,
      [key]: copyValue,
    });
  }
  throw new Error(`invalide key ${key} in object`);
};


NewRef.prototype.setIn = function set(nested, value) {
  let copyValue = null;
  if (Array.isArray(value)) copyValue = [...value];
  else if (typeof value === 'object') copyValue = { ...value };
  else copyValue = value;
  if (!Array.isArray(nested)) {
    throw new Error('setIn requires array of strings as first argument');
  } else {
    if (this[nested[0]] || ['object', 'boolean', 'number'].includes(typeof this[nested[0]])) {
      if (this[nested[0]][nested[1]] || ['object', 'boolean', 'number'].includes(typeof this[nested[0]][nested[1]])) {
        return Object.assign(Object.create(Object.getPrototypeOf(this)), {
          ...this,
          [nested[0]]: {
            ...this[nested[0]],
            [nested[1]]: copyValue,
          },
        });
      }
    }
    throw new Error(`invalid properties maybe ${nested.join(',')}`);
  }
};

NewRef.prototype.get = function get(key) {
  return this[key];
};

export default function NObject(jsObj) {
  const store = new NewRef(jsObj);
  return store;
}

with this piece of code i am able to do this.

const d1 = NObject({
   type: 'bool',
   data: {key:'value'}
   meta: {
      user: 'john doe'
   }
  })
const n1 = d1.set('type','mutated').set('data',[1,2])
const n2 = n1.setIn(['meta','user'],'foo bar');

both n1,n2 and d1 have different references. it works and is tested well.

please feel free to review it and give your suggestions. i am open to everything

\$\endgroup\$
  • \$\begingroup\$ if (typeof value === 'object') will always be true for Arrays (the typeof an Array is 'object') so your else if (Array.isArray(value)) will never get hit. I personally find it really confusing to read code that uses if/else if/else if with no indentation at all. \$\endgroup\$ – jfriend00 Jan 31 at 7:19
  • \$\begingroup\$ great catch. then i will move array validation above.@jfriend00 \$\endgroup\$ – hannad rehman Jan 31 at 10:23
1
\$\begingroup\$

Mutants are not evil.

I am not going to give a line by line review as I do not see this code as having any useful value. Rather is see it as dangerous as it gives a air of safety when none is present, thus much worse than the supposed monsters you wish to banash, as now you say there are none to worry about (in fact there are more).

First some basics..

Mutation

Object mutation is not a bad thing. In javascript it is a fundamental part of the language and contributes much to what makes it such an enjoyable language to use.

Mutation has turned into a cover all term referring to any change to an object, its structure, or content of its properties. The term is synonymous with bad practice, even assigning a variable is considered mutation and bad these days.

The term "Mutation" should be used to only when changes are made in such a way as to result in a state that can not be trusted to work.

Good and bad mutants

Consider the following simple object

var myNum = {
    valueOf : 0,
    add(num) { this.valueOf += num.valueOf; return this },
    toString() { return Number(this.valueOf).toFixed(2) }    
};

// created as follows
var numA = {...myNum, valueOf: 1};
var numB = {...myNum, valueOf: 2};

// use it 
console.log(numB.add(numA).valueOf); // >> 3
element.textContent = numB;  // >> 3.00 

Good mutants

We can mutate it in a variety of ways that do not effect its operational quality. These mutations must be consciously implemented and as long as all involved are aware of the mutation there is no problem

numA.message = "Foobar"; // add a property
numA.toString = function() { return Number(this.valueOf).toFixed(4) }; //instance output format
myNum.toString = function() { return Number(this.valueOf).toFixed(3) }; //default output format
myNum.value = 2; // change its default state

Bad mutants

However there are mutations that are not deliberate, or there is error in understanding that make the object or template untrustable.

numA = 1; // reassignment
numA.add(1); // improper use
numA.value = "2"; // improper type
numA.add = (num) => { this.valueOf += num; return this }; // ambiguous closure use
myNum.toString = function() { return this.valueOf.toFixed(3) }; // missing procedural logic

Does your code protect against mutants?

const numAO = NObject(numA); // original instance remains
const numA = NObject({...myNum, value: "3"}); // already mutated
// next assigns a mutant property
const numAA = numAO.set("toString",function() { return this.valueOf.toFixed(3) });
// There are many more

So... Does your code protect against mutants? No it does not.

  • The original reference still remains and can be used. Duplication does not stop mutation
  • The internal state is not protected and manipulated without using the wrappers methods.
  • Using the wrappers does not stop mutant assignment that can result in untrusted state.
  • Each wrapper call creates a new instance, yet does not delete old and likely in use references.

And quite franckly you have added another level of procedure that will have a very negative effect on the source code quality and the coders workload. You will stop using it within a few hundred lines of a project as it is cumbersome.

  • In terms of readability, massive bloat in source size
  • In terms of comprehension as it does not provide a clear semantic understanding via its naming and how it is used.
  • Striking at the heart of a coders need to be lazy (AKA succinct, efficient, and elegant) to much overhead and typing.

Trust is not an option.

To protect against bad mutation you can not rely on trust (trust that the object is used correctly). One mistake at any point in using your wrapper and the whole house of cards falls.

Protection MUST be enforceable. Protection against mutation means that you simply can not create bad mutations.

How to protect against mutants

JavaScript is particularly prone to mutation, it is neither type nor object (class) safe.

Mutation has long been a problem in programming, and there are many ways devized to protect against them. Some good some not so good.

  1. Not so good, Functional programing, relies on trust and is next to useless in my view.
  2. Good, Encapsulation. The best ever as it is enforced by fundamental language constructs.

    JavaScript provides a strong encapsulation mechanism that can ensure state safety no matter how much the coder wishes to break the rules.

    • Closure, encapsulates private properties safe from outside mutation.
    • Constants const as declared variables are immutable
    • Object methods to help protected properties eg myNum.toString protect against mutation Object.defineProperty(myNum,"tostring",{value : function() { return Number(this.valueOf).toFixed(2) }})
    • Getters and Setters create read and write only properties, and define a method of vetting state changes to prevent bad mutation.

Example

Taking the myNum example from above and turn it into an immutable object with enforced state safety and trust

const MyNum = (value) => {
    const error = () => { throw new ReferenceError("value is not a number") }
    const vet = (v, suppressError = true) => // vet state
        typeof (v = Number(v)) === "number" ? v : (
            suppressError ?  
                0/0 : // creates trusted NaN
                error()
        );
    value = vet(value, false); // vet new internal state
    const add = num => value += num; // encapsulated method safe from mutation 
                                     // and can run in trust (no vetting)
    return Object.freeze({ // Frozen object ensures no tampering
        set valueOf(num) { value = vet(num) },
        get valueOf() { return value },
        toString() { return value.toFixed(2) },
        add(myNum) { 
            add(vet(myNum.valueOf, false))
            return this;
        },
    });
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.