(JavaScript)对象创建的方式有哪些?

65 阅读5分钟

1.工厂模式

工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

工厂模式例子

想象你有一个生产汽车的工厂,每次都可以生产不同型号的汽车。

function createCar(model, color) {
  return {
    model: model,
    color: color,
    start: function() {
      console.log(model + ' car started!');
    }
  };
}

let myCar = createCar('Sedan', 'blue');
myCar.start();

问题: 无法识别对象的类型,每个对象都是独立的。

2.构造函数模式

js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。

执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象, 最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。

构造函数的优点

构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。

构造函数的缺点

造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

构造函数例子

就像造房子时,通过蓝图(构造函数)创建多个相似的房屋。

function Car(model, color) {
  this.model = model;
  this.color = color;
  this.start = function() {
    console.log(model + ' car started!');
  };
}

let myCar = new Car('Sedan', 'blue');
myCar.start();

问题: 每个实例都有独立的方法对象,可能导致内存浪费。

3.原型模式

由于每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。

原型模式的优点

这种方式相对于构造函数模式来说,解决了函数对象的复用问题。

原型模式的缺点

这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

原型模式的例子

类似于使用共享图纸(原型)来建造房屋,而不是为每个房屋都创建新的图纸。

function Car(model, color) {
  this.model = model;
  this.color = color;
}

Car.prototype.start = function() {
  console.log(this.model + ' car started!');
};

let myCar = new Car('Sedan', 'blue');
myCar.start()

问题: 无法通过构造函数传入参数初始化值,引用类型值会共享。

4.组合使用构造函数模式与原型模式

这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都会存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用

组合使用构造函数与原型的优点

这种方法很好的解决了两种模式单独使用时的缺点

组合使用构造函数与原型的缺点

因为使用了两种不同的模式,所以对于代码的封装性不够好。

组合使用构造函数与原型的例子

就像使用蓝图(构造函数)建造房屋,同时使用共享图纸(原型)来实现方法的复用。

function Car(model, color) {
  this.model = model;
  this.color = color;
}

Car.prototype.start = function() {
  console.log(this.model + ' car started!');
};

let myCar = new Car('Sedan', 'blue');
myCar.start();

5.动态原型模式

这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

动态原型模式的例子

就像在建造房屋时,只有在第一次需要时才创建并共享图纸。

function Car(model, color) {
  this.model = model;
  this.color = color;

  if (typeof this.start !== 'function') {
    Car.prototype.start = function() {
      console.log(this.model + ' car started!');
    };
  }
}

let myCar = new Car('Sedan', 'blue');
myCar.start();

6.寄生构造函数模式

这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函 数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

类似于在已有的房屋基础上,通过增加附加功能来扩展房屋。

function Car(model, color) {
  let car = {};
  car.model = model;
  car.color = color;
  car.start = function() {
    console.log(model + ' car started!');
  };
  return car;
}

let myCar = Car('Sedan', 'blue');
myCar.start();