- 工厂模式
- 构造函数模式
- 原型模式
- 组合使用构造函数模式和原型模式
- 动态原型模式
- 寄生构造函数模式
- 稳妥构造函数模式
其中,前三种是基础。好啦开始正文~
一、工厂模式
function createPerson (name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
// console.log(this); // window
o.sayName = function () {
// console.log(this); // 具体的obj
console.log(this.name);
}
return o;
}
var p1 = createPerson('Aico', 23, 'FE');
var p2 = createPerson('Jen', 26, 'RD');
p1.sayName();
p2.sayName();
函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。
this指向函数的直接调用者。可以看到,在函数createPerson()里,this指向的是window,因为此时调用该函数的正是全局。而在方法sayName()中,this指向的是具体的实例。因为调用该方法的是实例。
工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题,因为这样创建出来的实例类型都是Object,辨识度太差。
二、构造函数模式
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
};
}
var p1 = new Person('Nico', 21, 'FE');
var p2 = new Person('Jen', 22, 'RD');
和工厂模式相比,构造函数的不同之处在于:
没有显式地创建对象;
直接将属性和方法赋给了this对象;
没有return语句;
同时还有new操作符,new实际上干了四件事情:
创建一个新对象;
var obj = new Object();
将构造函数的作用域赋给新对象;
obj.__proto__= Person.prototype;
Person.call(obj);
执行构造函数中的代码(为新对象添加属性);
返回新对象;
构造函数带有Prototype原型对象,原型对象的一个默认属性是constructor,该属性又指回函数本身,即
console.log(Person.prototype.constructor === Person); // true
然而,
console.log(p1.constructor === Person); // true
实例本身并没有这个属性:
console.log(p1.hasOwnProperty(constructor)); // false
这充分的说明了,实例的constructor属性是沿着原型链找到的。
嗯,接下来再看两行代码。
console.log(p1 instanceof Object); // true
console.log(p1 instanceof Person); // true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。
当然,构造函数本身也是函数,它与其他函数的唯一区别,在于调用它们的方式不同。任何函数,只要拿new操作符来调用,那它就可以当作构造函数来用哒~比如,前面的Person()函数可以通过以下任何一种方式来调用。
// 当作构造函数来使用
var p = new Person('Nico', 23, 'RD');
p.sayName(); // 'Nico'
// 作为普通函数调用
Person('Ann', 25, 'FE');
window.sayName(); // 'Ann'
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'Bob', 26, 'QA');
o.sayName(); // 'Bob'
上述代码还间接说明了一个问题,就是,this指向直接调用它的函数~~
当然,构造函数也是有问题的,其主要问题,就是每个方法都要在每个实例中重新创建一遍。
在之前的例子中,p1和p2都有一个名为sayName()的方法,但是这两个方法不是同一个Function的实例。如下。
console.log(p1.sayName == p2.sayName); // false
Why~~?
在ECMAScript中,函数是对象,因此每定义一个函数,也就是实例化了一个对象。也就是说,此时的构造函数也可以这样定义。
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function('console.log(this.name)');
}
从这个角度上看构造函数,更容易看明白每个Person实例都包含一个不同的Function实例(以显示name属性)的本质。也就是说,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制是相同的。
所以,创建两个完成同样任务的Function实例完全没有必要况且有this
对象在,根本不用在执行代码前就把函数绑定到特定对象上面,因而,可以把函数定义转移到外部呀~
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName () {
console.log(this.name);
}
var p1 = new Person('Nico', 21, 'FE');
var p2 = new Person('Ann', 21, 'RD');
如此,sayName包含的是一个指向函数的指针,因此p1和p2对象就共享了在全局作用域中定义的同一个sayName()函数。
但素~~!!
在全局作用域中定义的函数实际上只能被某个函数调用,这让全局作用域有点名不副实;
还有~如果对象需要定义好多个方法,那么就要定义很多个全局函数,那么这个自定义的引用类型还有什么封装性可言?
三、原型模式
我们创建的每个函数都有一个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