Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve pseudo-classical inheritance right on the class declaration? [duplicate]

Note:

As the answers tell, the code proposed in the question does NOT really achieve inheritance(otherwise it becomes an answer rather than a question .. ) due to some issues described in the question and in my comments. It works as expected as a fake of inheritance(and not even prototypal).


  • Summary

    In short, make it as similar as we are writing a general OO language rather than javascript, but keep the inheritance be correct.

  • The story

    Object.create is a good way to achieve prototypal inheritance, but it's a bit confusing to a typed brain and new fans.

    There are various ways that we can write javascript code more like we are writing other OO languages with the pseudo-classical pattern. As it's pseudo-classical, we must deal with the underlying prototypal inheritance of javascript correctly.

    What I want to find is an approach that the pseudo-classical inheritance can be achieved right on the class declaration. The code for demonstration is put at the rear of the post, it works as expected, however, there are some annoying things:

    1. I cannot get rid of return in the class declaration or the inheritance won't work.

    2. I have no way except pass this in the class declaration to make the returning closure know what is this.

    3. I also want to get rid of function (instance, _super) {, but not yet have a good idea.

    4. The static(own properties) of a class are not inherited.

    A solution would be more of some syntactic sugar than the existing frameworks, a good pattern is applicable.


The _extends function:

function _extends(baseType) {
    return function (definition) {
        var caller=arguments.callee.caller;
        var instance=this;

        if(!(instance instanceof baseType)) {
            (caller.prototype=new baseType()).constructor=caller;
            instance=new caller();
        }

        var _super=function () {
            baseType.apply(instance, arguments);
        };

        definition(instance, _super);
        return instance;
    };
}

The Abc class:

function Abc(key, value) {
    return _extends(Object).call(this, function (instance, _super) {
        instance.What=function () {
            alert('What');
        };

        instance.getValue=function () {
            return 333+Number(value);
        };

        instance.Value=instance.getValue();
        instance.Key=key;
    });
}

The Xyz class:

function Xyz(key, value) {
    return _extends(Abc).call(this, function (instance, _super) {
        _super(key, value);

        instance.That=function () {
            alert('That');
        };
    });
}

Example code:

var x=new Xyz('x', '123');
alert([x.Key, x.Value].join(': ')+'; isAbc: '+(x instanceof Abc));

var y=new Xyz('y', '456');
alert([y.Key, y.Value].join(': ')+'; isAbc: '+(y instanceof Abc));

var it=new Abc('it', '789');
alert([it.Key, it.Value].join(': ')+'; isAbc: '+(it instanceof Abc));
alert([it.Key, it.Value].join(': ')+'; isXyz: '+(it instanceof Xyz));

x.What();
y.That();

it.What();
it.That(); // will throw; it is not Xyz and does not have That method
like image 685
Ken Kin Avatar asked Sep 12 '13 01:09

Ken Kin


2 Answers

No. No. No. This won't do. You're doing inheritance in JavaScript all wrong. Your code gives me migraines.

Creating a Pseudo-Classical Inheritance Pattern in JavaScript

If you want something similar to classes in JavaScript then there are a lot of libraries out there which provide it to you. For example using augment you could restructure your code as follows:

var augment = require("augment");

var ABC = augment(Object, function () {
    this.constructor = function (key, value) {
        this.key = key;
        this.value = value;
    };

    this.what = function () {
        alert("what");
    };
});

var XYZ = augment(ABC, function (base) {
    this.constructor = function (key, value) {
        base.constructor.call(this, key, value);
    };

    this.that = function () {
        alert("that");
    };
});

I don't know about you but to me this looks a lot like classical inheritance in C++ or Java. If this solves your problem, great! If is doesn't then continue reading.

Prototype-Class Isomorphism

In a lot of ways prototypes are similar to classes. In fact prototypes and classes are so similar that we can use prototypes to model classes. First let's take a look at how prototypal inheritance really works:

The above picture was taken from the following answer. I suggest you read it carefully. The diagram shows us:

  1. Every constructor has a property called prototype which points to the prototype object of the constructor function.
  2. Every prototype has a property called constructor which points to the constructor function of the prototype object.
  3. We create an instance from a constructor function. However the instance actually inherits from the prototype, not the constructor.

This is very useful information. Traditionally we've always created a constructor function first and then we've set its prototype properties. However this information shows us that we may create a prototype object first and then define the constructor property on it instead.

For example, traditionally we may write:

function ABC(key, value) {
    this.key = key;
    this.value = value;
}

ABC.prototype.what = function() {
    alert("what");
};

However using our newfound knowledge we may write the same thing as:

var ABC = CLASS({
    constructor: function (key, value) {
        this.key = key;
        this.value = value;
    },
    what: function () {
        alert("what");
    }
});

function CLASS(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

As you can see encapsulation is easy to achieve in JavaScript. All you need to do is think sideways. Inheritance however is a different issue. You need to do a little more work to achieve inheritance.

Inheritance and Super

Take a look at how augment achieves inheritance:

function augment(body) {
    var base = typeof this === "function" ? this.prototype : this;
    var prototype = Object.create(base);
    body.apply(prototype, arrayFrom(arguments, 1).concat(base));
    if (!ownPropertyOf(prototype, "constructor")) return prototype;
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

Notice that the last three lines are the same as that of CLASS from the previous section:

function CLASS(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

This tells us that once we have a prototype object all we need to do is get its constructor property and return it.

The first three lines of augment are used to:

  1. Get the base class prototype.
  2. Create a derived class prototype using Object.create.
  3. Populate the derived class prototype with the specified properties.

That's all that there is to inheritance in JavaScript. If you want to create your own classical inheritance pattern then you should be thinking along the same lines.

Embracing True Prototypal Inheritance

Every JavaScript programmer worth their salt will tell you that prototypal inheritance is better than classical inheritance. Nevertheless newbies who come from a language with classical inheritance always try to implement classical inheritance on top of prototypal inheritance, and they usually fail.

They fail not because it's not possible to implement classical inheritance on top of prototypal inheritance but because to implement classical inheritance on top of prototypal inheritance you first need to understand how true prototypal inheritance works.

However once you understand true prototypal inheritance you'll never want to go back to classical inheritance. I too tried to implement classical inheritance on top of prototypal inheritance as a newbie. Now that I understand how true prototypal inheritance works however I write code like this:

function extend(self, body) {
    var base = typeof self === "function" ? self.prototype : self;
    var prototype = Object.create(base, {new: {value: create}});
    return body.call(prototype, base), prototype;

    function create() {
        var self = Object.create(prototype);
        return prototype.hasOwnProperty("constructor") &&
            prototype.constructor.apply(self, arguments), self;
    }
}

The above extend function is very similar to augment. However instead of returning the constructor function it returns the prototype object. This is actually a very neat trick which allows static properties to be inherited. You can create a class using extend as follows:

var Abc = extend(Object, function () {
    this.constructor = function (key, value) {
        this.value = 333 + Number(value);
        this.key = key;
    };

    this.what = function () {
        alert("what");
    };
});

Inheritance is just as simple:

var Xyz = extend(Abc, function (base) {
    this.constructor = function (key, value) {
        base.constructor.call(this, key, value);
    };

    this.that = function () {
        alert("that");
    };
});

Remember however that extend does not return the constructor function. It returns the prototype object. This means that you can't use the new keyword to create an instance of the class. Instead you need to use new as a method, as follows:

var x = Xyz.new("x", "123");
var y = Xyz.new("y", "456");
var it = Abc.new("it", "789");

This is actually a good thing. The new keyword is considered harmful and I strongly recommend you to stop using it. For example it's not possible to use apply with the new keyword. However it is possible to use apply with the new method as follows:

var it = Abc.new.apply(null, ["it", "789"]);

Since Abc and Xyz are not constructor functions we can't use instanceof to test whether an object is an instance of Abc or Xyz. However that's not a problem because JavaScript has a method called isPrototypeOf which tests whether an object is a prototype of another object:

alert(x.key + ": " + x.value + "; isAbc: " + Abc.isPrototypeOf(x));
alert(y.key + ": " + y.value + "; isAbc: " + Abc.isPrototypeOf(y));

alert(it.key + ": " + it.value + "; isAbc: " + Abc.isPrototypeOf(it));
alert(it.key + ": " + it.value + "; isXyz: " + Xyz.isPrototypeOf(it));

In fact isPrototypeOf is more powerful than instanceof because it allows us to test whether one class extends another class:

alert(Abc.isPrototypeOf(Xyz)); // true

Besides this minor change everything else works just like it did before:

x.what();
y.that();

it.what();
it.that(); // will throw; it is not Xyz and does not have that method

See the demo for yourself: http://jsfiddle.net/Jee96/

What else does true prototypal inheritance offer? One of the biggest advantages of true prototypal inheritance is that there's no distinction between normal properties and static properties allowing you to write code like this:

var Xyz = extend(Abc, function (base) {
    this.empty = this.new();

    this.constructor = function (key, value) {
        base.constructor.call(this, key, value);
    };

    this.that = function () {
        alert("that");
    };
});

Notice that we can create instances of the class from within the class itself by calling this.new. If this.constructor is not yet defined then it returns a new uninitialized instance. Otherwise it returns a new initialized instance.

In addition because Xyz is the prototype object we can access Xyz.empty directly (i.e. empty is a static property of Xyz). This also means that static properties are automatically inherited and are no different from normal properties.

Finally, because the class is accessible from within the class definition as this, you can created nested classes which inherit from the class which they are nested within by using extend as follows:

var ClassA = extend(Object, function () {
    var ClassB = extend(this, function () {
        // class definition
    });

    // rest of the class definition

    alert(this.isPrototypeOf(ClassB)); // true
});

See the demo for yourself: http://jsfiddle.net/Jee96/1/

like image 61
Aadit M Shah Avatar answered Nov 16 '22 18:11

Aadit M Shah


There's an exhaustive tutorial on how to do what you're after.

oop-concepts

pseudo-classical-pattern

all-one-constructor-pattern

like image 39
Asken Avatar answered Nov 16 '22 16:11

Asken