How to properly bind this to a getter/setter in Javascript
Asked Answered
B

1

8

Let's say I have a class which stores the properties of its instances in a nested object:

this.Properties = {
  "Position":{
    "X": 400,
    "Y": 100
  },
  "Colour": "#007fff7f"
};

I wanted to define special getters/setters for each of the (nested) properties so that I could add range checks / automatically update the properties of instance-specific HTML elements, etc. When I tried it with the normal method, I realised that I couldn't bind the scope to an argument in the getters/setters:

//(based on https://stackoverflow.com/a/16400626)
//Define function prototype for binding an argument without overriding the old this:
Function.prototype.BindArgs = function(...boundArgs){
  const targetFunction = this;
  return function (...args) { return targetFunction.call(this, ...boundArgs, ...args); };
};

//...

{
  get X(){
    return this.__X__;
  },
  set X(Scope, Value){
    this.__X__ = Value;
    Scope.HTMLElement.style.left = Value + "px";
  }.BindArgs(this)  //This is incorrect syntax
}

The above code doesn't run: not because BindArgs is an invalid prototype, but instead, it doesn't work because the setter isn't actually a function. The answer suggested to use Object.defineProperty, which actually worked:

Object.defineProperty(this.Properties.Position, "X", {
  "get": function(){
    return this.__X__;
  }
  "set": function(Scope, Value){
    this.__X__ = Value;
    Scope.HTMLElement.style.left = Value + "px";
  }.BindArgs(this)
});

Now, when I've got a few properties like in the example above, this would be fine, but having to do this for dozens of properties becomes extremely tedious - especially for nested properties. Is there another, tidier way, of defining custom getters/setters and being able to bind arguments to them? The normal syntax would've been ideal since it would all be inside of the object definition and not scattered around like Object.defineProperty. The obvious answer would be to use normal functions to get/set the values, but doing that would mean having to refactor a lot of code...

Bobbybobbye answered 24/8, 2020 at 17:17 Comment(0)
G
5

I suggest you use Proxies for validation. It requires very minimal code changes and you can take care of multiple properties in one fell swoop.

let validator = {
  set: function(obj, prop, value) {
    //in any of these cases you can return false or throw an error to refuse the new value
    switch(prop) {
      case "X":
        Scope.HTMLElement.style.left = value + "px";
        break;
      case "Y":
        Scope.HTMLElement.style.top = value + "px";
        break;
      case "Colour":
        Scope.HTMLElement.style.color = value;
    }

    obj[prop] = value;

    return true;
  }
};

this.Properties.Position = new Proxy(this.Properties.Position, validator);
this.Properties = new Proxy(this.Properties, validator);

Note that this uses a shortcut (the same validator for both Properties and Properties.Position), if you find you may have property name overlaps you may need multiple validator objects.

Gatefold answered 24/8, 2020 at 17:31 Comment(2)
Great, thank you so much! It's a shame that I still have to define it for each nested object, but that's a lot more readable than my Object.defineProperty mess. It also means that I don't have to use the custom function prototype since it doesn't override this, which is great!Bobbybobbye
@Bobbybobbye It's likely you can write a simple loop to define it for every nested object in a couple lines. I'm not sure what the constraints are on what you need to validate etc.Gatefold

© 2022 - 2024 — McMap. All rights reserved.