Say I have two classes, A
and B
. B
extends A
and therefore inherits all of its methods. I can override them if I want to as well. My question is whether or not I can prevent B
from inheriting a specific method of A
. What I've tried so far looks like this.
// setup
class A {
constructor(x) {
this.x = x;
}
valueOf() {
return this.x;
}
toString() {
return `{x:${this.x}}`;
}
}
class B extends A {
constructor(x) {
super(x);
delete this.valueOf;
}
}
delete B.prototype.valueOf;
// example
const a = new A(42);
const b = new B(42);
// should work
console.log(a.valueOf());
// should throw TypeError, not a function
console.log(b.valueOf());
Say I have two classes, A
and B
. B
extends A
and therefore inherits all of its methods. I can override them if I want to as well. My question is whether or not I can prevent B
from inheriting a specific method of A
. What I've tried so far looks like this.
// setup
class A {
constructor(x) {
this.x = x;
}
valueOf() {
return this.x;
}
toString() {
return `{x:${this.x}}`;
}
}
class B extends A {
constructor(x) {
super(x);
delete this.valueOf;
}
}
delete B.prototype.valueOf;
// example
const a = new A(42);
const b = new B(42);
// should work
console.log(a.valueOf());
// should throw TypeError, not a function
console.log(b.valueOf());
Share
Improve this question
asked Jul 17, 2017 at 19:08
kamoroso94kamoroso94
1,7351 gold badge16 silver badges19 bronze badges
1
- 2 I would say always prefer Composition over Inheritance. A simple rule for good software as suggested by the Gang of Four. Create/Compose objects and each object should have your required functionality. No hassle :) – Abdul Samad Commented Sep 14, 2017 at 19:34
7 Answers
Reset to default 8A couple folks have already told you how to solve your issue. Now I'd like to try to convince you to not do it. ;-)
Inheritance is supposed to model an IS-A relationship. Or, more specifically, an IS-SUBSTITUTABLE-FOR-A relationship. That's the Liskov Substitution Principle (the "L" in SOLID).
Any piece of code that is expecting to operate on an "A" object is supposed to be able to be given a "B" object instead ("A" is substituted for a "B") and everything should still just work. But if "B" is missing a method, if it doesn't provide the full interface of "A", then it isn't substitutable anymore, and odds are very good this isn't the right situation to use inheritance.
Actually valueOf
is bad example because every object gets one from Object.prototype
. Try console.log(({}).valueOf())
But you could do a trick by hiding this property
// setup
class A {
constructor(x) {
this.x = x;
}
valueOf() {
return this.x;
}
toString() {
return `{x:${this.x}}`;
}
}
class B extends A {
get valueOf() { return undefined }
}
class C extends A {
}
Object.defineProperty(C.prototype, 'valueOf', {})
// example
const a = new A(42);
const b = new B(42);
const c = new C(42);
// should work
console.log(a.valueOf());
// should throw TypeError, not a function
try {
console.log(b.valueOf());
} catch (e) {
console.log(e.message)
}
try {
console.log(c.valueOf());
} catch (e) {
console.log(e.message)
}
delete this.valueOf
and delete B.prototype.valueOf
don't work because there's no valueOf
property to delete. Inheritance works by searching the prototype chain when a property isn't found in the object itself, not by copying properties from the parent.
What you can do is assign something to this.valueOf
so it won't follow the chain:
this.valueOf = null;
Then it will get an error that null
is not a function.
And rather than doing it in each object, you can do it in B
's prototype:
B.prototype.valueOf = null;
No, this is impossible. Inheritance in JS means inheritance from another object (the prototype), not inheritance of its individual properties. If you inherit from an object, you inherit all its properties.
As you said, you can override the properties of course. You cannot delete
them as the inheriting object doesn't contain them at all - you'd need to delete them from the prototype, which you don't want. You can however easily shadow it with an undefined
value:
class A {
constructor(x) {
this.x = x;
}
valueOf() {
return this.x;
}
toString() {
return `{x:${this.x}}`;
}
}
class B extends A {
}
B.prototype.valueOf = undefined;
In your example B does not extend A but rather it extends a portion of A. That subset of features is the actual common ancestor, so you could refactor so that A and B both extend a common class X.
You could also implement the function in B so that it throws an error explicitly but that doesn't seem to be your goal. In Javascript it has become popular to use composition to create classes with the same functionality but one implementation point.
Here is how you would write it with composition:
const ValueProto = {
getValue() {
return this.x
}
}
const StringProto = {
toString() {
return `{x:${this.x}}`;
}
}
const ClassA = (x) => Object.assign({}, ValueProto, StringProto, {x});
const ClassB = (x) => Object.assign({}, StringProto, {x});
const object1 = ClassA(5)
console.log(object1.toString()) // 'x:5'
console.log(object1.getValue()) // 5
const object2 = ClassB('hello')
console.log(object2.toString()) // 'x:hello'
console.log(object2.getValue()) // not a function
This is a quick example and there are a number of different models for implementing composition some use the Class object but I don't know that one off the top of my head.
@Bergi already did explain why inheritance is not the right tool dealing with this kind of shaping types.
What one is looking for are mixin based composition techniques. Due to the provided special example it even will be traits for they can handle composition of behavior with overwriting, aliasing, omitting and even modifying it while having involved both traits and classes.
Since JavaScript does not support traits a solution that comes close to it might look like the one provided at SO to "Mixins for ES6 classes, transpiled with babel"
The underlying patterns there are functions and proxy objects via closures, delegation via apply
/call
as well as forwarding.
A straightforward approach that refers to the OP's provided example code and also uses some of the above mentioned techniques and mixin-patterns could look like this one ...
function withFullyExposeX() { // function based mixin pattern ...
//
this.valueOf = function () { // ... implementing `this` bound
return this.x; // behavior that always has to be
}; // applied via `call` or `apply`
this.toString = function () { // onto any object type.
return `{x:${this.x}}`; //
}; //
} //
var withExposeXValueAndShadowXStringify = (function (mixin) {
return function () {
mixin.call(this); // - apply existing mixin/trait.
this.toString = function () {}; // - overwrite specific behavior.
};
}(withFullyExposeX));
var withProxyOnlyExposesValueOfX = (function (mixin) { // function based trait pattern ...
var localProxy = {}; //
mixin.call(localProxy); // ... that also is mixin based but
// uses a local proxy object in order
return function () { // to reuse the implementation of an
// already existing mixin or trait.
this.valueOf = function () { //
return localProxy.valueOf.call(this); // thus such a pattern not only uses
}; // explicit delegation via `apply`/`call`
//this.toString = function () {} // - overwrite. // but it in addition features forwarding
} // via it's encapsulated proxy object.
}(withFullyExposeX)); //
class X {
constructor(x) {
this.x = x;
}
}
class A extends X {}
// constructor(x) {
// //withFullyExposeX.call(this); // applying the mixin does work for both positions ...
// } //
//} //
withFullyExposeX.call(A.prototype); // ... but prototype in this case is the better choice.
class B extends X {}
withExposeXValueAndShadowXStringify.call(B.prototype);
class C extends X {}
withProxyOnlyExposesValueOfX.call(C.prototype);
var
x = new X('x'),
a = new A('a'),
b = new B('b'),
c = new C('c');
console.log('x.valueOf : ', x.valueOf);
console.log('a.valueOf : ', a.valueOf);
console.log('b.valueOf : ', b.valueOf);
console.log('c.valueOf : ', c.valueOf);
console.log('x.valueOf() : ', x.valueOf());
console.log('a.valueOf() : ', a.valueOf());
console.log('b.valueOf() : ', b.valueOf());
console.log('c.valueOf() : ', c.valueOf());
console.log('x.toString : ', x.toString);
console.log('a.toString : ', a.toString);
console.log('b.toString : ', b.toString);
console.log('c.toString : ', c.toString);
console.log('x.toString() : ', x.toString());
console.log('a.toString() : ', a.toString());
console.log('b.toString() : ', b.toString());
console.log('c.toString() : ', c.toString());
.as-console-wrapper { max-height: 100%!important; top: 0; }
Here is workaround to remove unnecessary prop from derived constructor:
class Man {
constructor(name, lastName, age){
//console.log(arguments);
this.name = name;
this.lastName = lastName;
this.age = age;
}
}
const guy = new Man('Peter', 'Singh', 26);
console.log(guy);
class Worker extends Man {
constructor(lastName, age, experience){
super(name,lastName, age);
delete this.name;
this.experience = experience;
}
}
const worker = new Worker('Singh', 26, '11 years');
console.log(worker);