0

Using JS getters and setters, I am aiming to create the following API case:

// the user should be able to write this to update thing

thing = {
   x: 1,
   y: 2
}

// OR they should be able to write this

thing.x = 1
thing.y = 2

Right now I am using code like this:

 get thing() {
   return {
     x: this._thing.x,
     y: this._thing.y
   };
 }

 set thing(value) {
   this._thing.x = value.x,
   this._thing.y = value.y
 }

This supports the first case, but not the second case.

Can this be done in any reasonably simple way?

EDIT: I will type up an example, but a use case for this might be that thing.x and thing.y should always round to an integer using Math.round().

suncannon
  • 194
  • 1
  • 9
  • 1
    Have you looked at how to define classes. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes – funcoding Apr 07 '17 at 17:32
  • 1
    `thing` returns a new object. – Daniel A. White Apr 07 '17 at 17:33
  • Yes this is part of a class. Or are you suggesting I turn _thing into a class instance? Because, in my project, it already is, actually... – suncannon Apr 07 '17 at 17:35
  • Why have getters and setters at all? Unless there is a specific reason you are not mentioning, not using them would give you what you want for free. – Felix Kling Apr 07 '17 at 17:35
  • I shortened by code for clarity purposes of the question. Please believe me that I need getters and setters here. I will try to post a fuller code example. – suncannon Apr 07 '17 at 17:36
  • Yes `thing` returns a plain object literal, but if the user then alters that object literal by writing `this.thing.x = 5`, I cannot control it with a setter. – suncannon Apr 07 '17 at 17:37
  • *This supports the first case* - it doesn't. There's no way to do `thing = { ... }`. Only if `thing` is a property of another object. It is possible to do with proxies, but most likely you have XY problem. The one should write code considering what language is capable of, not in the opposite way. – Estus Flask Apr 07 '17 at 17:45
  • @estus yes `thing` is a property of a class – suncannon Apr 07 '17 at 17:55

3 Answers3

3

If you have to use getters and setters, a relatively simple solution would be define x and y as getters and setters as well.

get thing() {
  var self = this;
  return {
    get x() {
      return self._thing.x;
    }
    set x(value) {
      self._thing.x = value;
    }
    // same for y
  };
}

But you have to be aware that every time thing is accessed for reading, a new object will be created. Though you could avoid this by caching that object and reuse it.

In fact, I probably wouldn't have _thing at all. I'd just store _x and _y and generate an object once on demand:

class Foo {
  get thing() {
    if (this._thing) {
      return this._thing;
    }

    var self = this;
    return this._thing = {
      get x() {
        return self._x;
      }

      set x(value) {
        self._x = value;
      }
    };
  }

  set thing(value) {
    this._x = value.x;
    this._y = value.y;
  }
}
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • This looks promising! I will try it out... Would caching the object mean essentially creating it as a prior variable? Am I looking at needing `thing`, `_thing`, and `__thing`? I can see how that would get ugly but maybe useful in this specific case. – suncannon Apr 07 '17 at 17:46
  • See the more complete example I added. – Felix Kling Apr 07 '17 at 17:47
  • This is useful. It is a little tricky because for me, `_thing` is already another class instance ... but I think that the extra layer of abstraction you are describing can solve this. Returning a getter and setter w/in the getter looks right. One fix -- I think you want to use `self._x` on line 10, is that right? – suncannon Apr 07 '17 at 17:49
  • Thanks this is great. As soon as I test this out, I will mark as answered. – suncannon Apr 07 '17 at 17:51
  • This is exactly the case when we would want to have XY class instead of returning an object literal. It will look cleaner. And [it will be faster](http://stackoverflow.com/questions/36338289/object-descriptor-getter-setter-performance-in-recent-chrome-v8-versions). – Estus Flask Apr 07 '17 at 18:00
  • @estus: Yeah, that seems to make sense. – Felix Kling Apr 07 '17 at 18:02
1

...a use case for this might be that thing.x and thing.y should always round to an integer using Math.round().

Consider using a Proxy

class Parent {
  constructor() {
    this.thing = {x: 15.6, y: 2.1};
  }

  set thing(value) {
    this._thing = new Proxy(value, {
      get: function(target, property, receiver) {
        // Catch all get access to properties on the object
        if (['x', 'y'].includes(property)) {
          // for x and y, return the real value, rounded
          return Math.round(target[property]);
        }
      }
    });
  }
  
  get thing() {
    return this._thing;
  }
}

var parent = new Parent();

console.log(parent.thing.x); // 16

parent.thing.x = 13.2;

console.log(parent.thing.x); // 13

parent.thing = {x: 10.1, y: 5.4};

console.log(parent.thing.y); // 5

// This works for the Jacque Goupil's conundrum
var anotherRef = parent.thing;
anotherRef.x = 5.8;
console.log(parent.thing.x); // 6

// In fact, if you wanted to dance with the devil...
var plainObj = {x: 10.1, y: 5.4};
parent.thing = plainObj;
plainObj.x = 7.2;
console.log(parent.thing.x); // 7
parent.thing.x = 22.2;
console.log(plainObj.x); // 22.2

The proxy allows you to catch a get or set operation on a property.

Caveat: IE doesn't natively support proxies for the moment. If I had a CDN for Google's Proxy polyfill I would add it to the snippet, but I don't. Also if you use Babel, there's babel-plugin-proxy

mikeapr4
  • 2,830
  • 16
  • 24
0

I don't think you should use this pattern, and I think you'll have trouble making it work. Consider the following cases:

// calls the position setter on foo
foo.position = {x: 10, y: 20};

// calls the position getter on foo, obtaining a simple {x:10,y:20} object
var pos = foo.position;

// calls the position getter on foo and then the x getter on position
var xx = foo.position.x;

So far, everything makes sense. Then we get to this situation:

// calls the position getter on foo and then the x setter on position
foo.position.x = 7;

Since we returned a simple map with the position getter, position.x simply assigns to the returned copy and doesn't modify foo's actual position. One way to fix this would be to have the position getter return a smarter object that has a reference to the foo instance and proper getter/setters. This would allow doing:

foo.position = bar.position;
bar.x += 10;

The bar.position getter would return an object that merely acts as a view for bar's x/y properties. Then, the foo.position setter would copy bar's x/y properties into its private storage. Makes sense. bar.x += 10 ads only to bar's position.

However, the following situation would be very confusing:

var temp = foo.position;
foo.position = bar.position;
bar.position = temp;

This creates the illusion that temp is a backup copy of foo.position but it isn't. It's a view. As soon as foo's position changes, we lose the data for good. There's no easy way to copy the position.

You could try making your position objects actually store both a copy of the data as well as a reference to the original, so that you can do both set and get... It's an endless problem.

But at this point, your sugar syntax is making everything much harder to maintain as well as much less efficient.

So instead of doing all this, I'd actually make is so that the x and y getter/setters are on the foo/bar objects themselves. I'd also make any code inside the foo/bar object treat positions as if they were immutable.

Domino
  • 6,314
  • 1
  • 32
  • 58