文章为《JavaScript高级程序设计》(第三版)笔记。
虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这一问题,我们可以使用一些模式。
一.工厂模式
这种模式抽象了创建具体对象的过程。
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
}
retrun o;
}
var person1 = createPerson('Tom', 2, 'Catch a mouse');
var person2 = createPerson('Jerry', 2, 'Funny cat');
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
二.构造函数模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
}
}
var person1 = new Person('Tom', 2, 'Catch a mouse');
var person2 = new Person('Jerry', 2, 'Funny cat');
Person()与createPerson()的不同之处在于:
- 没有显示的创建对象;
- 直接将属性和方法赋值给了this对象;
- 没有return语句。
使用new操作符将经历以下4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
上面例子中的person1和person2都是Person的实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person。
alert(person1.constructor === Person); // true
alert(person2.constructor === Person); // true
检测对象类型,我们还可以使用instanceof操作符。这个例子中创建的对象既是Object的实例,也是Person的实例,我们可以通过instanceof操作符来验证。
alert(person1 instanceof Person); // true
alert(person2 instanceof Person); // true
alert(person1 instanceof Object); // true
alert(person2 instanceof Object); // true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
注:以这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的。
1.将构造函数当作函数
我们一般将通过new调用的函数称为构造函数,但构造函数也是普通的函数。
// 当构造函数使用
var person = new Person('Tom', 2, 'Catch a mouse');
person.sayName(); // "Tom"
// 当普通函数调用
Person('Jerry', 2, 'Funny cat');
window.sayName(); // "Jerry"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'Tom', 2, 'Catch a mouse');
o.sayName(); // "Tom"
2.构造函数的问题
使用构造函数的主要问题,就是每个方法都要在实例上重新创建一遍。比如上面例子中的person1和person2都用一个sayName()方法,而且两个方法不是同一个Function的实例。
alert(person1.sayName === person2.sayName); // false
然而,创建两个完全相同的实例是没必要的,为了解决这个问题,我们可以这么写:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person('Tom', 2, 'Catch a mouse');
var person2 = new Person('Jerry', 2, 'Funny cat');
alert(person1.sayName === person2.sayName); // true
将sayName()方法放在全局作用域中,这样,实例就共享了一个全局方法。但这样写又产生了新的问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是就没有丝毫封装可言了。
三.原型模式
原型模式就可以很好的解决上述问题。
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。上面的例子,我们可以改写成如下这般:
function Person() {}
Person.prototype.name = 'Tom';
Person.prototype.age = 2;
Person.prototype.job = 'Catch a mouse';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
person1.sayName(); // "Tom"
var person2 = new Person();
person2.sayName(); // "Tom"
person1.name = 'Jerry';
person1.sayName(); // "Jerry" ———— 来自实例
person2.sayName(); // "Tom" ————来自原型
1.理解原型对象
我们可以通过下面这张图理解整个原型链:
详细的介绍可以看另一篇文章:前端面试:原型链类
根据上图,我们可以得到
Person.prototype === person1.__proto__; // true
我们也可以使用isPrototypeOf()方法来确定上述关系:
Person.prototype.isPrototypeOf(person1); // true
我们还可以通过Object.getPrototypeOf()方法获取实例的__proto__属性(书中说的是[[Prototype]]属性)
Object.getPrototypeOf(person1) === Person.prototype; // true
在上面的例子中,我们给实例person1的name赋值为"Jerry",调用sayName()返回的是实例上的值,那怎样访问原型上的值呢?
person1.__proto__.name // "Tom" ———— 来自原型
接着上面的例子,我们使用delete操作,可以删除实例中的属性,这样也能访问原型上的值了
// ...
person1.name = 'Jerry';
person1.sayName(); // "Jerry" ———— 来自实例
person2.sayName(); // "Tom" ————来自原型
delete person1.name;
person1.sayName(); // "Tom" ———— 来自原型
那么问题来了,如何判断我们访问的是实例上的属性还是原型上的属性呢?
JS提供了hasOwnProperty()方法,这个方法只在给定属性存在于对象实例中时,才会返回true。
function Person() {}
Person.prototype.name = 'Tom';
Person.prototype.age = 2;
Person.prototype.job = 'Catch a mouse';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
alert(person1.hasOwnProperty('name')); // false
person1.name = 'Jerry';
alert(person1.name); // "Jerry" ———— 来自实例
alert(person1.hasOwnProperty('name')); // true
delete person1.name;
alert(person1.name); // "Tom" ———— 来自原型
alert(person1.hasOwnProperty('name')); // true
2.原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时 ,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
function Person() {}
Person.prototype.name = 'Tom';
Person.prototype.age = 2;
Person.prototype.job = 'Catch a mouse';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
alert('name' in person1); // true
person1.name = 'Jerry';
alert('name' in person1); // true
同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中:
// 判断属性是否在原型上
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中即包括存在于实例中的属性,也包括存在于原型中的属性。
默认不可枚举的属性和方法,包括:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()。constructor和prototype的[[Enumberable]]特性为false。
可以使用Object.keys()方法获得对象上所有可枚举的实例属性。该方法接收一个对象,返回一个包含所有可枚举属性的字符串数组。
function Person() {}
Person.prototype.name = 'Tom';
Person.prototype.age = 2;
Person.prototype.job = 'Catch a mouse';
Person.prototype.sayName = function() {
alert(this.name);
}
var keys = Object.keys(Person.prototype);
console.log(keys); // ["name", "age", "job", "sayName"]
var p1 = new Person();
p1.name = 'Tom';
var p1keys = Object.keys(p1);
console.log(p1keys); // ["name"]
如果想要得到所有实例属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // ["constructor", "name", "age", "job", "sayName"]
3.更简单的原型语法
我们可以使用对象字面量的形式来个原型对象赋值:
function Person() {}
Person.prototype = {
name: 'Tom',
age: 2,
job: 'Catch a mouse',
sayName: function() {
alert(this.name);
}
}
但这样会造成一个问题:constructor属性不再指向Person了。我们这里完全重写了默认的prototype对象,因此constructor属性就变成了新对象的constructor属性(指向Object构造函数)。此时,instanceof操作符还能返回正确结果,但constructor已经无法确定对象的类型了。
var p1 = new Person();
alert(p1 instanceof Object); // true
alert(p1 instanceof Person); // true
alert(p1.constructor === Object); // true
alert(p1.constructor === Person); // false
如果constructor的值真的很重要,可以像下面这样写
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Tom',
age: 2,
job: 'Catch a mouse',
sayName: function() {
alert(this.name);
}
}
但这样写会导致原型对象的constructor属性的[[Enumerable]]特性被设置为true,即constructor属性变成可枚举的。我们可以使用Object.defineProperty()来解决。
function Person() {}
Person.prototype = {
name: 'Tom',
age: 2,
job: 'Catch a mouse',
sayName: function() {
alert(this.name);
}
};
// 重置构造函数,只适用于 ECMAScript5 兼容的浏览器
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
4.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所作的任何修改都能够立即从实例上反应出来。
function Person() {}
var friend = new Person();
Person.prototype.sayHi = function() {
alert('hi');
};
friend.sayHi(); // "hi"
但如果以字面量的形式去重写原型对象,则会切断已存在实例与最初原型对象之间的联系。
function Person() {}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Tom',
age: 2,
job: 'Catch a mouse',
sayName: function() {
alert(this.name);
}
};
friend.sayName(); // error
5.原生对象的原型
所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。比如,Array.prototype中可以找到sort()方法。
我们也可以在原生引用类型的原型上添加新方法。尽管可以实现,但不推荐这样做,因为这样可能会意外地重写原生方法。
6.原型对象的问题
原型模式的最大问题是由其共享的本性所导致的。
对于包含引用类型的属性来说,就会存在如下问题:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Tom',
age: 2,
job: 'Catch a mouse',
friends: ['Spike', 'Tektronix'],
sayName: function() {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Jerry');
console.log(person1.friends); // ["Spike", "Tektronix", "Jerry"]
console.log(person2.friends); // ["Spike", "Tektronix", "Jerry"]
console.log(person1.friends === person2.friends); // true
如上,person1和person2都添加了"Jerry"。但,实例一般都是要有属于自己的全部属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
四.组合使用构造函数模式和原型模式
这种模式是创建自定义类型最常见的方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这样就能最大限度地节省内存。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Spike', 'Tektronix'];
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
};
var person1 = new Person('Tom', 2, 'Catch a mouse');
var person2 = new Person('Jerry', 2, 'Funny cat');
person1.friends.push('Bob');
console.log(person1.friends); // ["Spike", "Tektronix", "Bob"]
console.log(person2.friends); // ["Spike", "Tektronix"]
console.log(person1.friends === person2.friends); // false
五.动态原型模式
动态原型模式,把所有的信息都封装到了构造函数中,而通过在构造函数中初始化原型(仅在必要条件下),又保持了同时使用构造函数和原型的优点。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName != 'function') {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var person1 = new Person('Tom', 2, 'Catch a mouse');
person1.sayName()
六.寄生构造函数模式
这种模式的思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。从外观上看,有些像工厂模式和构造函数模式的混合体。
function Person(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 friend = new Person('Tom', 2, 'Catch a mouse');
friend.sayName(); // "Tom"
需要注意的是:返回的对象与构造函数或者与构造函数的模型属性之间没有关系,因此,不能通过instanceof操作符来确定对象类型。所以,在能使用其他模式的情况下,不要使用这种模式。
七.稳妥构造函数模式
稳妥对象,指的是没有公共属性,而且其它方法也不引用this的对象。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。
function Person(name, age, job) {
var o = new Object();
// 在这里定义私有变量和函数
// 添加方法
o.sayName = function() {
alert(name);
}
return o;
}
var friend = Person('Tom', 2, 'Catch a mouse');
friend.sayName(); // "Tom"