JavaScript小特性(7)——面向对象
面向对象编程(OOP),是目前主流的编程方式,似乎能够OOP的语言,才会被大多数人视为好语言,不能OOP的语言都是“奥特曼”。而JavaScript,则是常常被人误解成“奥特曼”的一种语言,殊不知,JavaScript有着一种更高级的OOP特性。
在传统的OOP语言中,Object是Class的一个实例,一个Class可以继承自另一个Class,我们可以理解为“基于类型(Class)”;而JavaScript的语法中并没有Class的概念,Object传承自哪里并不重要,重要的是它能做什么,我们可以理解为“基于原型(Prototype)”。下面就去看看JavaScript的OOP特性吧。
1、一切皆对象
在JavaScript中,一切都是对象(除了null、undefined),数字(Number)、字符串(String)、布尔值(Boolean)、函数(Function)、数组(Array)都是对象,都有属于自己的Method。不过要注意的是,Number、String、Boolean这几个基本类型对象是不可变的,即你无法添加、修改它们的方法、属性。
一个常见的误解是,Number的字面量(literal)并不是对象,因为无法直接调用它的方法(符号“.”会被解释为小数点),不过还是有很多方法可以让它看起来像一个Object:
1
2
3
4
|
// 2.toString(); 直接调用出错:SyntaxError 2..toString(); // 第二个点号可以正常解析 2 .toString(); // 注意点号前面的空格 (2).toString(); // 2先被计算 |
2、对象的创建
2.1、原始模式
在JavaScript中创建一个对象很简单:
1
2
3
4
5
6
7
8
9
|
//用new关键字创建对象 var gg = new Object(); //对象字面量(literal)的方式创建对象 var mm = {}; //给对象添加属性 gg.appearance = '帅' ; gg.character = '体贴' ; mm.appearance = '靓' ; mm.character = '温柔' ; |
在JavaScript中,Object其实就是一个Map,属性名就是key,值就是value,通过key就可以找到对应的value,value的值没有类型限制,可是基本类型、自定义对象、函数、数组等。创建对象时可以直接给对象添加属性:
1
2
3
4
5
6
7
8
9
|
var father = { appearance : '正当壮年' , character : '和蔼可亲' , //GG、MM是刚才创建的对象 son : gg, daughter : mm, //属性也可以是函数 say : function (){alert( 'GGMM,你妈喊你回家吃饭~' );} } //GG、MM是失散多年的兄妹呀…… |
2.2、构造函数模式
如果用上面那种原始模式创建对象,肯定非常麻烦,代码重用率低,而且容易出现一些拼写错误的低级bug,要是有个构造函数(Constructor)就好啦。这种方式就有点像我们在Java、C++中用的Class(本质是不同的):
1
2
3
4
5
6
7
8
9
10
11
|
//构造函数的this指向即将创建的对象 //构造函数默认return this function Person(ap, ch){ this .appearance = ap; this .character = ch; this .say = function (){alert( 'Hello world' );} } //有了构造函数的封装,创造GGMM就容易多了 //不要忘了new关键字,不然返回的是undefined var gg2 = new Person( '老实' , '忠厚' ); var mm2 = new Person( '清纯' , '可爱' ); |
通过构造函数创造的对象都有一个默认的constructor属性,指向它们的构造函数;同时可以用instanceof运算符,验证构造函数与实例对象之间的关系:
1
2
|
alert(gg2.constructor == Person); //true alert(mm2 instanceof Person); //true |
2.3、Prototype
然而单纯的构造函数模式存在一个内存浪费的问题,因为构造函数里面创建的东西都是创建出来的对象独自拥有的,都需要分配独立的内存空间,但是有些属性/方法应该是公用的(例如Person的say方法),在内存中只需生成一次。在JavaScript中,每个构造函数都有一个prototype的属性,表示它创造出来的对象的原型是什么,对这个属性进行修改,我们就可以给创建的对象一些公有的属性/方法:
1
2
3
4
5
6
7
8
9
10
|
//Person共有的属性/方法 Person.prototype = { type : '地球人' , sing : function (){alert( '唱歌' );}, dance : function (){alert( '跳舞' );} } var gg3 = new Person( '肌肉猛男' , '威武霸气' ); var mm3 = new Person( '窈窕淑女' , '婀娜多姿' ); alert(gg3.type); //地球人 mm3.dance(); //跳舞 |
2.4、动态特性
或许有人会问,构造函数创建对象的方式不就很像传统的Class吗,有啥区别呢?最大的区别就在于,JavaScript的对象是动态的,不受任何Class的限制,可以随时对它进行修改,对象之间的关系仅来自于原型(prototype)的继承。
1
2
3
4
5
6
|
//gg3唱歌和别人不同 gg3.sing = function (){alert( '我唱歌很霸气' );} //mm3跳舞和别人也不同 mm3.dance = function (){alert( '我跳舞很优雅' );} //gg3除了唱歌跳舞还会写书法 gg3.write = function (){alert( '兰亭临帖 行书如行云流水' );} |
每个对象实例都有一个hasOwnProperty的方法,用来判断某一个属性到底是自己的属性,还是继承自prototype的属性:
1
2
|
gg3.hasOwnProperty( 'sing' ); //true mm3.hasOwnProperty( 'sing' ); //false |
构造函数的prototype属性也是一个对象,我们对它进行的修改将反映到所有的实例中:
1
2
3
4
5
|
//地球很危险,搬家去火星 Person.prototype.type = '火星人' ; //GG、MM很伤心的搬去了火星 alert(gg3.type); //火星人 alert(mm3.type); //火星人 |
3、继承
之前提到过,JavaScript的继承是基于原型的继承:
原型继承的含义是指,如果构造函数(Constructor)有个原型对象A,则由该构造函数创建的对象实例(Object Instance)都必然复制于A。
JavaScript中,每个对象都有一个隐性的__proto__原型(不可访问,但可通过调试工具查看),包含从上级继承过来的属性、方法,而__proto__也是一个对象,也会有隐性的原型,因此就形成了一条原型链,链的尽头是原生对象Object,这个老祖宗包含了诸如toString等天生就有的方法。
对象的原型是隐性的,而构造函数的原型是显性的,也就是它的prototype属性。给构造函数的prototype赋值一个对象A后,该构造函数创建的对象实例都将拥有A的所有属性/方法,从而实现属性/方法的共用和继承。我们上面用prototype属性添加公有属性/方法,便是利用了原型继承的特性,公有属性/方法其实是继承自某个原型对象的。
在JavaScript中有两种继承的方式,分别是原型方式(Prototypal)、伪类方式(Pseudoclassical)。
3.1、原型方式
原型方式显得比较古朴,但是最能体现JavaScript基于“原型链”的继承原理。
这种方式也就是我们之前给Person添加公有属性/方法的办法,直接对prototype进行赋值:
1
2
3
4
5
6
7
8
9
|
function Chinese(){ this .country = '中国' ; }; //此处创建一个Person对象作为Chinese的原型 Chinese.prototype = new Person( '黄皮肤黑眼睛' , '勤劳善良' ); Chinese.prototype.say = function (){alert( '你好, 世界' );} var me = new Chinese(); alert(me.type); //地球人 me.say(); //你好, 世界 |
对于一些没有构造函数的对象(例如直接通过对象字面量创建的对象),我们可以通过一个空函数来实现它的继承:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Object.prototype.create = function (parentObj){ var F = function (){}; F.prototype = parentObj; return new F(); } var robot = { type: '机器人' , sing: function (){alert( '不会' );}, dance: function (){alert( '不会' );} } var transformor = Object.create(robot); transformor.type = '变形金刚' ; transformor.transform = function (){alert( '我会变身' )}; |
3.2、伪类方式
这种继承的方式看起来更像传统Class的继承,或许有点掩盖了JavaScript基于原型继承的特性,但对于习惯Java、C++的开发者来说这种方式比较好理解。这也可以理解为用JavaScript去模拟Java、C++的继承方式/语法。
1
2
3
4
5
6
7
8
|
//GG和MM应该是有点不同的吧,通过继承来给他们区别一下 function Male(ap, ch){ //调用父类的构造函数(不需要也可去掉) Person.apply( this , [ap,ch]); this .sex = '男人' ; //性别不同 } //继承父类的公有属性/方法(此处是有问题的) Male.prototype = Person.prototype; |
但是上面的代码是有问题的,直接将父类的prototype赋值给了子类,则子类和父类保持同一个prototype的引用,子类对prototype的修改也会反应到父类中,这不是我们希望见到的。因此,我们可以用一个空函数作为中介来解决这个问题:
1
2
3
4
5
6
7
8
|
//用空函数作为中介, 对prototype的修改 //将反应到空函数的实例中, 不会影响父类 var F = function (){}; F.prototype = Person.prototype; Male.prototype = new F(); //Male的constructor变成了F, 需要修正它 Male.prototype.constructor = Male; |
对这种思路进行进一步的封装,作为一个公用的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function inherits(Child,Parent){ var Inherit = function (){}, proto = Child.prototype, newProto, key; Inherit.prototype = Parent.prototype; newProto = new Inherit(); for (key in proto){ newProto[key] = proto[key]; } newProto.parent = Parent; newProto.constructor = Child; Child.prototype = newProto; } |
使用的时候,方法如下(逻辑上是不是有点像传统OOP的继承了):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//注意,需要用匿名函数赋值方法创建函数 var Female = function (ap, ch){ //调用父类构造函数 Person.apply( this , [ap,ch]); //定义子类属性 this .sex = '女人' ; } //定义子类方法 Female.prototype = { makeup: function (){alert( '我会化妆' )} } //实现继承 inherits(Female,Person); |
3.3、拷贝继承
除了上面两种继承方法,还有一种称之为“拷贝继承”,即将原型对象的所有属性都拷贝到新的对象当中,个人认为这种方式实际上破坏了JavaScript原有的继承机制,只能称为是一种“克隆”,而不是继承了,这里就不详细介绍了(具体可参考jQuery的extend方法)。
4、一些个人看法
一个面向对象的语言必须具备三个特性:封装、继承、多态。封装和继承上面都说到了,而JavaScript作为一种弱类型语言,多态特性更是不在话下,可见JavaScript是一个非常纯粹的面向对象语言。
如果真要分出“基于原型”和“基于类型”的优劣,我会更喜欢JavaScript的方式。
JavaScript更能体现出对象之间的差异,对象可以不断拓展、递增;不像“基于类型”方式那样,从模子里出来就不变了,为了对象的多样化需要创造很多的类。JavaScript的方式更加的灵活,也更加贴近真实世界的行为方式,原型继承模型通过提供一个有代表性的对象为基础来产生各种新的对象,并由此继续产生更符合实际应用的对象,一切只与对象有关,不受Class模板的限制。