JavaScript之面向对象编程之创建对象的几种设计模式(笔记二)

281 阅读14分钟

创建对象的5种方式

  1. 对象字面量
  2. 工厂模式
  3. 构造函数模式
    • 构造函数拓展模式
    • 寄生构造函数模式
    • 稳妥构造函数模式
  4. 原型模式
  5. 组合模式
    • 动态原型模式

对象字面量创建

一般地,我们创建一个对象会使用对象字面量的形式。

有三种方式来创建对象,包括new构造函数、对象直接量和Object.create()函数

new构造函数

var obj = new Object();
obj.name="小太阳";
console.log(obj)

这是通过Object这个构造函数来初始化一个新创建的对象

对象字面量(常用的一种方式,也是一种语法糖)

var person = {
   name: "小太阳",
   age: 18
}
console.log(person);

JavaScript提供了字面量的快捷方式,用于创建大多数原生对象值。使用字面量只是隐藏了与new操作符相同的基本过程,于是也可以叫做语法糖

Object.create()

Object.create(这里面接收一个对象作为参数),然后把这个参数充做一个生成对象的原型,那么返回的这个实例它就继承了当前参数传进来的对象

需求:从一个实例对象中生成另一个实例对象,通过一个实例对象b生成另一个实例对象a,参数对象a作为了b的原型

var a = {
	getX:function(){
    	console.log("小太阳")
    }
}
var b = Object.create(a);
b.getX();

create()中的参数a作为返回实例对象b的原型对象,在a中定义的属性和方法都能被b实例对象继承下来。

上述是创建一个对象比较简单但是创建多个就显得很累赘,出现冗余性的代码造成系统资源浪费,如何解决这一问题呢?在之前也会出现冗余性的代码,当出现冗余性的代码的时候,这个时候我们会想到用函数把代码封装起来。所以说在JS中它创建对象引用处我们第二种方案我们的工厂模式

工厂模式

我们创建每个对象相当于创建每个产品,工厂模式相当于去造相同类似的产品,那么我们可以用一个函数作为接口去构造这些产品。

function createPerson(name,age){
    var w = new Object();
    w.name = name;
    w.age = age;
    w.sayName = function(){
        console.log(this.name);
        console.log(this.age);
    }
    return w;
}
var p1 = createPerson('小太阳',18);
var p2 = createPerson('小胖子',16);
var p3 = createPerson('阿黄',8);
p1.sayName();
p2.sayName();
p3.sayName();

首先创建一个人物createPerson,在这个函数中作为一个接口,这个接口在内部我们可以去构造我们的对象,然后去调用这个函数接口的时候把这个对象返回。

这就是工厂模式,工厂模式虽然解决了我们前面代码冗余的问题,但是也存在自己的问题,好处就是能创建多个类似的对象,但是每个对象p1和p2到底指向哪个对象的类型?这个时候我们不确定。也就是说没有解决对象识别的问题,因此使用该模式的话并没有给出我们对象的一个类型,只是给了一个Object类型,但是在我们的代码中所有的对象都指向Objcet。这样的指向是没有意义的。这样的问题就通过构造函数模式来解决。

优点:解决了创建多个相似对象的问题,避免了大量重复的代码;

缺点:不能进行对象识别,即如何才能知道实例的对象的类型;

构造函数模式

通过创建自定义的构造函数来定义自定义构造函数的属性和方法,创建自定义构造函数就意味着可以将它的实例标识为一种特定的类型

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name);
        console.log(this.age);
    };
}
var person1 = new Person("小太阳",28);
var person2 = new Person("小胖子",25);
person1.sayName();
person2.sayName();
console.log(person1,person2)

person1和person2是两个相同的对象只是取了不同的名字而已,都是属于Person,这就能把我么当前的实例对象标识为一种特定的类型,它就属于我们当前Person,这也正是我们构造函数模式胜过我们工厂模式的地方,该模式没有说在当前的函数内部显示的去创建这么一个对象比如new Objcet(),它直接将我们的实例对象赋值给当前函数内部的this。没有return语句。

优点:解决了对象识别的问题,可以用alert(person1 instanceof Person)来进行对象识别;

缺点:构造函数中的每个方法都要在每个实例上重新创建一遍,即以这种方式创建函数会导致不同的作用域链和标识符解析。

构造函数之构造函数拓展模式

使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍,创建多个完成相同任务的方法完全没有必要,浪费内存空间。有相同的sayName方法在person1和person2实例中占用了不同的内存空间

在构造函数模式的基础上,把方法定义转移到构造函数外部,可以解决方法被重复创建的问题

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName1 = sayName;
}
function sayName(){
    alert(this.name);
}
var p1 = new Person("小太阳",28);
var p2 = new Person("小胖子",25);
console.log(p1.sayName1 === p2.sayName1);//true

现在,新问题又来了。sayName方法定义在外部,定义在外部的话这个函数归一个对象所有,一开始定义在外部它属于全局对象,那这个时候被当前的对象sayName1所占有,那这样的话就与我们全局作用域对象window冲突了,如果当前Person实例对象定义了各个方法,那是不是我们就要在全局下定义多个函数吗?当然不是,如果这样做了,那么将会严重污染全局空间,在全局下定义的sayName就没有封装可言了。在此引出函数拓展模式之寄生构造函数模式。

寄生构造函数模式

寄生函数构造模式结合了工厂模式和构造函数模式里面的特点:创建一个函数,函数体内部实例化一个对象,并且将对象返回,在外部的时候使用new来实例化对象。该模式是工厂模式和构造函数模式的结合。

工厂模式的特点在于创建一个函数,这个函数内部实例化一个对象并且将这个对象返回,但是里面没有用到new关键字。构造函数模式的特点在于上面是工厂模式的特点,下面通过new关键字来实例化对象。意味着构造函数模式是通过new关键字,工厂模式通过创建一个函数实例化一个对象并且将对象返回。

function Person(name,age){
      var p = new Object();
      p.name = name;
      p.age = age;
      p.sayName = function(){
          alert(this.name);
    }
    return p;
}
var p1 = new Person('小胖子',28);
var p2 = new Person('小太阳',28);
//具有相同作用的sayName()方法在person1和person2这两个实例中却占用了不同的内存空间
console.log(p1.sayName === p2.sayName);//false
console.log(p1.__proto__ === Person.prototype);//false
console.log(p1 instanceof Person);//false

寄生构造函数模式带来的问题:

  1. 寄生构造函数模式与构造函数模式有相同的问题,每个方法都要在每个实例上重新创建一遍,创建多个完成相同任务的方法完全没有必要,浪费内存空间
  2. 还有一个问题是,使用该模式返回的对象与构造函数之间没有关系。因此,使用instanceof运算符和prototype属性都没有意义。所以,该模式要尽量避免使用

稳妥构造函数模式

稳妥构造函数模式:没有公共属性,并且它的方法也不引用this对象,稳妥对象适合在安全环境下去使用它。它和寄生构造函数模式有点类似。调用的时候不使用new关键字

function Person(name,age){
    //创建要返回的对象
    var p = new Object();
    //可以在这里定义私有变量和函数
    //添加方法
    p.sayName = function (){
        console.log(name,age);
    }
    //返回对象
    return p;
}
//在稳妥模式创建的对象中,除了使用sayName()方法之外,没有其他方法访问name的值
var p1 = Person('小太阳',28);
p1.sayName();//小太阳 28

稳妥构造函数模式在一些为了数据安全的情况下,我们一般会采用它,其实就是结合了闭包函数,让数据安全。

原型模式

原型模式就是使用我们的一个原型对象,prototype属性来定制它共享的属性和方法,因为我们的实例都继承了当前的原型对象,只要在原型对象上定制了它的一些属性和方法,那么这些属性和方法都归我们所有的实例所共享。

​ 使用原型对象,可以让所有实例共享它的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function Person(){
      Person.prototype.name = "小太阳";
      Person.prototype.age = 17;
      Person.prototype.sayName = function(){
          console.log(this.name);
          console.log(this.age);
      }
  }
  var p1 = new Person();
  p1.sayName();//"小太阳"
  var p2 = new Person();
  p2.sayName();//"小太阳"
  console.log(p1.sayName === p2.sayName);//true

也可以这样写

function Person(){}
Person.prototype.name = "小太阳";
Person.prototype.age = 17;
Person.prototype.sayName = function(){
   console.log(this.name);
   console.log(this.age);
}
var p1 = new Person();
p1.sayName();//"小太阳"
var p2 = new Person();
p2.sayName();//"小太阳"
console.log(p1.sayName === p2.sayName);//true

上述是我们的一种原型对象,把我们当前的属性和方法都共享出来了,我们为了减少没必要的像name,age这样相同的属性和方法,那如果实例里面有多个方法应该怎么去解决呢? 为了封装原型上的功能,我们用一个可以自己去创建对象的对象封装包装它。

Person.prototype是一个对象,来修改当前的原型对象就可以了

function Person(){}
Person.prototype={
    name:"小太阳",   //定制一个name
    age:17,         //定制一个年龄
    sayName:function(){     //定制一个方法
        console.log(this.name);
        console.log(this.age)
    }
}
var p1 = new Person();
p1.sayName();//"小太阳  17"
var p2 = new Person();
p2.sayName();//"小太阳  17"
console.log(p1.sayName === p2.sayName);//true

之前原型对象中我们说过一旦我们修改当前实例的原型对象,那么紧随着我们要更改它的constructor属性,比如说当前的p1和p2都是由Person这个构造函数构造出来的,那么如果现在验证一下结果p1.constructor===p2.constructor此时的结果为false。因为现在一旦修改了原型对象它内部的构造函数,原型对象上的构造属性constructor还没有被修改。所以我们紧随着要修改constructor的指向。

Person.prototype.constructor=Person


function Person(){}
Person.prototype={
    name:"小太阳",   //定制一个name
    age:17,         //定制一个年龄
    sayName:function(){     //定制一个方法
        console.log(this.name);
        console.log(this.age)
    }
}
Person.prototype.constructor=Person;     //此时可以把constructor属性当做prototype里面的一个属性,constructor的指向指向了Person。那么这句话就可以完全删掉,然后在Person.prototype方法内部定制一个constructor属性。
var p1 = new Person();
p1.sayName();//"小太阳"
var p2 = new Person();
p2.sayName();//"小太阳"
console.log(p1.constructor===p2.constructor)
console.log(p1.sayName === p2.sayName);//true

可以显示地设置原型对象的constructor属性

constructor:Person

function Person(){};    //构造出构造函数
Person.prototype = {
    constructor:Person,   //因为我们已经构造出这个构造函数出来了。就可以直接这样写,这样就是一个更简单的原型模式
    name:'小太阳',
    age:17,
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
p1.sayName();//"mjj"
console.log(p1.constructor === Person);//true
console.log(p1.constructor === Object);//false

原型对象的问题 就是我们引用类型的值,一旦一个实例被修改了,其他实例也共有了。原型模式问题在于引用类型值属性会被所有的实例对象共享并修改,这也是很少有人单独使用原型模式的原因。

function Person(){};
Person.prototype = {
    constructor:Person,
    name:'小太阳',
    age:17,
    friends:['小红','小明'],
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
var p2 = new Person();
p1.friends.push('小胖');
console.log(p1.friends);//['小红','小明','小胖']
console.log(p2.friends);//['小红','小明','小胖']
console.log(p1.friends === p2.friends);//true

组合模式

组合模式在原型模式基础上结合了构造函数模式来构成一个组合模式。它能够解决原型模式里面的一些属性一旦被一个实例所修改那么其他的实例中也会拥有被修改的这个属性并且修改。

function Person(name, age) {
构造函数模式定制了当前对象自己的属性。
    this.name = name;
    this.age = age;
    this.friends = ["小胖", "娜娜"];
};
//原型定制各个实例对象的共享属性
Person.prototype = {
//改变原型对象的同时要改变改原型对象的constructor属性,让它指向当前构造函数Person
    constructor: Person,
    sayName: function() {  //sayName虽然是个方法但是我们可以看做是Person的一个属性
        console.log(this.name)
    }
}
var me = new Person("小太阳", 18)
me.friends.push("小红")
console.log(me.friends)
var you = new Person("小小鸟", 17)
console.log(you.friends)
me.sayName()
you.sayName()

组合模式解决了我们内存空间的问题并且我们没有在我们的全局作用域下去声明这么一些方法,sayName方法解决了内存空间消耗的问题,所以组合模式是我们使用最广泛认同度最高的一种创建自定义对象模式。

动态原型模式

动态原型模式是将这个组合模式中在分开使用的像构造函数、原型模式 都封装到当前的构造函数中

​ 动态原型模式将组合模式中分开使用的构造函数和原型对象都封装到构造函数中,然后通过检查方法是否被创建,来决定是否初始化原型对象.使用这种方法将分开的构造函数和原型对象合并到了一起,使得代码更加整齐,也减少了全局控件的污染

function Person(name,age){
//属性
this.name = name;
this.age = age;
//方法
if(typeof this.sayName != "function"){
    Person.prototype.sayName = function(){
        console.log(this.name);
          console.log(this.age);
    }
}
}
var p1 = new Person('小太阳',18);
p1.sayName();//"小太阳  18"

注意:如果原型对象中包含多个语句,只需要检查其中一个语句即可

结语

从使用对象字面量形式创建一个对象开始说起,创建多个对象会造成代码冗余;使用工厂模式可以解决该问题,但存在对象识别的问题;接着介绍了构造函数模式,该模式解决了对象识别的问题,但存在关于方法的重复创建问题;接着介绍了原型模式,该模式的特点就在于共享,但引出了引用类型值属性会被所有的实例对象共享并修改的问题;最后,提出了构造函数和原型组合模式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这种组合模式还支持向构造函数传递参数,该模式是目前使用最广泛的一种模式

总结

1. 字面量方式:

问题:创建多个对象会造成代码冗余的问题

2. 工厂模式:

优点:解决对象字面量创建方式代码冗余的问题

问题:对象识别的问题(不知道对象的类型)

3.构造函数模式

优点:解决工厂模式的对象识别问题

问题:每个方法都要在每个实例上重新创建一遍

构造函数模式与工厂模式的不同之处在于:

1)没有显式地创建对象;

2)直接将属性和方法赋给了this对象;

3)没有return语句

4.原型模式

每个函数都以一个原型prototype属性,是一个指针,指向一个对象。

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象实例的信息,而是可以直接将这些信息添加到原型对象中。

优点:解决构造函数模式创建对象的问题

特点:在于方法可以被共享

问题:

  1. 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域名不副实。
  2. 如果对象需要定义很多方法,那么就需要定义很多个全局函数,那么就毫无封装性可言了。

5.组合模式

组合模式(构造函数模式和原型模式的结合)

使用构造函数模式来定义实例当前实例的属性

使用原型模式来定义方法和共享的属性,这种模式还支持像构造函数中传递参数,该模式是使用最广泛应用度最高的模式。

需要了解的:

  1. 构造函数拓展模式
  2. 寄生构造函数模式
  3. 构造函数模式
  4. 动态原型模式