Concatenative inheritance vs class inheritance in JavaScript

≡放荡痞女 提交于 2020-12-25 05:00:13

问题


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 }
}

回答1:


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 properties - salary 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 global 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.




回答2:


Concatenative inheritance and prototypal inheritance (in JS, sometimes implemented using the class keyword) are two different approaches to delegation. Delegation is a mechanism whereby an object may acquire some or all of its state and behavior from other objects rather than from class (in the Java sense) definitions.

In JS, the word inheritance has a strong association with delegation to an ancestor object via prototypical inheritance, but (surprisingly) according to some, this is not always the case.

Concatenative inheritance is the process of combining the properties of one or more source objects into a new destination object. Believe it or not, it is the most commonly used form of inheritance in JavaScript.

Also:

In JS, the essence of concatenative inheritance is often masked by the common name “mixins”. Confusingly, “mixins” have other meanings in other languages, and even in some JavaScript libraries. It also has uses that would be confusing to call “mixins”. Because of those reasons, I prefer the more precise term “concatenative inheritance”.

Note that although JavaScript includes a "class syntax" (eg. class foo extends bar {}), it does not have "classical" or class-based inheritance in the usual sense of the word. In JavaScript, inheritance using the class syntax is always achieved via prototypical inheritance. Thus in JavaScript, class-based inheritance is almost entirely syntactic sugar over the original prototypical inheritance model that has always existed in JavaScript since Brendan Eich's first ten-day version of the language.

"Composition" is an overloaded term. Object composition usually involves having one object delegate functionality or state to another object contained within it. Technically it specifically means the lifetime of the sub-object is tied to that of the composite, however, in my experience "composition" is usually used to mean "aggregation", which is composition, but whereby the lifetime of the sub-objects is not controlled by the composite, with them usually being injected via a constructor function. Functional composition, on the other hand, is a pattern of combining programming elements whereby function calls are nested in the form f(g(x)).

You can implement concatenative inheritance by following the object compositional pattern, with objects being supplied to a function for concatenation onto the composite object.

In the following example, instances of p will include the functionality present on the object supplied as ancestor.

function createP(ancestor) {
  const p = { 
    ...ancestor, 
    bar() {} 
  }
  return p
}
const o = { foo() {} }
const p = createP(o) // `p` now has both methods `foo` and `bar`

For prototypical inheritance in JS there is a fixed, language supported mechanism for the dynamic look-up of functionality via the prototype chain.

In prototypical inheritance the inherited functionality sits on a separate object somewhere on the prototype chain. This indirection gives this flavour of inheritance different non-functional characteristics.

For example:

  • It is arguably less clear what functionality is being included via prototypal inheritance, than it is via concatenative inheritance, because functionality can exist anywhere along a (potentialy long) prototype chain. Furthermore, functionality can be added and removed from the prototype chain after object creation.
  • For APIs, prototypical inheritance can be useful to keep functionality off child objects, making them appear simple, while enabling them to conveniently expose functionality located on the prototype chain, in an object oriented fashion. eg. You can trivially call Array#splice directly on every array you create: [].splice(...), even though the splice function is located elsewhere (on Array.prototype).
  • In prototypal inheritance, methods on ancestor objects need to be written in such a way that their target (ie. this) could be another object. Arguably in concatenative inheritance use of this is de-emphasised.
  • In prototypal inheritance there exists an ongoing implicit coupling between the inheritor and the ancestor. This can be a source of bugs and make code harder to reason about.
  • More generally, in concatenative inheritance the built-in JS prototypical inheritance mechanism (encompassing new, this, extends, super, the prototype chain etc) is de-emphasised. There is a line of thought, popularized by Douglas Crockford, that this part of the JavaScript language complicates more often than it simplifies, and that it should be avoided where possible. Concatenative inheritance provides an alternative inheritance mechanism.
  • Concatenative inheritance bypasses some of the ontological constraints of prototypical inheritance. A prototype link implies an "is-a" relationship, whereby in concatenative inheritance no ontological relationship is implied. You can mix together as many objects as you want to get precisely the functionality you need (and nothing more).

Choosing between the two approaches is a matter of subjective preference and style. Both approaches have their place.



来源:https://stackoverflow.com/questions/64412038/concatenative-inheritance-vs-class-inheritance-in-javascript

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!