英文:
Combine two classes, a.k.a. multiple inheritance
问题
I have gone down the "JavaScript can't do multiple inheritance, but..." rabbit-hole and this is where I have ended up. I have got some basic functionality going, but a large part of why I am using classes is to enforce some basic validation and there are some key aspects I just can't get working despite a lot of searching. I have done up some example code below – in this example I have a Car class and an Aircraft class and I want to combine the both into a FlyingCar class.
class Car {
#color
constructor() {
Object.defineProperty(this, "Color", {
configurable: true,
enumerable: true,
get: function () {
return this.#color
},
set: function (value) {
if (value && typeof value !== 'string') {
return console.error('Error: Color must be a string')
}
this.#color = value
}
})
}
}
class Aircraft {
#wingspan
constructor() {
Object.defineProperty(this, "Wingspan", {
configurable: true,
enumerable: true,
get: function () {
return this.#wingspan
},
set: function (value) {
if (value && typeof value !== 'number') {
return console.error('Error: Wingspan must be a number')
}
this.#wingspan = value
}
})
}
static fly() {
console.log('whhhhheeeee! I am flying!')
}
}
class FlyingCar extends Car {
constructor() {
super()
Object.assign(this, Aircraft) // using Aircraft as a kind of mixin
// Object.preventExtensions(this) // can’t do this as it prevents me from accessing Aircraft properties
}
}
// put it all together
const flyingCar = new FlyingCar()
flyingCar.Color = 42 // returns an error as expected :-)
flyingCar.Wingspan = 'green' // should fail as wingspan is not a number. But is allowed :(
FlyingCar.fly() // doesn't work. Seems like static properties are not carried through with Object.assign :(
flyingCar.FavoriteIceCream = 'chocolate' // should fail with object is not extensible, but is allowed :(
The code at the bottom shows what isn't working but to pull this out:
- I can copy properties across from Aircraft using
Object.assign
but I can't seem to copy across the property definitions Object.assign
doesn't copy across static methods- I can't close off the class using
Object.preventExtensions
as this prevents me from assigning properties belonging to the Aircraft class
I'd like to have a class-based solution, but would consider a factory (a function that returns a new FlyingCar). I haven't tried proxies as they just seem overly complex but maybe they could be useful.
I am after a JavaScript-only solution, no TypeScript.
英文:
I have gone down the "JavaScript can't do multiple inheritance, but..." rabbit-hole and this is where I have ended up. I have got some basic functionality going, but a large part of why I am using classes is to enforce some basic validation and there are some key aspects I just can't get working despite a lot of searching. I have done up some example code below – in this example I have a Car class and an Aircraft class and I want to combine the both into a FlyingCar class.
class Car {
#color
constructor() {
Object.defineProperty(this, "Color", {
configurable: true,
enumerable: true,
get: function () {
return this.#color
},
set: function (value) {
if (value && typeof value !== 'string') {
return console.error('Error: Color must be a string')
}
this.#color = value
}
})
}
}
class Aircraft {
#wingspan
constructor() {
Object.defineProperty(this, "Wingspan", {
configurable: true,
enumerable: true,
get: function () {
return this.#wingspan
},
set: function (value) {
if (value && typeof value !== 'number') {
return console.error('Error: Wingspan must be a number')
}
this.#wingspan = value
}
})
}
static fly() {
console.log('whhhhheeeee! I am flying!')
}
}
class FlyingCar extends Car {
constructor() {
super()
Object.assign(this, Aircraft) // using Aircraft as a kind of mixin
// Object.preventExtensions(this) // can’t do this as it prevents me from accessing Aircraft properties
}
}
// put it all together
const flyingCar = new FlyingCar()
flyingCar.Color = 42 // returns an error as expected :-)
flyingCar.Wingspan = 'green' // should fail as wingspan is not a number. But is allowed :(
FlyingCar.fly() // doesn't work. Seems like static properties are not carried through with Object.assign :(
flyingCar.FavoriteIceCream = 'chocolate' // should fail with object is not extensible, but is allowed :(
The code at the bottom shows what isn't working but to pull this out:
- I can copy properties across from Aircraft using
Object.assign
but I can't seem to copy across the property definitions Object.assign
doesn't copy across static methods- I can't close off the class using
Object.preventExtensions
as this prevents me from assigning properties belonging to the Aircraft class
I'd like to have a class-based solution, but would consider a factory (a function that returns a new FlyingCar). I haven't tried proxies as they just seem overly complex but maybe they could be useful.
I am after a JavaScript-only solution, no TypeScript.
答案1
得分: 2
I'd strongly recommend using composition, not inheritance, to create your FlyingCar
class. JavaScript simply does not have multiple inheritance, and attempts to fake it are doomed to fail in various ways, sometimes subtle ways.
But let's first look at why your current implementation doesn't work, and what we could do to make it work following that approach.
Object.assign
copies the current value of an accessor, not the accessor itself, as though you'd done target.prop = source.prop
. You can see that here:
const obj = {
get random() {
return Math.random();
}
};
console.log("Using obj directly:");
console.log(obj.random); // Some random value
console.log(obj.random); // Another random value
console.log(obj.random); // Some third random value
const copy = Object.assign({}, obj);
console.log("Using copy from Object.assign:");
console.log(copy.random); // The same
console.log(copy.random); // ...value every...
console.log(copy.random); // ...time
So we can't just use Object.assign
for this.
Separately, to use Aircraft
instance properties and fields (both Wingspan
and #wingspan
are instance-specific), you'll need an instance of Aircraft
— those don't exist on the Aircraft
constructor function or on Aircraft.prototype
. We can solve that by having a private field on the FlyingCar
instance. Then we can create instance properties for the Aircraft
properties and methods and delegate to that Aircraft
instance. Similarly, to get the fly
static method, we can delegate Aircraft
properties and methods to Aircraft
from FlyingCar
.
That could look something like this (note that addFacades
is just a sketch, not an all-singing, all-dancing implementation [for instance, it doesn't try to handle inherited properties]):
"use strict";
function addFacades(target, source) {
for (const name of Object.getOwnPropertyNames(source)) {
if (
typeof source !== "function" ||
(name !== "prototype" && name !== "name" && name !== "length")
) {
const sourceDescr = Object.getOwnPropertyDescriptor(source, name);
const targetDescr = {
enumerable: sourceDescr.enumerable,
get: () => {
return source[name];
},
};
if (sourceDescr.writable || sourceDescr.set) {
targetDescr.set = (value) => {
source[name] = value;
};
}
Object.defineProperty(target, name, targetDescr);
}
}
}
class Car {
#color;
constructor() {
Object.defineProperty(this, "Color", {
configurable: true,
enumerable: true,
get: function () {
return this.#color;
},
set: function (value) {
if (value && typeof value !== "string") {
return console.error("Error: Color must be a string");
}
this.#color = value;
},
});
}
}
class Aircraft {
#wingspan;
constructor() {
Object.defineProperty(this, "Wingspan", {
configurable: true,
enumerable: true,
get: function () {
return this.#wingspan;
},
set: function (value) {
if (value && typeof value !== "number") {
return console.error("Error: Wingspan must be a number");
}
this.#wingspan = value;
},
});
}
static fly() {
console.log("whhhhheeeee! I am flying!");
}
}
class FlyingCar extends Car {
#aircraft;
constructor() {
super();
this.#aircraft = new Aircraft();
// Handle instance/prototype properties
addFacades(this, this.#aircraft);
Object.preventExtensions(this);
}
}
// Handle statics
console.log("fly" in Aircraft);
console.log(Object.hasOwn(Aircraft, "fly"));
addFacades(FlyingCar, Aircraft);
// put it all together
const flyingCar = new FlyingCar();
flyingCar.Color = 42; // returns an error as expected
flyingCar.Wingspan = "green"; // returns an error as expected
FlyingCar.fly(); // works
flyingCar.FavoriteIceCream = "chocolate"; // fails
Note that in order to get an actual error from assigning to flyingCar.FavoriteIceCream
, you need this code to be in strict mode. (Maybe yours is, if it's in a module. In the above, since it's not a module, I needed the directive.)
At this point, FlyingCar
inherits from Car
and mixes in Aircraft
. That is, it's half inheritance and half composition. That might be reasonable if the class is "mostly" a Car
and only partially an Aircraft
, but it's still...assymmetrical. That lack of balance suggests it's not the best approach.
Which brings us back to: I'd use composition, not inheritance, and I'd probably lean toward doing it manually rather than using addFacades
above. Perhaps something like this:
"use strict";
class Car {
#color; // If this should always be a string, it should have a default value
get color() {
return this.#color;
}
set color(value) {
// I removed the `value &&` that used to be on the `if` below, because it
// shouldn't be there. `0` is a falsy value that isn't a string (as are
// several others -- `NaN`, `undefined`, `null`, `false`, ...), you don't
// want to assign those to your `#color` field either.
if (typeof value !== "string") {
// (In your real code I assume you `throw` here, but doing it this way
// is convenient for your example so I suspect that's why you did it).
return console.error("Error: color must be a string");
}
this.#color = value;
}
}
class Aircraft {
#wingspan; // If this should always be a number, it should have default value
get wingspan() {
return this.#wingspan;
}
set wingspan(value) {
// As with `color`, I've removed the `value &&` part of the below. `""` is a
// falsy value (as are several others that aren't numbers -- `undefined`,
// `null`, `false`, ...). You don't want to assign those to `#wingspam`.
if (value && typeof value !== "number") {
// (In your real code I assume you `throw` here, but doing it this way
// is convenient for your example so I suspect that's why you did it).
return console.error("Error: wingspan must be a number");
}
this.#wingspan = value;
}
static fly() {
console.log("whhhhheeeee! I am flying!");
}
}
class FlyingCar {
#car
<details>
<summary>英文:</summary>
I'd strongly recommend using composition, not inheritance, to create your `FlyingCar` class. JavaScript simply does not have multiple inheritance, and attempts to fake it are doomed to fail in various ways, sometimes subtle ways.
But let's first look at why your current implementation doesn't work, and what we could do to make it work following that approach.
`Object.assign` copies the *current value* of an accessor, not the accessor itself, as though you'd done `target.prop = source.prop`. You can see that here:
<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-js -->
const obj = {
get random() {
return Math.random();
}
};
console.log("Using obj directly:");
console.log(obj.random); // Some random value
console.log(obj.random); // Another random value
console.log(obj.random); // Some third random value
const copy = Object.assign({}, obj);
console.log("Using copy from Object.assign:");
console.log(copy.random); // The same
console.log(copy.random); // ...value every...
console.log(copy.random); // ...time
<!-- language: lang-css -->
.as-console-wrapper {
max-height: 100% !important;
}
<!-- end snippet -->
So we can't just use `Object.assign` for this.
Separately, to use `Aircraft` instance properties and fields (both `Wingspan` and `#wingspan` are instance-specific), you'll need an instance of `Aircraft`&nbsp;&mdash; those don't exist on the `Aircraft` constructor function or on `Aircraft.prototype`. We can solve that by having a private field on the `FlyingCar` instance. Then we can create instance properties for the `Aircraft` properties and methods and delegate to that `Aircraft` instance. Similarly, to get the `fly` static method, we can delegate `Aircraft` properties and methods to `Aircraft` from `FlyingCar`.
That could look something like this (note that `addFacades` is just a *sketch*, not an all-singing, all-dancing implementation [for instance, it doesn't try to handle inherited properties]):
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
"use strict";
function addFacades(target, source) {
for (const name of Object.getOwnPropertyNames(source)) {
if (
typeof source !== "function" ||
(name !== "prototype" && name !== "name" && name !== "length")
) {
const sourceDescr = Object.getOwnPropertyDescriptor(source, name);
const targetDescr = {
enumerable: sourceDescr.enumerable,
get: () => {
return source[name];
},
};
if (sourceDescr.writable || sourceDescr.set) {
targetDescr.set = (value) => {
source[name] = value;
};
}
Object.defineProperty(target, name, targetDescr);
}
}
}
class Car {
#color;
constructor() {
Object.defineProperty(this, "Color", {
configurable: true,
enumerable: true,
get: function () {
return this.#color;
},
set: function (value) {
if (value && typeof value !== "string") {
return console.error("Error: Color must be a string");
}
this.#color = value;
},
});
}
}
class Aircraft {
#wingspan;
constructor() {
Object.defineProperty(this, "Wingspan", {
configurable: true,
enumerable: true,
get: function () {
return this.#wingspan;
},
set: function (value) {
if (value && typeof value !== "number") {
return console.error("Error: Wingspan must be a number");
}
this.#wingspan = value;
},
});
}
static fly() {
console.log("whhhhheeeee! I am flying!");
}
}
class FlyingCar extends Car {
#aircraft;
constructor() {
super();
this.#aircraft = new Aircraft();
// Handle instance/prototype properties
addFacades(this, this.#aircraft);
Object.preventExtensions(this);
}
}
// Handle statics
console.log("fly" in Aircraft);
console.log(Object.hasOwn(Aircraft, "fly"));
addFacades(FlyingCar, Aircraft);
// put it all together
const flyingCar = new FlyingCar();
flyingCar.Color = 42; // returns an error as expected
flyingCar.Wingspan = "green"; // returns an error as expected
FlyingCar.fly(); // works
flyingCar.FavoriteIceCream = "chocolate"; // fails
<!-- end snippet -->
Note that in order to get an actual *error* from assigning to `flyingCar.FavoriteIceCream`, you need this code to be in strict mode. (Maybe yours is, if it's in a module. In the above, since it's not a module, I needed the directive.)
At this point, `FlyingCar` inherits from `Car` and mixes in `Aircraft`. That is, it's half inheritance and half composition. That might be reasonable if the class is "mostly" a `Car` and only partially an `Aircraft`, but it's still...assymmetrical. That lack of balance suggests it's not the best approach.
Which brings us back to: I'd use composition, not inheritance, and I'd probably lean toward doing it manually rather than using `addFacades` above. Perhaps something like this:
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
"use strict";
class Car {
#color; // If this should always be a string, it should have a default value
get color() {
return this.#color;
}
set color(value) {
// I removed the `value &&` that used to be on the `if` below, because it
// shouldn't be there. `0` is a falsy value that isn't a string (as are
// several others -- `NaN`, `undefined`, `null`, `false`, ...), you don't
// want to assign those to your `#color` field either.
if (typeof value !== "string") {
// (In your real code I assume you `throw` here, but doing it this way
// is convenient for your example so I suspect that's why you did it).
return console.error("Error: color must be a string");
}
this.#color = value;
}
}
class Aircraft {
#wingspan; // If this should always be a number, it should have default value
get wingspan() {
return this.#wingspan;
}
set wingspan(value) {
// As with `color`, I've removed the `value &&` part of the below. `""` is a
// falsy value (as are several others that aren't numbers -- `undefined`,
// `null`, `false`, ...). You don't want to assign those to `#wingspam`.
if (value && typeof value !== "number") {
// (In your real code I assume you `throw` here, but doing it this way
// is convenient for your example so I suspect that's why you did it).
return console.error("Error: wingspan must be a number");
}
this.#wingspan = value;
}
static fly() {
console.log("whhhhheeeee! I am flying!");
}
}
class FlyingCar {
#car;
#aircraft;
constructor() {
this.#car = new Car();
this.#aircraft = new Aircraft();
Object.preventExtensions(this);
}
get color() {
return this.#car.color;
}
set color(value) {
this.#car.color = value;
}
get wingspan() {
return this.#aircraft.wingspan;
}
set wingspan(value) {
this.#aircraft.wingspan = value;
}
static fly() {
return Aircraft.fly();
}
}
// put it all together
const flyingCar = new FlyingCar();
flyingCar.color = 42; // returns an error as expected
flyingCar.wingspan = "green"; // returns an error as expected
flyingCar.color = "green"; // works
flyingCar.wingspan = 42; // works
console.log(`color = ${flyingCar.color}`);
console.log(`wingspan = ${flyingCar.wingspan}`);
FlyingCar.fly(); // works
flyingCar.favoriteIceCream = "chocolate"; // fails
<!-- language: lang-css -->
.as-console-wrapper {
max-height: 100% !important;
}
<!-- end snippet -->
(In the above, I've also used accessor definitions rather than `Object.defineProperty` to create the accessors for `#color` and `#wingspan`, and used standard JavaScript naming conventions [`color`, rather than `Color`, and the same for `wingspan`/`Wingspan`].)
But if you wanted to do it more automatically, look for the various mixin helpers that people have built, or build your own (perhaps starting from `addFacades`).
</details>
# 答案2
**得分**: 0
以下是翻译好的部分:
如果你想使用 mixins 的方式,代码可能会像这样。但使用 `Object.assign` 的问题在于它将属性分配给 FlyingCar 类,但这些属性是在类本身之外分配的,这不太符合面向对象编程。
```javascript
let carMixin = {
color: "red",
drive() {
console.log(`vroom`);
}
};
let planeMixin = {
wingspan: 4,
fly() {
console.log(`flying`);
}
};
class FlyingCar {
constructor(){
console.log(this.color)
console.log(this.wingspan)
}
}
Object.assign(FlyingCar.prototype, carMixin);
Object.assign(FlyingCar.prototype, planeMixin);
let f = new FlyingCar()
f.drive()
f.fly()
英文:
If you want to go the route of mixins, it could look like this. But the problem of using Object.assign
is that you assign properties to the FlyingCar class outside of the class itself, which is not very OOP.
let carMixin = {
color:"red",
drive() {
console.log(`vroom`);
}
};
let planeMixin = {
wingspan:4,
fly() {
console.log(`flying`);
}
};
class FlyingCar {
constructor(){
console.log(this.color)
console.log(this.wingspan)
}
}
Object.assign(FlyingCar.prototype, carMixin);
Object.assign(FlyingCar.prototype, planeMixin);
let f = new FlyingCar()
f.drive()
f.fly()
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论