Leonardo
Leonardo

Reputation: 4228

Inheritance implementation in CoffeeScript

I'm learning the different inheritance implementations in javascript, mostly following the Javascript Patterns book by Stoyan Stefanov.

Now I was inspecting how Coffescript implements it. So given a parent and a child classes or constructors:

class Animal
  constructor: (@name) ->

  move: (meters) ->
    alert @name + " moved #{meters}m."

class Snake extends Animal
  move: ->
    alert "Slithering..."
    super 5

sam = new Snake "Sammy the Python"
sam.move()

They are compiled to:

var Animal, Horse, Snake, sam,
    _extends = function(child, parent) {
        for (var key in parent) {
            if (_hasProp.call(parent, key)) child[key] = parent[key];
        }

        function ctor() {
            this.constructor = child;
        }
        ctor.prototype = parent.prototype;
        child.prototype = new ctor();
        child.__super__ = parent.prototype;
        return child;
    },
    _hasProp = {}.hasOwnProperty;

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

    Animal.prototype.move = function(meters) {
        return alert(this.name + (" moved " + meters + "m."));
    };

    return Animal;

})();

Snake = (function(_super) {
    _extends(Snake, _super);

    function Snake() {
        return Snake.__super__.constructor.apply(this, arguments);
    }

    Snake.prototype.move = function() {
        alert("Slithering...");
        return Snake.__super__.move.call(this, 5);
    };

    return Snake;

})(Animal);

sam = new Snake("Sammy the Python");
sam.move();

As I've understood the implementation of the inheritance in coffescript result from the combination of different patterns:

1. The Classical proxy Constructor

In this case we we also reset the constructor pointer and store the Superclass reference. What Stefanov defines 'Holy Grail'. With this pattern the child only inherits properties of the prototype.

// the proxy function
function ctor() {
  this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;

2. The Inheritance by Copying Properties

With this pattern we simply copy the properties of one object into another

_hasProp = {}.hasOwnProperty;
for (var key in parent) {
  if (_hasProp.call(parent, key)) child[key] = parent[key];
}

3. Classical Pattern - Rent-a-Constructor (or Borrow a Constructor)

function Snake() {
  return Snake.__super__.constructor.apply(this, arguments);
}

QUESTION:

  1. Are my assumptions correct? Is coffescript compiler using 1+2+3?
  2. The inheritance by copying seems to use a shallow copy, meaning that it is not inspecting to check if the property is an object/array and starting a recursion. Even tough the result seems a perfect deep copy (objects/arrays are copies, not references). Why/how?
  3. Isn't the rent-a-constructor creating a repetition of the inheritance? Properties copied and then parent constructor called again?
  4. Can the _extends function also be used between objects instead of Constructors?

Thanks

Upvotes: 0

Views: 274

Answers (1)

user229044
user229044

Reputation: 239250

  1. Isn't the rent-a-constructor creating a repetition of the inheritance? Properties copied and then parent constructor called again?

The properties being copied here...

    for (var key in parent) {
        if (_hasProp.call(parent, key)) child[key] = parent[key];
    }

... are not prototypal properties, they are "class level" properties, methods defined on the function itself. It's copying properties from the function Animal to the function Horse.

The difference is:

class Animal
  # Not part of prototype, part of Animal, must be copied
  @build: (name) ->
    new @(name)

  constructor: (name) ->
    @name = "An animal named #{name}"

  # Part of prototype
  sayName: ->
    alert(@name)

class Bird extends Animal
  constructor: (name) ->
    @name = "A bird named #{name}"


# Both Animal and Bird have build because of the copying of properties:
a = Animal.build('sam') # an animal named sam
b = Bird.build('bob') # a bird named bob

Some annotation on the compiled JavaScript:

var Animal, Bird, a, b,
  __extends = function(child, parent) {
      for (var key in parent) {
          # Copies Animal.build to Bird.build
          if (__hasProp.call(parent, key)) child[key] = parent[key];
      }

      function ctor() {
          this.constructor = child;
      }
      # Makes sayName available to Bird via prototypal inheritance
      ctor.prototype = parent.prototype;
      child.prototype = new ctor();
      child.__super__ = parent.prototype;
      return child;
  },
  __hasProp = {}.hasOwnProperty;

Animal = (function() {
  Animal.build = function(name) {
    return new this(name);
  };

  function Animal(name) {
    # This still (theoretically) needs to be invoked, regardless of whether
    # the properties are copied over, though it isn't invoked in this example
    this.name = "An animal named " + name;
  }

  Animal.prototype.sayName = function() {
    return alert(this.name);
  };

  return Animal;

})();

Bird = (function(_super) {
  __extends(Bird, _super);

  # There is no "Bird.build" defined here, it is copied from Animal

  function Bird(name) {
    this.name = "A bird named " + name;
  }

  # There is no "move" defined here, it is provided by our prototyep
  return Bird;

})(Animal);

a = Animal.build('sam');

b = Bird.build('bob');

Regardless, the properties being copied and then "the parent constructor being called again" isn't really what would be happening.

The properties are not defined in the parent constructor, the parent constructor is just an executable blob of code that needs to run. It may not define any properties, or it might define a bunch of properties, but those properties are not going to be set by the prototype or by the _hasOwnProperty loop.

Upvotes: 2

Related Questions