js中的面向对象程序设计(2)-创建对象

前面使用例如:

var person = new Object();

或者

var person = {};

缺点:使用同一个接口创建很多对象,会产生大量重复的代码。

下面介绍几种创建对象的方式。

一、工厂模式

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 = createPerson('tom',29,'software engineer');
var person2 = createPerson('jack',20,'Doctor');

缺点:无法解决对象识别的问题。即你创建出来的一个对象,你不知道对象的类型。

二、构造函数模式

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',29,'software engineer');
var person2 = new Person('jack',20,'Doctor');

构造函数本身也是函数,只是用来创建对象而已。
构造函数如果不创建对象,和普通函数没有什么区别。

每个对象都有个constructor属性。

alert(person1.constructor==Person);//true
alert(person2.constructor==Person);//true

识别对象的类型,还是用专业的instanceof操作符

alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true
alert(person2 instanceof Object);//true
alert(person2 instanceof Person);//true

注意:以该种方式定义的构造函数,是定义在全局对象Global对象的属性,在浏览器中是window对象的属性。

下面来讨论两个问题:
1.将构造函数当做普通函数使用

两者除了调用方式不同之外,没有什么区别。普通函数如果使用new来调用,也可以作为构造函数,反之亦可。

当做构造函数来调用

var person = new Person('tom',20,'software engineer');
person.sayName();//'tom'

作为普通函数调用

Person('greg',21,'Doctor');//添加到window对象中
window.sayName();//'greg'

在另一个对象的作用域中调用

var o = new Object();
Person.call(o,'Kristen',25,'Nurse');
o.sayName();//Kristen

2.构造函数的问题

前面Person的构造函数等价于下面的代码:

function Person(name,age,job){
	this.name = name;
	this.age = age;
	this.job = job;
	
	this.sayName = new Function("alert(this.name)");//与声明函数在逻辑上是等价的
}

很明显,其实Person构造函数中声明函数,在每个实例对象中都不是同一个Function实例。ECMAScript中的函数是对象,因此每定义一个函数就相当于实例化一个对象。

alert(person1.sayName == person2.sayName);//false

因为,person1和person2的sayName操作是一样的,所以没必要声明两段重复的代码。所以,可以这么干:

function Person(name,age,job){
	this.name = name;
	this.age = age;
	this.job = job;
	this.sayName = sayName;//sayName属性是一个函数指针,指向一个全局函数。
}

function sayName(){
	alert(this.name);
}

var person1 = new Person('tom',20,'software engineer');
var person2 = new Person('greg',20,'Doctor');

问题是解决了。但是新的问题又来了:
<1>sayName是一个全局函数,但是只能供Person对象的实例使用,这不是笑话吗?还特么还能叫全局函数?
<2>如果Person有100个方法,你难道在全局作用域中定义100个全局函数?那这个自定义类的封装性也太差劲了吧。

所以,还有更好的创建对象的模式。

三、原型模式

每个函数都有一个原型属性,这个属性是一个指针,指向一个对象。这个对象的用途就是包含由特定类型的所有实例共享的属性和方法。简单说prototype就是调用构造函数创建的那个对象实例的原型对象。

使用原型对象的好处:
可以让所有对象实例共享它所包含的属性和方法。

function Person(){}

Person.prototype.name = 'tom';
Person.prototype.age = 20;
Person.prototype.job = 'software engineer';
Person.prototype.sayName = function(){
	alert(this.name);
}

var person1 = new Person();
person1.sayName();//'tom'

var person2 = new Person();
person2.sayName();//'tom'

alert(person1.sayName == person2.sayName);//true

明眼人一看就知道,这明明创建两个对象,默认属性值都是一样的,那还搞啥。别急,往后看。

1.先理解什么是原型对象

这里写图片描述

只要创建一个函数,都会根据特定的规则为该函数创建一个prototype属性,该属性指向函数的原型对象。在默认情况下,函数的原型对象都会自动获得一个constructor属性,该属性指向prototype属性所在函数的指针。

实例与原型对象之间的连接,而不是实例与构造函数之间的连接。

实例中的[[Prototype]]在js中无法直接访问。但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。

alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true

ECMAScript新增了一个判断方法:Object.getPrototypeOf()

alert(Object.getPrototypeOf(person1)==Person.prototype);//true
alert(Object.getPrototypeOf(person2).sayName);//'Nicholas'

*属性搜索

每当代码读取一个对象的属性,首先看该属性是否在实例中,如果在,则取得该属性值,不在向下搜索。如果不在,则搜索该实例[[Prototype]]指向的原型对象是否包含这个属性,如果包含则取得该属性值,否则,则没有属性。

原型中的constructor属性也是可以为实例所共享的。

只能访问原型中的属性而不能修改原型中的属性。

function Person(){}

Person.prototype.name = 'tom';
Person.prototype.age = 20;
Person.prototype.job = 'software engineer';

Person.prototype.sayName = function(){
	alert(this.name);
}

var person1 = new Person();
var person2 = new Person();

person1.name = 'jack';

alert(person1.name);//'jack',来自实例
alert(person2.name);//'tom',来自原型

delete person1.name;//删除实例中的name属性
alert(person1.name);//取得原型中的name属性

*检测属性实例还是原型:hasOwnProperty();

只有属性在实例中才返回true。

alert(person1.hasOwnProperty('name'));//false

person1.name = 'jack';

alert(person1.hasOwnProperty('name'));//true 

alert(person2.hasOwnProperty('name'));//false

delete person1.name;

alert(person1.hasOwnProperty('name'));//false

【插图2】

2.原型与in操作符

判断属性在实例中还是在原型中,无论是在实例中还是在原型中,只要在,就返true。

alert(person1.hasOwnProperty('name'));//false
alert('name' in person1);//true


person1.name = 'jack';
alert(person1.name);//'jack'
alert(person1.hasOwnProperty('name'));//true
alert('name' in person1);//true


alert(person2.name);//'Nicholas'
alert(person2.hasOwnProperty('name'));//false
alert('name' in person2);//true


delete person1.name
alert(person1.name);//'Nicholas'
alert(person1.hasOwnProperty('name'));//false
alert('name' in person1);//true

扩展:判断属性是否在原型中。

function hasPrototypeProperty(object,name){
	return !object.hasOwnProperty(name) && (name in object);
}


alert(hasPrototypeProperty('name'));//true

person1.name = 'jack';

alert(hasPrototypeProperty('name'));//false

枚举一个对象的所有属性:Object.keys(obj);//传递一个对象作为参数

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"    -  只包含实例属性,因为传递进去的是实例对象

得到所有的实例属性:Object.getOwnPropertyNames(obj);

var keys = getOwnPropertyNames(Person.prototype);
alert(keys);//'constructor,name,age,job,sayName'

3.更简单的原型语法

function Person(){ 
} 

Person.prototype = { 
	name : "Nicholas", 
	age : 29, 
	job: "Software Engineer", 
	
	sayName : function () { 
		alert(this.name); 
	} 
}; 

注意:此时,Person.prototype.constructor属性已经不再指向Person了。因为,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。

这里的语法,完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性,指向Object构造函数。

var friend = new Person(); 
alert(friend instanceof Object); //true 
alert(friend instanceof Person); //true 

alert(friend.constructor == Person); //false ,通过constructor已经无法确定对象了。
alert(friend.constructor == Object); //true 

解决方法:手动设置constructor属性

function Person(){ 
} 

Person.prototype = { 
	constructor : Person, //手动设置constructor属性
	name : "Nicholas", 
	age : 29, 
	job: "Software Engineer", 
	
	sayName : function () { 
		alert(this.name); 
	} 
}; 

但是有一个问题,这样设置constructor属性,会导致他的[[Enumerable]]特性被设置为true,而原生的constructor属性是不可枚举的。

所以,规范的写法应该是这样的,使用Object.defineProperty()

function Person(){}

Person.prototype = { 
	name : "Nicholas", 
	age : 29, 
	job : "Software Engineer",
	
	sayName : function () { 
		alert(this.name); 
	} 
}; 

//重设构造函数,只适用于ECMAScript 5兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", { 
	enumerable: false, //明确设置constructor属性不可被枚举
	value: Person 
}); 

4.原型的动态性

对原型的任何修改,能够立即从实例上反映出来。

var friend = new Person(); 

Person.prototype.sayHi = function(){ 
	alert("hi"); 
}; 

friend.sayHi(); //"hi"(没有问题!)

实例中的[[Prototype]]指针仅指向原型,而不指向构造函数

function Person(){ 
} 

var friend = new Person(); //[[Prototype]]指向默认的原型对象

//相当于重新定义了原型对象
Person.prototype = { 
	constructor: Person, 
	name : "Nicholas", 
	age : 29, 
	job : "Software Engineer", 
	
	sayName : function () { 
		alert(this.name); 
	} 
}; 

//实例对象中没有该方法,而默认的原型对象中也没有该方法。
friend.sayName(); //error

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系

【插图3】

5.原生对象的原型

alert(typeof Array.prototype.sort);/'function'
alert(typeof String.prototype.substring);//'function'

为原生包装类型添加自定义的方法:

String.prototype.starstWith = function(text){
	return this.indexOf(text) == 0 ;
}

var msg = 'Hello World!';
alert(msg.startsWith('Hello'));//true

虽然这么干是可以的,但是不建议直接在原生的类型上添加自定义的方法。

6.原型对象的问题

function Person(){ 
} 

Person.prototype = { 
	constructor: Person, 
	name : "Nicholas", 
	age : 29, 
	job : "Software Engineer", 
	
	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 

好了,现在我们说说原型对象的缺点。

<1>所有的实例对象都具有相同的属性值。
<2>对于引用类型的属性,所有的实例共享同一份内存,一个实例改变了该属性值,在其他实例上也会反映出来。

四、组合使用构造函数模式和原型模式

属性定义在构造函数中,方法定义在原型中。

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,Count,Van" 
alert(person2.friends); //"Shelby,Count" 

alert(person1.friends === person2.friends); //false 
alert(person1.sayName === person2.sayName); //true 

目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

五、动态原型模式

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 friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.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("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas"

除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。

构造函数在不返回值的情况下,默认会返回新对象实例。

而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

//扩展原生对象的方法,但是又不能直接修改原生对象。可以使用下面这种方法:

function SpecialArray(){ 
	//创建数组
	var values = new Array(); 
	//添加值
	values.push.apply(values, arguments); //arguments为构造函数接受的所有参数数组
	
	//添加方法
	values.toPipedString = function(){ 
		return this.join("|"); 
	}; 
	//返回数组
	return values; 
} 

var colors = new SpecialArray("red", "blue", "green"); 
alert(colors.toPipedString()); //"red|blue|green" 

注意:
对于寄生构造函数模式:

返回的对象与构造函数或者与构造函数的原型属性之间没有关系。因此,不能依赖instanceof操作符来确定对象类型。

所以,在能够使用其他模式创建对象的时候,尽量不要使用这种方法创建对象。

七、稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。

稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
1.新创建对象的实例方法不引用this
2.不使用new操作符调用构造函数

function Person (name,age,job){

	var o = new Object();
	
	//定义一些私有属性
	
	//添加一些方法
	o.sayName = function(){
		alert(name);
	}
	
	return o;
	
}

var friend = Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas" 

内部私有属性,只能通过调用实例的方法来访问。

即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境。

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值