Concatenative inheritance vs class inheritance in JavaScript

Tags: , , ,



concatenative inheritance works like a composition to me when I look to it at the beginning, but people keep naming it as an inheritance. classes, however, use the prototype to create a prototype chain that connects objects together. the question now is, if both concatenative inheritance and class inheritance do the same thing which one to use? here is an example of both scenarios
concatenative inheritance

function Person(name, address) {
 const _name = name
 const _address = address
 const toString = () => `name: ${this.name}, address: ${this.address}`
 return {
   _name,
   _address,
   toString
 }
}


function Employee(name, address, salary) {
 const getAnnualSalary = () => 12 * salary
 return Object.assign({ getAnnualSalary }, Person(name, address))
}



the class inheritance

class Person {
  constructor(name, address) {
    this.name = name
    this.address = address
  }
  toString() { return `name: ${this.name}, address: ${this.address}` }
}


class Employee extends Person {
  constructor(name, address, salary) {
    super(name, address)
    this.salary = salary
  }

  getAnnualSalary() { return 12 * this.salary }
}

Answer

The following explanation tries to be brief but comprehensive.

Let’s focus first on the different implementations of Person and also let’s start with the class based version, for its implementation is clean unlike the in many aspects error prone one of its factory counterpart.

class Person { ... toString() { ... } } features a Person type specific toString method. The latter is implemented as a prototype method of Person. Thus any Person instance like myPerson does not feature its own toString method.

In case toString gets invoked at e. g. myPerson, the method will be looked up at this very instance’ prototype chain. Because the method was found (immediately) at Person.prototype.toString, it automatically gets invoked within myPerson‘s context (something one also can achieve by explicitly invoking … Person.prototype.toString.call(myPerson);).

class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
  toString() {
    return `name: ${ this.name }, address: ${ this.address }`
  }
}
const myPerson = new Person('John Doe', '123 Main St Anytown');

console.log(
  'Object.keys(myPerson) :',
  Object.keys(myPerson)
);
console.log('n');

console.log(
  "myPerson.hasOwnProperty('toString') ?",
  myPerson.hasOwnProperty('toString')
);
console.log(
  "Person.prototype.hasOwnProperty('toString') ?",
  Person.prototype.hasOwnProperty('toString')
);
console.log('n');

// automatic protoypal delegation, hence an inherited method.
console.log(
  'myPerson.toString() :',
  myPerson.toString()
);

// explicit protoypal delegation ... easy and expectable.
console.log(
  'Person.prototype.toString.call(myPerson) :',
  Person.prototype.toString.call(myPerson)
);
console.log('n');

// explicit protoypal delegation ... with an *alien* object.
console.log(
`Person.prototype.toString.call({
  name: 'Jane Doe',
  address: '123 Main St Anytown',
}) :`,
Person.prototype.toString.call({
  name: 'Jane Doe',
  address: '123 Main St Anytown',
}));
.as-console-wrapper { min-height: 100%!important; top: 0; }

Regarding the factory implementation of Person provided by the OP, one has to comment on the code and also is in need of sanitizing it (, with the sanitizing part of cause being an opinion based one) …

function Person(name, address) {
  const _name = name;
  const _address = address;
  const toString = () => `name: ${ this.name }, address: ${ this.address }`
  return {
    _name,
    _address,
    toString
  };
}
const myPerson = Person('John Doe', '123 Main St Anytown');

console.log('myPerson :', myPerson);
console.log('myPerson + "" :', myPerson + "");
.as-console-wrapper { min-height: 100%!important; top: 0; }

… Besides the toString method featuring two sources of reference failures … on one hand the naming conflict of this.name vs this._name and this.address vs this._address and on the other hand choosing an arrow function which in this case only “knows” about the global context as the toString method’s this context … there is also no (technical) need of the additional function scope of the constants _name, _address and toString.

All these problems are solved if one does implement the factory as straightforward as …

function Person(name, address) {
  return {
    name,
    address,
    toString: function () {
      return `name: ${ this.name }, address: ${ this.address }`;
    }
  };
}
const myPerson = Person('John Doe', '123 Main St Anytown');

console.log('myPerson :', myPerson);
console.log('myPerson + "" :', myPerson + "");


// There is no inheritance involved for
// any object created by the above factory.

console.log(
  'Object.keys(myPerson) :',
  Object.keys(myPerson)
);

console.log(
  "myPerson.hasOwnProperty('toString') ?",
  myPerson.hasOwnProperty('toString')
);

console.log(
  "(Object.getPrototypeOf(myPerson) === Object.prototype) ?",
  (Object.getPrototypeOf(myPerson) === Object.prototype)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

As one can see too, from the additional logging of the above sanitized factory example, there is no inheritance involved for any object created by the above factory (besides the most basic one of Object.prototype).


It’s time now for the “sub classing” versus “augmentation / composition / mixin” part …

… and again, let’s start with the class based version of an Employee as provided by the OP.

Having sub classed Employee from Person via extends and having implemented the super call within the Employee‘s constructor, one does, with every invocation of the latter, create an instance which features three own propertiessalary from directly having invoked the Employee constructor as well as name and address from the super call which one also could achieve by a delegation call like … Person.call(this, name, address) … in case Person was not a class constructor but an ordinary constructor function (which is not related to JavaScript class). In the same time this instance gets associated with a prototype chain that will be unveiled by the logging of the next example code …

class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
  toString() {
    return `name: ${ this.name }, address: ${ this.address }`
  }
}

class Employee extends Person {
  constructor(name, address, salary) {
    super(name, address)
    this.salary = salary
  }

  getAnnualSalary() { return 12 * this.salary }
}

const myEmployee = new Employee('John Doe', '123 Main St Anytown', 6000);


console.log(
  '(myEmployee instanceof Employee) ?',
  (myEmployee instanceof Employee)
);
console.log(
  '(myEmployee instanceof Person) ?',
  (myEmployee instanceof Person)
);
console.log('n');

console.log(
  '(Object.getPrototypeOf(myEmployee) instanceof Employee) ?',
  (Object.getPrototypeOf(myEmployee) instanceof Employee)
);
console.log(
  '(Object.getPrototypeOf(myEmployee) instanceof Person) ?',
  (Object.getPrototypeOf(myEmployee) instanceof Person)
);
console.log('n');

console.log(
  'Object.keys(myEmployee) :',
  Object.keys(myEmployee)
);
console.log('n');

console.log(
  "myEmployee.hasOwnProperty('getAnnualSalary') ?",
  myEmployee.hasOwnProperty('getAnnualSalary')
);
console.log(
  "Employee.prototype.hasOwnProperty('getAnnualSalary') ?",
  Employee.prototype.hasOwnProperty('getAnnualSalary')
);
console.log('n');

console.log(
  "myEmployee.hasOwnProperty('toString') ?",
  myEmployee.hasOwnProperty('toString')
);
console.log(
  "Employee.prototype.hasOwnProperty('toString') ?",
  Employee.prototype.hasOwnProperty('toString')
);
console.log(
  "Person.prototype.hasOwnProperty('toString') ?",
  Person.prototype.hasOwnProperty('toString')
);
console.log('n');

// automatic protoypal delegation,
// hence an inherited method via
// `Employee.prototype.getAnnualSalary`.
console.log(
  'myEmployee.getAnnualSalary() :',
  myEmployee.getAnnualSalary()
);

// automatic protoypal delegation,
// hence an inherited method via
// `Person.prototype.toString`.
console.log(
  'myEmployee.toString() :',
  myEmployee.toString()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

In comparison to the above class based approach the implementation of an Employee factory which augments an object (literal) by mixing in additional properties via Object.assign is downright slim …

function Employee(name, address, salary) {
  const getAnnualSalary = () => 12 * salary;
  return Object.assign({ getAnnualSalary }, Person(name, address));
}

… But again, the OP’s implementation is error prone. This time it is due to keeping salary within the factory’s local function scope. Thus salary never becomes (turns into) a public property like it does with its classy counterpart. It remains immutable within a closure that will be created every time the Employee factory gets invoked.

An implementation of Employee which does not create closures and makes salary a public and mutable property too might look close to the following code …

function Person(name, address) {
  return {
    name,
    address,
    toString: function () {
      return `name: ${ this.name }, address: ${ this.address }`;
    }
  };
}

function Employee(name, address, salary) {
  return Object.assign(Person(name, address), {
    salary,
    getAnnualSalary: function () {
      return (12 * this.salary);
    }
  });
}

const myEmployee = Employee('John Doe', '123 Main St Anytown', 6000);

console.log(
  'myEmployee :',
  myEmployee
);

console.log(
  'myEmployee.getAnnualSalary() :',
  myEmployee.getAnnualSalary()
);
console.log(
  'myEmployee.toString() :',
  myEmployee.toString()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

From the above logging it should be quite obvious that the so called Concatenative Inheritance produces data blobs. There is no separation in between publically carried state (data properties) and behavior (methods that operate/process such state/data). More importantly, if it comes to managing encapsulation and controlled access of encapsulated data this approach does lose its advantage of being lightweighted and easy to grasp on.

One might consider this approach for a somehow limited amount of references, each with a manageable amount of properties. In my opinion, this technique of code-reuse, within the context of a prototype based language, should also not feature the name inheritance for it actively prevents any delegation; and the latter is the very core of how JavaScript manages inheritance.

I would call this approach and its related system of thoughts what it actually is … “Factory based Composable Units Of Reuse“.

And just to be clear, I personally am a strong advocate of Composition over Inheritance … there are, again in my opinion, just much nicer approaches for composition/mixin based code-reuse than the one the OP was struggling with.



Source: stackoverflow