Constructors
So, let's suppose you have a program, and you need to have a Student object. Students have a name and an array of courses that they are taking. The quickest way to create a Student object is to use the object literal syntax:
var student = { name: 'Joe', courses: [], addCourse: function(course) { courses.push(course); } };
If you need to create many Student objects, you can simply create a function that creates them for you:
function makeStudent(student_name) { return { name: student_name, courses: [], addCourse: function(course) { courses.push(course); } }; } var student = makeStudent('Joe');
You can also use a constructor function, which essentially does the same thing:
function Student(student_name) { this.name = student_name; this.courses = []; this.addCourse = function(course) { courses.push(course); }; } var joe = new Student('Joe'); var ann = new Student('Ann'); var sue = new Student('Sue');
There is a small problem with using constructors in this way. When you create many Student objects, the same name, courses, and addCourse properties show up in each Student. Everything still works, but it's not as efficient as it could be.
To make things more efficient, we need all the Student objects to share some properties instead of having separate, identical properties in each Student. For objects to share properties, we need to make use of JavaScript prototypes. In JavaScript, every object has a prototype object. When you read a property, JavaScript first looks if the property exists in the current object, if it doesn't, JavaScript then looks to see if the property exists in the chain of prototype objects. When you write to a property, the property is changed in the current object and not the prototype. Different Student objects can share the same prototype. Initially, all the properties of the prototype will appear as properties of the Student. As the Student objects change, they will get their own versions of the properties.
What all this amounts to is that prototypes are useful for describing the initial, "prototypical" view of what a certain type of object should look like. Over time, objects will change and stray from looking like the prototype, but that's fine.
For our Student example, let's first create a generic Student object that can serve as a prototype.
var studentPrototype = { name: 'Generic Name', courses: [], addCourse: function(course) { courses.push(course); } };
Now, in order to create objects that have this object as a prototype, you need to set the prototype field of a constructor function. When you create an object using the constructor, the constructor will set the prototype of the object to whatever its prototype field is set to. It can be a bit confusing. Setting the prototype field of a function does not change the prototype of that Function object. In general, JavaScript does not provide a variable where you can set or get the prototype of an object. You have to do it all indirectly by using these constructor functions.
var studentPrototype = { name: 'Generic Name', courses: [], addCourse: function(course) { courses.push(course); } }; function Student() { } Student.prototype = studentPrototype;
Now, when you create Student objects using the constructor, you will get objects that share a lot of fields.
var joe = new Student(); var ann = new Student(); var sue = new Student();
As you set the names of the Student objects, the prototype remains the same, but the name property of the individual Student objects will be set. This name property in the individual objects essentially "overrides" the name property of the prototype.
joe.name = 'Joe'; sue.name = 'Sue';
You can also set the name inside the constructor function, which will give the same result:
var studentPrototype = { name: 'Generic Name', courses: [], addCourse: function(course) { courses.push(course); } }; function Student(student_name) { this.name = student_name; } Student.prototype = studentPrototype; var joe = new Student('Joe'); var sue = new Student('Sue');
Unfortunately, a problem happens though when you start dealing with courses.
joe.addCourse('math'); alert(sue.courses);
Somehow, Sue ends up enrolled in the math course, even though the course was only added to Joe. It seems that when modifying the courses property, the property of the prototype gets modified and not of the Joe object. Since all the Student objects share the same courses property of the prototype, it looks like Sue is taking math.
The problem is that the courses property of Joe is never written to. When addCourse is called, the method reads the courses property and gets an array. The "math" course is then added to this array. The array is modified, but the courses property is never written to, so Joe ends up modifying the prototype's array of courses instead of creating its own copy that isn't shared. To get around this problem, we should create a new array of courses for each Student, and assign it to the courses property in the Student constructor function.
Constructors Summary
When you want to create many objects with the same structure, you should use constructors and prototypes. You should take all the properties of these objects and divide them up into
- properties that are constant and that can be shared between different instances of an object (i.e. methods)
- properties that are unique to each object and that can be different in each object (i.e. data fields and pretty much everything that isn't a method)
Constant properties should be put into the prototype of the objects while the other properties should be set in the constructor function.
So suppose we want to be able to make multiple Student objects:
student = { name: 'Joe', courses: [], addCourse: function(course) { courses.push(course); } };
The method addCourse is the same between all Student objects, so it should appear in the prototype. The other properties will be set in the constructor.
var studentPrototype = { addCourse: function(course) { courses.push(course); } }; function Student(student_name) { this.name = student_name; this.courses = []; } Student.prototype = studentPrototype; var joe = new Student('Joe');
Inheritance
Inheritance in JavaScript is less useful than in class-based object-oriented languages. JavaScript has a dynamic type system, so inheritance is not used to indicate which objects share the same interfaces. Inheritance is only used in JavaScript for code reuse. If you have many different objects that all have the same interface (e.g. in a graphics system, you might have objects for different shapes like lines and circles, and these shapes all understand a draw command), these objects do NOT need to inherit from the same base object unless these objects share code for implementing these interfaces.
The easiest way to think about inheritance hierarchies in JavaScript is to ignore it and to simply focus on the concept of prototypes.
Suppose we have a program that makes use of Student objects, which have the fields name and courses in addition to a method called addCoruse, as in the previous section:
function Student(student_name) { this.name = student_name; this.courses = []; } Student.prototype.addCourse = function(course) { courses.push(course); };
Some of the students are special because take their courses via the Internet. For these students, we not only need to know their name and the courses they are taking, but also their e-mail information so that lectures and homework can be e-mailed to them. We'll use an InternetStudent object to represent these types of students.
function makeInternetStudent(student_name, student_mail) { return { name: student_name, courses: [], addCourse: function(course) { courses.push(course); }, email: student_email, sendEmail: function(subject, body) { ... } }; }
To make a constructor for an InternetStudent, we need a prototype that serves as a good initial template for what an InternetStudent object looks like. Since an InternetStudent shares most of the properties of a Student object, we can use a Student as the prototype.
function InternetStudent() { } InternetStudent.prototype = new Student('GenericName');
There are two new properties in an InternetStudent that aren't in a Student: an emailfield that varies from student to student and a constant sendEmail method. The field should go in the constructor function while the method should be added to the prototype.
function InternetStudent(student_name, student_email) { this.name = student_name; this.email = student_email; } InternetStudent.prototype = new Student('GenericName'); InternetStudent.prototype.sendEmail = function(subject, body) { ... };
As in the section on constructors, you have to be careful when dealing with prototypes that you don't accidentally share data structures between different objects. In the above example, the courses array is shared between all InternetStudent objects because they all use the version in the prototype even though each InternetStudent should get their own separate array.
If you recall, the prototype contains all the properties of an object that shouldn't change between different objects while the constructor function initializes the properties that do change from object to object. The constructor function for Student objects creates new name and courses properties for each Student object. We can use that function to create new versions of these properties in the InternetStudent through constructor chaining:
function InternetStudent(student_name, student_email) { // Chain the constructor Student.call(this, student_name); // Initialize the fields that are unique to InternetStudent objects this.email = student_email; } InternetStudent.prototype = new Student('GenericName'); InternetStudent.prototype.sendEmail = function(subject, body) { ... }; var john = new InternetStudent('John', 'john@example.com');
And we're done.
Inheritance Summary
Inheritance is used for code reuse in JavaScript. In JavaScript inheritance, instead of using a plain Object as a prototype in a constructor, you create some other type of object to use as a prototype base instead.
As always with prototypes, you have to be careful not to accidentally share data structures in your objects. This is especially important when inheriting from something other than a plain Object because you may not know all of the internal data structures of the object you are inheriting from. If you are careful to initialize all non-constant data fields in your constructor functions, you can use constructor chaining to properly initialize these inherited internal data structures.
No comments:
Post a Comment