javaScript自定义对象和继承的模式探究

来源:互联网 发布:小型论坛系统源码 编辑:程序博客网 时间:2024/04/29 07:21

color=#0099ff size=72 face=”黑体”
本文承接于JavaScript基础 : http://blog.csdn.net/wang2963973852/article/details/52945187
本文承接于 javaScript高级特性(类比JAVA理解javaScript):
http://blog.csdn.net/wang2963973852/article/details/53007550

JavaScript的动态性:
用函数创建对象
本系列第一篇中,我们指出,函数是一种特殊的对象,最大的特殊之处在于它是可以被调用的(a object which can be called !),其基本的调用方法与Java是一致的。
实际上,任何一个函数还有一种特殊的调用方式,姑且称之为 new 调用,这种调用会创建一个对象返回。
举例如下:

function add(a,b){
var result = a + b ;
return result ;
}
这是一个再普通不过的一个函数,我们可以这样使用它:
var sum = add(5,9) ; //sum的值为14
然而,我们还可以这样使用它:
var obj = new add(5,9) ; // typeof obj == object
上面,我们仅仅是在正常的调用前面加了一个关键字 new ,整个函数的执行逻辑就完全发生了变化,最大的变化是它不再返回函数体内的return语句中的值,而是返回了一个对象!

我想,每一个从Java转来学JavaScript的人,看到这样的情况,都会觉得不可思议吧。

有没有感到在JavaScript的世界中,函数作为一个特殊的对象,似乎凌驾于普通对象之上了?这货竟然可以生成对象
确实是这样的,如果你学习过JavaScript,你应该会听过一句话,在JavaScript中,函数是一等公民,说的就是函数的这种特殊性

让我们沉下心来,看看上面的new调用到底是怎么执行的:

JavaScript引擎执行到 new 调用所在的行时,它立马明白了,这里不是一个普通的函数调用,而是一个new调用,用户想要通过函数调用生成一个对象,于是JavaScript创建出来一个新的对象,姑且称其为obj。
然后,JavaScript引擎会将函数的prototype属性所指向的对象设为obj的原型。

实际上,每个函数都有一个prototype属性。当你用一个函数创建一个对象时,新建对象的原型会被自动设置为函数prototype属性指向的对象。
然后,JavaScript引擎会把关键字 this绑定到新创建的对象obj上,也就是说,之后在函数体内对this关键字的操作就是对新创建的对象obj的操作。

之后,JavaScript引擎会根据用户在new调用中传入的参数(本例中为5和9,不同的函数要求的参数也不相同,也可以没有参数)来一句一句执行函数, 如果函数最后没有return语句,那么当函数体执行完毕后,JavaScript会直接把对象obj返回给调用者。如果函数最后有return语句,JavaScript会判断一下return语句中的返回值是不是一个对象,如果是一个对象,那么就把这个对象返回给调用者,如果return语句返回的是一个基本数据类型,而不是一个对象,那么JavaScript仍然把对象obj返回给调用者。在本例中,JavaScript会首先执行语句 var result = a + b ;然后遇到了return语句,JavaScript发现这个return语句返回的是一个基本数据类型,不是一个对象,JavaScript果断丢弃了result这个值,函数计算出来的result在new调用中完全没有任何用!JavaScript转而把函数开始执行时新建的对象obj返回给了我们。

注:这个对象obj和变量result真的是一点关系也没有!在本例中,整个函数体内我们没有对关键字this进行任何操作,所以对象obj一直没有发生什么变化。
观察上面创建对象的过程,我们发现有几点比较别扭:

add函数首字母是小写,new add(5,9)形式上不够优美,在面向对象语言实践中,我们更习惯首字母大写的new调用。
语句var result = a + b ;对最后的对象没有什么影响,浪费CPU资源
return语句最后返回一个数字,也没有什么用,还让JavaScript多了一次判断
于是,大家就约定(仅仅是一个约定,不是语言本身的要求):

1、当你创建一个专门用来生成对象的函数时,就把函数名字的首字母大写
2、函数体内只保留对最后生成对象有影响的语句,也就是对this有影响的语句
3、不要最后的return语句,确保this所代表的对象能够返回给用户

使用各种模式解决JavaScript创建自定义类型对象问题(优缺点分析)

(1)工厂模式:
javaScript中无法创建类,因此javaScript中的工厂模式是一种封装了创建对象细节的函数
缺点:无法解决对象识别的问题(例如需要创建一个person对象,但是工厂模式却无法判断创建出的对象是否是person对象)
(2)构造函数模式:
优点:解决了对象的识别问题
缺点:构造函数模式的主要缺点是:每一次new一个对象,都会为该对象创建一遍所有此对象所拥有的方法,
而因为在javaScript中方法的本质是一个对象,那么我们没有理由不对方法进行复用(以节省内存空间)
(3)原型模式:
每次新创建一个函数,javaScript就会按照魔种规则为每一个函数添加一个prototype属性,
此属性是一个指针指向一个包含所有公用的属性和方法的对象,利用这一特性创建了原型模型
优点:实现了所有相同类型的对象都可以在原型继承函数原型对象中的公用属性和方法,并且可以自行覆盖继承来的属性和方法(类似JAVA中的子类和父类的关系)
缺点:此模式下无法对构造函数的参数进行初始化,导致每个new出来的对象在默认情况下具有相同的属性和方法
(4)组合使用构造函数模型和原型模型
这种模式下,既可以实现对构造函数的参数的初始化,也可以实现原型模式下的对共有的属性的方法的继承,是最优的创建自定义类型的方法

上面说完了JavaScript创建自定义对象的事,下面说说自定义类型的继承问题

javaScript的面向对象的程序设计–继承:

继承是面向对象语言中的一大概念,例如JAVA就支持两种标准的继承方式:1、接口继承 2、实现继承
接口继承值继承方法签名,实现继承则继承实际的方法体(主要形式是类的继承体系)

JavaScript实现继承基本原理是原型机制:

javaScript的函数有个原型的概念,就是说每个函数都有一个指向一个唯一的对象的属性,这个对象就是此函数的原型对象
javaScript中实现继承即使通过原型链的方式来实现的:其基本思想是:利用原型链让一个引用对象继承另一个引用对象的属性和方法

我们之前在实现javaScript中的自定义对象的创建模式里面看到:
一个构造函数的原型对象中定义的方法和属性,可以被利用此构造函数new出来的所有实例对象所共有,
也就是说javaScript中的机制可以保证你的实例对象可以引用到原型对象中的方法和属性
(原理:每一个实例对象都保留了一个指针,此指针指向构造函数的原型对象)

JavaScript实现继承的模式探究:

1、原型链的实现:

我们在创建继承关系的时候,创建出来的两个类,super类和child类,child类继承super类,怎么实现呢?
实例化child类,然后将其原型指针重定向指向super的实例化对象,于是child实例继承了super类实例对象中的方法和属性,而且因为super的实例对象里面保留有super原型对象的指针,因此也可以调用父类原型对象中的方法的属性

事实上所有JavaScript中的对象都直接或者间接继承Object对象,其原理也是原型链的实现

注意:
1、当子类需要重写超类中的某个方法的时候,或者要给原型添加方法的时候,一定要在之前进行原型替换的操作
2、在通过原型链实现继承时,不能使用对象字面量的方式来替换子类中的原型,因为这样会导致直接切断了原型链(现象就是子类实例的原型已经无法和父类实例联系上了)

缺点:
因为JavaScript中的引用属性只是保存的对象的指针,而不是副本,所以函数原型中的引用属性会和方法一样被所有实例对象共享,而原型链的原理就是:替换子类原型为父类实例,因此导致父类实例中的属性成为了子类原型中的属性,因此子类所有的实例都会共享其中的引用属性例如:数组、对象等

2、借用构造函数模式

JavaScript中有一个机制,在一个函数里面调用另一个函数的call方法,可以执行另一个函数,
因此我们在子类的构造函数里面调用父类的call方法,就相当于在将来要执行子类的实例化方法时会先执行一次父类的实例化方法,
这样就相当于子类的实例继承了父类实例的属性(即:每个子类实例都会有父类实例中引用属性的副本,而不是共享一个引用属性了)

缺点:仅仅使用借用构造函数模式,无法实现方法的复用:
因为子类的实例直接继承父类的实例,但是却没有指向父类原型的指针,因此要想子类继承父类的方法,那父类的方法就只能定义在父类构造函数里面,但是构造函数里面定义的方法是所有实例各自一份的,所以无法实现方法的复用

3、组合继承模式(组合原型链和借用构造函数)(最常用)

思路:
使用原型链使子类继承父类原型对象的属性和方法的继承(所有实例公用方法),通过借用构造函数实现子类对父类实例属性的继承(每个实例一个属性副本)
这样既保证了每个实例都继承了各自的一份属性,有保证了每个实例公用一份继承的方法

实现方式:
1、在子类构造函数里面调用父类的call方法(借用构造函数),继承父类实例属性(副本)
2、将子类的原型重定向为父类的实例(原型链),继承父类原型方法(公用)

缺点:
最大的问题就是会调用两次父类的构造函数,一次是子类调用call方法,一次是重定向子类的原型

4、寄生模式:寄生模式就是增强对象的一种方法

效果是:
可以根据一个已经存在的对象来创建出一个更强大的新的对象
方法是:
创建一个函数接收一个已存在的对象为参数,然后在函数内部,增强这个对象,最后再把增强的对象返回,

5、寄生组合方式继承(寄生模式和组合模式)(最优)

实际上此模式是三种模式的集合:1、原型链模式 2、借用构造函数模式 3、寄生模式

原理:
1、 利用借用构造函数模式来实现子类实例继承父类实例属性(副本)
2、 利用寄生模式来增强父类原型对象
3、然后将子类的原型重定向为增强之后的对象,实现了子类实例继承父类原型方法(公用)

优点:
即保存了组合继承模式的所有优点,又解决了组合继承模式的两次调用父类构造函数的问题

因此寄生组合方式继承是JavaScript中基于类型继承的最有效方式

要理解上面的模型,就必须要理解JavaScript实现这些模型的基础:原型链

加入我们定义了一个函数Person,我们可以在Person.prototype中添加方法,这样,所有用函数Person创建的对象都会具有我们添加的方法。

神奇的是,当我们改变Person.prototype时,那些在改变之前用Person创建的对象也会随之改变。因为所有用Person创建的对象的原型是同一个Person.prototype对象。这是体现JavaScript动态性的典型例子。

现在我们知道,用Person创建的对象的原型是Person.prototype,那么,问题来了,Person函数的原型又是什么呢?
答案是:所有的函数的原型都是一个特殊的对象:Function.prototype,该对象中包含了作为一个函数的普遍属性和方法,而Function.prototype的原型又是Object.prototype
所以,Person函数的原型链是:

Person —>Function.prototype—->Object.prototype

而用Person创建的对象的原型链是:

man—>Person.prototype—->Object.prototype

也就是说,函数的原型和函数创建的对象的原型不是一回事,一定要搞清楚,初学者很容易将二者混为一谈。

明白了这一点,我们也就知道:
Person.eat = function (){
//eating
}
为Person添加的方法eat并不会被用Person创建的对象所继承,它属于Person函数本身的方法。

0 0