javascript中创建对象的几种方式

373 阅读4分钟

虽然new Object( )和对象字面量的都可以用来创建单个对象,但有个缺点:如果使用同一接口创建许多对象,那么会产生冗余代码。

一、工厂模式

function createPerson(name, age, job) {
  let person = new Object();
  person.name = name;
  person.age = age;
  person.job = job;
  person.sayName = () => {
    console.log(this.name);
  };
  return person;
};
let person1 = createPerson('cc', 22, 'worker');

缺点:虽然解决了代码冗余的问题,但无法识别每个对象的类型

二、构造函数模式

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = () => {
    console.log(this.name);
  };
};
let person1 = new Person('cc', 22, 'worker');
console.log(person1.constructor === Person); //true
console.log(person1 instanceof Person);//true

以这种方式创建的对象实际上经历了4个过程:

  1. 创建一个对象
  2. 将构造函数中的this指向该对象
  3. 执行构造函数(为该对象添加属性和方法)
  4. 返回该对象

通过构造函数创建的对象有个constructor属性(该属性在对象的_proto_即原型中,指向构造函数)。 如果要检测对象的类型,instanceof是操作符最为靠谱

缺点:每个方法都要在每个对象上重新创建一遍,对象之间不会共享方法,造成了内存空间的浪费,以下代码证明了这一点

let person1 = new Person('cc', 22, 'worker');
let person2 = new Person('bb', 22, 'coder');
console.log(person1.sayName === person2.sayName); //false

三、原型模式

我们创建的每个函数都会自动有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。(prototype就是通过调用构造函数而创建的那个对象实例的原型对象)。prototype中又会自动生成一个constructor属性,该属性指向我们创建的那个函数。我们通过该函数创建的实例中也会自动生成一个指向prototype的指针[[Prototype]](这个属性一般不能访问,但在浏览器的实现里,该属性可以访问,通常为__proto__)
使用原型对象的好处是。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

以下为代码实现

function Person(){
}

Person.prototype.name="cc";
Person.prototype.age=22;
Person.prototype.job="worker";
Person.prototype.sayName=function(){
    console.log(this.name)
};

let person1=new Person();
person1.sayName();//cc

更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,并重设constructor属性。

function Person(){
}

Person.prototype={
    name:"cc",
    age:22,
    job:"worker",
    sayName:function(){
        console.log(this.name)
    }
};

Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    value:Person,
});

单独使用原型的弊端:
省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,虽然这会在一定程度带来一定的不便,但不是最大的问题,最大的问题是由其共享的本性所决定的。对于包含引用数据类型的值来说,会导致问题。(例如数组,如下修改person1的friends导致person2的friends也发生变化)

Person.prototype = {
  name: 'cc',
  age: 22,
  job: 'worker',
  friends: ['aa', 'bb', 'dd']
};

Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
});
let person1 = new Person();
let person2 = new Person();
person1.friends.push('ff');
console.log(person1.friends); //aa,bb,dd,ff
console.log(person2.friends); //aa,bb,dd,ff

四、组合使用构造函数和原型模式

这是创建自定义类型的最常见的方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。所以每个实例都会有自己的一份实例属性的副本,但同时共享着对方法的引用,最大限度的节省了内存。同时支持向构造函数传递参数。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["aa","bb"];
}

Person.prototype={
    constructor:Person,
    sayName:function(){
        console.log(this.name);
    }
};

let person1=new Person('cc',22,'worker');

五、动态原型模式

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;

    if(typeof this.sayName!="function"){
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
    }
}

这里只有sayName()不存在的情况下,才会将它添加到原型中,这段代码只会在初次调用构造函数时才执行。这里对原型所做的修改,能够立刻在所有实例中得到反映。

六、寄生构造函数模式和稳妥构造函数模式

寄生构造函数模式和不太常见,所以在这里不赘述