• JavaScript创建对象、原型与继承


          JavaScript是一门极其灵活的语言,烂七八糟的设计是它最大的优点。不同于其他严格类型的语言例如java,学习曲线比较友好。JavaScript个人感觉上手基本不用费劲,要想上高度那就是一个悲催而且毁三观的故事。特别是有面向对象语言基础的人来说,JavaScript真像一个噩梦。JavaScript更加的零碎,封装的不是很好。你必须理清脉络深入理解了,才能写出来高大上的优雅的代码。在下尽量的用简练易懂的语言,简单的阐述一下我对JavaScript面向对象的一点粗浅的理解。

    1,要想面向对象先得创建对象

    a,原始模式

    var object = new Object();
    object.name="huazi";
    object.age="22";
    object.sayHi = function(){
           console.log("hi"+this.name);
    }
    
    object.sayHi(); //hi huazi

    首先通过一个名称为object的对象,为其添加name和age属性以及一个sayHi的方法。this.name将会被解析成object.name。这种方式的缺点显而易见:使用同一个接口(Object)创建对象,产生大量的冗余代码。所以一般不会这么使用。

    b,工厂模式

    function createObject(name,age){
          var obj = new Object();
          obj.name=name;
          obj.age=age;
    
          obj.sayHi=function(){
                console.log("hi "+this.name);
          }
          return obj;
    }
    
    var obj = createObject("huazi",22);
    console.log(obj.sayHi(); //hi huazi

    此种方式创建对象叫做工厂模式,你给我属性,我给你个包含这些属性和方法的对象.这种模式创建的对象也有很大的问题:得到的对象属于什么类型的是不确定的.instanceof发现只能匹配到Object,不能匹配到createObject。

    c,构造器模式

    function createObject(name,age){
        this.name=name;
        this.age=age;
        this.sayHi=function(){
            console.log("hi "+this.name);
        }
    }
    var obj = new createObject("huazi",22);
    obj.sayHi(); //hi huazi
    obj instanceof createObject;//true

    构造器模式创建对象的过程分为四步:1,创建一个Object类型对象.2,将执行环境交给这个对象(this指向).3,执行构造很熟.4,返回对象.此种方式解决了类型不确定的问题.但是缺点是,在每次创建对象的过程中,都会重新创建类如sayHi的对象.而这明显是不必要的.修改如下:

    function createObject(name,age){
        this.name=name;
        this.age=age;
        this.sayHi=sayHi;
    }
    function sayHi(){
        console.log("hi "+this.name);
    }
    var obj = new createObject("huazi",22);
    obj.sayHi(); //hi huazi
    obj instanceof createObject;//true

    很简单,只需要把函数放到全局作用域即可。可是问题又来了,全局作用域中的函数只能被某一个对象调用。这在逻辑上实在有点牵强。更严重的情况是,往往我们需要定义很多方法来实现一个对象。所以就会出现大量的全局函数,并且全局函数不能在其他对象上使用。

    d,原型模式

    function createObject(){};
    createObject.prototype.name = "huazi";
    createObject.prototype.age=22;
    createObject.prototype.sayHi=function(){
        console.log("hi "+this.name);
    }
    
    var obj1 = new createObject();
    var obj2 = new createObject();
    obj1.sayHi(); //hi huazi
    obj1.sayHi===obj2.sayHi; //true

    好了,现在好像解决了刚才的问题,把属性和方法都加到了原型中。这样就不会出现全局属性和重复函数对象了。这种模式的缺点也显而易见:构造不能传参,也就是说所有对象将长的一模一样。还有就是内存共享的问题。属性要是引用类型比如Array那么就热闹了。牵一发动全身。

    e,组合模式(构造+原型)

    function createObject(name,age){
        this.name=name;
        this.age=age;
        this.array = [1,2,3];
    }
    createObject.prototype.sayHi = function(){
        console.log("hi "+this.name);
    }
    
    var obj1 = new createObject("huazi",22);
    var obj2 = new createObject("huazi",22);
    obj1.sayHi(); //hi huazi
    obj1.array===obj2.array;//false

    除了此种写法之外,在给原型加方法的时候还可以使用字面量的方式添加。但是需要注意的是使用字面量添加等于重写了prototype了,所以需要显示的申明,constructor的指向。

    createObject.prototype={
        constructor:createObject,
        sayHi:function(){
            console.log("hi "+this.name);
        }
    }

    还有需要注意的一点是,在使用字面量之前不能创建对象。前面说过此种方式等于是重写了prototype。所以之前创建的对象实例,不会更新新的属性和方法。为啥?自行脑补一下。

    f,动态原型模式

    function createObject(name,age){
        this.name=name;
        this.age=age;
        if(typeof this.sayHi != "function"){  //不存在的情况下添加方法
            createObject.prototype.sayHi = function(){
                console.log("hi "+this.name);
            }
        }
    }
    
    var obj1 = new createObject("huazi",22);
    var obj2 = new createObject("saint",22);
    obj1.sayHi(); //hi huazi
    obj2.sayHi(); //hi saint

    这种方式就算比较完美的了。但是还还需要注意,在构造内部不能使用字面量的方式去添加原型属性。回溯到构造创建对象过程,我们知道第一步就已经创建好对象了。所以使用字面量也会发生意外的事情。

    g,寄生构造模式和稳妥模式

    寄生构造模式的思路是:创建一个函数,这个函数的职责就是封装一个对象所需要的所有属性和方法,然后返回对象。从样子上来看,此种方式几乎与工厂模式无异。只是早创建对象的时候方式有些变化。就是使用new关键字来创建。

    function createObject(name,age){
        var obj = new Object();
        obj.name=name;
        obj.age=age;
        obj.sayHi=function(){
            console.log("hi "+this.name);
        }
        return obj;
    }
    var o  = new createObject("huazi",22);
    o.sayHi();

    再回忆一下构造模式创建对象的最后一步,返回对象。寄生模式其实是覆盖了返回对象的那一步。同时此种模式也没有摆脱类型不能确定的问题。那么此种模式在什么时候可以用到呢?

    function createObject(){
        var array = new Array();
        //Array构造中创建对象是使用push的
        array.push.apply(array,arguments);
        //添加新的方法
        array.toPipeString=function(){
            return this.join("|");
        }
        return array;
    }
    var o  = new createObject("huazi",22);
    o.toPipeString(); //huazi|22

    为Array添加了一个方法,同时没有修改Array的构造和原型。实际上就还对Array的二次包装,添加新的方法。这种方式比较靠谱。安全性也比较好,够封闭。

    还有一种非常安全的创建对象模式叫做稳妥模式。与寄生模式非常的类似

    function createObject(name,age){
        var obj = new Object();
        obj.sayHi=function(){
            console.log("hi "+name);
        }
        return obj;
    }
    var o  = new createObject("huazi",22);
    o.sayHi(); //hi huazi

    可以发现,name值只能在sayHi中访问,在构造中包装的数据是绝对安全可靠的。这就有点private的意思了。sayHi是暴露给外部的接口。和寄生模式一样采用稳妥模式创建的对象,类型是无法确定的。

    2,原型

           无论什么时候只要是创建了一个新函数,随之就会根据一组特定的规则为该函数创建一个prototype属性。在默认情况下prototype属性会自动获得一个constructor(构造函数)属性,这个函数非常的特殊。包含一个指向prototype属相所在函数的指针。

    function createObject(name,age){}
    var o  = new createObject("huazi",22);
    console.log(createObject.prototype.constructor);//function createObject(name,age) {}
    console.log(createObject.prototype)//createObject(name,age) {}
    //判断对象o是否是原型对象的派生。
    console.log(createObject.prototype.isPrototypeOf(o));//true

          实际上是这样的。每个对象都会有一个_proto_属性,这个属性指向的是函数原型createObject.prototype。而crateObject.prototype中存在一个constructor属性,此属性指向了createObject构造函数。等于指来指去,指出了一个回路。isPrototypeOf函数的参数是函数对象实例,作用是判断该实例的归属,是否是该原型对象的派生。使用hasOwnProperty()可以检测一个方法是存在于原型中还是存在于实例中,当然前提是确定可以访问这个方法或者这个方法存在才能确定。使用delete操作符可以删除掉实例中的方法或者属性。在原型中的属性方法默认是不能被delete的。还有一个坑就是delete不存在的属性或者方法也会返回true所以在使用delete的时候需要小心一些。

          在给prototype添加属性之前创建了一个对象,那么这个对象是否可以引用新添加的原型上的属性呢?答案是可以的。在访问属性的时候首先会查看实例对象是否存在此属性,不然就去原型找。而_proto_就是指向原型对象的。没错是指向,所以不管何时更新原型属性都是ok的。

    3,继承

          ECMAScript中描述了原型链的概念,并说明原型链将作为实现继承的主要方法。原型链顾名思义,就是讲原型链接起来实现继承。子类的prototype指向父类的实例对象就是很简单的一种实现。

    function superClass(){
        this.name="huazi";
    }
    superClass.prototype.getName=function(){
        console.log(this.name);
    }
    function childClass(){
        this.name="bob";
    }
    childClass.prototype = new superClass();
    
    childClass.prototype.getName = function(){
        console.log(this.name+"v");
    }
    
    var instance = new childClass();
    //重写了父类方法和属性
    instance.getName();    //bobv
    console.log(instance instanceof Object); //true
    console.log(instance instanceof childClass);//true
    console.log(instance instanceof superClass);//true
    
    console.log(Object.prototype.isPrototypeOf(instance));//true
    console.log(superClass.prototype.isPrototypeOf(instance));//true
    console.log(childClass.prototype.isPrototypeOf(instance));//true

    和其他的情况一样,一旦使用到prototype,就不建议使用字面量的方式为原型添加属性了。脑补即可。

    同时此种不加区分的继承方式,任然保留内存共享的问题(引用类型属性的问题)。同原型模式创建对象的问题是一样的。当然解决这个问题的办法和解决创建对象时的方法也是一样的,那就是混合使用构造和原型来实现继承。

    function superClass(name){
        this.name=name;
        this.number=[1,2,4];
    }
    superClass.prototype.getName=function(){
        console.log(this.name);
    }
    function childClass(name,age){
        //继承属性
        superClass.call(this,name);
        this.age=age;
    }
    childClass.prototype = new superClass();
    
    childClass.prototype.getName = function(){
        console.log(this.name+"v");
    }
    
    var instance = new childClass("huazi",22);
    //重写了父类方法和属性
    instance.getName();    //huaziv
    instance.number.push("1");
    instance.number===new childClass().number;  //false
    instance.number//[1,2,4,'1']

    此种方法的原则就是属性靠构造,方法靠原型。可以简单地这么理解一下。此种模式也有个问题就是效率没有达到极致,因为多次调用了父类构造。还有一个让人不舒服的地方就是原型和实例上都包括有name和age属性。这是不能接受的。

    最后放一个大招,叫做寄生组合模式。其实也就是避免了上面这个方式的缺点,即绕过父类构造来继承父类原型。

    /**
     * 1,找到父类原型对象
     * 2,修改构造的指向
     * 3,父类原型 
     */
    function inheritPrototype(superClass,childClass){
        var _prototype = new Object(superClass.prototype);
        _prototype.constructor = childClass;
        childClass.prototype=_prototype;
    }
    
    function superClass(name){
        this.name=name;
        this.number=[1,2,4];
    }
    superClass.prototype.getName=function(){
        console.log(this.name);
    }
    function childClass(name,age){
        //继承属性
        superClass.call(this,name);
        this.age=age;
    }
    //不同点就再这
    inheritPrototype(superClass,childClass);
    
    childClass.prototype.getName = function(){
        console.log(this.name+"v");
    }
    
    var instance = new childClass("huazi",22);
    //重写了父类方法和属性
    instance.getName();    //huaziv
    instance.number.push("1");
    instance.number===new childClass().number;  //false
    instance.number//[1,2,4,'1']

    恶心死我了,终于写完了!

  • 相关阅读:
    父div不会被子div撑高
    ie6兼容问题
    浏览器兼容性技巧
    css hack基本语法
    网站设置为灰色
    .net cookie跨域请求指定请求域名
    实体对象属性和值转为键值对Dictionary
    C#通过对象属性名修改值
    jQuery.noConflict()解决imgBox.js依赖jquery版本问题
    华为OJ之最长公共子序列
  • 原文地址:https://www.cnblogs.com/huaziWEB/p/4491994.html
Copyright © 2020-2023  润新知