如何正确实现 getter/setter 功能,无论是针对属性的 'get' 和 'set',还是针对两个方法和属性的 'get'?

How to properly implement getter/setter functionality, either for both a property's `get` and `set`, or for two methods and a property's `get`?

提问人: 提问时间:10/16/2023 最后编辑:Peter Seliger 更新时间:10/17/2023 访问量:122

问:

我有一个逻辑,如果类内部或外部更改了 a,则需要被动地运行某些函数
this.variableName

问题出在这个逻辑上,getter 不起作用,我得到.undefined

不仅如此,还有最大堆栈误差。
因为就像:设置内部的 setter,重新设置 ,重新调用另一个时间的 setter,设置 this,依此类推......(但这不是我的意思)
thisthis


示例版本,没有参数,因此,只是错误的最小示例。

class MyClass {
  constructor() {
     this.isOn = false;
  }

  set isOn(value) {
    this.isOn = value;
    // another functions that do stuff. you can put here a bunch of console.logs if you want just for the demo
  }

  // the idea is another, but i will show you the simpliest method, I could imagine, just to make you get the idea
  
  turnOn() {
     this.isOn = true;
  }
 
  turnOff() {
     this.isOn = false;
  }

  // using this approach, 
  // I don't need to copy paste the same functions in this 2 methods, 
  // so I just need to care about the var, 
  // without using a imperative approach, 
  // but a declarative approach
}

我知道我们可以使用 _isOn,但这实际上与我们想要的相反,因为我希望 setter 与 getter 同名,


并且仍然在 setter 上执行逻辑。

希望你明白我的想法。谢谢

JavaScript oop getter-setter 封装

评论

0赞 Konrad 10/16/2023
不可能做到。当你运行时,你引用的是 setter,而不是变量this.isOn =
0赞 Konrad 10/16/2023
使用私有类属性。有什么理由不使用吗?this.#isOn_isOn
0赞 10/16/2023
@Konrad我不想使用它,因为最后 getter 和 setter 会有不同的名称
0赞 Oskar Grosser 10/16/2023
getter 和 setter 将是内部变量的接口。如果同时命名 getter 和 setter,则两者将具有相同的名称。或者你的意思是你希望 getter/setter 与内部变量同名?如果是这样,那么康拉德的评论适用。isOn
1赞 Peter Seliger 10/16/2023
1/2 ...无论如何,存在一个设计缺陷,一个要么使用一个 get/set 解决方案来公开为单个公共属性(这里),要么使用两个不同名称的公共方法(这里 /)。由于这两种解决方案都至少实现了功能,因此两者都需要通过更改/控制某种私有值(无论是私有实例字段还是局部变量,甚至是基于解决方案)来实现这一点。isOnturnOnturnOffsetWeakMap

答:

2赞 Konrad 10/16/2023 #1

您可以使用私有属性

class MyClass {
  #isOn = false;

  set isOn(value) {
    this.#isOn = value;
  }
  
  get isOn() {
    return this.#isOn
  }

  turnOn() {
     this.isOn = true;
  }
 
  turnOff() {
     this.isOn = false;
  }
}

const x = new MyClass

x.isOn = true

console.log(x.isOn)

// This will throw an error:
// console.log(x.#isOn)

评论

0赞 10/16/2023
对不起,但这不是我想要的。我想以某种方式使用 set 和 get 拥有相同的名称。我希望有人知道如何利用这一点,并使实际工作,因为在您的示例中:turnOn 或 turnOff 我们仍然使用 #,它们不运行该函数,而只是更改值
2赞 Konrad 10/16/2023
在,你可以直接使用turnOffturnOffthis.isOn
0赞 10/16/2023
编辑答案,我会给你的答案投赞成票,并接受它。也许这就是我想要的,让我们看看
1赞 Bergi 10/16/2023
您应该使用 setter 方法,而不是两者兼而有之。turnOn/Off
1赞 Bergi 10/16/2023
@Konrad 没有代理。该函数(或函数调用)的代码应该放在 setter 或方法中 - OP 在其代码中已经有这方面的注释。
-2赞 Oskar Grosser 10/16/2023 #2

正如 Konrad 所评论的,你不能用一个标识符引用两个不同的对象。相反,在从内部引用时,可以使用内部成员

或者,如果我们将数据与类分开,我们可以实例化两个对象:一个是更改数据(例如通过代理)会产生副作用,另一个是直接更改。

例:

class MyClass {
  #data = null;

  constructor(data) {
    this.#data = data;
  }
  
  set isOn(value) {
    this.#data.isOn = value;
  }
  get isOn() {
    return this.#data.isOn;
  }
  
  turnOn() {
    this.isOn = true;
  }

  turnOff() {
    this.isOn = false;
  }
}

const data = {
  isOn: false
};

const dataProxy = new Proxy(data, {
  get(target, prop, receiver) {
    if (prop === "isOn") {
      console.log("get side-effect");
    }
    return Reflect.get(...arguments);
  },
  set(target, prop, value, receiver) {
    if (prop === "isOn") {
      console.log("set side-effect");
    }
    return Reflect.set(...arguments);
  }
});

const directObject = new MyClass(data);
const indirectObject = new MyClass(dataProxy);

console.log("Direct isOn:", directObject.isOn);
console.log("Direct turnOn()");
directObject.turnOn();

console.log("Indirect isOn:", indirectObject.isOn);
console.log("Indirect turnOff()");
indirectObject.turnOff();

在这里,我们有两个类对象,它们作用于相同的数据,其中一个通过代理。被代理的方法会引起副作用。MyClassMyClass

这允许行为的一个实现(一个类)和一组数据。代理允许控制访问并(如您所愿)在访问时引起副作用。MyClass

1赞 Peter Seliger 10/16/2023 #3

从我上面的评论......

无论如何,存在一个设计缺陷,要么使用 / 解决方案作为单个公共属性公开的内容(此处),要么使用两个不同名称的公共方法(此处 /)。由于这两种解决方案都至少实现了功能,因此两者都需要通过更改/控制某种私有值(无论是私有实例字段还是局部变量,甚至是基于 WeakMap 的解决方案)来实现这一点。 到目前为止,OP 的代码试图实现的目标,以及公认的答案提供的内容,都不是必需的。已经有两个明确的 setter,比如 /,它们都确实改变了公共属性是完全错误的,至少它容易出错。更糟糕的是,已经通过 / 实现了。要么选择后者,要么选择前者,并且正确实现任何一种变体。不要将两者混合使用;这没有任何意义。getsetisOnturnOnturnOffsetturnOnturnOffisOnisOngetset

考虑到上面所说的,下面有两个代码示例。

第一个侧重于单个公共 isOn 属性的 get/set 实现,而第二个则选择显式方法(如 turnOn/turnOff)的方法,以及唯一公共 isOn 属性单个 get

每种方法都提供两种实现,一种选择私有实例字段,另一种基于封装的局部变量

class SwitchStateWithGetSetAndPrivateFields {
  #isOn;
  #handleSwitchChange;

  constructor(switchDefault = false, onSwitchChange) {
    this.#isOn = Boolean(switchDefault);
    this.#handleSwitchChange = onSwitchChange;
  }
  set isOn(value) {
    if ('boolean' === typeof value) {

      if (this.#isOn !== value) {

        this.#isOn = value;
        this.#handleSwitchChange(this);
      }
    } else {
      throw new TypeError('The assigned value needs to be a boolean type.');
    }
  }
  get isOn() {
    return this.#isOn;
  }
}

class SwitchStateWithGetSetAndLocalValues {

  constructor(switchDefault = false, handleSwitchChange) {
    let isOn = Boolean(switchDefault);

    Object.defineProperty(this, 'isOn', {
      set(value) {
        if ('boolean' === typeof value) {

          if (isOn !== value) {

            isOn = value;
            handleSwitchChange(this);
          }
        } else {
          throw new TypeError('The assigned value needs to be a boolean type.');
        }
      },
      get() {
        return isOn;
      },
      enumerable: true,
    });
  }
}

function logSwitchStateOfBoundFieldtype(fieldtype, instance) {
  console.log({ [`${ fieldtype }IsOn`]: instance.isOn });
}

const onOffStateRadio =
  new SwitchStateWithGetSetAndPrivateFields(
    true, logSwitchStateOfBoundFieldtype.bind(null, 'radio')
  );
const onOffStateText =
  new SwitchStateWithGetSetAndLocalValues(
    false, logSwitchStateOfBoundFieldtype.bind(null, 'text')
  );

document
  .querySelector('form')
  .addEventListener('submit', evt => {
    evt.preventDefault();

    const booleanMap = {
      'true': true,
      'false': false,
      '': false,
    };
    const formElements = evt.currentTarget.elements;

    const radioValue = booleanMap[formElements['is_on'].value];
    const textValue = booleanMap[formElements['is_on_text'].value.trim().toLowerCase()];

    onOffStateRadio.isOn = radioValue;
    onOffStateText.isOn = textValue;
  });

logSwitchStateOfBoundFieldtype('radio', onOffStateRadio);
logSwitchStateOfBoundFieldtype('text', onOffStateText);
body { margin: 0; }
form { width: 40%; }
fieldset { margin: 5px 0; }
form > label { display: block; margin: 5px 0 10px 14px; }
form > label > input, form > button { float: right; }
.as-console-wrapper { left: auto!important; min-height: 100%; width: 59%; }
<form>

  <fieldset>
    <legend>isOn:</legend>
    <label>
      <span>true</span>
      <input type="radio" name="is_on" value="true" checked/>
    </label>
    <label>
      <span>false</span>
      <input type="radio" name="is_on" value="false"/>
    </label>
  </fieldset>

  <label>
    <span>isOn:</span>
    <input type="text" name="is_on_text" placeholder="whatever"/>
  </label>

  <button type="submit">apply values</button>
</form>

class SwitchStateWithExplicitMethodsSingleGetAndPrivateFields {
  #isOn;
  #handleSwitchChange;

  constructor(switchDefault = false, onSwitchChange) {
    this.#isOn = Boolean(switchDefault);
    this.#handleSwitchChange = onSwitchChange;
  }
  get isOn() {
    return this.#isOn;
  }

  turnOn() {
    if (this.#isOn === false) {

      this.#isOn = true;
      this.#handleSwitchChange(this);
    }
  }
  turnOff() {
    if (this.#isOn === true) {

      this.#isOn = false;
      this.#handleSwitchChange(this);
    }
  }
}

class SwitchStateWithExplicitMethodsSingleGetAndLocalValues {

  constructor(switchDefault = false, handleSwitchChange) {
    let isOn = Boolean(switchDefault);

    Object.defineProperty(this, 'isOn', {
      get() {
        return isOn;
      },
      enumerable: true,
    });

    this.turnOn = function turnOn() {
      if (isOn === false) {

        isOn = true;
        handleSwitchChange(this);
      }
    }
    this.turnOff = function turnOff() {
      if (isOn === true) {

        isOn = false;
        handleSwitchChange(this);
      }
    }
  }
}

function logSwitchStateOfBoundFieldtype(fieldtype, instance) {
  console.log({ [`${ fieldtype }IsOn`]: instance.isOn });
}

const onOffStateRadio =
  new SwitchStateWithExplicitMethodsSingleGetAndPrivateFields(
    true, logSwitchStateOfBoundFieldtype.bind(null, 'radio')
  );
const onOffStateText =
  new SwitchStateWithExplicitMethodsSingleGetAndLocalValues(
    false, logSwitchStateOfBoundFieldtype.bind(null, 'text')
  );

document
  .querySelector('form')
  .addEventListener('submit', evt => {
    evt.preventDefault();

    const booleanMap = {
      'true': true,
      'false': false,
      '': false,
    };
    const formElements = evt.currentTarget.elements;

    const radioValue = booleanMap[formElements['is_on'].value];
    const textValue = booleanMap[formElements['is_on_text'].value.trim().toLowerCase()];

    onOffStateRadio[radioValue && 'turnOn' || 'turnOff']();
    onOffStateText[textValue && 'turnOn' || 'turnOff']();
  });

logSwitchStateOfBoundFieldtype('radio', onOffStateRadio);
logSwitchStateOfBoundFieldtype('text', onOffStateText);
body { margin: 0; }
form { width: 40%; }
fieldset { margin: 5px 0; }
form > label { display: block; margin: 5px 0 10px 14px; }
form > label > input, form > button { float: right; }
.as-console-wrapper { left: auto!important; min-height: 100%; width: 59%; }
<form>

  <fieldset>
    <legend>isOn:</legend>
    <label>
      <span>true</span>
      <input type="radio" name="is_on" value="true" checked/>
    </label>
    <label>
      <span>false</span>
      <input type="radio" name="is_on" value="false"/>
    </label>
  </fieldset>

  <label>
    <span>isOn:</span>
    <input type="text" name="is_on_text" placeholder="whatever"/>
  </label>

  <button type="submit">apply values</button>
</form>

评论

0赞 Peter Seliger 10/19/2023
@stackdeveloper......OP 可能会查看上面提供的延迟答案和代码示例。