Although using the object constructor or an object literal are convenient ways to create single objects, there is an obvious downside: creating multiple objects with the same interface requires a lot of code duplication. To solve this problem, developers began using a variation of the factory pattern.
With no way to define classes in ECMAScript, developers created functions to encapsulate the creation of objects with specific interfaces, such as in this example:
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
Though this solved the problem of creating multiple similar objects, the factory pattern didn't address the issue of object identification(what type of object an object is).
constructors in ECMAScript are used to create specific types of objects. For instance, the previous example can be rewritten using the constructor pattern as following:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
By convention, constructor functions always begin with an uppercase letter, whereas nonconstructor functions begin with a lowercase letter.
To create a new instance of Person, use the new operator. Calling a constructor in this manner essentially causes the following four steps to be taken:
Constructors as Functions
Any function that is called with the new operator acts as a constructor, whereas any function called without it acts just as you would expect a normal function call to act. For instance, the Person() function from the previous example may be called in any of the following ways:
// use as a constructor
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// call as a function
Person("Greg", 27, "Doctor"); // adds to window
window.sayName(); // "Greg"
// call in the scope of another object
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
Probles with Constructors
The major downside to constructors is that methods are created once for each instance. So, in the previous example, both person1 and person2 have a method called sayName(), but those methods are not the same instance of Function. Remember, functions are objects in ECMAScript, so every time a function is defined, it's actually an object being instantiated. Logically, the constructor actually looks like this:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); // logical equivalent
}
functions of the same name on different instances are not equivalent, as the following code proves:
alert(person1.sayName === person2.sayName); // false
It's possible to work around this limitation by moving the function definition outside of the constructor, as follow:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
This solves the problem of having duplicate functions that do the same thing but also creates some clutter in the global scope by introducing a function that can realistically be used only in relation to an object.
The benefit of using the prototype is that all of its properties and methods are shared among object instances.
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); // "Nicholas"
var person2 = new Person();
person2.sayName(); // "Nicholas"
alert(person1.sayName === person2.sayName); // true
How Prototypes work
Whenever a function is created, its prototype property is also created according to a specific set of rules. By default, all prototypes automatically get a property called constructor that points back to the function on which it is a property.
Each time the constructor is called to create a new instance, that instance has a internal pointer to the constructor's prototype. In ECMA-262 fifth edition, this is called [[Prototype]]. There is no standard way to access [[Prototype]] form script, but Firefox, Safari and Chrome all support a property on every object called __proto__; in other implementations, this property is completely hidden from script.
Even though [[Prototype]] is not accessible in all implementations, the isPrototypeOf() method can be used to determine if this relationship exists between objects;
alert(Person.prototype.isPrototypeOf(person1)); // true
ECMAScript 5 adds a new method called Object.getPrototypeOf(), which returned the value of [[Prototype]] in all supporting implementations. For example:
alert(Object.getPrototypeOf(person1) == Person.prototype); // true
alert(Object.getPrototypeOf(person1).name); // "Nicholas"
Although it's possible to read values on the prototype from object instances, it is not possible to overwrite them. If you add a property to an instance that has the same name as a property on the prototype, you create the property on the instance, which then masks the property on the prototype.
The delete operator completely removes the instance property and allows the prototype property to be access again as follows:
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); // "Greg" - from instance
alert(person2.name); // "Nicholas" - from prototype
delete person1.name;
alert(person1.name); // "Nicholas" - from the prototype
The hasOwnProperty() method determines if a property exists on the instance or on the prototype.
Prototypes and the in Operator
There are two ways to use the in operator: on its own or as a for-in loop;
When used on its own, the in operator returns true when a property of the given name is accessible by the object, which is to say that the property may exist on the instance or on the prototype.
When using a for-in loop, all properties that are accessible by the object and can be enumerated will be returned, which includes properties both on the instance and on the property.
To retrieve a list of all enumerable instance properties on an object, you can use the ECMAScript 5 Object.keys() method, which accepts an Object as its argument and returns an array of strings containing the names of all enumerable properties. For example:
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); // "name, age, job, sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); // "name, age"
If you'd like a list of all instance properties, whether enumerable or not, you can use Object.getOwnPropertyNames() in the same way:
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // "constructor, name, age, job, sayName"
Alternate Prototype Syntax
To limit this redundancy and to better visually encapsulate functionality on the prototype, it has become more common to simply overwrite the prototype with an object literal that contains all of the properties and methods, as in this example:
function Person(){}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
};
Keep in mind that restoring the constructor in this manner creates a property with [[Enumerable]] set to true. Native constructor properties are not enumerable by default, so if you're using an ECMAScript 5-compliant JavaScript engine, you may wish to use Object.defineProperty() instead;
function Person(){}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
};
// ECMAScript 5 only - restore the constructor
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
Problems with Prototypes
The prototype pattern isn't without its faults. For one, it negates the ability to pass initialization arguments into the constructor, meaning that all instances get the same property values by default. The main problem comes with their shared nature.
All properties on the prototype are shared among instances, which is ideal for function. Properties that contain primitive values also tend to work well, as shown in the previous example, where it's possible to hide the prototype property by assigning a property of the same name to the instance. The real problem occurs when a property contains a reference value. Consider the following example:
function Person(){}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
friends: ["Shelby", "Court"],
sayName: function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); // "Shelby, Court, Van"
alert(person2.friends); // "Shelby, Court, Van"
alert(person1.friends === person2.friends); // true
function Person(){}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
friends: ["Shelby", "Court"],
sayName: function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends = ["linxd"];
alert(person1.friends); // "linxd"
alert(person2.friends); // "Shelby, Court"
alert(person1.friends === person2.friends); // false
通过这两段代码的对比,我们可以知道,最开始的person1是不包含friends属性的。当执行person1.friends.push("Van")语句时,JavaScript引擎实际上先搜索friends属性,并且在Person.prototype中找到了该属性。需要注意的是,所有属性值是reference value(包括Array, Function, Object), 他们存储的都只是一个指向对象的指针,而不是对象的一个副本。因此,person1.friends和person2.friends实际上都查询到Person.prototype的同一个属性值。而通过person1.friends.push("Van")改变值后,在person2.friends中也能体现出来。
而在第二段代码中,Person1声明了自己的实例属性friends,这个实例属性覆盖了继承的Person.prototype.friends属性。同时,需要明白的是,Person1.friends的属性值也是一个指向Array对象的指针,并不是Array对象的副本。
The most common way of defining custom types is to combine the constructor and prototype patterns. The constructor pattern defines instance properties, whereas the prototype pattern defines methods and shared properties. This pattern allows arguments to be passed into the constructor as well, effectively combining the best parts of each pattern. The previous example can now be rewritten as follows:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); // "Shelby, Court, Van"
alert(person2.friends); // "Shelby, Court"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true
手机扫一扫
移动阅读更方便
你可能感兴趣的文章