上回说到创建对象最常用的三个方式。工厂模式、构造函数模式和原型模式。工厂模式不去准确确定对象的类型,构造函数模式封装性不好,对此,第三种模式华丽诞生~这就是原型模式。
三、原型模式
我们创建的每个函数都有一个prototype
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person () {
//console.log(this);
}
Person.prototype.name = 'Nico';
Person.prototype.age = 23;
Person.prototype.job = 'FE';
Person.prototype.sayName = function () {
console.log(this); // obj
console.log(this.name);
}
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
每一个构造函数都有ptototype
原型对象,而构造函数创建的每一个实例都具有一个__proto__
属性,指向构造函数的原型对象,即
p1.__proto__ === Person.prototype
以上说明这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
因为此时实例并没有属性,而是通过原型链的方式共享原型对象的属性。因而,sayName()
方法对实例对象来说,是调用的原型对象的方法。因,
console.log(p1.sayName() === p2.sayName()); // true
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会对其进行修改。即使将这个属性设置为null
,也只会在实例中设置,不会恢复其指向原型的链接。
function Person () {}
Person.prototype.name = 'Nico';
Person.prototype.age = 23;
Person.prototype.job = 'FE';
Person.prototype.sayName = function () {
console.log(this.name);
}
var p1 = new Person();
p1.name = 'Ann';
p1.sayName(); // 'Ann'
var p2 = new Person();
p2.sayName(); // 'Nico'
console.log(p1.sayName === p2.sayName); // true
原因还是因为,当给实例赋属性时,当实例具有该属性时,自然不会向上追溯原型链,也不会对原型链有什么改变。
但是如果这种方式想删除属性,需要用delete
关键字。
delete p1.name;
p1.sayName(); // 'Nico'
重写原型
为了从视觉上更舒服,以及写代码的时候更省力。一般用一个包含所有属性和方法的对象来重写整个原型对象。
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Ann',
age: 23,
job: 'FE'
};
但这种方法相当于对原型对象进行重写,比较明显的一个变化就是,constructor
属性指向的不是Person
,而是Object
。
var f1 = new Person();
console.log(f1.constructor == Person); // false
console.log(f1.constructor == Object); // true
原因,
此时用字面量方式重写原型对象,相当于走了以下几步。
var obj = new Object();
// 实例对象obj是不具有constructor属性的,它的隐式原型指向创建该对象函数的原型对象
obj.__proto__ = Object.prototype;
// 但是根据原型链上溯,obj的constructor属性其实是Object原型对象的属性
因而就有了console.log(f1.constructor == Object); // true
原型方式带来的问题
对于原型对象中包含引用类型值得属性,修改实例对象会影响到原型对象属性值的变化。
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Ann',
friends: ['a', 'b']
};
var p1 = new Person();
var p2 = new Person();
var p3 = new Person();
p1.friends = ['a', 'b', 'v'];
p3.friends.push('z');
console.log(p1.friends); // ['a', 'b', 'v']
console.log(p1.hasOwnProperty('friends')); // true
console.log(p3.friends); // ['a', 'b', 'z']
console.log(p3.hasOwnProperty('friends')); // false
console.log(p2.friends); // ['a', 'b', 'z']
console.log(p2.hasOwnProperty('friends')); // false
可以看出,p1
在自己的实例中真正创建了friends
属性,而p3
并不具有该属性,因而它的操作是直接在原型对象上操作的。
四、组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['r', 'g', 'b'];
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name);
}
}
var p1 = new Person('Nico', 23, 'FE');
var p2 = new Person('Ann', 24, 'RD');
p1.friends.push('y');
console.log(p1.friends); // ['r', 'g', 'b', 'y']
console.log(p2.friends); // ['r', 'g', 'b']
console.log(p1.friends === p2.friends); // false
console.log(p1.sayName === p2.sayName); // true