4
\$\begingroup\$

I'm new to JavaScript and came across classes in ES6. Though they are great, I don't want to learn something that can be done in other ways. So, I created this code that does somehow similar job to what classes in JS do. Please give me feedback on whether classes does more than what I think they can do and whether my code is missing something that classes can do. This is the JS code:

<script>
const person = function(first, second, age, sex){
    return {
        firstName:first,
        lastName:second,
        age:age,
        gender:sex,
        fullName:function(){
            return this.firstName + " " + this.lastName;
        }
    }
}
var John = person("John", "Anderson", 21, "Male");
var Mary = person("Mary", "Smith", 32, "Female");

document.getElementById("demo1").innerHTML = John.firstName;
document.getElementById("demo2").innerHTML = Mary.fullName();
</script>

And the output of the code is:

John

Mary Smith
\$\endgroup\$
6
\$\begingroup\$

A short review;

  • In stacktraces, person will be a nameless function, use function person(){} instead
  • There is no good reason to have different names (first->firstName) for your attributes. Using the same name allows you to use some nifty JS syntax
  • Your code will create a new function for each instance of person, unless you have a smart VM, that could be heavy on memory
  • I would extend the .prototype of person for fullName to prevent that memory issue
  • I also strive to name functions with a <verb><thing> scheme so I would call it getFullName()
  • Ideally functions that are akin to a class are capitalized, so person -> Person

So, all in all, this is my counter proposal:

    function Person(firstName, lastName, age, gender){
      Object.assign(this, {firstName, lastName, age, gender});
    }

    Person.prototype.getFullName = function personGetFullName(){
      return this.firstName + " " + this.lastName;
    }

    const John = new Person("John", "Anderson", 21, "Male");
    const Mary = new Person("Mary", "Smith", 32, "Female");
    console.log(John.firstName);
    console.log(Mary.getFullName());

\$\endgroup\$
3
\$\begingroup\$

This style is the best for encapsulation as it can use closure to protect the objects state. However if you expose all the properties without getters and setters you destroy the encapsulation.

Exposed example

  • This exposes all the properties.
  • Uses property shorthand to assign properties.
  • Uses function shorthand to define the function.

Code

function person(firstName, lastName, age, gender) {
  return {
      firstName, lastName, age, gender,
      fullName() { return this.firstName + " " + this.lastName },
  };
}

// or with getter

function person(firstName, lastName, age, gender) {
  return {
      firstName, lastName, age, gender,
      get fullName() { return this.firstName + " " + this.lastName },
  };
}

// or compact form

function fullName() { return this.firstName + " " + this.lastName }
const person=(firstName, lastName, age, gender)=>({firstName, lastName, age, gender, fullName});

Encapsulated example

  • Protects state via closure, using getters and setters to vet properties and maintain trusted state.

  • Object is frozen so that the existing properties can not be changed or new ones added. As setters are used the object though frozen can still change state.

Code

function person(firstName, lastName, age, gender) {
    return Object.freeze({
        get firstName() { return firstName },
        get lastName() { return lastName }, 
        get age() { return age }, 
        get gender() { return gender },
        get fullName() { return this.firstName + " " + this.lastName },

        set firstName(str) { firstName = str.toString() },
        set lastName(str) { lastName = str.toString() }, 
        set age(num) { age = num >= 0 && num <= 130 ? Math.floor(Number(num)) : "NA" }, 
        set gender(str) { gender = str === "Male" || str === "Female" ?  str : "NA" },
   });
}

Note that age and genders vet the values using the setters. However if the object is created with bad age or gender values the values remain invalid.

To avoid the this you can either have the getters vet the state and return the correct state or not return the object immediately and assign the properties within the function. Personally I use API to hold the closure copy, but what ever suits your style is ok.

function person(firstName, lastName, age, gender) {
    const API = {
        get firstName() { return firstName },
        get lastName() { return lastName }, 
        get age() { return age }, 
        get gender() { return gender },
        get fullName() { return this.firstName + " " + this.lastName },

        set firstName(str) { firstName = str.toString() },
        set lastName(str) { lastName = str.toString() }, 
        set age(num) { age = num >= 0 && num <= 130 ? Math.floor(Number(num)) : "NA" }, 
        set gender(str) { gender = str === "Male" || str === "Female" ?  str : "NA" },
   };

   API.age = age;
   API.gender = gender;
   API.firstName = firstName; // ensures these are not objects that can change
   API.lastName = lastName;   // the toString in the setter makes a copy
   return Object.freeze(API);
}

Object's prototype

For modern browsers the functions are cached and copies are references so prototypes do not give any advantage in terms of memory and performance.

In older browsers (> 3 years approx) this style has an overhead when creating a new instance. Each function within the function person needs to be evaluated and instantiated as a new instance of that function thus using more memory and CPU cycles at instantiation.

This overhead is not large but if you are creating many 1000s that have lives longer than the current execution but quickly released, this becomes a noticeable problem. Example may be a particle system with each particle being a short lived object.

If performance is important use the prototype as follows. You could also reuse objects by keeping a pool of unused objects.

Notes

  • You will lose encapsulation.
  • The object name is changed to "Person" and though not required should be instantiated with new
  • Can not use setters for the properties assign at instantiation so the object state remains untrust-able.
  • Freeze the prototype as that should not be changed (if that is a priority)
  • Adds toString which will be called automatically as needed for type coercion. Can also be done with the above versions.
  • Adds valueOf as handy way to copy object without having to deal with fullName as a getter. Can also be done with the above versions.

function Person(firstName, lastName, age, gender) {
    return Object.assign(this, {firstName, lastName, age, gender});
}
// or but MUST use new to create
function Person(firstName, lastName, age, gender) {
    this.firstName = firstName; 
    this.lastName = lastName;  
    this.age = age >= 0 && age <= 130 ? Math.floor(Number(age)) : "NA";
    this.gender = typeof gender === "string" && gender.toLowerCase() === "male" ? "Male" : "Female";
    // without the return this will return undefined if called as Person()
    // but will return this if called with new Person();
}

Person.prototype = Object.freeze({
    get fullName() { return (this.gender === "Male" ? "Mr " : "Miss ") + this.firstName + " " + this.lastName },
    toString() { return this.fullName + " " + this.age + "y " + this.gender },
    get valueOf() { return [this.firstName, this.lastName, this.age, this.gender] },
});

const foo = new Person("Foo", "Bar", 20, "Male");
console.log("Object foo: " + foo); // auto toString
const boo = new Person(...foo.valueOf);
boo.firstName = "Boo";
boo.gender = "Female";
console.log("Object boo: " + boo); 
console.log(boo);

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.