这篇文章是好多年前,在公司里跟同事做分享时写的一点东西,先拿出来充充数。
文章中借鉴了很多书中的东西。比如犀牛书,红皮书等,这些书看似基础,实则博大精深,每看一次都有不同的感受。
一、概念:
面向对象编程(Object Oriented Programming, OOP, 面向对象程序设计)是一种计算机编程架构,OOP的一条基本原则是计算机程序是由单个能够起到子程序作用的单元或对象组合而成,OOP达到了软件工程的三个目标:重用性、灵活性和扩展性。为了实现整体运算,每个对象都能够接收信息、处理数据和向其它对象发送信息。首先,面向对象符合人类看待事物的一般规律。其次,采用面向对象方法可以使系统各部分各司其职、各尽所能。为编程人员敞开了一扇大门,使其编程的代码更简洁、更易于维护,并且具有更强的可重用性。
二、javascript中的面向对象:
我们可以把对象想象成现实世界中的某一个具体的事物,比如一部手机,就是一个对象,它有长、宽、高、颜
色、重量,这叫对象的属性,手机有打电话、发短信等功能,这些功能叫对象的方法。js中的对象是由系列名值对组成的,在js中,一切都是对象,如:数组,函数,连字符串,数值,布尔值也有包装对象。
1.创建对象:
要写面向对象的程序,得先创建一个对象,在js中创建对象有多种方法:对象字面量、Object
构造函数还有Object的create方法,如下:
1 2 3 4 5
| var person={}; var person=new Object(); var person=Object.create(null);
|
2.添加属性与方法:有了对象,就可以为对象添加属性与方法(方法其实与一般函数无异,当一个函数属于一个对象时,就叫对象的方法),如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var person={ name:"William", sayName:function(){ alert(this.name); } }; person.name="William"; person.sayName=function(){ alert(this.name); } person["name"]="William"; person["sayName"]=function(){ alert(this.Name); }
|
定义好了对象的属性与方法之后,我们就可以读取对象的属性,调用对象的方法,如下:
1 2 3
| var myName=person.name; person.sayName();
|
3.存在的问题:
上面的概念里提到,面向对象编程要实现软件工程的三个目标:重用性、灵活性和扩展性 ,那我们看上面的方式能不能实现这个目标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var person2=person; person2.name='Jack'; person2.sing=function(){ alert('I am singing'); } person2.sayName() person.sayName(); person.sing();
|
sing本来是person2扩展的方法,但现在person也有了此方法。也就是说,我们改变子对象的属性和方法时,父对象也受到了影响。这是什么原因造成的呢?那是因为,在js中,只有基本类型在赋值给另外的变量时,才会将自身复制一份,而对象是引用类型,在赋值给其它变量时,并不会复制自身,而只是将对象在内存中的地址(引用)(此处不确定)给了变量。也就是说,person与person2其实指向的是内存中的同一个对象。这也就不难理解,为什么我们改变person2的属性与方法时,person也会跟着改变了。如果我们有多个类似的对象,由于不能继承,我们就需要定义多个对象。
1 2 3 4 5 6 7 8 9 10 11
| var a=5; var b=a; b=10; alert(b===a); var a={name:'William'} var b=a; b={name:'Jack'} alert(b===a);
|
由此可见,这种方式不能实现代码的重用与扩展。我们需要其它构建对象的方法。
4.类的概念:
类是现实生活中一类具有共同特征的事物的抽象,是面向对象编程的基础。
上面是类的定义,如动物、植物,就是两种类。我们还可以把类看作是图纸或者模具,通过图纸和模具,可以生产出多个完全一样的产品。在程序中,模具就是类,生产出来的产品,就是类的实例。
5.javascript中的类:
javascript里并没有类的概念,但是有构造函数,它和其它语言的类是非常相似的。
顾名思义,构造函数是用来构造一个对象的,它需要通过关键字new来调用。如果你按普通的方式调用它,那它其实与普通函数并没有什么区别。
1 2 3 4 5 6 7 8 9 10 11 12
| function Person(name){ this.name=name; this.sayName=function(){ alert('My name is'+this.name); } } var p1=new Person('William'); var p2=Person('William');
|
其实构造函数就是一个构建对象的工厂,我们可以这样理解:构造函数执行时,首先创建一个对象,再给对象添加属性和方法,最后返回这个对象。
1 2 3 4 5 6 7 8 9 10 11 12
| function Person(name){ var obj={}; obj.name=name; obj.sayName=function(){ alert('My name is'+this.name); }; return obj; }
|
注意:对于普通函数,不要通过new关键字调用,不然可能得不到你想要的结果
1 2 3 4
| function sum(a,b){ return a+b; } var num=new sum(2,3);
|
实例化了对象,我们就可以调用它的方法
如果我们需要多个对象,只需多次调用构造函数来生成实例对象就可以了。
1 2 3 4
| var p2=new Person('Jack'); p2.sayName();
|
我们来扩展p2,给它添加一个方法,并重写它的sayName方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| p2.sayName=function(){ alert('hello'); }; p2.eat=function(){ alert('eating'); }; p2.sayName(); p2.eat(); p1.sayName(); p1.eat();
|
到现在为止,似乎面向对象的扩展性与灵活性都解决了。但写程序不只是解决问题,还对性能有要求。在现实世界中,一个人具有吃饭这个方法,另一个人具有另一个人的吃饭方法,这没有任何问题。但在程序中,存放对象及对象的属性与方法是需要内存空间的,一个对象的方法与属性越多,它占用的内存空间就越大。所以我们期望的是:两个对象具有不同的属性(就好比人有高矮胖瘦),但它们的方法应该共享(因为功能是一样的),下面我们看p1与p2是不是共享一个方法的
1 2
| alert(p1.sayName===p2.sayName);
|
p1与p2的sayName方法并不相等,也就是说p1与p2各自拥有自己的、独立的sayName方法。当对象过多时,内存开销会非常大。所以要解决共享方法的问题,我们必须找其它方法。
6.对象的原型(prototype):
每个javascript对象都有一个原型(prototype属性),原型上的方法,可以被所有实例共享。
利用这个特点,我们可以把需要共享的方法,定义在对象的原型上,如下:
1 2 3 4 5 6 7
| var Person=function(name){ this.name=name; }; Person.prototype.sayName=function(){ alert('My name is' +this.name); }
|
下面我们来检测,原型上的方法是不是被所有实例共享的
1 2 3
| var p1=new Person('William'); var p2=new Person('Jack'); alert(p1.sayName===p2.sayName);
|
p1与p2的sayName方法是相等的,可见,它们确实是共享的一个方法。也就是说,即使实例化一千个对象,它们都是共享的同一个sayName方法,大大的节约了内存的开销。这是怎么做到的?下面来看原型链。
7.原型链:
原型本身也是对象,它也有自己的原型,而它自己的原型对象又可以有自己的原型,这样就组成了一条链,这个链就是原型链。
每个实例对象都有一个__proto__属性,这个属性指向了该实例的构造函数的原型对象。我们知道对象是引用类型,也就是说__proto__属性只是存了一个指针,并不包含对象。当我们调用一个实例的方法时,程序首先会在实例本身上寻找这个方法,如果找不到,程序会通过__proto__属性找到prototype对象,在prototype上查找这个方法,如果找到了就调用,如果没找到,它又会通过原型对象的原型去查找,直到找到顶层,如果顶层对象上也没有,就会抛出错误。
1 2
| p1.sayName() p1.__proto__.sayName;
|
8.扩展与继承:
下面通过一个例子来看,如何实现对象的扩展与继承。
先定义一个基础类型:
1 2 3 4 5 6 7 8 9 10
| var Animal=function(weight){ this.weight=weight; } Animal.prototype.eatFood=function(){ alert('I\'m eating!'); }
|
从基类上继承方法,还可以继承构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var Vivipara=function(weight){ Animal.apply(this,arguments); }; Vivipara.prototype=Animal.prototype; Vivipara.prototype=new Animal(); Vivipara.prototype=Object.assign({},Animal.prototype); Vivipara.prototype.sayName=function(){ alert('叽哩呱啦'); }
|
最后我们试试重写基类的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var Human=function(weight,name){ Vivipara.apply(this,arguments); this.name=name; } Human.prototype=new Vivipara(); Human.prototype.sayName=function(){ alert('My name is' +this.name); }
|
现在我们定义好了三个类,尽管后面两个类都继承自别的类,但它们之间是互不影响的。
1 2 3 4 5 6 7 8 9 10 11 12 13
| var bird=new Animal(); bird.eatFood(); var dog=new Vivipara(); dog.sayName(); var p1=new Human('Lucy'); p1.eatFood(); p1.sayName();
|
三、需要注意的地方:
1.不要在实例化对象上设置或读取prototype。
实例上并没有prototype这个属性,取而代之的是,实例上有一个__proto__私有属性,指向了原型对象,既然是私有属性,我们也最好不要去读取或者修改它,因为不是所有浏览器都实现(开放)了它。
1 2 3 4 5 6
| var Person=function(){} var p1=new Person; p1.prototype.sayName=function(){}; p1.__proto__.sayName=function(){};
|
2.构造函数内及原型方法内关键字this
的指向:
这也是常常造成困惑的地方,在大多数情况下,方法内的this指向的是拥有此方法的对象。但在原型方法与构造函数内情况有点不一样了,看看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var Person=function(name){ this.name=name; }; Person.prototype={ sayName:function(){ console.log(this.name); } } var person=new Person('Lily'); person.sayName(); Person.prototype.sayName();
|
3.静态方法(属性)、实例方法(属性)、原型方法(属性):
直接在构造函数上添加的方法(属性),叫静态方法(属性)。可以通过构造函数直接调用。
1 2 3 4 5 6 7 8 9 10 11 12
| var Person=function(){}; Person.sayName=function(){ alert(this.name); }; Person.sayName(); var p1=new Person; p1.sayName();
|
在构造函数内,通过this添加的方法(属性),叫实例方法(属性)。必须要先实例化才能调用,构造函数是不能访问的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var Person=function(name){ this.name=name; this.eat=function(){ alert('I am eating'); } } var person=new Person('name'); person.eat(); Person.eat();
|
在构造函数的原型上添加的方法(属性) ,叫原型方法(属性)。原型方法可以实例化后调用,也可以能过原型调用(但比较少见)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Person.prototype={ sex:'female', saySex:function(){ alert(this.sex); } } var person=new Person(); person.saySex(); Person.prototype.saySex(); Person.saySex();
|
4.对象的私有属性与私有方法:
在其它面向对象的语言中,对象有私有属性与私有方法,外部程序是无法调用与读取的。只能通过对象的提供的接口访问或修改。而js并没有私有这个概念,任何对象任何属性在任何地点都可以被修改。所以是没有私有属性与私有方法的。
我们可以约定一种标识私有方法与私有属性的规则,如在前面加下划线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function Person(name){ this._name=name; } Person.prototype={ _sayName:function(){ alert(this.name); }, getName:function(){ return this._name; }, setName:function(name){ this._name=name; } }
|
也可以通过闭包实现私有属性与方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| var Person=(function() { var count=0; function Person(){ count++; } Person.prototype={ getCount:function() { return count; }, setCount:function(num){ if(!isNumber(num)){ throw new Error('参数类型不正确'); } count=num; } } function isNumber(num) { return typeof num==='number'; } return Person; })();
|
虽然js的对象属性可以在任何时间修改,但我们最好不要直接修改,而是通过该对象提供的接口进行修改。私有方法也不要去调用。
1 2 3 4
| var p1=new Person('xiaoli'); p1._name='taoge'; p1.setName('William'); p1._sayName();
|
最后,面向对象的概念与实现方式基本就这些了,最重要的是思维方式,要达到一切皆对象的境界,当然此对象不是女朋友,程序员哪来的女朋友。不过也可以用下面一段代码聊以自慰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| function Girl(name,weight,boyfriend){ this.name=name; this.weight=weight; this.boyfriend=boyfriend; } Girl.prototype.sayLove=function(){ console.log('I love you:'+this.boyfriend.name); }; Girl.prototype.kiss=function() { console.log('I want to kiss you:'+this.boyfriend.name); }; Girl.prototype.xxoo=function() { }; Girl.prototype.marry=function() { console.log('we are get married:'+ this.boyfriend.name); }; var me={ name:'写上你自己的名字' }; var myGF1=new Girl('小芳',95,me); var myGF2=new Girl('小丽',100,me); var myGF3000=new Girl('小美',105,me); myGF1.sayLove(); myGF1.kiss(); myGF1.xxoo(); myGF1.marry(); myGF3000.xxoo();
|