7

I want to make unique instances of a set of cars that different people can hold. The cars will have similar base specs but some of their properties and methods will vary.

The problem I have is that I can't work out how this should work. How do you deal with or create instances of instances in JavaScript?

var Car = function(make, country) {
    this.make = make;
    this.country = country;
};

var Ferrari = new Car('Ferrari', 'Italy');

var fred = new Person() {};
var fred.cars['Ferrari'] = new Ferrari(1200, 300000);

This causes this error, for obvious reasons. I am aware it is not a constructor (see below).

Uncaught TypeError: Ferrari is not a constructor

What I am looking for is something like this. Each different instance of a Ferrari will have a different price and milage.

var Ferrari = function(currentPrice, miles) }
    this.currentPrice = currentPrice;
    this.miles = miles;
    // this is an instance of car, aka it needs the result of this:
    // new Car('Ferrari', 'Italy');

};

Fred's Ferrari is an instance of Ferrari, which is an instance of Car. The problem is that I can't think of a way to make a constructor build a constructor. Is there a way to do this, or am I just going about this in the wrong way?

Other Notes:

I know I could essentially just make each type of car a static JSON-like object and then make instances of that and add new unique values. However, I would like to be able to keep the Car as a constructor so I can easily make more when I need to.

I am clearly missing some understanding of OOP or JavaScript here, but it would be great if someone could point me in the right direction.

jamcd
  • 101
  • 4
  • 1
    `Ferrari` should be a subclass, not an instance; it should `extends Car`. – jonrsharpe Apr 17 '16 at 13:06
  • For anyone who was as confused as me, there is a good page on MDN that explains more about how to make an object hierarchy in JS compared to class-based languages: https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Details_of_the_Object_Model – jamcd Apr 17 '16 at 13:44

2 Answers2

5

What you're looking for is a derived constructor and associated prototype, sometimes called a subclass.

In old-fashioned ES5 it looks like this:

var Car = function(make, country) {
    this.make = make;
    this.country = country;
};
var Ferrari = function(currentPrice, miles) {
    Car.call(this, "Ferrari", "Italy");
    this.currentPrice = currentPrice;
    this.miles = miles;
};
Ferrari.prototype = Object.create(Car.prototype);
Ferrari.prototype.constructor = Ferrari;

How that works:

  • Ferrari is a constructor function that, when called, calls Car with this referring to the new instance, along with the arguments Car needs. Car does its thing setting up those properties on the instance. Then we continue with Ferrari's code, which takes the arguments passed in and (in the above) remembers them as properties.

  • We ensure that the object that will be assigned to instances by new Ferrari (which is taken from Ferrari.prototype) uses Car.prototype as its prototype object, so that if you add things to Car.prototype, they'll be present on Ferraris as well.

  • We ensure that the standard constructor property on Ferrari.prototype refers to Ferrari.

Rather nicer in ES2015 (which you can use today via transpilation, such as with tools like Babel):

class Car {
    constructor(make, country) {
        this.make = make;
        this.country = country;
    }
}
class Ferrari extends Car {
    constructor(currentPrice, miles) {
        super("Ferrari", "Italy");
        this.currentPrice = currentPrice;
        this.miles = miles;
    }
}
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • @Jordão: Thank you! Fixed. – T.J. Crowder Apr 17 '16 at 13:14
  • The ES2015 example looks similar to what I have seen in PHP, but couldn't find a similar feature in Javascript. Thanks! – jamcd Apr 17 '16 at 13:18
  • 1
    It also bears mention that the OP's class model is broken - the `price` and `miles` properties really belong in the base class – Alnitak Apr 17 '16 at 13:19
  • 1
    @jamcd: It's relatively new, introduced by the specification from June 2015, which is why you need to transpile if you want to use it in the wild (you don't if you want to use it on current Chrome, or NodeJS). And yeah, it's intentionally similar to similar constructs in other languages (Java, for instance). – T.J. Crowder Apr 17 '16 at 13:20
  • @Alnitak: Thanks. Excuse my ignorance, but is this because they are common properties to multiple different cars? What if you only want to specify a price for some cars and not others? – jamcd Apr 17 '16 at 13:33
  • 1
    @jamcd every car has a price (even if you don't know what it is) – Alnitak Apr 17 '16 at 13:44
-1

If you want the Car instances to be constructors, Car must return a constructor.

The problem is that then it will inherit from Function.prototype instead of Car.prototype. If that's undesirable, use Object.setProtetypeOf to fix it:

function Person() {
  this.cars = {};
}
function Car(make, country) {
  var instance = function(currentPrice, miles) {
    this.currentPrice = currentPrice;
    this.miles = miles;
  };
  instance.make = make;
  instance.country = country;
  Object.setPrototypeOf(instance, Car.prototype); // slow!!
  return instance;
}
var Ferrari = new Car('Ferrari', 'Italy');
var fred = new Person();
fred.cars.ferrari = new Ferrari(1200, 300000);

Alternatively, you can add a method which will be used to "instantiate" your instances.

function Person() {
  this.cars = {};
}
function Car(make, country) {
  this.make = make;
  this.country = country;
}
Car.prototype.instantiate = function(currentPrice, miles) {
  var instance = Object.create(this);
  instance.currentPrice = currentPrice;
  instance.miles = miles;
  return instance;
};
var ferrari = new Car('Ferrari', 'Italy');
var fred = new Person();
fred.cars.ferrari = ferrari.instantiate(1200, 300000);
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Thanks for supplying an alternative. If you don't mind me asking, is there any benefit to using this instead of the 'call' method? – jamcd Apr 17 '16 at 13:51
  • @jamcd They are different approaches. `call` is used when subclassing, in order to call the super constructor from the extending one. But you want to instantiate instances, not a subconstructor. So the super constructor has already been called to create the instance you want to instantiate, and then there is no reason to call it again. The advantage of the approaches I provided over subclassing is that you don't need to know the car names (e.g. Ferrari) beforehand. From the question, I understood this was what you wanted, not subclassing. – Oriol Apr 17 '16 at 14:15