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);