What is the way to check and react to a change of state of an array using pure Javascript?

Asked

Viewed 153 times

6

I’m studying about the pattern Observer where in an example done in Java it was demonstrated that an X class any called a specific method Y and this called methods updateof the observers, that is to say a change of state X, notified the observers of X through update.

I was tempted to imagine how to implement this standard to notify that the state of any array had been changed by Javascript array methods, such as push and pop:

const arr = []

arr.push(...[1,2,3]);

console.log(arr)

arr.pop()

console.log(arr)

I wondered how to implement this pattern, or something similar, so that when the state of that array arr change, I call observer methods or observational functions as:

// para o "push"
function alterouEstadoPush() {
  console.log('Alterou o estado...')
  // faz alguma coisa...
}

// para o "pop"
function alterouEstadoPop() {
  console.log('Alterou o estado...')
  // faz outra coisa...
}
  • What or what possible ways to implement this for this array?
  • Some other project pattern could be implemented to reach the same end?

3 answers

5

A simpler alternative is to use a subclass that does nothing but execute any code - in this case some logs - and delegate to the original implementation of the superclass.

Is trivial:

class ObservedArray extends Array {
  push(...args) {
    console.log(`Called push with '${args.join(', ')}'.`);
    return super.push(...args);
  }
  
  pop() {
    const popped = super.pop();
    console.log(`Called pop and removed '${popped}'.`);
    return popped;
  }
}

const arr = new ObservedArray(1, 2, 3);
console.log(arr);

arr.push(4, 5);
arr.pop();

console.log(arr);


Of course, like the two other answers to this question, it has the disadvantage of being "burlable" when called by the method Array.prototype.push directly, using the Function.prototpye.call to pass the observed array as thisArg directly.

5

Possible solution

There is a way to call a function when method, which changes the state of that array, is called. Using the Object.defineProperty, where we could do something like extend the functionality of each method of arr.

Let’s go from example to the push, with some commented points:

// "yourArray" é o array que se quer monitorar a mudança de estado
// "push" é o método de array que vamos estender o comportamento
Object.defineProperty(yourArray, "push", {
    configurable: true,
    enumerable: false,
    writable: true, // valores mantidos com base em: Object.getOwnPropertyDescriptor(Array.prototype, "push")
    value: function (...args)
    {
        // mantemos o comportamento de original de "push" aqui
        const result = Array.prototype.push.apply(this, args);   

        // chama a função para essa mudanca de estado
        alterouEstadoPush();

        // retornamos o mesmo valor do comportamento original de "push"
        return result; 
    }
});

Using the above code for the arr:

const arr = []

// código antes de chamar o "push"
Object.defineProperty(arr, "push", {
  configurable: true,
  enumerable: false,
  writable: true,
  value: function(...args) {
    const result = Array.prototype.push.apply(this, args);

    alterouEstadoPush();

    return result;
  }
});

arr.push(...[1, 2, 3]);

console.log(arr)

function alterouEstadoPush() {
  console.log('Alterou o estado via "push".')
}

Creating better and more scalable code, creating a function that will receive a pointer to the array to be worked on, and informing for observer functions the value of the new array state:

const arr = []

addObserverMethods('push', arr, alterouEstadoPush)
addObserverMethods('pop', arr, alterouEstadoPop)

arr.push(...[1, 2, 3])

arr.pop()

// recebe o novo estado do array
function alterouEstadoPush (state) {
  console.log('Alterou o estado via "push".')
  console.log('Novo estado: ', state)
}

// recebe o novo estado do array
function alterouEstadoPop (state) {
  console.log('Alterou o estado via "pop".')
  console.log('Novo estado: ', state)
}

/**
 *
 * @param method método a ser adicionado a funcao observadora
 * @param array array que terá observadores em seu método
 * @param callback funcao a ser chamada após chamada do método de `array`
 */
function addObserverMethods (method, array, callback) {
  // métodos autorizados para observadores
  const methods = ['pop', 'push']

  // verificacao de seguranca
  if (typeof method !== 'string') {
    throw new TypeError('The "method" param must be string')
  }
  
  // verifica se o método é permitido
  if (!methods.includes(method)) {
    throw new Error('Method not allowed. Use one of these: ' + methods)
  }

  Object.defineProperty(array, method, {
    configurable: true,
    enumerable: false,
    writable: true,
    value: function (...args) {
      const result = Array.prototype[method].apply(this, args)

      // passa o valor do novo estado para as funcoes observadoras
      callback(this)

      return result
    }
  })
}

  • 1

    Only one detail: instead of arr.push(...[1, 2, 3]), why not just do arr.push(1, 2, 3)? After all, with fixed values there is no reason to create the array and then use spread in it... and in the last code, I think arr2 was left over. Finally, don’t forget thereof :-)

4

An option, which does not modify the array properties, would be to use a Proxy:

// funções que quero alterar
const functions = ['push', 'pop'];

// handler que intercepta uma chamada de função/método
var methodHandler = {
    apply: function (target, thisArg, argumentsList) {
        // aqui seria o "callback", a parte que eu faço antes de chamar a função/método original
        console.log(`Chamando ${target.name} no array [${thisArg.join(',')}], com argumentos: ${argumentsList.join(',')}`);
        // chamar a função/método original
        return target.apply(thisArg, argumentsList);
    }
};

// handler que intercepta propriedades do array
var arrayChangeHandler = {
    get: function (target, prop) {
        // se é uma das funções que você quer interceptar, retorne um proxy da função em vez dela mesma
        if (typeof target[prop] === 'function' && functions.includes(target[prop].name)) {
            return new Proxy(target[prop], methodHandler);
        }
        // se não for uma das funções a serem alteradas, ou se for qualquer outra propriedade, retorne-a sem modificação
        return target[prop];
    }
};

const arr = [];
const proxiedArray = new Proxy(arr, arrayChangeHandler);
proxiedArray.push('a', 'b', 'c');
proxiedArray.pop();
console.log(proxiedArray); // ['a', 'b']
// o array original também é modificado
console.log(arr); // ['a', 'b']

Here we do two main things:

  1. in the array, we define the Handler get, which is called when accessing a property of the object in question (in this case, the object is the array, and the property can be any one, including methods). However, this Handler does not intercept the method call, so we need the next item:
  2. in case the accessed property is one of the methods we want to change, I return another Proxy (but this is done in the method, not in the array, and I use the Handler apply to intercept the call from it). This, in turn, executes something (in the example I just put console.log, but it could be anything you wanted), and then calls the original method.
    • if it is not one of the methods to be changed, simply return the property without modification

In this case, the output of the above code is:

Chamando push no array [], com argumentos: a,b,c
Chamando pop no array [a,b,c], com argumentos: 
[ 'a', 'b' ]
[ 'a', 'b' ]

Note that even if calls are made on proxy, the original array is changed by them.


Of course there you can generalize, creating for example a more general function that receives an object and the callbacks to be executed when the methods are called. Something like:

// Recebe um objeto e os callbacks a serem executados quando os métodos são chamados.
// Cada função de callback recebe como parâmetros o objeto e os argumentos passados ao método original.
function proxyWithChangedMethods(object, callbacks) {
    // handler que intercepta a chamda de uma função/método
    var methodHandler = {
        apply: function (target, thisArg, argumentsList) {
            // chamar o respectivo callback
            if (typeof callbacks[target.name] === 'function') {
                callbacks[target.name](thisArg, argumentsList);
            }
            // chamar a função/método original
            return Reflect.apply(target, thisArg, argumentsList);
        }
    };

    const funcNames = Object.keys(callbacks);
    // handler que intercepta propriedades do objeto
    var objChangeHandler = {
        get: function (target, prop) {
            // se é uma das funções que você quer interceptar, retorne um proxy da função em vez dela mesma
            if (typeof target[prop] === 'function' && funcNames.includes(target[prop].name)) {
                return new Proxy(target[prop], methodHandler);
            }
            // se não for uma das funções a serem alteradas, ou se for qualquer outra propriedade, retorne-a sem modificação
            return Reflect.get(target, prop);
        }
    };

    return new Proxy(object, objChangeHandler);
}

// métodos que quero alterar, com os respectivos callbacks a serem chamados antes do método original
const callbacks = {
    'push': function(array, args) {
        console.log(`Adicionando os elementos ${args.join(', ')} no array [${array.join(',')}]`);
    },
    'pop': function(array, args) {
        console.log(`Removendo o último elemento do array [${array.join(', ')}]`);
    }
};

const arr = [];
const proxiedArray = proxyWithChangedMethods(arr, callbacks);
proxiedArray.push('a', 'b', 'c');
proxiedArray.pop();
console.log(proxiedArray);

// o array original também é modificado, ele só não executa os callbacks
console.log(arr); // [ 'a', 'b' ]
arr.push('d'); // não executa o callback
console.log(arr) //  // [ 'a', 'b', 'd' ]
console.log(proxiedArray) //  // [ 'a', 'b', 'd' ]

I also exchanged the method call and the acquisition of property for the object Reflect. There are some differences between Reflect.apply and Function.prototype.apply (one of them is that if the prototype of Function have a apply superscript, Reflect.apply will not be affected - see link for more details).

The exit code above is:

Adicionando os elementos a, b, c no array []
Removendo o último elemento do array [a, b, c]
[ 'a', 'b' ]
[ 'a', 'b' ]
[ 'a', 'b', 'd' ]
[ 'a', 'b', 'd' ]

Of course, if you want to modify the methods of the array itself, then just do as the another answer. And just to leave another alternative (very similar to your):

// métodos que quero alterar, com os respectivos callbacks a serem chamados antes do método original
const callbacks = {
    'push': function (array, args) {
        console.log(`Adicionando os elementos ${args.join(', ')} no array [${array.join(',')}]`);
    },
    'pop': function (array, args) {
        console.log(`Removendo o último elemento do array [${array.join(', ')}]`);
    }
};

const arr = [];
for (const[funcName, callback] of Object.entries(callbacks)) {
    const prop = arr[funcName];
    if (typeof prop === 'function') { // só pra garantir :-)
        const originalFunc = prop;
        Object.defineProperty(arr, funcName, { configurable: true, enumerable: false, writable: true,
            value: function (...args) {
                callback(arr, args); // chama o callback, passa o array e os argumentos do método
                return Reflect.apply(originalFunc, arr, args); // chama o método original
            }
        });
    }
}
arr.push('a', 'b', 'c');
arr.pop();
console.log(arr); // ['a', 'b']

The exit is:

Adicionando os elementos a, b, c no array []
Removendo o último elemento do array [a, b, c]
[ 'a', 'b' ]

Browser other questions tagged

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