Reputation: 6613
I am afraid to ask this question, as already there are so many on the same topic.
I am trying to understand cons/limitations of using approach-1
and approach-2
function Person(name, age) {
this.name = name || "de-name";
this.age = !!Number(age) ? Number(age) : 0;
}
Person.prototype.setName = function(name) {
this.name = name;
return this;
}
function Student(name) {
Person.call(this, name); // Copy instance properties of Person class
this.title = "Student";
}
// Suggested way
Student.prototype = Object.create(Person.prototype);
// Possible approach-1
// Student.prototype = Person.prototype;
// Possible approach-2
// Student.prototype = new Person();
Upvotes: 2
Views: 86
Reputation: 27201
In a derived old-style class, the .prototype
property ought to be initialized by something similar to this:
Student.prototype = Object.create(
Person.prototype, {
'constructor': {
configurable: true,
enumerable: false,
writable: true,
value: Student,
},
});
Defining the .constructor
property is optional, but if the base class contains it, then your derived class probably should as well, if only for the sake of avoiding being misled by its value when debugging. Going out of your way to set up the property descriptor like above is not that big of a deal either (simple assignment like Student.prototype.constructor = Student;
will often be good enough in practice, even if it can be made to fail in some corner cases), but for consistency with the way .constructor
is defined elsewhere in the language, it’s a good idea to perform nevertheless.
Alternatively, you can adjust the prototype of the pre-made derived instance prototype to point to the base instance prototype. Using ES6 features, one would do it like this:
Object.setPrototypeOf(Student.prototype, Person.prototype);
But this form is anachronistic – under actual pre-ES6 engines it would necessitate using then-not-yet-standard (and not-yet-behaving-like-in-the-standard) extensions, like __proto__
:
Student.prototype.__proto__ = Person.prototype;
Either way gives you more or less the same set-up that modern, ES6-style classes make (and how transpilers like Babel implement ES6 classes in the first place), so it should create few surprises when mixing code and interoperating with those.
As for why not use other solutions, see below.
Student.prototype = Person.prototype;
This one is easy. It sets Student.prototype
to be the very same object as Person.prototype
, which means that:
Student
instances that will not be available for Person
instances;x instanceof Person
and x instanceof Student
will return the same value – instances of Student
will be indistinguishable from instances of Person
.This means Student
would not behave like a derived class at all and will effectively act like an alternative constructor of the Person
type: objects constructed from one will have the same prototype chain as the other. The only case when it might be justifiable to use something like this is if you decided to define a single type with multiple constructors acting as separate entry points:
function Student(name) {
return Person.call(this, name);
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // sloppy job for brevity
Student.fromDbId = function (id) {
var record = lookupRecordInDb(id);
return Person.call(this, record.name);
}
Student.fromDbId.prototype = Student.prototype;
var s0 = new Student('Steve');
var s1 = new Student.fromDbId(12345);
console.log(s0 instanceof Student);
console.log(s1 instanceof Student);
Even then, a static factory function used without a new
operator would be a more typical idiom.
Student.prototype = new Person();
This is also a bad idea, but for much more subtle reasons; ultimately, it is because a prototype is not supposed to be a fully-formed object, but only a collection of properties to be attached to one. This is a point not very easy to see in a bare-bones example of an inert Student
deriving from an inert Person
, so instead of that, imagine you wanted to derive from a constructor that performs some non-trivial work, like for a class that represents a network connection:
// this is the base class you want to use
function Socket(hostname, port) {
// validate arguments
if (hostname == null || port == null)
throw new TypeError("invalid arguments");
// connect to ⟨hostname⟩:⟨port⟩, then store
// the connection handle/file descriptor/whatever somewhere
}
To create a Socket
, you have to pass the address you want to connect to; otherwise construction will fail. Now suppose you want to create your own derived socket class with extra methods (admittedly, inheritance is probably not a great tool to achieve this, but then, what is inheritance ever good for, really?):
function MyAwesomeSocket(hostname, port) {
return Socket.call(this, hostname, port); // I guess?
}
MyAwesomeSocket.prototype = new Socket(/* what goes here? */);
If you choose the approach of using a base instance as the prototype of derived instances, you will have to construct a Socket
here, and this Socket
will have to connect to something, because that is what the constructor does. This connection will not be actually used for anything, though – it only exists for the sake of being able to access methods defined by the Socket
class. This is, essentially, a resource leak. And it just does not make sense logically: you don’t want to create a connection, you only want the methods associated with one. It’s even worse when you consider the possibility that establishing this useless connection may fail. A Socket
class simply cannot be derived from in this manner.
Upvotes: 1
Reputation: 400
Approach 1 and Approach 2 are actually not the same, exactly. In approach 2, you're creating a new instance of Person
, and assigning that new instance to the prototype of Student.
Additionally, you're supposed to do it this way:
var Student = Object.create(Person.prototype);
According to MDN:
The Object.create() method creates a new object with the specified prototype object and properties.
So you don't assign it to the Student prototype, you assign it to the Student itself, and Student gets Person as its prototype.
Upvotes: 0
Reputation: 33517
In prototype-based languages, inheritance is performed by cloning existing objects that serve as prototypes rather than having classes.
So in each case, we should think about the object selected to use as the prototype to figure out the behavior.
In approach 1, you're setting the prototype of Student
to the same prototype object as Person
. This means that any changes made to Student.prototype
will affect Person.prototype
and vice versa.
In approach 2, you're setting the prototype of Student
to a new Person object that will have the following properties set {name: 'de-name', age: 0}
according to your initialization code. The name property will then be overriden by your call to Person.call()
in the Student
function. Since this an entirely new object, any modifications to Student.prototype
will only affect new Student objects, and any missing properties on this Person
instance that serves as the prototype will be delegated to the Person.prototype
.
To elaborate on that last bit (that missing properties are passed up the prototype chain), here's an example. Say we add a new method greet
to Person
:
Person.prototype.greet = function() { console.log("Hi! " + this.name; ) }
Calling new Student().greet()
will have JavaScript check through the prototype chain until it hits the appropriate property (otherwise you get a not defined error.)
// Walking up the chain
/* 1 */ new Student() // doesn't have a greet property
/* 2 */ Student.prototype // => Person {name: 'de-name', age: 0}
// This person instance doesn't have a greet property either
// because we instantiated it before adding the `greet` property
/* 3 */ Person.prototype // Has a greet property which gets called
In the suggested pattern with Object.create
, you're doing almost the same thing as Student.prototype = new Person()
except that Object.create
allows you to perform differential inheritance. You can even add additional properties as its second argument:
Student.prototype = Object.create(Person.prototype, {
age: 16,
study: function() { console.log("But I'm sooo lazy!!!"); }
});
Upvotes: 0
Reputation: 60056
approach-2 means one additional object in the prototype chain. If (new Student()).someAttr
doesn't resolve in the student object (new Student()
), with approach-2, the person object (new Person()
) is checked (because that's what's in Student.prototype
), then Person.prototype
. With approach-1, there's no person object.
Upvotes: 0