面向对象模式
- 工厂模式
- 构造函数模式
- 原型模式
- 组合使用构造函数模式和原型模式
- 动态原型模式
- 寄生构造函数模式
- 稳妥构造函数模式
创建对象有两种方式
- 一是new一个构造函数( var obj = new Object() )
- 二是对象字面量 ( var obj = { name: ‘tianxia’ })
这两种创建方式都存在一个明显的问题:创建多个对象,代码重复量很大,于是就有了工厂模式的诞生。
1.工厂模式
function createPerson(name,age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function() {
console.log(this.name);
}
return obj;
}
var person1 = createPerson('tianxia',22);
var person2 = createPerson('xiaoxiao',18);
工厂模式虽然解决了多个对象相似的问题,却没有解决对象识别的问题。
提问:对象识别是识别什么?
即怎样知道一个对象的类型。比如是Person类型、Animal类型。
随着javascript的发展,下一个模式诞生了。
2.构造函数模式
ECMAScript的构造函数不仅可以创建特定类型的构造函数,还可以创建自定义的构造函数,从而自定义对象类型的属性和方法,上面的例子用构造函数改写如下:
function Person(name,age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}
var person1 = new Person('tianxia',18);
var person2 = new Person('xiaoxiao',18);
new Person发生以下四步骤:
- 创建一个新对象。
- 把this指向新对象。
- 执行构造函数的代码,为新对象添加属性和方法。
- 返回新对象。
这个之前的显式创建对象有些不一样。
在前面的例子中,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor属性,指向构造函数。
console.log(person1.constructor == Person);
console.log(person2.constructor == Person);
console.log(person1.__proto__ == Person.prototype)
console.log(person1.__proto__ == Person.prototype);
对象的constructor属性最初是用来标示对象类型的。但是提到检测对象类型还是instaceof操作符更可靠一些。
我们这个例子中创建的所有对象既是Object的实例,同时也是Person的实例。
//标示对象类型,即对象实例和构造函数的关系
console.log(person1 instanceof Person);
console.log(person1 instanceof Object);
console.log(person2 instanceof Person);
console.log(person2 instanceof Object);
提问:
所有对象既是Object的实例,同时也是Person的实例?
person1和person2之所以同时是Object的实例又是Person的实例,是因为所有对象均继承自Object。(继承问题稍后学习总结)
构造函数的问题是:每个方法都要在实例上重新创建一遍。
在前面的例子中,person1和person2都有一个名为sayName的方法,但那两个方法不是同一个Function的实例。不要忘了——ECMAScript中的函数是对象,因此每定义一个函数,也就实例化了一个对象。
不同实例上的同名函数是不相等的,正如person1和person2不相等一样。
console.log(person1.sayName == person2.sayName); //false
然而,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此可以把函数定义转移到构造函数外部来解决这个问题。
function Person12(name,age) {
this.name = name;
this.age = age;
this.sayName = sayName
}
var person121 = new Person12('tianxia',1);
var person122 = new Person12('xiaoxiao',2);
function sayName() {
console.log(this.name);
}
person121.sayName();
把函数定义在构造函数外部虽然可以解决两个函数做同一件事情,可是新的问题又来了:
- 一是全局作用的函数只能被某个对象调用
- 二是定义多个函数就需要在定义多个全局函数,这样就丝毫没有封装性可言了。
function Person() {
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
在这里实例化了一次对象。
提问:
sayName为何包含的是一个指向函数的指针?
上文中提到函数就是对象,因此指向同一块内存。
sayName相当于一个变量(而这个变量存在栈内存中),但它指向的却是堆内存的地址。
3.原型模式
使用原型对象的好处是可以让所有对象实例共享它的属性和方法。换句话说不必在构造函数中定义对象实例的信息,而是直接将这些信息直接添加在原型对象中。
function Per() {}
Per.prototype.name = 'tianxia';
Per.prototype.age = 18;
Per.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Per();
person1.sayName(); //tianxia
var person2 = new Per();
person2.sayName(); //tianxia
console.log(person1.sayName == person2.sayName); //true
虽然在所有实现中都无法访问到[[prototype]],但可以通过isPrototypeOf()来确定对象之间是否存在这种关系。
Person.prototype.isPrototypeOf(person1)
ECMAScript 新增了一个方法,Object.getPrototypeOf(); 获取一个对象的原型值
Object.getPrototypeOf(person1);
hasOwnProperty() //检测一个属性是否存在实例中,返回true存在实例中,返回false说明存在原型中。
person1.hasOwnProperty('name');
如果删除属性name
delete person1.name;
console.log(person1.name); //'tianxia'
person1.hasOwnProperty('name'); //false 这里不能说明属性来自于原型,有可能原型对象上不存在此属性。
ECMAScript 5的Object.getOwnPropertyDescriptor()只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法。
Object.getOwnPropertyDescriptor(Per.prototype,'name') //{value: "tianxia", writable: true, enumerable: true, configurable: true}
原型与in操作符
有两种方式使用in操作符
- 一种是单独使用
- 一种是用for in
在单独使用时,in操作符会在通过对象能访问到属性时返回true,无论该属性存在实例中还是原型中。
console.log('name' in person1);
console.log('age' in person1);
同时使用in操作符和hasOwnProperty() 就可以确定该属性存在对象实例总还是原型中。
function hasPrototypeProperty(name,object) {
return object.hasOwnProperty('name') && (name in object);
}
console.log(hasPrototypeProperty('name',person1)); //false 存在与原型中
ECMASCript5 Object.keys() 取得对象上所有可枚举实例属性,这个方法接受一个对象为参数,返回一个包含所有可枚举属性的字符串数组。
console.log(Object.keys(Per.prototype)); //["name", "age", "sayName"]
console.log(Object.keys(person1)); //[]
如果你想得到所有实例属性,无论它是否可枚举,可以用Object.getOwnPropertyNames()方法。
console.log(Object.getOwnPropertyNames(Per.prototype)); //["constructor", "name", "age", "sayName"]
更简单的原型语法
前面的例子每添加一个属性就会敲一遍Per.prototype,为了减少不必要输入,也为了视觉上更好的封装原型功能,常用一个包含属性和方法的对象字面量来重写原型对象。
function Person() {}
Person.prototype = {
name: 'xiao',
age: 18,
sayName: function() {
console.log(this.name);
}
}
var person1 = new Person();
console.log(person1.name); //xiao
上面代码和前面一一在原型对象上添加属性和方法结果是一样的,但有一个例外,就是constructor的指向。
console.log(Per.prototype.constructor); //Object
console.log(person1.constructor); //Object
我们发现constructor属性指向了Object。这是为何?
字面量对象是否有构造函数
var obj = {
name: 'xiao',
age: 18
}
console.log(obj.constructor); //Object
事实证明当然有,并且还是Object。
上面对象字面量可以或和下面等价。
var obj = new Object();
obj.name = 'xiao';
obj.age = 18;
instanceof 检测对象类型
MDN上这样说的:instanceof运算符用来测试构造函数的prototype属性是否出现在对象的原型链上。
语法:
object instanceof constructor
参数:
- object 要检测的对象
- constructor 某个构造函数
console.log(person1 instanceof Per); //true 因为 Object.getPrototypeOf(person1) == Per.prototype
console.log(person1 instanceof Object); //true 因为Object.prototype.isPrototype(person1)返回true。
console.log(person1.constrctor == Per); //false
console.log(person1.constructor == Object); //true
如果值constructor真的很重要,可以像下面这样特意将它设置回适当的值。
function PP() {}
PP.prototype = {
constructor: PP,
name: 'xiao',
age: 18,
}
var p1 = new PP();
console.log(p1);
console.log(p1.constructor); //PP
console.log(Object.getOwnPropertyDescriptor(PP.prototype,'constructor'));
for(var prop in p1) {
console.log(prop);
}
for-in遍历既包括实例中的属性也包含原型中的。为何?但只遍历原型的话,只能遍历出原型的属性和方法,
不能遍历出实例的。
p1.job = 'web前端工程师';
for(var prop in PP.prototype) {
console.log('for-in',prop);
}
以这种方式设置constructor属性会导致它的[[enumerable]]特性设置成true。默认情况下,原生的constructor是不可枚举的,因此你使用兼容ECMAScript的兼容引擎,可以试一试Object.defineProperty();
Object.defineProperty(p1,'constructor',{
enumerable: false,
value: PP,
});
for(var prop in p1) {
console.log('遍历之后的结果',prop);
}
console.log(p1.constructor);
4.原型的动态性
function PersonX(){}
var friend = new PersonX();
PersonX.prototype = {
constructor: PersonX,
name: 'xiao',
age: 18,
sayName: function() {
console.log('hi');
}
}
console.log(friend.__proto__); //打印结果如下:
{constructor: ƒ}
constructor: ƒ PersonX()
__proto__: Object
friend.sayName(); //TypeError: friend.sayName is not a function
上面示例,我们先创建一个构造函数Person,然后重写原型对象,之后调用friend.sayName()报错,这是因为friend指向的原型中不包含以该名字命名的属性。
重写原型切断了现有原型与任何之前已经存在的实例之间的关系,它们引用的依然是最初的原型。
原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。
原型对象的问题
原型对象也不是没有缺点,它省略了构造函数传参这一环节,这还不是最大的问题。最大的问题是由共享本性所导致的。
对于包含引用类型值的属性就存在很大的问题。
function PX() {
}
PX.prototype = {
constructor: PX,
name: 'xiao',
age: 18,
colors: ['yellow','blue']
}
var person1 = new PX();
person1.colors.push('purple');
console.log(person1);
var person2 = new PX;
console.log(person2);
上面示例中,给person1.push值之后,person2也改变了。因为colors是引用类型的值,person1和person2中的colors指向同一地址,并且该colors属性在原型上。
为了解决原型模式存在的问题就诞生了组合使用构造函数模型和原型模式。
5.组合使用构造函数模式和原型模式
创建自定义最常见的方式,就是组合使用构造函数模式和原型模式。构造函数用于定义实例属性,而原型模式用于定义方法和共享的属性。这种混成模式还支持构造函数传递参数;可谓集两种模式之长。
//组合使用构造函数模式和原型模式
function Px1(name,age){
this.name = name;
this.age = age;
this.colors = ['yellow','blue']
}
Px1.prototype = {
constructor: Px1,
sayName: function() {
console.log(this.name);
}
}
var person1 = new Px1('tianxia',1);
var person2 = new Px1('xiaoxiao',2);
person1.colors.push('green');
console.log(person1.colors); // ["yellow", "blue", "green"]
console.log(person2.colors); // ["yellow", "blue"]
console.log(person1.sayName == person2.sayName); //true
console.log(person1.colors == person2.colors); //false
修改了person1.colors并不会影响person2.colors,因为它们分别引用了不同的数组(实例化了两次,相当new Array两次,因此是两个不同的数组)
6.动态原型模式
有了其他语言开发经验的人员看到独立的构造函数和原型时,很可能会感到很困惑。动态原型模式正是致力解决这一问题的一个方案,它把所有所有信息封装在构造函数中。
unction Px2(name,age){
this.name = name;
this.age = age;
if(typeof this.sayName != 'function'){
Px2.prototype.sayName = function(){
console.log(this.name);
}
}
}
var px1 = new Px2('tianxia',1);
var px2 = new Px2('xiaoxiao',2);
px1.sayName(); //tianxia
对于采用这种模式创建的对象可以用instanceof确定它的类型。
console.log(px1 instanceof Px2); //true
7.寄生构造函数模式
在前面几种模式都不适合的情况下可以使用寄生构造函数模式,
function Px3(name,age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
console.log(this.name);
}
return obj;
}
var px1 = new Px3('xiao',18);
var px2 = new Px3('tianxia',22);
console.log(px1); //{name: "xiao", age: 18, sayName: ƒ}
console.log(px2); //{name: "tianxia", age: 22, sayName: ƒ}
构造函数在返回值的情况下,会默认的返回新对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式。
function SpecialArray(){
var values = new Array();
values.push.apply(values,arguments);
values.toPipedString = function() {
return this.join('|');
}
return values;
}
var colors = new SpecialArray('red','blue','yellow');
console.log(colors.toPipedString()); //red|blue|yellow
console.log(colors.__proto__ == SpecialArray.prototype); //false
console.log(colors instanceof SpecialArray); //false
关于寄生构造函数模式,有一点需要说明:
首先,返回的对象与构造函数或者与构造函数的原型属性之间没有任何联系,为此不能通过instanceof来确定对象类型。
8.稳妥构造函数模式
所谓稳妥对象就是没有公共属性,而且其方法也不应用this对象。稳妥对象最适合在一些安全的环境中。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
- 一是新创建对象的实例方法不引用this。
- 二是不使用new操作符调用构造函数。
function Px4(name,age){
var o = new Object();
o.sayName = function() {
console.log(name);
}
return o;
}
var px4 = Px4('tianxia666',18);
px4.sayName(); //tianxia666
注意:
以这种模式创建的对象,除了用sayName方法之外,没有别的方式可以访问数据成员,就算是再添加属性和方法,但也不能访问传入构造函数的原始数据。
与寄生构造函数模式类似,使用稳妥模式创建的对象也与构造函数没有任何关系,因此也无法通过instanceof确定其对象类型。
console.log(px4 instanceof Px4); //false