前端干货搜集程序员让前端飞

JavaScript面向对象的构造模式

2017-07-24  本文已影响142人  科研者

目录

一、 优秀代码的原则
二、面向过程和面向对象
三、其它优秀编程思想的本质
四、类和实例
五、面向对象的构造模式
备注:
前四节只是介绍一些我个人理解的编程思想,如果想直奔主题,可以从第五节 <面向对象的构造模式> 开始看;

内容


一、优秀代码的原则:

我认为,优秀代码的充分原则如下:
高复用;
低耦合;

高复用的好处:

  1. 使程序高效;
  2. 方便改动;当复用度较低时,如果需要改动代码,不得不在每一处用到的地方更改;当复用度较高时,只需改动一处或很少的几处即可;

低耦合的好处:

  1. 灵活;程序各个组件间的依赖性较低,这可以使得当用其它类似组件来替换当前组件时,不需要过多的改动,即可使用新组件;

二、面向过程和面向对象

面向过程:以过程为中心;
面向对象:以数据为中心;

假如我们要实现关于动物介绍自己的功能,现在有2种动物:普通动物和较聪明的动物,
那么在面向过程的编程方式中会这样写:

function  animaiSpeak() {
    var type = "动物";
    var name = "小动物";
    var separator = "\n";

    alert("类型:" + type + separator + "名字:" + name + separator);
}



function  cleverAnimaiSpeak() {
    var type = "聪明的动物";
    var name = "小聪明";
    var separator = "\n";

    alert("类型:" + type + separator + "名字:" + name + separator);
}

现在只需要调用这两上函数,这两个动物就会分别介绍自己。
这就是面向过程,以代码的执行过程为中心,执行过程就是函数的实现;
它有以下缺点:

  1. 函数名字复杂,不能够很好地表达本质—--说(Speak)——,优其在当动物种类增多时;
  2. 代码复用度低;animaiSpeak和cleverAnimaiSpeak有太多相似的东西了,比如:一样的结构、一样的数据(type、name、separator)、一样的行为(speak),但他们却各自持有一份同样的东西;
  3. 不方便改动;如果我想改下分隔符,却需要在每种动物里分别更改一下;

如果使用面向对象的编程方式,就会解决以上问题,如下:

//动物
var  animai = {
    type:"动物",
     name:"小动物",
    separator:"\n",
    
    speak:function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
    }
};

//把动物定制成为聪明的动物
function CleverAnimai () {
    this.type = "聪明的动物";
    this.name = "小聪明";
}
//继承animai
CleverAnimai.prototype = animai;

//创建个聪明的动物
var  cleverAnimai = new CleverAnimai();

这样以来,便有如下效果:

  1. 说(介绍自己)的行为在animai和cleverAnimai中统一叫做speak,简化了行为的名字,解决了上述第1个问题;
  2. animai和cleverAnimai共用了同一个数据和行为——separator和说——解决了上述第2个问题;
  3. 如果需要更改分隔符,只需要更改animai中的separator的值,animai和cleverAnimai中的separator都会被改掉,解决了上述第3个问题;

这就是面向对象的好处,通过这个例子也能体会到:面向过程是以行为(函数)为中心的,面向对象是以数据(对象)为中心的;

面向对象的思想就是优秀代码原则的体现:

  1. 面向对象的继承性是高复用的体现;
  2. 面向对使得面向过程的变量和函数限制在对象里面,变成了对象的一部分,使得变量和函数有了单独的执行环境,从而使得变量和函数与处界隔离;这种思想就是低偶合的体现;

三、其它优秀编程思想的本质

除了面向对象外,还有很多优秀的编程思想,如:应用架构思想、设计模式的思想等等;我认为,所有的这些编程思想都包含于优秀代码原则所传达的思想;
即:

应用架构思想、设计模式思想、面向对象思想等都是优秀代码思想的子集!

四、类和实例

类,就像一类东西的模板,可以形象地比喻为模具、印字的章,它是用来快速生产东西的;生产出来的东西就是所谓的对象,又叫做实例;

因为ES5之前的JavaScript不是纯粹的面向对象语言,所以用ES5之前JavaScript来示例类和实例的概念并不是好的选择,这里就用一个纯粹的面向对象语言TypeScript(ES6的超集)来示范一类和实例的概念,示例代码如下:

//注意:此为TypeScript语言代码

//定义Animai类
class Animai {
    //定义属性,格式为: [属性名]:[类型]
    type:String;
    name:String;
    separator:string;

    //定义构造函数,当通过new创建此类的实例时,会自动调用这个方法;
    constructor(name:string){
        //初始化属性
        this.name = name;
        this.type = "动物";
        this.separator = "\n";
    }

    //定义方法
    speak(){
        alert("类型:" + this.type + this.separator + "名字:" + this.name);
    }

}


//定义CleverAnimai类,并指明CleverAnimai继承Animai
class CleverAnimai extends Animai{
    //定义CleverAnimai类自己属性
    age:number;

    //定义构造函数,当通过new创建此类的实例时,会自动调用这个方法;
    constructor(type:string,name:string,age:number){
        //调用父类的构造函数
        super(name);

        //初始化自己特有的属性
        this.age = age;

        //重写父类的属性
        this.type = "聪明的动物";
    }


}

//创建类的实例
var aAnimai = new Animai("小动物");
var aCleverAnimai = new CleverAnimai("小聪明");

//调用实例的方法
aAnimai.speak();
aCleverAnimai.speak();

通过以代码,可以明显体现出:类,定义的是实例的结构,是对实例的描述,是实例的生产说明书;所以,在面向对象语言中,这个比喻——“类是模版,实例是根据模板生产出来的东西”——体现的十分明显;

五、面向对象的构造模式

1. Object的方式

以创建动物为例,假设我们要创建活生生的动物,如下:

var animai = new Object();
animai.type = "动物";
animai.name = "小动物";
animai.separator = "\n";
animai.speak = function () {
    alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
};

或者用字面量的方式:

var  animai = {
    type:"动物",
     name:"小动物",
    separator:"\n",

    speak:function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
    }
};

2. 工厂模式

虽然用Object的方式也能创建了一个活生生的动物实例,但是如果若想再创建一个动物实例的话,就不得不再把上面的代码重复写一遍;为了能够方便地构造实例,可以写一个专门的函数,每调用一次,就会返回一个新的实例,就像工厂生产商品一样,每次执行,都是同一套流程,流程结束后,一个新的实例(商品)就被创造出来了,代码如下:

function creationAnimai() {
    var animai = new Object();
    animai.type = "动物";
    animai.name = "小动物";
    animai.separator = "\n";
    animai.speak = function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
    };

    return animai;
}

var animai1 = creationAnimai();
var animai2 = creationAnimai();

这样,每当需要一个新的动物实例时,只需要调用一下creationAnimai()工厂函数即可;

3. 构造函数模式

在使用工厂方法创建新的实例时时不需要在工厂方法前加new关键字,如果需要在方法前加new关键字来创建实例,则就需要用构造函数的方式了,如下:

function Animai() {
    this.type = "动物";
    this.name = "小动物";
    this.separator = "\n";
    this.speak = function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
    };
}

var animai1 = new Animai();
var animai2 = new Animai();

与工厂方法相比,有以下不同之处:
在构造函数内部没有显式地创建实例;
在构造函数内部将属性和方法赋给了this对象;
在构造函数内部没有return语句;
在用构造函数创建新的实例时,需要在其前面加上new关键字;

与工厂方法相比,还有如下优点:
工厂方法不可以用作其创建的实例的类型标识,因为它不能用instanceof操作符来验证;
但构造函数可以用instanceof操作符来验证,所以构造函数可以用作它的实例的类型标识,即可以把构造函数当作类来用;

构造函数的缺点:
构造函数的主要问题就是:每个实例都各自持有一份方法,但这些方式的操作都相同,这就违背了优秀代码的原则;在前面的例子中,animai1和animai2都有一个名为speak()的方法,且这两个方法是不同的Function实例,但这两个函数的功能却是一样的;如下图:

构造函数模式的实例结构图.jpg

4. 原型模式

每个函数对象都会有一个prototype属性,称为原型属性;原型属性prototype是在创建函数对象时自动被创建的,这个属性的值是Object类型的对象,叫做原型对象,原型对象有个默认的属性constructor,它指向一个函数对象,表示实例的构造者(构造函数);
当用构造函数的形式new TypeFun()创建实例时,构造函数TypeFun()的原型属性所引用的原型对象会自动成为新实例的原型;通过构造函数new出来的所有实例都共享一个原型对象,如图3-1;
所以,如果把属性和方法都加在构造函数的原型属性上,那么通过构造函数new出的所有实例都会共享同一份属性和方法了,这就解决了上述第3种方式——构造函数模式——的缺点,具体代码如下:

function Animai() {
}

//在原型上添加属性和方法
Animai.prototype.type = "动物";
Animai.prototype.name = "小动物";
Animai.prototype.separator = "\n";
Animai.prototype.speak = function () {
    alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
};


var animai1 = new Animai();
var animai2 = new Animai();

或者使用更简单的方式来定制原型对象,如下:

function Animai() {
}

//创建新的原型对象
Animai.prototype = {
    constructor:Animai,     //需要手动添加构造者属性,并让它指向正确的构造函数
    type:"动物",
    name:"小动物",
    separator:"\n",

    speak:function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name);
    }
};


var animai1 = new Animai();
var animai2 = new Animai();

这时,animai1、animai2与原型对象之间的结构关系如下图:

原型模式的实例结构图.jpg

此模式的缺点:

  1. 不能通过给构造函数传递参数来自定义实例的初始值;
  2. 原型中的所有属性均被所有实例所共享,而更好的是每个实例应该有自己的特有属性值;不过,即使所有实例共享所有属性,但对于那些基本类型的属性值也不会造成问题,因为如果在实例上给一个基本的属性赋值,则会在实例自身上创建属性从而覆盖原型对象中的同名属性,从而也不会影响其它实例;但是对于引用类型的属性,在修改引用类型的属性的属性时,就会修改原型对象上的引用属性的属性值,从而使所有实例的这个属性的值都被修改了,这不是我们想要的;

5. 组合使用构造函数模式和原型模式

鉴于原型模式和构造函数模式的优缺点,为了更完美的实现,就有了这两种模式结合的方式:用构造函数模式定义实例不需要共享的属性,用原型模式定义方法和实例需要共享的属性,这样,对于不需要共享的属性则会存在于每个实例中,且各自一份,而对于方法和需要共享的属性,则会存在于原型对象中,实现了高复用,具体代码示例如下:

//构造函数模式
function Animai() {
    //不需要共享的属性
    this.type = "动物";
    this.name = "小动物";
}


//原型模式

//需要共享的属性
Animai.prototype.separator = "\n";

//方法
Animai.prototype.speak = function () {
    alert("类型:" + this.type + this.separator + "名字:" + this.name);
};


var animai1 = new Animai();
var animai2 = new Animai();

或者用更便捷的方式

//构造函数模式
function Animai() {
    //不需要共享的属性
    this.type = "动物";
    this.name = "小动物";
}

//原型模式
//创建新的原型对象
Animai.prototype = {
    //需要共享的属性
    separator:"\n",

    //方法
    constructor:Animai,     //需要手动添加构造者属性,并让它指向正确的构造函数
    speak:function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name);
    }
};


var animai1 = new Animai();
var animai2 = new Animai();

6.动态原型模式

组合使用构造函数模式和原型模式也不够完美:每定义一个类都需要同一作用域下分两块才能完成类的定义,部件太分散,缺乏整体性,如果能把构造函数的定义和原型的定义放在一起就更好了;
为了实现整体性,于是就有了动态原型模式,具体代码示例如下:

function Animai() {

    //不需要共享的属性
    this.type = "动物";
    this.name = "小动物";
    
    //方法
    if (typeof  Animai.prototype.speak != "function"){      //判断speak方法是否已经存在,当不存在时才会创建,保证只创建一次;
        
        Animai.prototype.speak = function () {
            alert("类型:" + this.type + this.separator + "名字:" + this.name);
        };
    }
    
    //需要共享的属性
    if (typeof  Animai.prototype.separator != "string"){      //判断separator属性是否已经存在,当不存在时才会创建,保证只创建一次;

        Animai.prototype.separator = Animai.prototype.separator = "\n";
    }
    
    
}


var animai1 = new Animai();
var animai2 = new Animai();

注意:
对于方法和需要共享的属性是在构造函数中创建的并添加到原型上的,当创建多个实例时,为了防止重复创建方法和共享的属性,所以需要在创建每个方法和需要共享的属性前先判断原型对象上是否已经存在相应的方法和属性,如果存在就不用再创建了;由于每个方法和共享的属性都需要加一个判断,所以如果方法和需要共享的属性有很多的话,定义起来就比较麻烦了,为了简单起见,其实并不需要对每个方法和共享的属性都加判断,因为它们一定是同时创建的,若其中之一存在,则都存在,若其中之一不存在,则都不存在;所以所有方法和共享的属性可以共用一个判断语句;因为存在实例中需要覆盖共享属性的可能性,所以判断语句中最好选择一个不打算让实例覆盖的方法来作为判断条件;具体示例代码如下:

function Animai() {

    //不需要共享的属性
    this.type = "动物";
    this.name = "小动物";

    
    if (typeof  Animai.prototype.speak != "function"){      //判断原型中的相关方法和共享属性是否已经被创建

        //方法
        Animai.prototype.speak = function () {
            alert("类型:" + this.type + this.separator + "名字:" + this.name);
        };

        //需要共享的属性
        Animai.prototype.separator = Animai.prototype.separator = "\n";
    }


}


var animai1 = new Animai();
var animai2 = new Animai();

7.寄生构造函数模式

使用这个模式之前,需要知道一个关于new操作符的知识点,如下:
如果构造函数没有返回值,则会默认返回new新创建的对象;
如果构造函数有返回值,则会返回构造函数返回的值;

详情见[JavaScript发现与理解];

此模式的主要逻辑是:在构造函数中重新创建实例,并定制该实例,然后返回该实例;

示例代码如下:

function Animai() {
    var animai = new Object();
    animai.type = "动物";
    animai.name = "小动物";
    animai.separator = "\n";
    animai.speak = function () {
        alert("类型:" + this.type + this.separator + "名字:" + this.name + this.separator);
    };

    return animai;
}

var animai1 = new Animai();
var animai2 = Animai();

此模式本质上就是工厂模式,与工厂模式唯一的不同之处在于:此模式函数的名字更像是构造函数,即更像是一个类型的名字;所以此模式具备与工厂模式同样的优缺点;

备注:
使用此模式创建对象时,即可以使用new 操作符,也可以直接调用函数;但,即使是使用new操作符,也不能用instanceof操作符来正确地判断类型;

8.稳妥构造函数模式

此模式创建的对象没有实例属性,它利用的是闭包的特性;

示例代码如下:

function Animai(type,name,separator) {
    //创建新对象,但不设置它的属性;
    var animai = new Object();

    //设置新对象的方法,当需要用到属性时,引用构造函数的参数;
    animai.speak = function () {
        alert("类型:" + type + separator + "名字:" + name + separator);
    };
    
    //获取name的值
    animai.getNmae = function () {
        return name;
    };

    //设置name的值
    animai.setNmae = function (newName) {
        name = newName;
    };

    return animai;
}

//创建动物实例
var animai = Animai();

var name = animai.getNmae(“动物”,"小动物","\n")       //获取名字
animai.setName(“大动物")          //设置名字

使用这种模式创建的对象是没有属性的,要想存取属性,则必须通过方法,除此之外,无法存取属性,即使有其他代码会给这个对象添加方法或者属性,但也不可能有别的办不访问传入到构造函数中的原始数据,这就使得开发者可以完全控制属性的访问权限,所以这种模式最适合一些安全的环境中,或者在防止数据被其它应用程序改动时使用;

9.借用构造函数

以上构造函数都没有考虑到继承的情况,当用以上构造函数实现继承时,就会出现一些问题,其中最主要的问题来自于包含引用类型值的属性的原型;它的形成原因是:在通过原型来实现继承时,原型实际上是另一个类型的实例,于是,原先的实例属性也就顺理成章地变成了现在的原型的属性了。

为了解决这个问题,于是有了借用构造函数模式;

假设:RootType为根类型;SuperType类型为SubType类型的父类;

function SuperType(superNmae) {
    //初始化属性
    this.superNmae = superNmae;

    //设置方法
    this.superFun = function () {
        return this.superNmae;
    }
}

function SubType(subNmae,superNmae) {
    //用父类的构造函数配置当前实例—this,也可以使用apply()方法达到同样的效果
    SuperType.call(this,superNmae);

    //初始化属性
    this.subNmae = subNmae;

    //设置方法
    this.subFun = function () {
        return this.subNmae;
    }
}

var aSub = new SubType("子类","父类");

在每当用构造函数SubType()创建实例aSub时,首先会借用SuperType.call(this),即调用构造函数SuperType(),并将SuperType()的this指向当前的实例aSub(也可以使用apply()方法达到同样的效果),这样就可以用构造函数SuperType()的配置初始化实例aSub;然后再进行SubType()自己的定制操作;当构造函数SuperType()执行完后,实例aSub便同时具有了SuperType类弄的实例的属性和方法和SubType的属性和方法,所以,也可以理解为SubType类型继承了SuperType类型;

实例aSub的具体结构如下图所示:

借用构造函数模式的实例结构图.jpg

所示:

此模式的优点是:
所有的属性(无论是父类型的属性还是子类型的属性)都在每一份实例中单独保存一份;
可以在子类型的构造函数中调用超类型的构造函数,并向其传参数;

此模式的缺点是:
所有的方法(无论是父类型的方法还是子类型的方法)也都在每一份实例中单独保存一份,没有实现高复用;(上分说过,因为所有实例的方法的逻辑是相同的,所以方法应该共享。)

10. 组合继承模式

组合继承又叫做伪经典继承,组合的是原型模式和借用构造函数模式;它是通过原型模式实现对方法的继承;通过借用构造函数模式实现对属性的继承;

示例代码如下:

function SuperType(superNmae) {
    //初始化属性
    this.superNmae = superNmae;
}

//在原型上设置方法
SuperType.prototype.superFun = function () {
    return this.superNmae;
}




function SubType(subNmae,superNmae) {
    //用父类的构造函数配置当前实例---this
    SuperType.call(this,superNmae);     //第1次调用SuperType()

    //初始化属性
    this.subNmae = subNmae;
}

//继承父类型的方法
SubType.prototype = new SuperType();    //第2次调用SuperType()

//在原型上设置方法 
SubType.prototype.subFun = function () {
    return this.subNmae;
}

var aSub = new SubType("子类","父类");

此时,aSub实例的结构如下图所示:

组合继承模式的实例结构图.jpg

这样一来,SuperType的所有实例即各自分别独有一份属性,又共享所有的方法,并且也可以使用instanceof和isPrototypeof()判断类型;
不过它也有一些缺点,如下:
调用两次父类型的构造函数;
SubType实例的原型上保存有已被子类型覆盖的父类属性,这是多余的;

11. 寄生组合式继承模式

造成组合继承的那2个缺点的原因就是我们通过新创建SuperType类型的实例作为SubType类型的原型,具体代码片断如下:

//继承父类型的方法
SubType.prototype = new SuperType();    //第2次调用SuperType()

注释中也说了,这行代码的目的就是为了让SubType类型继承SuperType类型的方法。因为方法都存在于原型上,所以如果仅仅是为了继承SuperType类型的方法的话,没必要创建一个SuperType类型的实例,只需要创建一个SuperType的原型属性SuperType.prototype的副本就可以了;为了方便的实现这一操作,我们可以先定义一个便捷的函数,如下:

function inheritPrototype(subType,superType) {
    var prototype = Object.create(superType.prototype);     //创建父类型的原型的副本;
    subType.prototype = prototype;                          //配置子类的原型,实现让subType继承superType方法的效果;
    prototype.constructor = subType;                        //正确配置constructor,以便能使用instanceof等操作符判断类型;
}

通过inheritPrototype()函数,就能轻松地实现让子类继承父类的方法;接下可以用inheritPrototype()函数把组合继承模式改造成寄生组合式继承模式;

示例代码如下:

function SuperType(superNmae) {
    //初始化属性
    this.superNmae = superNmae;
}

//在原型上设置方法
SuperType.prototype.superFun = function () {
    return this.superNmae;
}




function SubType(subNmae,superNmae) {
    //用父类的构造函数配置当前实例---this
    SuperType.call(this,superNmae);

    //初始化属性
    this.subNmae = subNmae;
}

//继承父类型的方法
inheritPrototype(SubType,SuperType);

//在原型上设置方法
SubType.prototype.subFun = function () {
    return this.subNmae;
}



var aSub = new SubType(“子类","父类");

此时,aSub实例的结构如下图所示:

寄生组合式继承模式的实例结构图.jpg

这样,就解决组合继承模式的那2个缺点;

注意:
这种模式是引用类型最理想的继承构造模式;

上一篇下一篇

猜你喜欢

热点阅读