Apply immutability effect on objects of an ECMA6 JavaScript class

7

For example, 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 innumerability, configurability, or writing ability from being changed, ie transforming the essence of the object effectively immutable, as everyone knows, there is the freeze() method.

But unfortunately, as the example below 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 handle this exception and tune the Date() object contained in the% immutable data_entrada fault attribute using ECMA6?

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

    
asked by anonymous 10.07.2017 / 13:56

2 answers

6

The method documentation Object.freeze() says, among other things, that (emphasis added):

  

Note that values that are objects still can be modified, the   unless they also are frozen.

How to make an object immutable

To make a obj object completely immutable, it is necessary to freeze each object present in obj . Here is an example 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

Can I make Date immutable?

Complementing the answer, I believe your problem is to freeze a Date JavaScript.

Calling Object.freeze() to Date will not prevent changes to that date. This is because Date does 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 matches the internal state that is associated with the   objects and is used by various ECMAScript specification algorithms. The internal    slots are not object properties ...

Therefore, freezing the object or values that are objects have no effect on the ability to modify the internal slot [[DateValue]] . So you can not make Date immutable.

But there's no way?

You can almost totally freeze Date , even though it is not 100% tamper proof and therefore not entirely unchangeable: replace all modifier methods with non-operation functions which 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 does not work 100% because nothing prevents someone from doing the following:

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

Credits for T.J. Crowder in this answer .

    
10.07.2017 / 14:16
1

I was reading about defensive programming and I believe I have found an alternative to the problem of date being changeable.

To prevent object-type values from being changed in an object frozen by freeze() , such as the question, I can apply the following changes:

Whenever I need to return a date to the system, instead of passing the attribute's% object reference, I can pass a new reference to the object data_entrada

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

When passing a new instance of Date to the system, the system will work another object with the same value of Date . 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, this 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 constructor of the object this._data_entrada and try to change the date, as the 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 terminate the problem, upon receiving the date in the constructor, I create a new reference of the date received. So, now I'm no longer saving the reference of class LavarCarro {} , but rather creating a new reference, confirm the following example:

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
}

Lastly, by applying these changes I am able to avoid the problem of attributes of object type var hoje . I believe that this way there are contraindications, and this is the reason for the posting. Anyone who can comment and help me analyze if this concept remains unchanged thank you right away.

    
10.07.2017 / 22:15