深入理解Javascript面向对象编程

一:理解构造函数原型(prototype)机制

prototype是javascript实现与管理继承的一种机制,也是面向对象的设计思想.构造函数的原型存储着引用对象的一个指针,该指针指向与一个原型对象,对象内部存储着函数的原始属性和方法;我们可以借助prototype属性,可以访问原型内部的属性和方法。

当构造函数被实列化后,所有的实例对象都可以访问构造函数的原型成员,如果在原型中声明一个成员,所有的实列方法都可以共享它,比如如下代码:

原型具有普通对象结构,可以将任何普通对象设置为原型对象; 一般情况下,对象都继承与Object,也可以理解Object是所有对象的超类,Object是没有原型的,而构造函数拥有原型,因此实列化的对象也是Object的实列,如下代码:

如上代码,Function是Object的实列,也可以是Object也是Function的实列;他们是2个不同的构造器,我们继续看如下代码:

我们明白,在原型上增加成员属性或者方法的话,它被所有的实列化对象所共享属性和方法,但是如果实列化对象有和原型相同的成员成员名字的话,那么它取到的成员是本实列化对象,如果本实列对象中没有的话,那么它会到原型中去查找该成员,如果原型找到就返回,否则的会返回undefined,如下代码测试

二:理解原型域链的概念

原型的优点是能够以对象结构为载体,创建大量的实列,这些实列能共享原型中的成员(属性和方法);同时也可以使用原型实现面向对象中的继承机制~ 如下代码:下面我们来看这个构造函数AA和构造函数BB,当BB.prototype = new AA(11);执行这个的时候,那么B就继承与A,B中的原型就有x的属性值为11

实列化A new A(1)的时候 在A函数内this.x =1, B.prototype = new A(1);B.prototype 是A的实列 也就是B继承于A, 即B.prototype.x = 1;  如下代码:

C.prototype = new B(2); 也就是C.prototype 是B的实列,C继承于B;那么new B(2)的时候 在B的构造函数内 this.x = 2;那么 C的原型上会有一个属性x =2 即C.prototype.x = 2; 如下代码:

下面是实列化 var d = new C(3); 实列化C的构造函数时候,那么在C的构造函数内this.x = 3; 因此如下打印实列化后的d.x = 3;如下代码:

删除d.x 再访问d.x的时候 本实列对象被删掉,只能从原型上去查找;由于C.prototype = new B(2); 也就是C继承于B,因此C的原型也有x = 2;即C.prototype.x = 2; 如下代码:

删除C.prototype.x后,我们从上面代码知道,C是继承于B的,自身的原型被删掉后,会去查找父元素的原型链,因此在B的原型上找到x =1; 如下代码:

当删除B的原型属性x后,由于B是继承于A的,因此会从父元素的原型链上查找A原型上是否有x的属性,如果有的话,就返回,否则看A是否有继承,没有继承的话,继续往Object上去查找,如果没有找到就返回undefined 因此当删除B的原型x后,delete B.prototype.x; 打印出A上的原型x=0; 如下代码:

在javascript中,一切都是对象,Function和Object都是函数的实列;构造函数的父原型指向于Function原型,Function.prototype的父原型指向与Object的原型,Object的父原型也指向与Function原型,Object.prototype是所有原型的顶层;

如下代码:

三:理解原型继承机制

构造函数都有一个指针指向原型,Object.prototype是所有原型对象的顶层,比如如下代码:

给Object.prototype 定义一个属性,通过字面量构建的对象的话,都会从父类那边获取Object.prototype的属性;

从上面代码我们知道,原型继承的方法是:假如A需要继承于B,那么A.prototype(A的原型) = new B()(作为B的实列) 即可实现A继承于B; 因此我们下面可以初始化一个空的构造函数;然后把对象赋值给构造函数的原型,然后返回该构造函数的实列; 即可实现继承; 如下代码:

如上代码:我们先检测Object是否已经有Object.create该方法;如果没有的话就创建一个; 该方法内创建一个空的构造器,把参数对象传递给构造函数的原型,最后返回该构造函数的实列,就实现了继承方式;如上测试代码:先定义一个a对象,有成员属性name=’longen’,还有一个getName()方法;最后返回该name属性; 然后定义一个b空对象,使用Object.create(a);把a对象继承给b对象,因此b对象也有属性name和成员方法getName();

 理解原型查找原理:对象查找先在该构造函数内查找对应的属性,如果该对象没有该属性的话,

那么javascript会试着从该原型上去查找,如果原型对象中也没有该属性的话,那么它们会从原型中的原型去查找,直到查找的Object.prototype也没有该属性的话,那么就会返回undefined;因此我们想要仅在该对象内查找的话,为了提高性能,我们可以使用hasOwnProperty()来判断该对象内有没有该属性,如果有的话,就执行代码(使用for-in循环查找):如下:

如上使用for-in循环查找对象里面的属性,但是我们需要明白的是:for-in循环查找对象的属性,它是不保证顺序的,for-in循环和for循环;最本质的区别是:for循环是有顺序的,for-in循环遍历对象是无序的,因此我们如果需要对象保证顺序的话,可以把对象转换为数组来,然后再使用for循环遍历即可;

下面我们来谈谈原型继承的优点和缺点

B.prototype = new A(1);这句代码执行的时候,B的原型继承于A,因此B.prototype也有A的属性和方法,即:B.prototype.x1 = 1; B.prototype.getX1 方法;但是B也有自己的特权属性x2和特权方法getX2; 如下代码:

实列化B的时候 b.x1 首先会在构造函数内查找x1属性,没有找到,由于B的原型继承于A,因此A有x1属性,因此B.prototype.x1 = 1找到了;var c = new C(3); 实列化C的时候,从上面的代码可以看到C继承于B,B继承于A,因此在C函数中没有找到x1属性,会往原型继续查找,直到找到父元素A有x1属性,因此c.x1 = 1;c.getX3()方法; 返回this.x3+this.x2 this.x3 = 3;this.x2 是B的属性,因此this.x2 = 2;c.getX2(); 查找的方法也一样,不再解释

prototype的缺点与优点如下:

优点是:能够允许多个对象实列共享原型对象的成员及方法,

缺点是:1. 每个构造函数只有一个原型,因此不直接支持多重继承;

2. 不能很好地支持多参数或动态参数的父类。在原型继承阶段,用户还不能决定以

什么参数来实列化构造函数。

四:理解使用类继承(继承的更好的方案)

类继承也叫做构造函数继承,在子类中执行父类的构造函数;实现原理是:可以将一个构造函数A的方法赋值给另一个构造函数B,然后调用该方法,使构造函数A在构造函数B内部被执行,这时候构造函数B就拥有了构造函数A中的属性和方法,这就是使用类继承实现B继承与A的基本原理;

如下代码实现demo:

上面的代码实现了简单的类继承的基础,但是在复杂的编程中是不会使用上面的方法的,因为上面的代码不够严谨;代码的耦合性高;我们可以使用更好的方法如下:

下面我们来分析上面的代码:

在构造函数B内,使用A.call(this,x);这句代码的含义是:我们都知道使用call或者apply方法可以改变this指针指向,从而可以实现类的继承,因此在B构造函数内,把x的参数传递给A构造函数,并且继承于构造函数A中的属性和方法;

使用这句代码:B.prototype = new A();  可以实现原型继承,也就是B可以继承A中的原型所有的方法;console.log(B.prototype.constructor); 打印出输出构造函数A,指针指向与构造函数A;我们明白的是,当定义构造函数时候,其原型对象默认是一个Object类型的一个实例,其构造器默认会被设置为构造函数本身,如果改动构造函数prototype属性值,使其指向于另一个对象的话,那么新对象就不会拥有原来的constructor的值,比如第一次打印console.log(B.prototype.constructor); 指向于被实例化后的构造函数A,重写设置B的constructor的属性值的时候,第二次打印就指向于本身B;因此B继承与构造A及其原型的所有属性和方法,当然我们也可以对构造函数B重写构造函数A中的方法,如上面最后几句代码是对构造函数A中的getX方法进行重写,来实现自己的业务~;

五:建议使用封装类实现继承

封装类实现继承的基本原理:先定义一个封装函数extend;该函数有2个参数,Sub代表子类,Sup代表超类;在函数内,先定义一个空函数F, 用来实现功能中转,先设置F的原型为超类的原型,然后把空函数的实例传递给子类的原型,使用一个空函数的好处是:避免直接实例化超类可能会带来系统性能问题,比如超类的实例很大的话,实例化会占用很多内存;

如下代码:

注意:在封装函数中,有这么一句代码:Sub.sup = Sup.prototype; 我们现在可以来理解下它的含义:

比如在B继承与A后,我给B函数的原型再定义一个与A相同的原型相同的方法add();

如下代码

那么B函数中的add方法会覆盖A函数中的add方法;因此为了不覆盖A类中的add()方法,且调用A函数中的add方法;可以如下编写代码:

B.sup.add.call(this); 中的B.sup就包含了构造函数A函数的指针,因此包含A函数的所有属性和方法;因此可以调用A函数中的add方法;

如上是实现继承的几种方式,类继承和原型继承,但是这些继承无法继承DOM对象,也不支持继承系统静态对象,静态方法等;比如Date对象如下:

如上代码运行打印出object,我们可以看到使用类继承无法实现系统静态方法date对象的继承,因为他不是简单的函数结构,对声明,赋值和初始化都进行了封装,因此无法继承;

下面我们再来看看使用原型继承date对象;

我们从代码中看到,使用原型继承也无法继承Date静态方法;但是我们可以如下封装代码继承:

六:理解使用复制继承

复制继承的基本原理是:先设计一个空对象,然后使用for-in循环来遍历对象的成员,将该对象的成员一个一个复制给新的空对象里面;这样就实现了复制继承了;如下代码:

如上代码:先定义一个构造函数A,函数里面有2个属性x,y,还有一个add方法,该构造函数原型有一个mul方法,首先实列化下A后,再创建一个空对象obj,遍历对象一个个复制给空对象obj,从上面的打印效果来看,我们可以看到已经实现了复制继承了;对于复制继承,我们可以封装成如下方法来调用:

上面封装的扩展继承方法中的this对象指向于当前实列化后的对象,而不是指向于构造函数本身,因此要使用原型扩展成员的话,就需要使用constructor属性来指向它的构造器,然后通过prototype属性指向构造函数的原型;

复制继承有如下优点:

1. 它不能继承系统核心对象的只读方法和属性

2. 如果对象数据非常多的话,这样一个个复制的话,性能是非常低的;

3. 只有对象被实列化后,才能给遍历对象的成员和属性,相对来说不够灵活;

4. 复制继承只是简单的赋值,所以如果赋值的对象是引用类型的对象的话,可能会存在一些副作用;如上我们看到有如上一些缺点,下面我们可以使用clone(克隆的方式)来优化下:

基本思路是:为Function扩展一个方法,该方法能够把参数对象赋值赋值一个空构造函数的原型对象,然后实列化构造函数并返回实列对象,这样该对象就拥有了该对象的所有成员;代码如下:

2 19 收藏 1 评论

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部