javascript面向对象开发(一)

一、js面向对象简介

js以它的灵活多变、易学易用而成为使用最广的web前端脚本语言,之前我在项目中只是用js进行数据校验、事件绑定或者dom等操作,然而随着项目复杂度越来越高,不得不接触到js面向对象的开发。虽然js在许多地方都有借鉴java的痕迹,但是就面向对象程序设计这方面,与java还是有着较大的差别。它并没有诸如class、interface这样的关键字,相反它非常单一,只用到了function这个关键词,并且 它把类换了一个名字 ,叫做原型对象。我们在java中定义一个类,往往使用这样的格式:

<pre name="code" class="java">class A{
    private String name;
    ......
}
 

在js中,我们采用如下的方法定义一个类:

function Cat(){//定义一个猫原型对象(类)
     
}
如果我们进行Cat();调用,则将Cat作为一个普通的函数进行使用。如果var cat = new Cat();则将Cat作为一个类来使用。由于普通函数和类都是用同一个关键词function进行修饰,代码的可读性会有一定的降低。因此我们一般约定如果是函数则首字母小写(如cat),如果是类则首字母大写(如Cat)。

在java中,我们一般习惯先定义类,然后创建类的实例进行使用。遵循先定义模具,再用模具生产产品这样思想。但是js的却不同,它非常松散,甚至可以动态为实例添加属性和方法,如:

<pre name="code" class="javascript">//动态添加属性
var cat = new Cat();
cat.name = "小白";
alert(cat.name); //小白


 
//动态添加方法
cat.sayHello = function(){
    alert(this.name);
}
cat.sayHello(); //小白

这种方法虽然非常灵活,但是在某程度上违背了面向对象设计的原则,因此不建议这样使用,最好还是模仿java中的做法,先定义类,再实例化对象。

在java中,我们获取一个对象的属性,往往采用 对象名.属性名 进行获取。在js中,除了可以这么做,还可以使用对象名["属性名"](注意[]中传递的是字符串),这样的好处可以进行动态访问,我们可以利用这一特性进行一些技巧编程,后文会介绍到利用这一特性模拟java中map的例子。当然,如果该对象并不存在某一属性,如我们进行alert(cat.age);这样的调用,会显示undefined。

如果我们将cat这个实例赋值给另一个引用变量,如:var cat2 = cat,那么cat和cat2都指向了同一个实例对象。如果我们把cat.name = "小白2",那么cat2.name也将变成“小白2”,相信这个应该非常好理解。但是如果如果我们把cat2 = null那么cat究竟会怎么指向呢?它应该还会指向原来的堆,并不会也指向null,这点在编程中应该引起注意,我在以往项目中就吃过这个亏。

二、js创建对象的方式

1、工厂方式

和java类似,js也有一个叫做Object的超类。我们可以不定义类,而是直接采用Object得到一个实例,然后采用上文所说的动态添加属性和方法的方式进行开发。虽然这种方式在实际项目中用的并不多,但是作为最简单的一种创建对象的方式,我认为还是有必要了解一下其实现方法的。我们来看如下的代码:

var a = new Object(); //或者var a = {}
a.name = "123";
alert(a.name); //123 
a.getName = function(){
	return this.name;
}
alert(a.getName()); //123

var a = new Object();这是一种标准的工厂方式创建对象的方式,当然我们也可以简写为:var a = {},两种写法等价,但是明显第二种方法更加简单,在许多底层代码中,更多的会采用第二种的写法。介绍这种模式的另一个目的我是想引出Object的一个方法:defineProperty,它也可以为实例对象添加属性或方法,具体使用方法如下:

Object.defineProperty(a,"age",{
	writable:false, //false:只读 true:可修改
	configurable:true, //true:可以使用delete a.age删除该属性
	enumerable:true, //可以被枚举
	value:13 //初始值
});
alert(a.age); //13
defineProperty可以接收3个参数,第一个为实例化对象,第二个为需要添加的属性或方法名,第三个参数可以为属性和方法设置一些属性。上面的代码的含义为:为a这个实例化对象添加一个age的属性,并且它不可被修改、可以被删除、可以被枚举,它的值为13。我们在这里暂时先介绍一下这种用法,下文在讲到prototype的时候,我们会利用这种方法优化代码。

2、构造函数模式

function B(name,age){
	this.name = name; //public
	var age = age; //private
	this.getName = function(){ //public
		alert(this.name);
	}
	var getAge = function(){ //private
		alert(this.age);
	}
}
		
var b = new B("tim",9);
b.getName(); //tim 
b.getAge(); //报错
利用构造函数定义类应该是日常应用程序开发中使用最多的一种方法。这种方法多少有一点点java的踪影,这里需要注意的是,js中属性或者方法只有公有(public)和私有(private)两种方式,并没有protected。如果写上var则说明是私有的,写上 this. 则说明是公共的。因此getName能被成功调用而getAge无法被调用。 说明:js中的this一直是比较困惑人的关键词,我对它的归纳是:谁调用了它,this就指向了谁。

这里需要注意的是,使用构造函数定义类,每创建一个对象,都会拷贝一份代码。也就是说如果我们创建了var b1 = new B("tim",9);和var b2 = new B("tom",10);这两个实例化对象,它们所持有的代码是不共享的,是互相独立的,那么必然会造成内存空间的浪费。这也是构造函数定义类最大的缺点。为了弥补这个缺陷,提出了prototype这个概念,通过它去定义类的属性或方法,所有的对象将共享同一份代码,大大减少了内存空间的开销。

3、原型法

上文有提到prototype这个概念,它可以让所有的实例化对象共享同一份代码。我们先来看一下它的使用方法。

function C(){
}
C.prototype.name = "tom"; //类级属性
C.prototype.getName = function(){ //类级方法
<span style="white-space:pre">	</span>return this.name;
}
原型法创建对象和调用对象属性方法与构造函数模式是一样的。唯一的不同是使用prototype属性和方法是作用在类身上而不是对象身上,这与java的static的功能有些类似。我们需要注意的是prototype不能去访问类的私有方法和属性,比如我们为C这个类定义了一个私有变量age:var age = 10;然后再prototype上添加一个getAge的方法:C.prototype.getAge = function(){alert(age);} 这种方式是不允许的,如果调用它,会显示undefined。

prototype还有一个很强的作用,就是可以扩展原来的类。我们知道,Array单体类为我们提供了一个forEach的方法用于遍历数组元素,但是它无法处理多维数组。我们可以利用prototype为Array添加一个处理多维数组的方法each,那么我们可以这么做:

//为Array添加可以循环多维数组的方法
Array.prototype.each = function(fn){
	if(this && this.length > 0 && fn.constructor == Function){ //数组存在 且 长度大于0 且 传递的参数是一个函数(回调函数)
		var index = 0;
		for(var i=0;i<this.length;i++){
			var element = this[i];
			if(element.constructor == Array){
				//仍然是一个数组,再一次调用each,形成递归
				element.each(fn);
			}else{
				fn.call(element,element,i);
			}
		}
	}
}
//使用	
arr = [1,2,[3,4,[5,6]]];
arr.each(function(element,i){
	alert(element);
});
这里我们需要区分构造函数定义的属性与方法是作用在对象身上的,而prototype是作用在类身上的。其实prototype是一个指针,它指向了类原型对象。在这个原型对象中,我们可以添加属性方法,此外它还持有了一个constructor属性,它实质就是类的构造函数。下面我们通过伪代码的方式梳理一下构造函数、原型对象和实例对象之间的关系:

1、构造函数.prototype = 原型对象
2、原型对象.constructor = 构造函数模板
3、原型对象.isPrototypeOf(实例对象) = true

我们再来看一下下面这个实例,顺便也介绍一些js中与面向对象有关的常用的API。

function C(){
}
C.prototype.name = "tom"; //所有实例共享
var c = new C();
		
alert("name" in c); //true 如果能访问name,则为true 要和后面的hasOwnProperty区分开来
alert(c.name); //tom 但是这个属性不是属于实例的而是属于原型的
alert(c.hasOwnProperty("name")); //false 因为是原型的
c.name = "my"; //覆盖原型属性,name变成了自己的属性
alert(c.name); //my
alert(c.hasOwnProperty("name")); //true
delete c.name ; //自己的name属性没了,但是原型的还在
alert(c.name); //tom
c.name = 111;
alert(Object.keys(c)); //打印c自己的所有的属性(不包括原型的),其返回值为一个数组对象
说明:如果构造函数和prototype拥有同名的属性,在调用时会优先查询构造函数的。

4、重写原型模式

之前有讲到过prototype是一个指针,指向了这个类的原型对象,既然如此,那么我们是否可以改写指针指向的东西呢?答案是可以的。我们来看一下下面这种定义类的方法:

function Person(){		
}
//重写Person的原型
Person.prototype = {
	name:"py",
	age:11,
	sayHello : function(){
		alert(this.name);
	}
};
var person = new Person();
person.sayHello();
alert(person.constructor); //funciton Object(){}

我们可以看到,我们可以通过一种类似json的key-value的写法定义类级的属性和方法,有些人将其称为简单原型。相信大家阅读这段代码应该是比较轻松的,唯一的疑惑应该是最后行代码。因为之前有说过prototype有一个constructor属性,其值为类的构造函数本身,那么理论上应该alert出function Person(){},怎么会是Object呢?其原因是我们改写了prototype指针,它的原型变了,变成了Object。如果想要变回Person可能大家第一想法就是在Person.prototype = {}中写上:constructor:Person;。这种方法固然可行,但是在js中构造函数constructor是不能被枚举的,我们这样改写将会破坏这个规矩。这时我们可以用上文提到过的defineProperty进行改写。具体代码如下:

function Person(){
}
//重写Person的原型
Person.prototype = {
	//constructor:Person,
	name:"panye",
	age:11,
	sayHello : function(){
		alert(this.name);
	}
};
Object.defineProperty(person,"constructor",{
	enumerable:false, //不可被枚举
	value:Person
});
这样再一次执行alert(person.constructor);将会弹出funciton Person(){}。

三、面向对象应用

1、开发常用的面向对象设计方式

上文提到构造函数模式创建对象会浪费一定的内存空间,因为它的代码不共享。虽然prototype方式可以共享代码,但是正所谓“成也萧何,败也萧何”,这个特性也成为了它的弊端。比如我创建了两个Person类的实例,我将其中的一个实例的name属性改成“小白2”,那么另一个实例的name属性也会发生相应的变化,这是我们不愿看到的。所以我们在正式开发中,往往结合两者的做法,取长补短。我们知道,同一个类的不同实例,其本质的不同并不是在于方法的不同,而是在于属性的不同。比如人与人之间,大家都会说话,都会走路,那么互相之间的差异就在于我们的名字不同、体重不同。所以,我们在进行开发时,可以用构造函数的方式去定义属性,用原型法定义方法。

function Person(name,age){
	//属性不共享
	this.name = name;
	this.age = age;
}
		
//方法共享:
Person.prototype = {
	constructor:Person, //可以用defineProperty进行优化改进
	//共用的方法
	sayHello:function(){
		alert(this.name + this.age);
	}
}
		
//创建实例
var p1 = new Person("tim1",12);
var p2 = new Person("tim2",14);
p1.sayHello(); //tim112
p2.sayHello(); //tim214

2、利用面向对象特性模拟实现java的Map

js中并没有给我们提供集合类型,我们可以用数组等方式去模拟Map,但是考虑到Map的特性:key-value的键值对形式并且key不能重复。我们完全可以利用js中类的特性去模拟实现。

function Map(){
	var obj = new Object(); // 存放键值对,把key作为obj的属性,value作为属性的值
	//添加方法		
	this.put = function(key,value){
		obj[key] = value;
	}
	//获取长度		
	this.size = function(){
		var count = 0;
		for(var a in obj){
			count += 1;
		}
		return count;
	}
	//根据key获取	
	this.get = function(key){
		return obj[key];
	}
	//根据key删除
	this.remove = function(key){
		delete obj[key];
	}
	//循环遍历
	this.forEach = function(fn){ 
		for(var a in obj){
			fn(a,obj[a]);
		}
	}
}
		
var map = new Map();
map.put("01","aaa");
map.put("02","vvv");
map.put("03","bbb");
alert(map.size()); //3
alert(map.get("03")); //bbb
map.remove("03");
alert(map.size()); //2
alert(map.get("03")); //undfined
 
map.forEach(function(key,value){
	alert(key+"  " + value);
});
说明:以上的代码只是想说明编程思想,在正式业务处理中有一些特殊情况为考虑在内,读者若有兴趣可完善代码。

四、结束

本次的博客我介绍了js面向对象的基础以及创建对象的几种方式。在下一次博客中我将会和大家分享js中如何实现继承与接口编程,并分享基于js的常见设计模式实现。

由于本人是web前端菜鸟,在博客中若出现了错误的描述,请各位大牛指正!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值