Is It Bad Practice to Extend `this` in JavaScript classes?

Programming

What 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?

JavaScript