Understanding why true prototypal inheritance is b

2020-05-19 03:15发布

问题:

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.

回答1:

Similar questions have been asked and answered many times before. See:

Constructor function vs Factory functions Classical Vs prototypal inheritance

More learning: https://medium.com/javascript-scene/3-different-kinds-of-prototypal-inheritance-es6-edition-32d777fa16c9#.s0r3i5w6t http://vimeo.com/69255635

tl;dr

  • Constructors break the open / closed principle
  • Constructors conflate object creation with object initialization - sometimes hampering the reusability of the code
  • Constructors look a bit like classes, which is confusing. JavaScript doesn't need classes (I recommend avoiding the class keyword coming in ES6). JavaScript has something better than classes.
  • The combination of prototype delegation and dynamic object extension (concatenative inheritance) is much more powerful and flexible than classical inheritance.
  • The connections between the Constructor.prototype and instances are frail and untrustworthy in JavaScript. Using constructors can provide the illusion of a working instanceof, which could be confusing when it doesn't work across execution contexts, or doesn't work if the constructor prototype gets swapped out.
  • Swapping out the prototype is harder with constructors. You may want to do that to enable polymorphic object construction. With factories, hot swappable prototypes are easy, and can be done using .call() and .apply().

Edit - responding to the "answer" posted by the OP:

The best thing about Object.create is that it's a dedicated, low-level tool that lets you create a new object and assign any prototype you want to it without using a constructor function. There are lots of reasons to avoid constructors, covered in-depth here: Constructor function vs Factory functions

  1. The code you use to demonstrate "less code" doesn't really demonstrate the difference between classical and prototypal inheritance at all. A more typical example might look like:

Classical

var Animal = function Animal(name) {
  this.name = name;
};

Animal.prototype.walk = function walk() {
  console.log(this.name + ' goes for a walk.');
};

var Rabbit = function Rabbit(/* name */) {
  // Because construction and instantiation are conflated, you must call super().
  Animal.prototype.constructor.apply(this, arguments);
};

// Classical inheritance is really built on top of prototypal inheritance:
Rabbit.prototype = Object.create(Animal.prototype);

// Fix the .constructor property:
Rabbit.prototype.constructor = Rabbit;

Rabbit.prototype.jump = function jump() {
  console.log(this.name + ' hops around a bit.');
};

var myRabbit = new Rabbit('Bunny George');

myRabbit.walk();
// Bunny George goes for a walk.

Prototypal

var animalMethods =  {
  walk: function walk() {
    console.log(this.name + ' goes for a walk.');
  }
};

var animal = function animal(name) {
  var instance = Object.create(animalMethods);
  instance.name = name;
  return instance;
};

var rabbitMethods = {
  jump: function jump() {
    console.log(this.name + ' hops around a bit.');
  }
};

var rabbit = function rabbit(name) {
  var proto = rabbitMethods;

  // This is more commonly done like mixin({}, animalMethods, rabbitMethods);
  // where mixin = $.extend, _.extend, mout.object.mixIn, etc... It just copies
  // source properties to the destination object (first arg), where properties from
  // the last argument override properties from previous source arguments.
  proto.walk = animalMethods.walk;
  var instance = Object.create(rabbitMethods);

  // This could just as easily be a functional mixin,
  // shared with both animal and rabbit.
  instance.name = name;
  return instance;
};

var rabbit2 = rabbit('Bunny Bob');

rabbit2.walk();
// Bunny Bob goes for a walk.

The amount of code required is pretty similar, but to me, it's a LOT more clear what the prototypal stuff is doing, and it's also a lot more flexible, and has none of the classical inheritance arthritic baggage of the first example.



回答2:

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.



回答3:

In JavaScript, what people call "pseudo-classical" inheritance is prototypical inheritance. That's the only kind of inheritance JavaScript has. Avoiding new is like avoiding switch statements because you can do that using if/else if instead. Sure you can, and sometimes you should. Other times, switch is exactly the right choice. Same with new and Object.create: Use the best one for what you're doing.

To me, and this is a bit subjective (as is the whole "pseudo-classical inheritance is bad" meme, in my view):

  1. new is for when I'm doing class-like things. I use new and constructor functions because it fits well with how the language is designed. (Yes, that design is unusual, but that's how it is.) So if I'm going to have objects that will represent people and have common behavior, I'll use a Person constructor, assign behaviors (functions) to Person.prototype, and use new Person to create them. (I use my Lineage script to make this more concise, and to handle some hierarchy stuff easily.) This is straightforward, familiar, clean, clear: If you see new Person you know I'm creating a new object. (If I'm not — yes, it's possible to violate that expectation with a constructor function — then to my mind I shouldn't be writing a constructor function in the first place.)

    Now of course, you can define a builder function (createPerson, buildPerson, whatever) that does the same thing using Object.create or similar. I don't have a problem with people doing that if that's what they prefer (as long as the function name is clear it creates something). I do have a problem with people saying "you shouldn't use new" as though it were objective advice; it's an opinion, it's style advice.

  2. Object.create is for when I'm doing instance-level stuff. There's a project I work on that has a bunch of objects in a complex tree/graph. They're data-only, no behavior. Sometimes, we need to have data that's not yet verified, and so shouldn't overwrite the previous data. So the container has a reference to the verified data (verified) and to the unverified data (current). To avoid unnecessary branching in the code, the container always has both references, but in the normal case they refer to the same object (container.verified = container.current = {};). Nearly all code uses that current object because nearly all code should be using the most up-to-date information. If we need to add pending data, we do container.current = Object.create(container.verified); and then add the data to container.current. Since current's prototype is verified, there's no need to copy all the old data to it and have duplicated data all over the place. E.g., the classic use of facade. new would be the wrong tool for this job, it would only get in the way.

One of the many fantastic things about JavaScript is that you have both options. I use both in the same projects, for different things, all the time.



回答4:

Thank you for your amazing answer. But, I am not agree with most of your afirmation. Without see the equivalent in both patterns. Some arguments are subjetive for me. So i would like to focus on facts. And in this answer I am going to comment what are the best points of each. And without external libraries/snippets because then we can fight about which library is better.

Good points about Object.create

1. Create the instance/object using call/apply

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.call(Shape, zeroPoint, Point.new(3, 4));
var circle = Shape.Circle.call(Shape, origin, 3);

This is not possible using the new.

Good points about new

1. Less code

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

vs

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;
}

Whatever case you always have to write more code to implement the same.

2. Easier to maintain

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

What happen if tomorrow you want to change the name of Shape to Geometry? You have to review all your code and change all the words Shape for each instantiation of cicle or rectangle. This point is more remarkable when you are doing inheritance. Because you always have to call exacly the same name of the constructor to access to the super.methods

Bonus. Easier to read or understand (This is subjetive)

If in my code I'm seeing new Rectangle(...) i know I'm creating a new instance of the object Rectangle. However Shape.Rectangle(...) don't tell me if is a new object or if is just a function or if Shape is a unique instance like var Shape = new Whatever().

3. Private properties and methods

var Person = function() {
  var a = 5;
  this.method = function (b) { return a*b; };
};
var obj = new Person;
obj.method(5); // return 25
obj.a; // return undefined

vs

var Person = {
    a: 5,
    method: function (b) { return this.a*b; }
};
var obj  = Object.create(Person);
obj.method(5); // return 25
obj.a; // return 5

You always can have private methods and properties on the new pattern. On the object.create pattern you can if your implementation is specific. If you do this, your code is more difficult and verbose to write (But this is a personal opinion).

4. I can pass paramenter on the constructor

var Person = function(a) {
  this.method = function (b) { return a*b; };
};
var obj = new Person(5);
obj.method(5); // return 25

vs

var Person = {
    method: function (b) { return a*b; }
};
var obj  = Object.create(Person);
obj.method(4); //Error

5. instanceof

No way to do instanceof natively with the Object.create pattern.

6. Performance

I leave this for the last one, because most of the other points can be solved with extra javascript. But in this case can't be the same to the new pattern. Also i think this is the most important advantage for new pattern. Because if you are programming for browser the performance sometimes doesn't matter. But if you are working of the backend and you are making a very big and scalable app, the performance matter. Why Paypal leave java and go to node.js? Because the performance it's very important in big projects.

Conclusion

So, if new it's 10 times faster than Object.create. I think only for this reason, and only for this reason worth keep programming with new. Besides, give me more flexibility and i can do things with new pattern that I can't with Object.create. And I agree that the nature of the Javascript is to using the Object.create. But i think i get more benefits if i use new.

Sorry for my English.