Is It Bad Practice to Extend `this` in JavaScript classes?
ProgrammingWhat is better practice? To set properties on this
, or to extend this
using Object.assign
. I see merits for both.
Setting properties on this
results in better immutability and less, expensive Object.assign
calls.
Extending this
leads to more elegant classes that are easier to understand. But it comes with the danger of collisions.
Setting properties
Let’s start with a base class.
class BaseObject {
constructor(props = {}) {
this.props = props
}
set(props) {
this.props = props
return this
}
get(prop) {
return this.props[prop]
}
isEmpty() {
return Object.entries(this.props).length === 0
}
}
Great! Now we have a class that adds some useful features not available in vanilla JavaScript.
const empty = new BaseObject()
empty.isEmpty() // true
const filled = new BaseObject({ a: 1 })
filled.isEmpty() // false
But you can already see that the object itself has properties that aren’t intuitive to the end user.
console.log(filled) // BaseObject { props: { a: 1 } }
You can imagine a beginner wondering “I didn’t specify props
, where did it come from?" And in order access values you either have to use get()
, or remember to use props
first.
filled.get('a') // 1
filled.props.a // 1
Extending BaseObject
inherits these disadvantages. In particular always having to use this.props
instead of this
.
class Item extends BaseObject {
constructor(props) {
super(props)
this.has = this.has.bind(this)
}
has([key, value]) {
return this.props[key] === value
}
hasAll(args) {
return Object.entries(args).every(this.has)
}
}
const item = new Item().set({ a: 1, b: 2 })
console.log(item) // Item { props: { a: 1, b: 2 }, has: ƒ () }
item.hasAll({ a: 1, c: 3 }) // false
item.hasAll({ a: 1, b: 2 }) // true
Extending this
Implementing a class that extends this
is less intuitive than using properties. But the result is an elegant API.
Let’s start with that base class again.
class BaseObject {
constructor(props) {
Object.assign(this, props)
}
set(props) {
return Object.assign(this, props)
}
get(prop) {
return this[prop]
}
isEmpty() {
return Object.entries(this).length === 0
}
}
Now when we create a new instance of the object, the object is exactly what you set it to be.
const filled = new BaseObject({ a: 1 })
console.log(filled) // BaseObject { a: 1 }
Beautiful!
I added a getter for consistency but in practice it’s redundant. Accessing values is intuitive.
filled.get('a') // 1
filled.a // 1
Which has the added benefit of making extension easy. You simply have to use this
.
class Item extends BaseObject {
constructor(props) {
super(props)
this.has = this.has.bind(this)
}
has([key, value]) {
return this[key] === value
}
hasAll(args) {
return Object.entries(args).every(this.has)
}
}
const item = new Item().set({ a: 1, b: 2 })
console.log(item) // Item { a: 1, b: 2, has: ƒ () }
item.hasAll({ a: 1, c: 3 }) // false
item.hasAll({ a: 1, b: 2 }) // true
But wait a minute, what’s has
doing there in the same scope as the values I passed in?
That’s a side effect of creating bound methods. And now it is possible to collide with existing methods.
new Item({ place: 'Fruit Stand', has: 'Apples' })
// Uncaught TypeError: this.has.bind is not a function
Well… Damn.
I could refactor my code to avoid bound methods.
class SaferItem extends BaseObject {
constructor(props) {
super(props)
}
has(key, value) {
return this[key] === value
}
hasAll(args) {
return Object.entries(args).every(([key, value]) => this.has(key, value))
}
}
const fruitStand = new SaferItem({ place: 'Fruit Stand', has: 'Apples' })
console.log(item) // SaferItem { place: 'Fruit Stand', has: 'Apples' }
Which actually seems to be a better implementation. But now I need to have this awareness when developing classes.
The crux of extending this
is its danger. Having objects closer to vanilla JavaScript is beautiful. But collisions will lead to bugs. Because of that, setting properties is now my preferred method.
What do you think?