billyzaelani
billyzaelani

Reputation: 597

Javascript inheritance: calling parent method automatically if property name are the same

Is there a way to call parent method automatically if the property name are the same.

// library part
var parent = {
    foo: function() { console.log('foo from parent'); },
    bar: function() { console.log('bar from parent'); }
}

// User part
var child = Object.create(parent);

child.foo = function() { console.log('foo from child'); };
child.bar = function() { console.log('bar from child'); };

The output if i call child.foo() is foo from child, and my objective output is both foo from child and foo from parent is console logged out. For now when i need to do that, i use this code:

child.foo = function() {
     console.log('foo from child');
     this.__proto__.foo();
}

I have a reason to do that, it's like library that provide a parent object that can be extended by the user and i don't want to remind the user to call this.__proto__.foo() every time they assign the method which the property name are the same with the property from parent object.

The solution must be modifying the library part of course (parent object or maybe in object creation), not the user part.

Upvotes: 2

Views: 120

Answers (2)

Andrei CACIO
Andrei CACIO

Reputation: 2119

I guess another way to do this, without calling the proto explicitly would be to use a factory function:

const createChild = parent => Object.create(parent);
const enchanceChild = parent => ({
  foo() {
     console.log('from child');
     parent.foo();
  }
});

const parent = {
   foo(){ console.log('from parent') },
}

const child = createChild(parent);
const enchancedChild = enchanceChild(child);

enchancedChild.foo(); // from child form parent

It is just another approach of doing what you already did above.

What do you think?

EDIT:

Using Proxy Objects a more cleaner approach:

var createObject = parent => {
   var handler = {
      get(target, name) {
         if (typeof target[name] === 'function' && typeof parent[name] === 'function') {
            return function() {
                const res = target[name]();
                parent[name]();
                return res;
            }
         }
         return target[name];
      }
   }

   return new Proxy({}, handler);
}

const parent = { foo() { console.log('parent'); }, };
const child = createObject(parent);

child.foo = () => console.log('child');

child.foo();
// returns:
// child
// parent

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074138

Re your edit

The solution must be modifying the library part of course (parent object or maybe in object creation), not the user part.

If that means you really want the users to be able to write

child.foo = function() { ... };

...then that changes things a bit, but it's still possible without resorting to proxies (which are really slow). First, my original answer, since you'd accepted it, then an update accounting for that:


Original Answer

i don't want to remind the user to call this.__proto__.foo() every time they assign the method which the property name are the same with the property from parent object

In that case, you probably want to give them a worker function they use to set those function properties, and automatically detect the situation and handle it. You'll have to decide whether to call the prototype's version before or after the user's new code, if you're not going to let them control where the call goes, and decide whether to return the result of their function or the prototype's.

Naturally, nothing prevents them from choosing to ignore that, but there's only so much you can do.

For instance, this example assumes you want to call the original after (as shown in your example) and return the result of their override.

var mainProto = {
    addMethod: function(name, method) {
        var p = Object.getPrototypeOf(this); // See note later
        if (typeof p[name] === "function") {
            this[name] = function() {
                var result = method.apply(this, arguments);
                p[name].apply(this, arguments);
                return result;
            };
        } else {
            this[name] = method;
        }
        return this;
    }
};
var parent = Object.create(mainProto);
parent.addMethod("foo", function foo() { console.log('foo from parent'); });
parent.addMethod("bar", function bar() { console.log('bar from parent'); });
var child = Object.create(parent);
child.addMethod("foo", function foo() { console.log('foo from child'); });
child.addMethod("bar", function bar() { console.log('bar from child'); });
var grandChild = Object.create(child);
grandChild.addMethod("foo", function foo() { console.log('foo from grandChild'); });
grandChild.addMethod("bar", function bar() { console.log('bar from grandChild'); });

console.log("---");
parent.foo();
parent.bar();
console.log("---");
child.foo();
child.bar();
console.log("---");
grandChild.foo();
grandChild.bar();
.as-console-wrapper {
  max-height: 100% !important;
}


About __proto__ vs getPrototypeOf: __proto__ is officially only supported on browsers, not in other JavaScript environments, and is defined on Object.prototype so it's entirely possible to have an object without it (via Object.create(null)). In constrast, getPrototypeOf is defined across environments and works regardless of whether the object inherits from Object.prototype.


Update for edit

We can use a setter on the properties in question to insert our wrappers instead of having the user call addMethod:

// Library code
function wrapMethod(newMethod, parentMethod) {
    if (typeof parentMethod === "function") {
        return function() {
            var result = newMethod.apply(this, arguments);
            parentMethod.apply(this, arguments);
            return result;
        };
    }
    return newMethod;
}
function defineMethod(obj, name, method) {
    Object.defineProperty(obj, name, {
        set: function(newValue) {
            if (this === obj) {
                return;
            }
            defineMethod(this, name,wrapMethod(newValue, Object.getPrototypeOf(this)[name]));
        },
        get: function() {
            if (this === obj) {
                return method;
            }
            return this[name];
        }
    });
}
var parent = (function() {
    var _foo = function foo() { console.log("foo from parent"); };
    var _bar = function bar() { console.log("bar from parent"); };
    var parent = {};
    defineMethod(parent, "foo", _foo);
    defineMethod(parent, "bar", _bar);
    return parent;
})();

// User code
var child = Object.create(parent);
child.foo = function foo() { console.log('foo from child'); };
child.bar = function bar() { console.log('bar from child'); };
var grandChild = Object.create(child);
grandChild.foo = function foo() { console.log('foo from grandChild'); };
grandChild.bar = function bar() { console.log('bar from grandChild'); };

console.log("---");
parent.foo();
parent.bar();
console.log("---");
child.foo();
child.bar();
console.log("---");
grandChild.foo();
grandChild.bar();
.as-console-wrapper {
  max-height: 100% !important;
}

Upvotes: 3

Related Questions