How to correctly implement an own property getter/setter via Object.defineProperty and utilizing class syntax?

122 Views Asked by At

it's my code

"use strict";

class Employee {
  constructor(firstName, lastName, age, salary, gender) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._age = age;
    this._salary = salary;

    Object.defineProperty(this, "_gender", {
      enumerable: true,
      configurable: true,
      set(value) {
        if(typeof value !== "boolean"){
          throw "Wrong type of data.";
        }else{
          this._newGender = value;
        }
        
      },
      get() {
        return this._newGender;
      }
    });
    this._gender = gender; 


    this._retirementYears = 0;
  }

  retirement() {
    if (this._gender === true) {
      return this._retirementYears = 65 - this._age;
    } else if (this._gender === false) {
      return this._retirementYears = 60 - this._age;
    } else {
      console.error("Invalid gender.");
    }
  }
}

class Teacher extends Employee {
  constructor(firstName, lastName, age, salary, gender, subject, yearsOfExperience) {
    super(firstName, lastName, age, salary, gender);
    this._subject = subject;
    this._yearsOfExperience = yearsOfExperience;
  }
}

const mathTeacher = new Teacher("John", "Smith", 35, 6000, true, "Math", 5);

console.log(mathTeacher);

After console.log(mathTeacher) I get two properties relating to gender (_gender and _newGender). I want to get only one property called _gender and I have no idea how to do it. I tried to assign assign value to this._gender but it doesn't work due to "Maximum call stack size exceeded" error

2

There are 2 best solutions below

8
Alexander Nenashev On BEST ANSWER

There are several solution but what made the question interesting, whether cloning of an class instance works for each approach.

To clone an class instance we could use:

Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);

As turned out only the last approach works with cloning when the property is defined in the prototype. The provided cloning doesn't call the constructor. And calling the constructor is problematic since the arguments are unknown at the time of cloning.

But I would say that we need some clone() method in a class to make cloning more flexible. So the cloning failures seen here aren't critical imho.

So let's start it rolling:

You can use a private property (CLONING FAILED !!!):

class Employee {
  #_newGender = false;
  constructor(firstName, lastName, age, salary, gender) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._age = age;
    this._salary = salary;

    Object.defineProperty(this, "_gender", {
      enumerable: true,
      configurable: true,
      set(value) {
        if(typeof value !== "boolean"){
          throw "Wrong type of data.";
        }else{
          this.#_newGender = value;
        }
      },
      get() {
        return this.#_newGender;
      }
    });
    this._gender = gender; 


    this._retirementYears = 0;
  }

  retirement() {
    if (this._gender === true) {
      return this._retirementYears = 65 - this._age;
    } else if (this._gender === false) {
      return this._retirementYears = 60 - this._age;
    } else {
      console.error("Invalid gender.");
    }
  }
}

class Teacher extends Employee {
  constructor(firstName, lastName, age, salary, gender, subject, yearsOfExperience) {
    super(firstName, lastName, age, salary, gender);
    this._subject = subject;
    this._yearsOfExperience = yearsOfExperience;
  }
}

const mathTeacher = new Teacher("John", "Smith", 35, 6000, true, "Math", 5);

try{
  mathTeacher._gender = 'test';
} catch(e){
  console.log(e);
}

console.log(mathTeacher);

const clone = Object.assign(Object.create(Object.getPrototypeOf(mathTeacher)), mathTeacher)
console.log(clone);
try{
  clone._gender = 'test';
} catch(e){
  console.log(e);
}
clone._gender = false;
console.log(mathTeacher, clone);

Or make _newGender not enumerable (CLONING FAILED !!!):

class Employee {
  
  constructor(firstName, lastName, age, salary, gender) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._age = age;
    this._salary = salary;

    Object.defineProperty(this, "_newGender", {enumerable: false, value: false, writable: true});
    
    Object.defineProperty(this, "_gender", {
      enumerable: true,
      configurable: true,
      set(value) {
        if(typeof value !== "boolean"){
          throw "Wrong type of data.";
        }else{
          this._newGender = value;
        }
        
      },
      get() {
        return this._newGender;
      }
    });
    this._gender = gender; 


    this._retirementYears = 0;
  }

  retirement() {
    if (this._gender === true) {
      return this._retirementYears = 65 - this._age;
    } else if (this._gender === false) {
      return this._retirementYears = 60 - this._age;
    } else {
      console.error("Invalid gender.");
    }
  }
}

class Teacher extends Employee {
  constructor(firstName, lastName, age, salary, gender, subject, yearsOfExperience) {
    super(firstName, lastName, age, salary, gender);
    this._subject = subject;
    this._yearsOfExperience = yearsOfExperience;
  }
}

const mathTeacher = new Teacher("John", "Smith", 35, 6000, true, "Math", 5);

try{
  mathTeacher._gender = 'test';
} catch(e){
  console.log(e);
}

console.log(mathTeacher);

const clone = Object.assign(Object.create(Object.getPrototypeOf(mathTeacher)), mathTeacher)
console.log(clone);
try{
  clone._gender = 'test';
} catch(e){
  console.log(e);
}
clone._gender = false;
console.log(mathTeacher, clone);

Actually you could create some utility for this and keep the real value in its scope (CLONING FAILED !!!):

const createValidatedProp = (obj, prop, cb, value) => {

    let val = value;
    
    Object.defineProperty(obj, prop, {
      enumerable: true,
      configurable: true,
      set(value) {
        if(!cb(value)){
          throw "Wrong type of data.";
        }else{
          val = value;
        }
        
      },
      get() {
        return val;
      }
      
    });
  
};

class Employee {
  
  constructor(firstName, lastName, age, salary, gender) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._age = age;
    this._salary = salary;
    createValidatedProp(this, '_gender', val => typeof val === 'boolean', gender);
    this._retirementYears = 0;
  }

  retirement() {
    if (this._gender === true) {
      return this._retirementYears = 65 - this._age;
    } else if (this._gender === false) {
      return this._retirementYears = 60 - this._age;
    } else {
      console.error("Invalid gender.");
    }
  }
}

class Teacher extends Employee {
  constructor(firstName, lastName, age, salary, gender, subject, yearsOfExperience) {
    super(firstName, lastName, age, salary, gender);
    this._subject = subject;
    this._yearsOfExperience = yearsOfExperience;
  }
}

const mathTeacher = new Teacher("John", "Smith", 35, 6000, true, "Math", 5);

try{
  mathTeacher._gender = 'test';
} catch(e){
  console.log(e);
}

console.log(mathTeacher);

const clone = Object.assign(Object.create(Object.getPrototypeOf(mathTeacher)), mathTeacher)
console.log(clone);
try{
  clone._gender = 'test';
} catch(e){
  console.log(e);
}
clone._gender = false;
console.log(mathTeacher, clone);

If you want to use the getter/setter in the prototype (like a JS class would do (but the property isn't enumerable)) use a symbol to keep the real value (symbol properties aren't enumerable):

Symbol is a built-in object whose constructor returns a symbol primitive — also called a Symbol value or just a Symbol — that's guaranteed to be unique. Symbols are often used to add unique property keys to an object that won't collide with keys any other code might add to the object, and which are hidden from any mechanisms other code will typically use to access the object. That enables a form of weak encapsulation, or a weak form of information hiding.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol

(CLONING OK):

const createValidatedProp = (obj, prop, cb) => {

    let name = Symbol(prop);
    
    Object.defineProperty(obj, prop, {
      enumerable: true,
      configurable: true,
      set(value) {
        if(!cb(value)) throw "Wrong type of data.";
        this[name] = value;
      },
      get() {
        return this[name];
      }
    });
  
};

class Employee {
  
  constructor(firstName, lastName, age, salary, gender, test) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._age = age;
    this._salary = salary;
    this._retirementYears = 0;
    this._gender = gender;
    this._subject = test;
  }

  retirement() {
    if (this._gender === true) {
      return this._retirementYears = 65 - this._age;
    } else if (this._gender === false) {
      return this._retirementYears = 60 - this._age;
    } else {
      console.error("Invalid gender.");
    }
  }
}

createValidatedProp(Employee.prototype, '_gender', val => typeof val === 'boolean');
createValidatedProp(Employee.prototype, '_subject', val => typeof val === 'string');
    

class Teacher extends Employee {
  constructor(firstName, lastName, age, salary, gender, subject, yearsOfExperience) {
    super(firstName, lastName, age, salary, gender, subject);
    this._subject = subject;
    this._yearsOfExperience = yearsOfExperience;
  }
}

const mathTeacher = new Teacher("John", "Smith", 35, 6000, true, "Math", 5);

try{
  mathTeacher._gender = 'test';
} catch(e){
  console.log(e);
}

console.log(mathTeacher);

const clone = Object.assign(Object.create(Object.getPrototypeOf(mathTeacher)), mathTeacher)
console.log(clone);
try{
  clone._gender = 'test';
} catch(e){
  console.log(e);
}
clone._gender = false;
console.log(mathTeacher, clone);

3
Peter Seliger On

From my above comment ...

"What does the OP expect from an Employee's own _gender property which is implemented via get/set where the setter does explicitly assign to an additionally introduced own _newGender property and the getter always does return the value of the very own _newGender property? The entire implementation of Employee already is flawed."

A possible refactoring ...

  • could introduce a single configuration object for each constructor,
  • might get rid of the pseudo-private underscore-based prefix-annotation,
  • should get the getter/setter implementation for gender right,
  • is encouraged to correctly implement the calculation for "years until retirement".

class Employee {

  constructor({
    // ensure minimum default values.
    firstName = '',
    lastName = '',
    age = 0,
    salary = 0,
    gender = false,
  }) {
    Object.defineProperty(this, 'gender', {
      set(value) {
        if (typeof value !== 'boolean') {
          throw 'A boolean data type needs to be assigned.';
        } else {
          // assignement to the enclosed/local `gender` variable.
          gender = value;
        }
      },
      get() {
        // return value of the enclosed/local `gender` variable.
        return gender;
      },
      enumerable: true,
    });
    Object.assign(this, {
      firstName, lastName, age, salary, gender,
    });
  }

  // - either rename and correctly implement the `retirement` method
  // - or implement `yearsUntilRetirement` via getter and setter.
  getYearsUntilRetirement() {
    if (this.gender === true) {
      return 65 - this.age;
    } else {
      return 60 - this.age;
    }
  }
}

class Teacher extends Employee {
  constructor({
    // ensure minimum default values.
    subject = '',
    yearsOfExperience = 0,
    // assume additional base configuration.
    ...baseConfig
  }) {
    super(baseConfig);

    Object.assign(this, {
      subject, yearsOfExperience,
    });
  }
}

const mathTeacher = new Teacher({
  firstName: 'John',
  lastName: 'Smith',
  age: 35,
  salary: 6000,
  gender: true,
  subject: 'Math',
  yearsOfExperience: 5,
});

console.log({
  mathTeacher,
});
console.log(
  'mathTeacher.getYearsUntilRetirement() ...',
  mathTeacher.getYearsUntilRetirement(),
);

mathTeacher.gender = false;

console.log({
  mathTeacher,
});
console.log(
  'mathTeacher.getYearsUntilRetirement() ...',
  mathTeacher.getYearsUntilRetirement(),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }