// utility functions for Immutability and Multiple Inheritance
const createConstructor = function (funcArgs = {}) {
const { classesToInherit, classProps, defaultArgs, methods } = funcArgs
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).map(key => {
if (typeof obj[key] === 'object' && obj[key] !== null)
deepFreeze(obj[key])
})
Object.freeze(obj)
}
// the passed constructor
function InnerFunc (args = {}) {
//set object props
Object.assign(this, defaultArgs, args)
//bind the methods per object
const targetProto = Object.getPrototypeOf(this)
Object.getOwnPropertyNames(targetProto).map(key => {
if (key !== 'constructor') {
Object.defineProperty(this, key, {
enumerable: false,
value: targetProto[key] instanceof Function ?
targetProto[key].bind(this) :
targetProto[key]
})
}
})
//freeze the object
deepFreeze(this)
}
// set constructor props
Object.assign(InnerFunc, classProps)
// set inherited methods
classesToInherit.map(constr =>
Object.getOwnPropertyNames(constr.prototype).map(key => {
if (key !== 'constructor') {
InnerFunc.prototype[key] = constr.prototype[key]
}
})
)
// set manually defined methods
Object.assign(InnerFunc.prototype, methods)
// method to copy object with some props changed
InnerFunc.prototype.copyWithProps = function(props = {}) {
return new this.constructor(Object.assign({}, this, props))
}
// freeze the constructor
deepFreeze(InnerFunc)
return InnerFunc
}
// BADLY WRITTEN CLASSES
class Height {
constructor(args = {}) {
this.height = args.height
}
getHeight() { return `Height: ${this.height}` }
static heightInternal() { return 3 }
}
class Weight {
constructor(args = {}) {
this.weight = args.weight
}
getWeight() { return `Weight: ${this.weight}` }
static weightInternal() { return 3 }
}
// WELL WRITTEN CLASSES
const Person = createConstructor({
classesToInherit: [ Height, Weight ],
classProps: {
species: 'humans',
internalMethod() {
return 'Hello from inside the constructor'
}
},
defaultArgs: {
name: 'John',
age: 20,
weight: 60,
height: 160
},
methods: {
// Uncomment to replace the inherited one
//getHeight() { return 'CORRECT' },
// combines native with inherited methods
greet() {
console.log(
`Hello, ${this.name}. ` +
`${this.getWeight()}, ${this.getHeight()}`
)
},
// calculates the method name first
['get' + 'Info']() { return `Name: ${this.name}, age: ${this.age}` },
// can also use async, * generator
// which can also be static
}
})
for (let i = 0; i < 50; i++) {
mark = new Person({ name: 'Mark', weight: 90, otherArg: true })
skinnyMark = mark.copyWithProps({ age: 32, weight: 75 })
skinnyMark.greet()
mark.greet()
}
// utility functions for Immutability and Multiple Inheritance
const ClassUtils = class Self {
copyWithProps(props = {}) {
return new this.constructor(Object.assign({}, this, props))
}
finalizeInstance(props = {}) {
if (!Object.isFrozen(this)) {
//set object props
Object.assign(this, props)
//bind the methods per object
const targetProto = Object.getPrototypeOf(this)
Object.getOwnPropertyNames(targetProto).map(key => {
if (key !== 'constructor') {
Object.defineProperty(this, key, {
enumerable: false,
value: targetProto[key] instanceof Function ?
targetProto[key].bind(this) :
targetProto[key]
})
}
})
//freeze the object
Self.deepFreeze(this)
}
}
static deepFreeze(obj) {
Object.getOwnPropertyNames(obj).map(key => {
if (typeof obj[key] === 'object' && obj[key] !== null)
Self.deepFreeze(obj[key])
})
Object.freeze(obj)
}
static finalizeClass({ target, props = {}, sources = [] }) {
if (!Object.isFrozen(target)) {
// set constructor props
Object.assign(target, props)
// set prototype methods
sources.concat([Self, target]).map(constr =>
Object.getOwnPropertyNames(constr.prototype).map(key => {
if (key !== 'constructor') {
target.prototype[key] = constr.prototype[key]
}
})
)
// freeze the constructor
Self.deepFreeze(target)
}
}
}
ClassUtils.deepFreeze(ClassUtils)
// BADLY WRITTEN CLASSES
class Height {
constructor(args = {}) {
this.height = args.height
}
getHeight() { return `Height: ${this.height}` }
static heightInternal() { return 3 }
}
class Weight {
constructor(args = {}) {
this.weight = args.weight
}
getWeight() { return `Weight: ${this.weight}` }
static weightInternal() { return 3 }
}
// WELL WRITTEN CLASSES
const Person = class Self {
constructor({ name, age, weight, height } = {}) {
this.finalizeInstance({
name: name || 'John',
age: age || 20,
weight: weight || 60,
height: height || 160
})
}
// Uncomment to replace the inherited one
//getHeight() { return 'CORRECT' }
// combines native with inherited methods
greet() { console.log(`Hello, ${this.name}. ${this.getWeight()}, ${this.getHeight()}`) }
// calculates the method name first
['get' + 'Info']() { return `Name: ${this.name}, age: ${this.age}` }
// class method
static internalMethod() {
return 'Hello from inside the constructor'
}
// can also use async, * generator
// which can also be static
}
ClassUtils.finalizeClass({
target: Person,
sources: [ Height, Weight ],
props: { species: 'humans' }
})
for (let i = 0; i < 50; i++) {
mark = new Person({ name: 'Mark', weight: 90, otherArg: true })
skinnyMark = mark.copyWithProps({ age: 32, weight: 75 })
skinnyMark.greet()
mark.greet()
}
--enable-precise-memory-info
flag.
Test case name | Result |
---|---|
createConstructor | |
ClassUtils |
Test name | Executions per second |
---|---|
createConstructor | 382.0 Ops/sec |
ClassUtils | 373.5 Ops/sec |
A complex JavaScript class testing scenario!
After analyzing the provided code, I'll provide a high-level overview of my findings:
Good practices:
finalizer
: The finalizeClass
method is used to ensure that certain properties and methods are properly set on the Person
class.Person
class uses a constructor with an object initializer, which sets up the instance's properties.greet()
method combines both native and inherited methods.Improvement suggestions:
Height
, Weight
) follow camelCase, while others (e.g., Person
) follow PascalCase. It's essential to maintain consistency in naming conventions throughout the codebase.deepFreeze
. Consider breaking down the code into smaller, more focused modules or classes.Performance optimization opportunities:
heightInternal()
and weightInternal()
methods, which return a hardcoded value, could be replaced with more efficient approaches, such as using getters or computed properties.Height
and Weight
classes are frequently instantiated, consider caching their internal values to avoid redundant calculations.Security considerations:
Person
class's constructor accepts an object initializer with various properties (e.g., name
, age
, weight
, height
). Ensure that these properties are properly validated and sanitized to prevent potential security vulnerabilities, such as XSS attacks.By addressing these points, you can improve the maintainability, performance, and security of your JavaScript class testing codebase.