问题
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 thesplice
function is located elsewhere (onArray.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 ofthis
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