Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding why true prototypal inheritance is better than classical/pseudo prototypal inheritance and why i shouldn't use "new"

Reading some articles from Aadit M Shah like Why Prototypal Inheritance Matters or Stop Using Constructor Functions in JavaScript from Eric Elliott i think i understand all of their arguments, in theoric. But in practice i don't see the real advantages of this pattern.

Let's take a look two implementations from two snippets to make inheritance.

  1. First one is using augment.js it's a script from Aadit M Shah
  2. On this example we are going to use this script. Is made it by Aadit M Shah as well.

Implementation 1:

    var AugmentPerson = Object.augment(function() {
      this.constructor = function(name) {
          this.name = name;
      };
      this.setAddress = function(country, city, street) {
          this.country = country;
          this.city = city;
          this.street = street;
      };
    });
    var AugmentFrenchGuy = AugmentPerson.augment(function(base) {
      this.constructor = function(name) {
          base.constructor.call(this,name);
      };
      this.setAddress = function(city, street) {
          base.setAddress.call(this, "France", city, street);
      };
    });
    var AugmentParisLover = AugmentFrenchGuy.augment(function(base) {

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

      this.setAddress = function(street) {
          base.setAddress.call(this, "Paris", street);
      };
    });
    var t = new AugmentParisLover("Mary");
    t.setAddress("CH");
    console.log(t.name, t.country, t.city, t.street); //Mary France Paris CH

In this example we are using function constructors instead of inherit directly from a object.

Implementation 2:

    var CreatePerson = {
        create: function (name) {
            this.name = name;
            return this.extend();
        },
        setAddress: function(country, city, street) {
             this.country = country;
             this.city = city;
             this.street = street;
        }
    };
    var CreateFrenchGuy  = CreatePerson.extend({
        create: function (name) {
            return CreatePerson.create.call(this,name);
        },
        setAddress: function(city, street) {
            CreatePerson.setAddress('France', city, street);
        }
    });
    var CreateParisLover  = CreateFrenchGuy.extend({
        create: function (name) {
            return CreateFrenchGuy.create.call(this,name);
        },
        setAddress: function(street) {
            CreateFrenchGuy.setAddress('Paris', street);
        }
    });

    var t = CreateParisLover.create("Mary");
    t.setAddress("CH");
    console.log(t.name, t.country, t.city, t.street); //Mary France Paris CH

To be honest, i'm trying to see the benefits of the second implementation. But i am not able. The only point i see is more flexible is because we can create the instance using apply:

var t = CreateParisLover.create.apply(CreateParisLover, ["Mary"]);

This give us more flexibility, it's true. But we can do the same with this:

  Function.prototype.new = function () {
     function functor() { return constructor.apply(this, args); }
     var args = Array.prototype.slice.call(arguments);
     functor.prototype = this.prototype;
     var constructor = this;
     return new functor;
  };

Then we can:

var t = AugmentParisLover.new.apply(AugmentParisLover, ["Mary"]);

What is the real benefits in terms of flexibility, re-usability, difficulty... Because if you check the performance of both cases. Object.create() is pretty much slower than new: http://jsperf.com/inheritance-using-create-vs-new I'm confusing.

like image 531
EnZo Avatar asked Nov 28 '13 12:11

EnZo


People also ask

What is the difference between classical inheritance and prototypal inheritance?

Classical inheritance is limited to classes inheriting from other classes. However prototypal inheritance includes not only prototypes inheriting from other prototypes but also objects inheriting from prototypes.

What is the point of prototypal inheritance?

Prototypical inheritance allows us to reuse the properties or methods from one JavaScript object to another through a reference pointer function.

What are some drawbacks of classical inheritance?

Unfortunately, classical inheritance isn't very flexible. This is because inheritance tightly couples classes and their objects. A piece of code is tightly coupled to another piece of code when they are reliant on each other for functionality.

What languages use prototypal inheritance?

Prototypal inheritance is a form of object-oriented code reuse. Javascript is one of the only [mainstream] object-oriented languages to use prototypal inheritance. Almost all other object-oriented languages are classical.


1 Answers

Programming is a lot like fashion. Subconsciously most programmers write code which to them looks aesthetically pleasing. This is the main reason why Java programmers want to implement classical inheritance in JavaScript. Yes, trying to implement classical inheritance in JavaScript is a monolithic task but that doesn't stop people from doing it. It's an overkill but people still do it because they just want their code to look like classes (e.g. jTypes).

In much the same way Eric and I have been trying to popularize the use of factory functions instead of constructor functions. However this shift from factories to constructors is not just for aesthetic reasons. The two of us are trying to change the mentality of JavaScript programmers because in certain aspects we both believe that JavaScript is fundamentally flawed. The new operator in JavaScript is one such aspect. Although it's broken yet it's central to the language and hence it cannot be avoided.

The bottom line is this:

If you want to create prototype chains in JavaScript then you have to use new. There is no other way around it (except .__proto__ which is frowned upon).

Interestingly you need neither prototypes nor classes to inherit from multiple objects. Using object composition you can achieve strong behavioral subtyping in JavaScript as Benjamin Gruenbaum describes in the following answer: https://stackoverflow.com/a/17008693/783743

In this answer I'll touch upon the following topics:

  1. Why are we stuck with new?
  2. Why are factories better than constructors?
  3. How do we get the best of both worlds?

1. Why are we stuck with new?

The new keyword is put on a pedestal in JavaScript. There's no way to create a prototype chain in JavaScript without using new. Yes you can change the .__proto__ property of an object but only after it's created, and that practice is frowned upon. Even Object.create uses new internally:

Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F;
};

As Douglas Crockford mentioned:

The Object.create function untangles JavaScript's constructor pattern, achieving true prototypal inheritance. It takes an old object as a parameter and returns an empty new object that inherits from the old one. If we attempt to obtain a member from the new object, and it lacks that key, then the old object will supply the member. Objects inherit from objects. What could be more object oriented than that?

The point is that although the new keyword in JavaScript is "tangled" up there's no other way to create prototype chains in JavaScript. The Object.create function, even when implemented natively, is still slower than using new and hence for performance reasons alone most people still use new even though Object.create is a more logically sound option.

2. Why are factories better than constructors?

Now you might wonder whether new is really so bad. After all performance wise it is indeed the best solution. In my opinion however it shouldn't be so. Whether you use new or Object.create performance should always be the same. This is where the language implementations are lacking. They should really strive towards making Object.create faster. So besides performance does new have any other redeeming qualities? In my humble opinion it doesn't.

Oftentimes you don't really know what's wrong with a language until you start using a better language. So let's see some other languages:

a) Magpie

Magpie is a hobby language created by Bob Nystrom. It has a bunch of very interesting features which interact very nicely with each other, namely:

  1. Patterns
  2. Classes
  3. Multimethods

Classes in Magpie however are more akin to prototypes in JavaScript or data types in Haskell.

In Magpie instantiation of classes is split into two steps:

  1. Constructing a new instance.
  2. Initializing the newly constructed instance.

In JavaScript the new keyword combines the construction and the initialization of instances. This is actually a bad thing because as we'll see soon splitting construction and initialization is actually a good thing.

Consider the following Magpie code:

defclass Point
    var x
    var y
end

val zeroPoint = Point new(x: 0, y: 0)

def (this == Point) new (x is Int, y is Int)
    match x, y
        case 0, 0 then zeroPoint
        else this new(x: x, y: y)
    end
end

var origin = Point new(0, 0)
val point = Point new(2, 3)

This is equivalent to the following JavaScript code:

function Point(x, y) {
    this.x = x;
    this.y = y;
}

var zeroPoint = new Point(0, 0);

Point.new = function (x, y) {
    return x === 0 && y === 0 ?
    zeroPoint : new Point(x, y);
};

var origin = Point.new(0, 0);
var point = Point.new(2, 3);

As you can see here we've split the construction and the initialization of instances into two functions. The Point function initializes the instance and the Point.new function constructs the instance. In essence we have simply created a factory function.

Separating construction from initialization is such a useful pattern that the good people of the JavaScript room have even blogged about it, calling it the Initializer Pattern. You should read about the initializer pattern. It shows you that initialization in JavaScript is separate from construction.

  1. Factories like Object.create (+1): Construction is separate from initialization.
  2. The new operator (-1): Construction and initialization are inseparable.

b) Haskell

JavaScript has been my favorite language since the past 8 years. Recently however I started programming in Haskell and I must admit that Haskell has stolen my heart. Programming in Haskell is fun and exciting. JavaScript still has a long way to go before it'll be in the same league as Haskell and there's much that JavaScript programmers can learn from Haskell. I would like to talk about algebraic data types from Haskell apropos to this question.

Data types in Haskell are like prototypes in JavaScript and data constructors in Haskell are like factory functions in JavaScript. For example the above Point class would be written as follows in Haskell:

data Point = Point Int Int
zeroPoint = Point 0 0
origin = zeroPoint
point = Point 2 3

Succinct isn't it? However I'm not here to sell Haskell so let's take a look at some other features Haskell offers:

data Shape = Rectangle Point Point | Circle Point Int
rectangle = Rectangle origin (Point 3 4)
circle = Circle zeroPoint 3

Here rectangle and circle are both instances of type Shape:

rectangle :: Shape
circle :: Shape

In this case Shape is our prototype (data type in Haskell) and rectangle and circle are instances of that data type. More interestingly however the Shape prototype has two constructors (data constructors in Haskell): Rectangle and Circle.

Rectangle :: Point -> Point -> Shape
Circle :: Point -> Int -> Shape

The Rectangle data constructor is a function which takes a Point and another Point and returns a Shape. Similarly the Circle data constructor is a function which takes a Point and an Int and returns a Shape. In JavaScript this would be written as follows:

var Shape = {};

Rectangle.prototype = Shape;

function Rectangle(p1, p2) {
    this.p1 = p1;
    this.p2 = p2;
}

Circle.prototype = Shape;

function Circle(p, r) {
    this.p = p;
    this.r = r;
}

var rectangle = new Rectangle(origin, Point.new(3, 4));
var circle = new Circle(zeroPoint, 3);

As you can see a prototype in JavaScript can have more than one constructor and that makes sense. It's also possible for one constructor to have different prototypes at different instances of time but that makes no sense at all. Doing so would break instanceof.

As it turns out having multiple constructors is a pain when using the constructor pattern. However it's a match made in heaven when using the prototypal pattern:

var Shape = {
    Rectangle: function (p1, p2) {
        var rectangle = Object.create(this);
        rectangle.p1 = p1;
        rectangle.p2 = p2;
        return rectangle;
    },
    Circle: function (p, r) {
        var circle = Object.create(this);
        circle.p = p;
        circle.r = r;
        return circle;
    }
};

var rectangle = Shape.Rectangle(zeroPoint, Point.new(3, 4));
var circle = Shape.Circle(origin, 3);

You could also use the extend function from my blog post on Why Prototypal Inheritance Matters to make the above code more succinct:

var Shape = {
    Rectangle: function (p1, p2) {
        return this.extend({
            p1: p1,
            p2: p2
        });
    },
    Circle: function (p, r) {
        return this.extend({
            p: p,
            r: r
        });
    }
};

var rectangle = Shape.Rectangle(zeroPoint, Point.new(3, 4));
var circle = Shape.Circle(origin, 3);

Factories written in this way look a lot like the module pattern and it feels natural to write code like this. Unlike with the constructor pattern everything is wrapped up nicely in an object literal. Nothing is dangling here, there and everywhere.

Nevertheless if performance is your main concern then stick with the constructor pattern and new. In my opinion however modern JavaScript engines are fast enough that performance is no longer the main factor. Instead I think JavaScript programmers should invest more time in writing code that's maintainable and robust and the prototypal pattern is indeed more elegant and understandable than the constructor pattern.

  1. Factories (+1): You can easily create multiple factories for each prototype.
  2. Constructors (-1): Creating multiple constructors for each prototype is hacky and clumsy.
  3. Prototypal Pattern (+1): Everything is encapsulated within a single object literal. Looks a lot like the module pattern.
  4. Constructor Pattern (-1): It's unstructured and looks incohesive. Difficult to understand and maintain.

In addition Haskell also teaches us about pure functional programming. Since factories are simply functions we can call and apply factories, compose factories, curry factories, memoize factories, make factories lazy by lifting them and much more. Because new is an operator and not a function you can't do that using new. Yes you can make a functional equivalent of new but then why not just use factories instead? Using the new operator in some places and the new method in other places is inconsistent.

3. How do we get the best of both worlds?

Alright so factories do have their advantages, but still the performance of Object.create sucks doesn't it? It does, and one of the reasons is because every time we use Object.create we create a new constructor function, set its prototype to the prototype we want, instantiate the newly created constructor function using new and then return it:

Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F;
};

Can we do better than this? Let's try. Instead of creating a new constructor every time why don't we just instantiate the .constructor function of the given prototype?

Object.create = function (o) {
    return new o.constructor;
};

This works in most cases but there are a few problems:

  1. The prototype of o.constructor might be different from o.
  2. We only want to construct a new instance of o, but o.constructor might have initialization logic as well which we can't separate from the construction.

The solution is pretty simple:

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

Using defclass you can create classes as follows:

var Shape = defclass({
    rectangle: function (p1, p2) {
        this.p1 = p1;
        this.p2 = p2;
        return this;
    },
    circle: function (p, r) {
        this.p = p;
        this.r = r;
        return this;
    }
});

var rectangle = (new Shape).rectangle(zeroPoint, Point.new(3, 4));
var circle = (new Shape).circle(origin, 3);

As you can see we've separated construction and initialization and the initialization can be deferred to multiple constructors. It can even be chained as follows: (new Shape).rectangle().circle(). We've replaced Object.create with new which is much faster and we still have the flexibility to do whatever we want. In addition everything is nicely encapsulated within a single object literal.

Conclusion

As you can see the new operator is a necessary evil. If new was a implemented as a factory function then that would be great but it's implemented as an operator instead and operators in JavaScript are not first class. This makes it more difficult to do functional programming with new. Factories on the other hand are flexible. You can tailor make any number of factory functions for your prototypes and the ability to do whatever you want is the biggest selling point of factory functions.

like image 75
Aadit M Shah Avatar answered Jan 10 '23 10:01

Aadit M Shah