Apply immutability effect to objects in a Javascript class ECMA6

Asked

Viewed 252 times

7

To illustrate, I have the following class:

class LavarCarro {
    constructor(cor, placa, data_entrada) {
        this._cor = cor;
        this._placa = placa;
        this._data_entrada = data_entrada;

        Object.freeze(this); // congela a instância do objeto
    }

    get cor() {
        return this._cor;
    }

    get placa() {
        return this._placa;
    }

    get dataEntrada() {
        return this._data_entrada;
    }
}

To prevent existing properties, or their innumerable, configurable, or writing ability from being altered, that is to say, transform the essence of the effectively immutable object, as everyone knows, there is the method freeze().

But unfortunately, as the following example shows that object-type values in a frozen object can be changed (Freeze is shallow).

var data = new LavarCarro('Preto', 'ABC1234', new Date());
console.log(data.dataEntrada);
//Mon Jul 10 2017 08:45:56 GMT-0300 (-03) - Resultado do console
data.dataEntrada.setDate(11);
console.log(data.dataEntrada);
//Tue Jul 11 2017 08:45:56 GMT-0300 (-03) - Resultado do console

How can I treat this exception and tone the object Date() contained in the attribute data_entrada immutable using ECMA6?

Note: The purpose is that class properties LavarCarro are read-only. However, the Javascript language - until the current date - does not allow us to use access modifiers. So I use the convention underline ( _ ) attributes of class properties to indicate that they cannot be modified.

2 answers

6


Documentation of the method Object.freeze() says, among other things, that (emphasis added):

Note what values they are objects yet may be modified, the less than they also are frozen.


How to make an object immutable

To make an object obj completely immutable, it is necessary to freeze each object present in obj. Following is an example of code that does this, present in MDN:

obj1 = {
  internal: {}
};

Object.freeze(obj1);

// Objeto foi congelado mas o valor do tipo objeto ainda pode ser alterado
obj1.internal.a = 'aValue';

console.log(obj1.internal.a); // 'aValue'

// Para fazer um obj completamente imutável, congele cada objeto em obj.
// Para fazer isso, nós usamos essa função.
function deepFreeze(obj) {

  // Recuperar os nomes de propriedade definidos em obj
  var propNames = Object.getOwnPropertyNames(obj);

  // Congelar as propriedades antes de congelar-se
  propNames.forEach(function(name) {
    var prop = obj[name];

    // Congele prop se for um objeto
    if (typeof prop == 'object' && prop !== null) {
      deepFreeze(prop);
    }
  });

  // Congele-se (não faz nada se já estiver congelado)
  return Object.freeze(obj);
}

obj2 = {
  internal: {}
};

deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
console.log(obj2.internal.a); // undefined


I can make Date immutable?

Complementing the answer, I believe your problem is freezing a Javascript Date.

Call Object.freeze() in a Date nay will prevent changes at that date. This is due to the fact that Date do not use an object property to store its internal value. Instead, Date uses [[DateValue]] Internal slot. Internal slots are not properties (emphasis added):

The Internal slots correspond to the internal state that is associated with objects and used by various Ecmascript specification algorithms. The Internal slots are not properties of the object...

Therefore, freezing the object or values that are objects has no effect on the ability to modify the Internal slot [[DateValue]]. So you can’t make Date immutable.

But there’s no way at all?

You can almost fully freeze Date, despite not being 100% proof of modifications and therefore not entirely immutable: replace all modifier methods with functions without operation (or functions that throw an error) and then freeze Date. Example:

"use strict";

function semoperacao() {
}

function congelaData(data) {
  todosNomes(data).forEach(nome => {
    if (nome.startsWith("set") && typeof data[nome] === "function") {
      data[nome] = semoperacao;
    }
  });
  Object.freeze(data);
  return data;
}

function todosNomes(obj) {
  var nomes = Object.create(null);
  var thisObj;
  
  for (thisObj = obj; thisObj; thisObj = Object.getPrototypeOf(thisObj)) {
    Object.getOwnPropertyNames(thisObj).forEach(nome => {
      nomes[nome] = 1;
    });
  }
  return Object.keys(nomes);
}

let data = new Date();
congelaData(data);
console.log("Data antes de congelar: " + data);
data.setTime(0);
console.log("Data depois de congelar e tentar modificar: " + data);

It doesn’t work 100% because nothing stops someone from doing the following:

Date.prototype.setTime.call(d, 0)

Credits for T.J. Crowder in this answer.

  • I appreciate the answer. About the part of freezing each object I already knew and use the freeze() in the Date I saw in practice that it doesn’t work. The question was precisely to try to find other people’s experiences on the same subject. As if it were a hope in search of another alternative. About the information of Date immutable were of great help and added knowledge. Thank you.

  • 1

    @Djalmamanfrin you can even freeze Date partially, replacing all its modifier methods with functions without operation and then freezing it. Partially because there are still ways to modify the date. If you want I can complement the answer with this way.

  • you are referring to using the methods getter to prevent the attribute from being a value attribute? Example: get dataEntrada(){}. All knowledge is valid, If you can change thank you.

  • @Djalmamanfrin updated the answer.

  • 1

    Interesting this last change you added. I will take a look at it calmly and perform some tests.

1

I was reading about defensive programming and I believe I’ve found an alternative to the problem of date be mutable.

To prevent object type values in an object frozen by freeze() be amended, such as the question, I can apply the following amendments:

Every time I need to return a date to the system, instead of passing the attribute reference to the data_entrada of the object, I can pass a new object reference Date

get dataEntrada() {
    return new Date(this._data_entrada.getTime());
}

When passing a new instance of Date for the system, the system will work another object with the same value of this._data_entrada. So, if someone tries to change the date, it will only change a copy and not the date of the object.

Now if I apply the test to change the date of the object, it will not occur.

var data = new LavarCarro('Preto', 'ABC1234', new Date());
console.log(data.dataEntrada);
//Mon Jul 10 2017 08:45:56 GMT-0300 (-03) - Resultado do console
data.dataEntrada.setDate(11);
console.log(data.dataEntrada);
//Tue Jul 10 2017 08:45:56 GMT-0300 (-03) - Resultado do console

But if I create a date reference and pass to the object builder class LavarCarro {} and try to change the date, as example below the date will continue to be edited.

var hoje = new Date();
var data = new LavarCarro('Preto', 'ABC1234', hoje);
console.log(data.dataEntrada);
//Mon Jul 10 2017 08:45:56 GMT-0300 (-03) - Resultado do console
hoje.setDate(11);
console.log(data.dataEntrada);
//Tue Jul 11 2017 08:45:56 GMT-0300 (-03) - Resultado do console

To resolve and finish the problem, when receiving the date in the constructor, I create a new reference of the received date. That way, now I’m no longer keeping the reference to var hoje, but creating a new reference, confirm example below:

constructor(cor, placa, data_entrada) {
    this._cor = cor;
    this._placa = placa;
    this._data_entrada = new Date(data_entrada.getTime());

    Object.freeze(this); // congela a instância do objeto
}

Finally, by applying these changes I can avoid the object type attribute problem Date. I believe that this way there are contraindications, this being the reason for the posting. Who can comment and help me to analyze if this concept remains unchanged thank you.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.