深刻理解Javascript面向对象编程

深刻理解Javascript面向对象编程javascript

阅读目录java

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

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

   当构造函数被实列化后,全部的实例对象均可以访问构造函数的原型成员,若是在原型中声明一个成员,全部的实列方法均可以共享它,好比以下代码:app

// 构造函数A 它的原型有一个getName方法
function A(name){
    this.name = name;
}
A.prototype.getName = function(){
    return this.name;
}
// 实列化2次后 该2个实列都有原型getName方法;以下代码
var instance1 = new A("longen1");
var instance2 = new A("longen2");
console.log(instance1.getName()); //longen1
console.log(instance2.getName()); // longen2

原型具备普通对象结构,能够将任何普通对象设置为原型对象; 通常状况下,对象都继承与Object,也能够理解Object是全部对象的超类,Object是没有原型的,而构造函数拥有原型,所以实列化的对象也是Object的实列,以下代码:函数

// 实列化对象是构造函数的实列
console.log(instance1 instanceof A); //true
console.log(instance2 instanceof A); // true

// 实列化对象也是Object的实列
console.log(instance1 instanceof Object); //true
console.log(instance2 instanceof Object); //true

//Object 对象是全部对象的超类,所以构造函数也是Object的实列
console.log(A instanceof Object); // true

// 可是实列化对象 不是Function对象的实列 以下代码
console.log(instance1 instanceof Function); // false
console.log(instance2 instanceof Function); // false

// 可是Object与Function有关系 以下代码说明
console.log(Function instanceof Object);  // true
console.log(Object instanceof Function);  // true

如上代码,Function是Object的实列,也能够是Object也是Function的实列;他们是2个不一样的构造器,咱们继续看以下代码:性能

var f = new Function();
var o = new Object();
console.log("------------"); 
console.log(f instanceof Function);  //true
console.log(o instanceof Function);  // false
console.log(f instanceof Object);    // true
console.log(o instanceof Object);   // true

咱们明白,在原型上增长成员属性或者方法的话,它被全部的实列化对象所共享属性和方法,可是若是实列化对象有和原型相同的成员成员名字的话,那么它取到的成员是本实列化对象,若是本实列对象中没有的话,那么它会到原型中去查找该成员,若是原型找到就返回,不然的会返回undefined,以下代码测试测试

function B(){
    this.name = "longen2";
}
B.prototype.name = "AA";
B.prototype.getName = function(){
    return this.name;
};

var b1 = new B();
// 在本实列查找,找到就返回,不然到原型查找
console.log(b1.name); // longen2

// 在本实列没有找到该方法,就到原型去查找
console.log(b1.getName());//longen2

// 若是在本实列没有找到的话,到原型上查找也没有找到的话,就返回undefined
console.log(b1.a); // undefined

// 如今我使用delete运算符删除本地实列属性,那么取到的是就是原型属性了,以下代码:
delete b1.name;
console.log(b1.name); // AA

二:理解原型域链的概念优化

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

function AA(x){
    this.x = x;
}
function BB(x) {
    this.x = x;
}
BB.prototype = new AA(11);
console.log(BB.prototype.x); //11

// 咱们再来理解原型继承和原型链的概念,代码以下,都有注释
function A(x) {
    this.x = x;
}
// 在A的原型上定义一个属性x = 0
A.prototype.x = 0;
function B(x) {
    this.x = x;
}
B.prototype = new A(1);

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

console.log(B.prototype.x); // 1
// 定义C的构造函数
function C(x) {
    this.x = x;
}
C.prototype = new B(2);

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

console.log(C.prototype.x); // 2

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

var d = new C(3);
console.log(d.x); // 3

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

delete d.x;
console.log(d.x);  //2

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

delete C.prototype.x;
console.log(d.x);  // 1

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

delete B.prototype.x;
console.log(d.x);  // 0

// 继续删除A的原型x后 结果没有找到,就返回undefined了;
delete A.prototype.x;
console.log(d.x);  // undefined

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

 以下代码:

Function.prototype.a = function(){
    console.log("我是父原型Function");
}
Object.prototype.a = function(){
    console.log("我是 父原型Object");
}
function A(){
    this.a = "a";
}
A.prototype = {
    B: function(){
        console.log("b");
    }
}
// Function 和 Object都是函数的实列 以下:
console.log(A instanceof Function);  // true
console.log(A instanceof Object); // true

// A.prototype是一个对象,它是Object的实列,但不是Function的实列
console.log(A.prototype instanceof Function); // false
console.log(A.prototype instanceof Object); // true

// Function是Object的实列 同是Object也是Function的实列
console.log(Function instanceof Object);   // true
console.log(Object instanceof Function); // true

/*
 * Function.prototype是Object的实列 可是Object.prototype不是Function的实列
 * 说明Object.prototype是全部父原型的顶层
 */
console.log(Function.prototype instanceof Object);  //true
console.log(Object.prototype instanceof Function);  // false

三:理解原型继承机制

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

var obj = {};
Object.prototype.name = "tugenhua";
console.log(obj.name); // tugenhua

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

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

if(typeof Object.create !== 'function') {
    Object.create = function(o) {
        var F = new Function();
        F.prototype = o;
        return new F();
    }
}
var a = {
    name: 'longen',
    getName: function(){
        return this.name;
    }
};
var b = {};
b = Object.create(a);
console.log(typeof b); //object
console.log(b.name);   // longen
console.log(b.getName()); // longen

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

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

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

var obj = {
    "name":'tugenhua',
    "age":'28'
};
// 使用for-in循环
for(var i in obj) {
    if(obj.hasOwnProperty(i)) {
        console.log(obj[i]); //tugenhua 28
    }
}

如上使用for-in循环查找对象里面的属性,可是咱们须要明白的是:for-in循环查找对象的属性,它是不保证顺序的,for-in循环和for循环;最本质的区别是:for循环是有顺序的,for-in循环遍历对象是无序的,所以咱们若是须要对象保证顺序的话,能够把对象转换为数组来,而后再使用for循环遍历便可;

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

// 先看下面的代码:
// 定义构造函数A,定义特权属性和特权方法
function A(x) {
    this.x1 = x;
    this.getX1 = function(){
        return this.x1;
    }
}
// 定义构造函数B,定义特权属性和特权方法
function B(x) {
    this.x2 = x;
    this.getX2 = function(){
        return this.x1 + this.x2;
    }
}
B.prototype = new A(1);

B.prototype = new A(1);这句代码执行的时候,B的原型继承于A,所以B.prototype也有A的属性和方法,即:B.prototype.x1 = 1; B.prototype.getX1 方法;可是B也有本身的特权属性x2和特权方法getX2; 以下代码:

function C(x) {
    this.x3 = x;
    this.getX3 = function(){
        return this.x3 + this.x2;
    }
}
C.prototype = new B(2);
C.prototype = new B(2);这句代码执行的时候,C的原型继承于B,所以C.prototype.x2 = 2; C.prototype.getX2方法且C也有本身的特权属性x3和特权方法getX3,
var b = new B(2);
var c = new C(3);
console.log(b.x1);  // 1
console.log(c.x1);  // 1
console.log(c.getX3()); // 5
console.log(c.getX2()); // 3
var b = new B(2); 

实列化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:

function A(x) {
    this.x = x;
    this.say = function(){
        return this.x;
    }
}
function B(x,y) {
    this.m = A; // 把构造函数A做为一个普通函数引用给临时方法m
    this.m(x);  // 执行构造函数A;
    delete this.m; // 清除临时方法this.m
    this.y = y;
    this.method = function(){
        return this.y;
    }
}
var a = new A(1);
var b = new B(2,3);
console.log(a.say()); //输出1, 执行构造函数A中的say方法
console.log(b.say()); //输出2, 能执行该方法说明被继承了A中的方法
console.log(b.method()); // 输出3, 构造函数也拥有本身的方法

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

function A(x) {
    this.x = x;
}
A.prototype.getX = function(){
    return this.x;
}
// 实例化A
var a = new A(1);
console.log(a.x); // 1
console.log(a.getX()); // 输出1
// 如今咱们来建立构造函数B,让其B继承与A,以下代码:
function B(x,y) {
    this.y = y;
    A.call(this,x);
}
B.prototype = new A();  // 原型继承
console.log(B.prototype.constructor); // 输出构造函数A,指针指向与构造函数A
B.prototype.constructor = B;          // 从新设置构造函数,使之指向B
console.log(B.prototype.constructor); // 指向构造函数B
B.prototype.getY = function(){
    return this.y;
}
var b = new B(1,2);
console.log(b.x); // 1
console.log(b.getX()); // 1
console.log(b.getY()); // 2

// 下面是演示对构造函数getX进行重写的方法以下:
B.prototype.getX = function(){
    return this.x;
}
var b2 = new B(10,20);
console.log(b2.getX());  // 输出10

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

在构造函数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的原型为超类的原型,而后把空函数的实例传递给子类的原型,使用一个空函数的好处是:避免直接实例化超类可能会带来系统性能问题,好比超类的实例很大的话,实例化会占用不少内存;

以下代码:

function extend(Sub,Sup) {
    //Sub表示子类,Sup表示超类
    // 首先定义一个空函数
    var F = function(){};

    // 设置空函数的原型为超类的原型
    F.prototype = Sup.prototype; 

// 实例化空函数,并把超类原型引用传递给子类
    Sub.prototype = new F();
            
    // 重置子类原型的构造器为子类自身
    Sub.prototype.constructor = Sub;
            
    // 在子类中保存超类的原型,避免子类与超类耦合
    Sub.sup = Sup.prototype;

    if(Sup.prototype.constructor === Object.prototype.constructor) {
        // 检测超类原型的构造器是否为原型自身
        Sup.prototype.constructor = Sup;
    }

}
测试代码以下:
// 下面咱们定义2个类A和类B,咱们目的是实现B继承于A
function A(x) {
    this.x = x;
    this.getX = function(){
        return this.x;
    }
}
A.prototype.add = function(){
    return this.x + this.x;
}
A.prototype.mul = function(){
    return this.x * this.x;
}
// 构造函数B
function B(x){
    A.call(this,x); // 继承构造函数A中的全部属性及方法
}
extend(B,A);  // B继承于A
var b = new B(11);
console.log(b.getX()); // 11
console.log(b.add());  // 22
console.log(b.mul());  // 121

注意:在封装函数中,有这么一句代码:Sub.sup = Sup.prototype; 咱们如今能够来理解下它的含义:

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

以下代码

extend(B,A);  // B继承于A
var b = new B(11);
B.prototype.add = function(){
    return this.x + "" + this.x;
}
console.log(b.add()); // 1111

那么B函数中的add方法会覆盖A函数中的add方法;所以为了避免覆盖A类中的add()方法,且调用A函数中的add方法;能够以下编写代码:

B.prototype.add = function(){
    //return this.x + "" + this.x;
    return B.sup.add.call(this);
}
console.log(b.add()); // 22

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

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

// 使用类继承Date对象
function D(){
    Date.apply(this,arguments); // 调用Date对象,对其引用,实现继承
}
var d = new D();
console.log(d.toLocaleString()); // [object object]

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

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

function D(){}
D.prototype = new D();
var d = new D();
console.log(d.toLocaleString());//[object object]

咱们从代码中看到,使用原型继承也没法继承Date静态方法;可是咱们能够以下封装代码继承:

function D(){
    var d = new Date();  // 实例化Date对象
    d.get = function(){ // 定义本地方法,间接调用Date对象的方法
        console.log(d.toLocaleString());
    }
    return d;
}
var d = new D();
d.get(); // 2015/12/21 上午12:08:38

六:理解使用复制继承

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

function A(x,y) {
    this.x = x;
    this.y = y;
    this.add = function(){
        return this.x + this.y;
    }
}
A.prototype.mul = function(){
    return this.x * this.y;
}
var a = new A(2,3);
var obj = {};
for(var i in a) {
    obj[i] = a[i];
}
console.log(obj); // object
console.log(obj.x); // 2
console.log(obj.y); // 3
console.log(obj.add()); // 5
console.log(obj.mul()); // 6

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

// 为Function扩展复制继承方法
Function.prototype.extend = function(o) {
    for(var i in o) {
        //把参数对象的成员复制给当前对象的构造函数原型对象
        this.constructor.prototype[i] = o[i];
    }
}
// 测试代码以下:
var o = function(){};
o.extend(new A(1,2));
console.log(o.x);  // 1
console.log(o.y);  // 2
console.log(o.add()); // 3
console.log(o.mul()); // 2

上面封装的扩展继承方法中的this对象指向于当前实列化后的对象,而不是指向于构造函数自己,所以要使用原型扩展成员的话,就须要使用constructor属性来指向它的构造器,而后经过prototype属性指向构造函数的原型;

复制继承有以下优势:

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

 2. 若是对象数据很是多的话,这样一个个复制的话,性能是很是低的;

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

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

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

Function.prototype.clone = function(o){
    function Temp(){};
    Temp.prototype = o;
    return Temp();
}
// 测试代码以下:
Function.clone(new A(1,2));
console.log(o.x);  // 1
console.log(o.y);  // 2
console.log(o.add()); // 3
console.log(o.mul()); // 2
相关文章
相关标签/搜索