1

创建对象的几种方式

在逻辑上从低级到高级:工厂模式、构造函数模式、原型模式、组合模式。当然还有其他模式,但是这四者逻辑关系强,总结起来很有感觉。之所以和继承一起分析,也是因为逻辑关系很清晰:原型模式对应原型链继承,构造函数模式对应借用构造函数模式继承,组合模式对应组合继承。逻辑上按照“哪个模式有什么缺点,为什么有这个缺点,我们怎么解决这个缺点”逐步分析,这样分析完后就会豁然开朗。

1)工厂模式

使用函数创建对象,该函数来封装创建对象的细节。

                function createObject(para1,para2,para3){
                  var o = new Object();//显式创建对象
                  o.colors = [para1, para2, para3];
                  o.sayColors = function() {
                    console.log(this.colors[0]);
                  }
                  return o;//返回对象
                }
                var ob1 = createObject("red", "green", "blue");
                console.log(ob1);
                console.log(ob1.colors);
                ob1.sayColors();
                console.log(ob1 instanceof createObject);//false,无法判断对象的类型


缺点:无法判断对象类型。

2)构造函数模式

通过构造函数创建的实例被标识为一种特定的类型,可以通过instanceof 判断对象类型。


            function createObject(para1, para2, para3) {
              //通过this完成函数属性定义,不用显示创建对象,因此也不用返回对象
              this.colors = [
                para1,
                para2,
                para3
              ];
              this.sayColors = function () {
                console.log(this.colors[0]);
              }
            }
            var ob1 = new createObject('red', 'green', 'blue');
            console.log(ob1);
            console.log(ob1.colors);
            ob1.sayColors();
            console.log(ob1 instanceof createObject); //true,可以判断对象的类型
               

缺点:通过构造函数创建的实例具有不同的方法和属性,属性可以不同,这对实例来说是好的,我们希望实例属性独立。但是方法即函数,函数即对象,也就是说创建了太多重复的对象。

  var ob1 = new createObject("red", "green", "blue");
  var ob2 = new createObject("red", "green", "blue");
  alert(ob1.sayColors == ob2.sayColors);//false,不同实例具有不同的方法

解决方式:把方法的定义放到构造函数外部,即在构造函数内部引用外部全局函数。这样就可以一次定义,多次引用。但是当外部全局函数增多时,明显降低了封装性,《JavaScript精粹》上提到,全局对象是EScript的一大败笔。

    <script type="text/javascript">
      function createObject(para1,para2,para3){
       //通过this完成函数属性定义,不用显示创建对象,因此也不用返回对象
        this.colors = [para1, para2, para3];
        this.sayColors=sayColors;
      }
      function sayColors(){
          alert(this.colors[0]);
        }
      var ob1 = new createObject("red", "green", "blue");
      var ob2 = new createObject("red", "green", "blue");
      alert(ob1.sayColors == ob2.sayColors);//true
      alert(ob1.colors == ob2.colors);//false ,在构造函数中创建的引用类型属性是不同的
</script>

值得一提的是,构造函数创建的实例中的引用类型属性是很特殊的,这一点会随后提到。

3) 原型模式

:每一个函数都有一个prototype属性,这个属性指向通过构造函数创建的实例对象的原型对象。原型对象的方法和属性可以被它的所有实例共享。因此,通过把属性和方法添加到实例的原型对象上,可以实现属性和方法共享。

<script type="text/javascript">
      function createObject(){
  }
    createObject.prototype.colors = ["red", "green", "blue"];
    createObject.prototype.sayColors = function(){
    alert(this.colors[0]);
}

  var ob1 = new createObject();
  var ob2 = new createObject();
  alert(ob1.sayColors == ob2.sayColors);//true,通过原型创建的属性和方法共享
  alert(ob1.colors == ob2.colors);//true
</script>

缺点:“成也萧何,败也萧何”,原型模式的缺点就在于过强的共享能力,方法的共享可以减少多余的对象实例创建。但是属性共享导致实例难以拥有自己独特属性。当然,如果是一些不会修改的属性值,共享也就罢了;但是如果是需要修改的属性,并且该属性值是引用类型(基本类型属性值可以在实例中定义,会覆盖掉原型属性,但是不会修改原型属性,其他的实例访问该属性依旧对应原型属性),那么实例对这个属性值的修改就会在原型中反映出来,这其实就是修改了原型。糟糕的是其他实例中的该属性也同步变化,然后就会出现奇怪的问题。

<script type="text/javascript">
  function createObject(){
  }
    createObject.prototype.colors = ["red", "green", "blue"];
    createObject.prototype.sayColors = function(){
      alert(this.colors[0]);
    }

  var ob1 = new createObject();
  var ob2 = new createObject();
  ob1.colors.push("yellow");
  alert(ob1.colors);//red,green,blue,yellow
  alert(ob2.colors);//red,green,blue,yellow,通过ob1做的改变在ob2反映出来
</script>

4) 组合模式(最常用的一种对象创建方式,兼顾优点,避免缺点)

:使用构造函数模式定义各个实例属性,使用原型模式定义方法和共享的属性。

<script type="text/javascript">
  function createObject(){
            this.colors = ["red", "green", "blue"];
  }
    createObject.prototype.sayColors = function(){
      alert(this.colors[0]);
    }

  var ob1 = new createObject();
  var ob2 = new createObject();
  alert(ob1.sayColors == ob2.sayColors);//true,通过原型创建的方法共享
  alert(ob1.colors == ob2.colors);//flase,构造函数创建的引用类型属性不共享
</script>


补充关于对象的几个方法:
isPrototypeOf(): 确定一个对象是否是另一个对象的原型,只要是原型链中出现的原型,就返回true,使用方法:

  alert(createObject.prototype.isPrototypeOf(ob1));//rue

instanceof操作符:检测是否是某一构造函数的实例,前面的参数是实例, 后面的的参数是构造函数名,只要是原型链中出现的构造函数就返回true。

    alert(ob1 instanceof createObject);//true

hasOwnProperty(): 检测一个实例是否拥有某个属性,使用方法

      alert(ob1.hasOwnProperty("colors"));//true

in操作符:单独使用时可以检查实例属性或者原型属性是否存在,in后跟的一般是实例,因此可以理解为检查实例以及实例对应的原型中是否有这个属性或非法。使用方法:

  alert("sayColors" in ob1);//true
  alert("sayColors" in createObject);//false,直接对原型检查没意义   

for in 的另一种用法,类似于数组的forEach()方法,是一个循环,返回实例或原型中的可枚举的属性

<script type="text/javascript">
  function createObject(){
            this.colors = ["red", "green", "blue"];
  }
    createObject.prototype.sayColors = function(){
      alert(this.colors[0]);
    }

   var ob1 = new createObject();
    for (var prop in ob1) {
         alert(prop); //先后弹出colors   sayColors
       }   
</script>

继承的实现:

1.原型链

:把超类型的实例复制给子类型的原型,这样超类型的方法和属性就由子类型继承。

<script type="text/javascript">
  //组合模式定义超类型对象
  function SuperType(){
    this.property = true;
  }
  SuperType.prototype.getSuperValue = function(){
    return this.property;
  };
  function SubType(){
    this.subproperty = false;
  }
  //通过创建超类型对象实例并且赋值给子类型原型实现继承
  SubType.prototype = new SuperType();
  var instance = new SubType();
  //true,子类型访问超类型的方法,实现了继承
  alert(instance.getSuperValue()); 
</script>

问题:子类型原型会继承超类型实例的属性,如果这个属性值是引用类型,就会导致子类型的所有实例都共享了这个属性,导致不同实例属性的耦合,这是原型模式创建对象的固有问题。并且,在创建子类型的实例时,无法向超类型的构造函数传递参数。因此,实际中很少单独使用原型链。

2 借用构造函数(经典继承)

在子类型构造函数的内部调用超类型构造函数。这个方法之所以被称为借用构造函数,我觉得就是因为这种方法和前面介绍的通过构造函数创建实例的私有属性是一样的道理,只不过是在子类型构造函数内部调用了超类型构造函数。

<script type="text/javascript">
  function SuperType(){
    this.colors = ["red", "blue", "green"];
  }
  //借用构造函数,在子类型构造函数的内部调用超类型构造函数
  function SubType(){
    SuperType.call(this);
  }
  //每次调用产生的实例具有不同的引用类型属性
  var instance1 = new SubType();
  alert(instance1.colors);//red,blue,green
  instance1.colors.push("yellow");
  alert(instance1.colors);//red,blue,green,yellow

  var instance2 = new SubType();
  alert(instance2.colors);//red,blue,green,从原型继承的实例属性是私有的
  alert(ob1.colors == ob2.colors);//false,属性私有
  alert(ob1.sayColors == ob2.sayColors);//false,方法也是私有
</script>


而且可以在创建子类型实例时向超类型传递参数,因为这就相当于调用了一个函数,函数当然是可以有参数的。
缺点是同样具有构造函数模式创建对象的固有弊端:构造函数中烦人方法(函数对象)重复创建。并且,只有在超类型构造函数中定义的属性和方法才会被继承,通过超类型原型定义的方法和属性对子类型不可见,这是因为只执行了构造函数内部的语句。因此实际中这个方法也很少单独使用。

<script type="text/javascript">
  function SuperType(){
    this.colors = ["red", "blue", "green"];

  }
  //超类型的原型上创建方法
  SuperType.prototype.sayColors = function(){
      alert(this.colors[0]);
      return 0;
    }
  //借用构造函数,在子类型构造函数的内部调用超类型构造函数
  function SubType(){
    SuperType.call(this);
  }

  var ob1 = new SubType();
 var ob2 = new SuperType();

  alert(ob1.sayColors);//undefined,子类型不具有这个方法
  ob2.sayColors();//red,超类型具有这个方法
</script>


3 组合继承(伪经典模式继承)

将原型链继承和借用构造函数模式组合到一起,使用原型链实现对原型属性和方法的继承(实现了方法复用),但是通过借用构造函数实现对实例属性的继承(实现了实例属性私有)。避免了缺陷,融合了优点,是最常用的继承模式,而且,instanceof操作符和isPrototypeOf()都适用于组合继承创建的对象。

<script type="text/javascript">
  function SuperType(){
    this.colors = ["red", "blue", "green"];

  }
  //超类型的原型上创建方法
  SuperType.prototype.sayColors = function(){
      alert(this.colors[0]);
      return 0;
    }
  //借用构造函数,在子类型构造函数的内部调用超类型构造函数,实现属性继承
  function SubType(){
    SuperType.call(this);
  }
  //原型链实现方法继承
  SubType.prototype = new SuperType();

  var ob1 = new SubType();
  ob1.sayColors();//red//子类型继承了超类型原型的方法
</script>

zhangding
358 声望23 粉丝

JavaScript+React+Redux