合并两个类,也称为多继承。

huangapple go评论69阅读模式
英文:

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&#39;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&#39;s first look at why your current implementation doesn&#39;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&#39;d done `target.prop = source.prop`. You can see that here:

&lt;!-- begin snippet: js hide: true console: true babel: false --&gt;

&lt;!-- language: lang-js --&gt;

    const obj = {
        get random() {
            return Math.random();
        }
    };
    console.log(&quot;Using obj directly:&quot;);
    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(&quot;Using copy from Object.assign:&quot;);
    console.log(copy.random); // The same
    console.log(copy.random); // ...value every...
    console.log(copy.random); // ...time

&lt;!-- language: lang-css --&gt;

    .as-console-wrapper {
        max-height: 100% !important;
    }

&lt;!-- end snippet --&gt;

So we can&#39;t just use `Object.assign` for this.

Separately, to use `Aircraft` instance properties and fields (both `Wingspan` and `#wingspan` are instance-specific), you&#39;ll need an instance of `Aircraft`&amp;nbsp;&amp;mdash; those don&#39;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&#39;t try to handle inherited properties]):

&lt;!-- begin snippet: js hide: false console: true babel: false --&gt;

&lt;!-- language: lang-js --&gt;

    &quot;use strict&quot;;

    function addFacades(target, source) {
        for (const name of Object.getOwnPropertyNames(source)) {
            if (
                typeof source !== &quot;function&quot; ||
                (name !== &quot;prototype&quot; &amp;&amp; name !== &quot;name&quot; &amp;&amp; name !== &quot;length&quot;)
            ) {
                const sourceDescr = Object.getOwnPropertyDescriptor(source, name);
                const targetDescr = {
                    enumerable: sourceDescr.enumerable,
                    get: () =&gt; {
                        return source[name];
                    },
                };
                if (sourceDescr.writable || sourceDescr.set) {
                    targetDescr.set = (value) =&gt; {
                        source[name] = value;
                    };
                }
                Object.defineProperty(target, name, targetDescr);
            }
        }
    }

    class Car {
        #color;

        constructor() {
            Object.defineProperty(this, &quot;Color&quot;, {
                configurable: true,
                enumerable: true,
                get: function () {
                    return this.#color;
                },
                set: function (value) {
                    if (value &amp;&amp; typeof value !== &quot;string&quot;) {
                        return console.error(&quot;Error: Color must be a string&quot;);
                    }
                    this.#color = value;
                },
            });
        }
    }

    class Aircraft {
        #wingspan;

        constructor() {
            Object.defineProperty(this, &quot;Wingspan&quot;, {
                configurable: true,
                enumerable: true,
                get: function () {
                    return this.#wingspan;
                },
                set: function (value) {
                    if (value &amp;&amp; typeof value !== &quot;number&quot;) {
                        return console.error(&quot;Error: Wingspan must be a number&quot;);
                    }
                    this.#wingspan = value;
                },
            });
        }

        static fly() {
            console.log(&quot;whhhhheeeee! I am flying!&quot;);
        }
    }

    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(&quot;fly&quot; in Aircraft);
    console.log(Object.hasOwn(Aircraft, &quot;fly&quot;));
    addFacades(FlyingCar, Aircraft);

    // put it all together
    const flyingCar = new FlyingCar();
    flyingCar.Color = 42; // returns an error as expected
    flyingCar.Wingspan = &quot;green&quot;; // returns an error as expected
    FlyingCar.fly(); // works
    flyingCar.FavoriteIceCream = &quot;chocolate&quot;; // fails

&lt;!-- end snippet --&gt;

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&#39;s in a module. In the above, since it&#39;s not a module, I needed the directive.)

At this point, `FlyingCar` inherits from `Car` and mixes in `Aircraft`. That is, it&#39;s half inheritance and half composition. That might be reasonable if the class is &quot;mostly&quot; a `Car` and only partially an `Aircraft`, but it&#39;s still...assymmetrical. That lack of balance suggests it&#39;s not the best approach.

Which brings us back to: I&#39;d use composition, not inheritance, and I&#39;d probably lean toward doing it manually rather than using `addFacades` above. Perhaps something like this:

&lt;!-- begin snippet: js hide: false console: true babel: false --&gt;

&lt;!-- language: lang-js --&gt;

    &quot;use strict&quot;;

    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 &amp;&amp;` that used to be on the `if` below, because it
            // shouldn&#39;t be there. `0` is a falsy value that isn&#39;t a string (as are
            // several others -- `NaN`, `undefined`, `null`, `false`, ...), you don&#39;t
            // want to assign those to your `#color` field either.
            if (typeof value !== &quot;string&quot;) {
                // (In your real code I assume you `throw` here, but doing it this way
                // is convenient for your example so I suspect that&#39;s why you did it).
                return console.error(&quot;Error: color must be a string&quot;);
            }
            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&#39;ve removed the `value &amp;&amp;` part of the below. `&quot;&quot;` is a
            // falsy value (as are several others that aren&#39;t numbers -- `undefined`,
            // `null`, `false`, ...). You don&#39;t want to assign those to `#wingspam`.
            if (value &amp;&amp; typeof value !== &quot;number&quot;) {
                // (In your real code I assume you `throw` here, but doing it this way
                // is convenient for your example so I suspect that&#39;s why you did it).
                return console.error(&quot;Error: wingspan must be a number&quot;);
            }
            this.#wingspan = value;
        }

        static fly() {
            console.log(&quot;whhhhheeeee! I am flying!&quot;);
        }
    }

    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 = &quot;green&quot;; // returns an error as expected
    flyingCar.color = &quot;green&quot;; // works
    flyingCar.wingspan = 42; // works
    console.log(`color = ${flyingCar.color}`);
    console.log(`wingspan = ${flyingCar.wingspan}`);
    FlyingCar.fly(); // works
    flyingCar.favoriteIceCream = &quot;chocolate&quot;; // fails

&lt;!-- language: lang-css --&gt;

    .as-console-wrapper {
        max-height: 100% !important;
    }

&lt;!-- end snippet --&gt;

(In the above, I&#39;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:&quot;red&quot;,
  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()

huangapple
  • 本文由 发表于 2023年5月10日 16:18:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76216285.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定