Search code examples
javascriptarraysobserversproxy-pattern

Javascript observer or proxy without all changes going through proxy


I'm writing a subclass of arrays in Javascript to have better support for matrix operations (I know others exist, this is partially for me to re-teach myself linear algebra), and what I want is to have some properties that are reset whenever any values in the matrix are adjusted. Some calculations like the determinant are computationally intensive, and I'd like to be able to store them to avoid re-calculation, but then they need to be reset to null whenever any matrix elements are changed.

Essentially, it seems like what i want is the deprecated Array.observe(). And the replacement, proxies, seem like a lot of overhead for this one thing. As alluded to in some of the comments on Detecting Changes in a Javascript Array using the proxy object that were not directly addressed, I don't want to have to access my matrices only ever through proxies. I use a lot of handy [i][j] indexing and [mat[i], mat[j]] = [mat[j], mat[i]] in the code I've written so far.

class Matrix extends Array {
  constructor() {
    var args  = [];
    for (var i = 0; i < arguments.length; i++) {
        if (Array.isArray(arguments[i])) {
        args.push(new Matrix(...arguments[i]));
      } else {
        args.push(arguments[i]);
      }
    }
    super(...args);
    this._determinant = null;
  }
  determ(forceRecalculate = false) {
    if (this._determinant === null || forceRecalculate) {
        this.upperEchelon();
    }
    return this._determinant;
  }
  upperEchelon(reduced = false) {
  //There's a lot of code here but in the process of doing this other thing
  //you get 99% of the way to calculating the determinant so it does this
  this._determinant = factor;
  }
}

Basically, I want anything like mat[0][0] = 10 or mat.push([2,4,5]) that updates the values in the matrix to set mat._determinant = null. Or any equivalent method of flagging that it needs to be re-calculated next time it's asked for. I'm not opposed to using proxies necessarily if someone can help me figure out the implementation, I would just rather have this set-to-null-on-update property be inherent to my class functionality.

What I really want is a way to overload base methods like [] a la C# so the functions that do the updating would trigger this without changing syntax, but I've resigned myself to not having that in JS.


Solution

  • While a Proxy would work, it would also be pretty slow. A different approach would be for every method that needs to use the value of _determinant go through a different function first to check to see if the _determinant needs to be updated (and if so, updates it). This way, the expensive recalculation is not done every time the array changes, but only just in time for the result to be used. For example:

    class Matrix extends Array {
      constructor() {
        var args  = [];
        for (var i = 0; i < arguments.length; i++) {
          if (Array.isArray(arguments[i])) {
            args.push(new Matrix(...arguments[i]));
          } else {
            args.push(arguments[i]);
          }
        }
        super(...args);
        this._determinant = null;
      }
      // next method is effectively a recursive deep join
      // could also use toString if it doesn't interfere with anything else
      getString() {
        const itemsStr = this.map((item) => (
        item instanceof Matrix
          ? item.getString()
          : item
        ))
        .join(',');
        const result = '[' + itemsStr + ']';
        return result;
      }
      getDeterm() {
        const newString = this.getString();
        if (newString !== this._lastString) {
          this._lastString = newString;
          this.upperEchelon();
        }
        return this._determinant;
      }
      upperEchelon() {
        console.log('running upperEchelon');
        this._determinant = Math.random();
      }
    }
    
    const m = new Matrix([2, 3, 4], 5);
    console.log(m.getDeterm());
    // Not calculated again:
    console.log(m.getDeterm());
    // Mutation, next call of getDeterm will run upperEchelon:
    m[0][0] = 1;
    console.log(m.getDeterm());